big refactor on core logic.

This commit is contained in:
fiatjaf 2021-12-10 21:44:58 -03:00
parent 598dd7459e
commit c04c4d7b52
21 changed files with 329 additions and 1045 deletions

View File

@ -11,12 +11,12 @@
},
"dependencies": {
"@quasar/extras": "^1.0.0",
"bip32": "^3.0.1",
"bip39": "^3.0.4",
"core-js": "^3.6.5",
"crypto": "^1.0.1",
"identicon.js": "^2.3.3",
"md-gum-polyfill": "^1.0.0",
"nostr-tools": "^0.5.0",
"nostr-tools": "^0.6.0",
"quasar": "^2.0.0",
"stream": "^0.0.2",
"vuex": "^4.0.1"

View File

@ -8,7 +8,7 @@
/* eslint-env node */
const ESLintPlugin = require('eslint-webpack-plugin')
const { configure } = require('quasar/wrappers');
const {configure} = require('quasar/wrappers')
module.exports = configure(function (ctx) {
return {
@ -21,13 +21,10 @@ module.exports = configure(function (ctx) {
// app boot file (/src/boot)
// --> boot files are part of "main.js"
// https://quasar.dev/quasar-cli/boot-files
boot: [
],
boot: [],
// https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-css
css: [
'app.scss'
],
css: ['app.scss'],
// https://github.com/quasarframework/quasar/tree/dev/extras
extras: [
@ -40,15 +37,15 @@ module.exports = configure(function (ctx) {
// 'roboto-font-latin-ext', // this or either 'roboto-font', NEVER both!
'roboto-font', // optional, you are not bound to it
'material-icons', // optional, you are not bound to it
'material-icons' // optional, you are not bound to it
],
// Full list of options: https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-build
build: {
vueRouterMode: 'hash', // available values: 'hash', 'history'
vueRouterMode: 'history', // available values: 'hash', 'history'
// transpile: false,
// publicPath: '/',
publicPath: '/',
// Add dependencies for transpiling with Babel (Array of string/regex)
// (from node_modules, which are by default not transpiled).
@ -66,10 +63,11 @@ module.exports = configure(function (ctx) {
// https://quasar.dev/quasar-cli/handling-webpack
// "chain" is a webpack-chain object https://github.com/neutrinojs/webpack-chain
chainWebpack (chain) {
chain.plugin('eslint-webpack-plugin')
.use(ESLintPlugin, [{ extensions: [ 'js', 'vue' ] }])
},
chainWebpack(chain) {
chain
.plugin('eslint-webpack-plugin')
.use(ESLintPlugin, [{extensions: ['js', 'vue']}])
}
},
// Full list of options: https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-devServer
@ -111,14 +109,15 @@ module.exports = configure(function (ctx) {
// manualPostHydrationTrigger: true,
prodPort: 3000, // The default port that the production server should use
// (gets superseded if process.env.PORT is specified at runtime)
// (gets superseded if process.env.PORT is specified at runtime)
maxAge: 1000 * 60 * 60 * 24 * 30,
// Tell browser when a file from the server should expire from cache (in ms)
// Tell browser when a file from the server should expire from cache (in ms)
chainWebpackWebserver (chain) {
chain.plugin('eslint-webpack-plugin')
.use(ESLintPlugin, [{ extensions: [ 'js' ] }])
chainWebpackWebserver(chain) {
chain
.plugin('eslint-webpack-plugin')
.use(ESLintPlugin, [{extensions: ['js']}])
},
middlewares: [
@ -134,9 +133,10 @@ module.exports = configure(function (ctx) {
// for the custom service worker ONLY (/src-pwa/custom-service-worker.[js|ts])
// if using workbox in InjectManifest mode
chainWebpackCustomSW (chain) {
chain.plugin('eslint-webpack-plugin')
.use(ESLintPlugin, [{ extensions: [ 'js' ] }])
chainWebpackCustomSW(chain) {
chain
.plugin('eslint-webpack-plugin')
.use(ESLintPlugin, [{extensions: ['js']}])
},
manifest: {
@ -193,13 +193,11 @@ module.exports = configure(function (ctx) {
packager: {
// https://github.com/electron-userland/electron-packager/blob/master/docs/api.md#options
// OS X / Mac App Store
// appBundleId: '',
// appCategoryType: '',
// osxSign: '',
// protocol: 'myapp://path',
// Windows only
// win32metadata: { ... }
},
@ -211,16 +209,18 @@ module.exports = configure(function (ctx) {
},
// "chain" is a webpack-chain object https://github.com/neutrinojs/webpack-chain
chainWebpackMain (chain) {
chain.plugin('eslint-webpack-plugin')
.use(ESLintPlugin, [{ extensions: [ 'js' ] }])
chainWebpackMain(chain) {
chain
.plugin('eslint-webpack-plugin')
.use(ESLintPlugin, [{extensions: ['js']}])
},
// "chain" is a webpack-chain object https://github.com/neutrinojs/webpack-chain
chainWebpackPreload (chain) {
chain.plugin('eslint-webpack-plugin')
.use(ESLintPlugin, [{ extensions: [ 'js' ] }])
},
chainWebpackPreload(chain) {
chain
.plugin('eslint-webpack-plugin')
.use(ESLintPlugin, [{extensions: ['js']}])
}
}
}
});
})

View File

@ -1,10 +1,8 @@
import Generate from '../components/Generate.vue'
import Publish from '../components/Publish.vue'
import Reply from '../components/Reply.vue'
import Post from '../components/Post.vue'
export default ({app}) => {
app.component('Generate', Generate)
app.component('Publish', Publish)
app.component('Reply', Reply)
app.component('Post', Post)

View File

@ -1,222 +0,0 @@
<template>
<q-card class="q-pa-md q-pt-lg q-mt-md">
<q-stepper v-model="step" vertical color="primary" animated>
<q-step
:name="1"
title="Generate/Restore"
icon="settings"
:done="step > 1"
>
Nostr.org uses a word list of 12 words is used to create your keys, to
restore either enter a word list or a Nostr private key.
<q-input
v-model="recover"
:loading="loading"
autogrow
type="textarea"
label="Word List/Private Key"
></q-input
><br />
<q-btn
color="primary"
label="Generate"
class="q-mr-md"
@click="createKeys"
/>
<q-btn
color="primary"
label="Restore"
class="q-mr-md"
@click="createKeys"
/>
<q-btn
v-if="privatekey"
color="primary"
label="Continue"
@click="step = 2"
/>
</q-step>
<q-step :name="2" title="Your keys" icon="vpn_key" :done="step > 2">
In this client you can restore from a word list but for other clients
you will need to use your keys as well.<br /><br />
Your private key is used to sign/publish posts.
<br />
<q-input
v-model="privatekey"
filled
:type="isPwd ? 'password' : 'text'"
>
<template #prepend>
<q-icon
name="content_copy"
class="cursor-pointer"
@click="copyToClip(privatekey)"
></q-icon>
</template>
<template #append>
<q-icon
:name="isPwd ? 'visibility_off' : 'visibility'"
class="cursor-pointer"
@click="isPwd = !isPwd"
></q-icon>
</template>
</q-input>
<br />
Your public key allows other people to read your posts, follow you, and
send you private messages.
<br />
<q-input v-model="publickey" filled type="text">
<template #prepend>
<q-icon
name="content_copy"
class="cursor-pointer"
@click="copyToClip(publickey)"
></q-icon>
</template>
</q-input>
<q-stepper-navigation>
<q-btn color="primary" label="Continue" @click="step = 3" />
<q-btn
flat
color="primary"
label="Back"
class="q-ml-sm"
@click="step = 1"
/>
</q-stepper-navigation>
</q-step>
<q-step :name="3" title="Key storage" icon="lock">
To publish your posts this client needs to sign messages with your
private key. Choose how this client will access your private key.
<div class="q-pa-md q-gutter-sm">
<div class="q-gutter-sm">
<q-radio
v-model="keystoreoption"
dense
val="local"
label="Local Storage (Recommended)"
/><br />
<q-radio
v-model="keystoreoption"
dense
disable
val="url"
label="URL (coming soon)"
/><br />
<q-radio
v-model="keystoreoption"
dense
disable
val="external"
label="Hardware wallet (coming soon)"
/><br />
</div>
</div>
<q-stepper-navigation>
<q-btn color="primary" label="Finish" @click="finalGenerate" />
<q-btn
flat
color="primary"
label="Back"
class="q-ml-sm"
@click="step = 2"
/>
</q-stepper-navigation>
</q-step>
</q-stepper>
</q-card>
</template>
<script>
import crypto from 'crypto'
import {getPublicKey} from 'nostr-tools'
import {copyToClipboard} from 'quasar'
import helpersMixin from '../utils/mixin'
const bip39 = require('bip39')
const bip32 = require('bip32')
export default {
mixins: [helpersMixin],
data() {
return {
step: 1,
loading: false,
recover: '',
privatekey: null,
publickey: null,
keystoreoption: 'local',
isPwd: true
}
},
methods: {
async createKeys() {
this.loading = true
this.recover = this.recover.trim()
setTimeout(() => {
if (this.recover.split(/ +/).length === 12) {
// recover mnemonic
let mnemonic = this.recover.split(/ +/).join(' ')
let seed = bip39.mnemonicToSeedSync(mnemonic)
let root = bip32.fromSeed(seed)
this.privatekey = root.privateKey.toString('hex')
this.recover = mnemonic
} else if (/^[0-9a-f]{64}$/.exec(this.recover.toLowerCase())) {
// recover private key
this.privatekey = this.recover.toLowerCase()
this.recover = this.privatekey
} else {
// generate a new seed
let randomBytes = crypto.randomBytes(16)
let mnemonic = bip39.entropyToMnemonic(randomBytes.toString('hex'))
let seed = bip39.mnemonicToSeedSync(mnemonic)
let root = bip32.fromSeed(seed)
this.privatekey = root.privateKey.toString('hex')
this.$q.notify({
message: 'MAKE SURE YOU BACKUP YOUR WORD LIST'
})
this.recover = mnemonic
}
this.publickey = getPublicKey(this.privatekey)
this.loading = false
}, 1)
},
copyToClip(text) {
copyToClipboard(text)
.then(() => {
this.$q.notify({
message: 'COPIED'
})
})
.catch(() => {
this.$q.notify({type: 'negative', message: 'FAILED'})
})
},
finalGenerate() {
this.$store.dispatch('finalGenerate', {
privatekey: this.privatekey,
publickey: this.publickey,
keystoreoption: this.keystoreoption
})
if (this.keystoreoption === 'external') {
this.$router.push('/?pub=' + this.publickey + '&prv=' + this.privatekey)
} else {
this.$router.push('/')
}
}
}
}
</script>

View File

@ -17,7 +17,7 @@
<q-card-section class="col no-shadow">
<q-card-section class="q-pa-none" @click="dialogReply = true">
<q-item-label
>{{ $store.getters.handle(post.pubkey) }}
>{{ $store.getters.displayName(post.pubkey) }}
<small style="color: grey">
{{ niceDate(post.created_at * 1000) }}
</small>
@ -38,29 +38,6 @@
@click="dialogReply = true"
>
</q-btn>
<q-btn
v-if="post.retry"
class="float-right q-mr-xs"
round
unelevated
color="pink"
flat
icon="settings_backup_restore"
size="sm"
@click="postAgain(post)"
/>
<q-btn
v-if="post.retry"
class="float-right q-mr-xs"
round
unelevated
color="pink"
flat
icon="cancel"
size="sm"
@click="deletePost(post)"
/>
</div>
</q-card-section>
</q-card-section>
@ -77,15 +54,6 @@ export default {
return {
dialogReply: false
}
},
methods: {
postAgain(post) {
this.$store.dispatch('postAgain', post)
},
deletePost(post) {
this.$store.dispatch('deletePost', post.id)
}
}
}
</script>

View File

@ -3,17 +3,15 @@
<div class="row">
<q-form style="width: 100%" class="q-gutter-md" @submit="sendPost">
<q-input
v-model="publishtext"
v-model="text"
style="font-size: 20px"
label="Say something"
maxlength="280"
>
<template #before>
<q-btn round @click="toProfile($store.state.myProfile.pubkey)">
<q-btn round @click="toProfile($store.state.keys.pub)">
<q-avatar size="42px">
<img
:src="$store.getters.avatar($store.state.myProfile.pubkey)"
/>
<img :src="$store.getters.avatar($store.state.keys.pub)" />
</q-avatar>
</q-btn>
</template>
@ -21,7 +19,7 @@
<div class="float-right">
<q-btn
v-if="publishtext.length < 280"
v-if="text.length < 280"
class="float-left q-mr-md"
round
unelevated
@ -37,7 +35,7 @@
rounded
unelevated
dense
@click="publishtext = publishtext + emoji.item"
@click="text = text + emoji.item"
>{{ emoji.item }}</q-btn
>
<br />
@ -48,7 +46,7 @@
rounded
unelevated
dense
@click="publishtext = publishtext + emoji.item"
@click="text = text + emoji.item"
>{{ emoji.item }}</q-btn
>
</q-popup-proxy>
@ -87,13 +85,13 @@ export default {
data() {
return {
publishtext: ''
text: ''
}
},
methods: {
sendPost() {
this.$store.dispatch('sendPost', {message: this.publishtext})
this.publishtext = ''
this.$store.dispatch('sendPost', {message: this.text})
this.text = ''
}
}
}

View File

@ -12,7 +12,7 @@
<q-card-section class="col no-shadow q-pb-none">
<q-item-label
>{{ $store.getters.handle(post.pubkey) }}
>{{ $store.getters.displayName(post.pubkey) }}
<small style="color: grey">{{ niceDate(post.created_at) }}</small>
</q-item-label>
{{ post.content }}
@ -25,11 +25,11 @@
<q-form
style="width: 100%"
class="q-gutter-md"
@submit="sendReply(replytext, [['e', post.id]])"
@submit="sendReply(text, [['e', post.id]])"
><q-tooltip> Coming soon </q-tooltip>
<q-input
disable
v-model="replytext"
v-model="text"
dense
style="font-size: 20px"
autogrow
@ -40,7 +40,7 @@
<div class="float-right">
<q-btn
disable
v-if="replytext.length < 280"
v-if="text.length < 280"
class="float-left q-mr-md"
round
unelevated
@ -56,7 +56,7 @@
rounded
unelevated
dense
@click="replytext = replytext + emoji.item"
@click="text = text + emoji.item"
>{{ emoji.item }}</q-btn
>
<br />
@ -67,7 +67,7 @@
rounded
unelevated
dense
@click="replytext = replytext + emoji.item"
@click="text = text + emoji.item"
>{{ emoji.item }}</q-btn
>
</q-popup-proxy>
@ -109,18 +109,16 @@ export default {
props: ['post'],
data() {
return {
publishtext: '',
replytext: ''
text: ''
}
},
methods: {
sendReply() {
console.log(this.post.id)
this.$store.dispatch('sendPost', {
message: this.replytext,
message: this.text,
tags: [['e', this.post.id]]
})
this.replytext = ''
this.text = ''
}
}
}

View File

@ -4,10 +4,6 @@
<Publish />
</q-dialog>
<q-dialog v-model="dialogGenerate" position="top">
<Generate />
</q-dialog>
<div class="flex-center column">
<div class="row" style="width: 100%">
<div
@ -106,22 +102,6 @@
<q-item-section>Settings</q-item-section>
</q-item>
<q-item
v-ripple
clickable
:active="$route.name === 'help'"
active-class="my-menu-link"
style="padding: 15px"
:to="'/help'"
>
<q-item-section avatar>
<q-icon name="help"></q-icon>
</q-item-section>
<q-item-section>Help</q-item-section>
</q-item>
<br />
</q-list>
<q-btn
v-if="!$store.getters.disabled"
@ -151,8 +131,8 @@
color="primary"
size="md"
dense
:label="$store.getters.handle($store.state.myProfile.pubkey)"
@click="copyToClip($store.state.myProfile.pubkey)"
:label="$store.getters.displayName($store.state.keys.pub)"
@click="copyPubKey"
>
<q-tooltip> Copy public key </q-tooltip></q-btn
>
@ -203,13 +183,11 @@
</q-input>
</q-form>
</q-card-section>
<q-card-section
v-if="Object.keys($store.state.theirProfile).length"
>
<q-card-section v-if="$store.state.following.length">
<h6 class="q-ma-none">Following</h6>
<q-list>
<q-item
v-for="(_, pubkey) in $store.state.theirProfile"
v-for="(_, pubkey) in $store.state.following"
:key="pubkey"
v-ripple
clickable
@ -222,7 +200,7 @@
</q-item-section>
<q-item-section>
{{ $store.getters.handle(pubkey) }}
{{ $store.getters.displayName(pubkey) }}
</q-item-section>
</q-item>
</q-list>
@ -234,44 +212,7 @@
</div>
<q-footer bordered style="bottom: 0%; position: fixed" class="bg-white">
<q-banner
v-if="showInstallBanner"
inline-actions
dense
class="bg-primary text-white"
>
<template #avatar>
<q-avatar>
<img src="/icons/favicon-16x16.png" />
</q-avatar>
</template>
<b> INSTALL NOSTR?</b>
<template #action>
<q-btn
flat
dense
class="q-px-sm"
label="Yes"
@click="installApp()"
></q-btn>
<q-btn
flat
dense
class="q-px-sm"
label="Later"
@click="showInstallBanner = false"
></q-btn>
<q-btn
flat
dense
class="q-px-sm"
label="Never"
@click="neverInstallApp()"
></q-btn>
</template>
</q-banner>
<center>
<div class="text-center">
<q-tabs class="text-primary small-screen-only">
<q-route-tab style="width: 20%" name="home" icon="home" to="/" />
@ -287,85 +228,39 @@
icon="settings"
to="/settings"
/>
<q-route-tab style="width: 20%" name="help" icon="help" to="/help" />
</q-tabs>
</center>
</div>
</q-footer>
</q-layout>
</template>
<script>
let deferredPrompt
import {copyToClipboard} from 'quasar'
import helpersMixin from '../utils/mixin'
export default {
name: 'MainLayout',
mixins: [helpersMixin],
data() {
return {
showInstallBanner: null,
dialogGenerate: false,
dialogPublish: false,
addPubKey: ''
}
},
mounted() {
let value = this.$q.localStorage.getItem('neverShowBanner')
if (!value) {
window.addEventListener('beforeinstallprompt', e => {
e.preventDefault()
deferredPrompt = e
this.showInstallBanner = true
})
}
},
created: function () {
if (this.$store.getters.disabled) {
this.$router.push('/help')
return
}
this.$store.dispatch('launch')
},
methods: {
installApp() {
this.showInstallBanner = false
deferredPrompt.prompt()
deferredPrompt.userChoice.then(choiceResult => {
if (choiceResult.outcome === 'accepted') {
console.log('User accepted the install prompt')
} else {
console.log('User dismissed the install prompt')
}
})
},
neverInstallApp() {
this.showInstallBanner = false
async copyPubKey() {
try {
this.$q.localStorage.setItem('neverShowBanner', true)
} catch (e) {
console.log(e)
}
},
copyToClip(text) {
copyToClipboard(text)
.then(() => {
this.$q.notify({
message: 'COPIED'
})
await copyToClipboard(this.$store.state.keys.pub)
this.$q.notify({
message: 'COPIED'
})
.catch(() => {
this.$q.notify({type: 'negative', message: 'FAILED'})
})
},
addPubFollow() {
if (this.addPubKey.trim() !== this.$store.state.myProfile.pubkey) {
this.$store.dispatch('startFollowing', this.addPubKey.trim())
} else {
this.$q.notify({color: 'pink', message: 'You cant follow yourself!'})
} catch (err) {
this.$q.notify({type: 'negative', message: 'FAILED'})
}
this.addPubKey = ''
}
}
}

View File

@ -1,8 +1,8 @@
<template>
<q-page>
<center>
<div class="text-center">
<strong class="text-h6 q-pa-lg">Chat</strong>
</center>
</div>
<q-btn
v-go-back.single
flat
@ -21,31 +21,28 @@
>
<q-scroll-area
ref="chatScroll"
:thumb-style="thumbStyle"
:thumb-style="{
left: '102%',
backgroundColor: '#26A69A',
width: '10px',
opacity: 0.35
}"
style="height: 100%; max-width: 100%"
>
<div v-for="message in messages" :key="message.id">
<div
v-for="event in $store.state.events.kind4[$route.params.pubkey] ||
[]"
:key="event.id"
>
<q-chat-message
:text="[
message.text +
(message.loading
? '<small>sending...</small>'
: message.failed
? '<small>failed. <a class=delete><i class=material-icons>cancel</i></a> <a class=retry><i class=material-icons>settings_backup_restore</i></a></small>'
: '')
]"
:name="message.from.substring(0, 6) + '...'"
:avatar="$store.getters.avatar(message.from)"
:sent="
message.from === $store.state.myProfile.pubkey ? true : false
"
:stamp="niceDate(new Date(message.created_at * 1000))"
:text="[event.plaintext]"
:name="$store.getters.displayName(event.pubkey)"
:avatar="$store.getters.avatar(event.pubkey)"
:sent="event.pubkey === $store.state.keys.pub"
:stamp="niceDate(new Date(event.created_at * 1000))"
:bg-color="
message.from === $store.state.myProfile.pubkey
? 'primary'
: 'tertiary'
event.pubkey === $store.state.keys.pub ? 'primary' : 'tertiary'
"
@click="ev => clickMessageAction(ev, message.id, message.text)"
>
</q-chat-message>
</div>
@ -56,7 +53,7 @@
<q-form
class="q-gutter-md"
@submit="submitMessage"
@reset="resetMessage"
@reset="this.text = ''"
>
<div class="row">
<div class="col-8">
@ -139,28 +136,7 @@ export default {
data() {
return {
text: '',
reload: 0, // a hack to recompute messages,
thumbStyle: {
left: '102%',
backgroundColor: '#26A69A',
width: '10px',
opacity: 0.35
}
}
},
computed: {
messages() {
this.$store.state.chatUpdated // hack to recompute
this.scroll()
return (
LocalStorage.getItem(`messages.${this.$route.params.pubkey}`).sort(
function (a, b) {
return a.created_at - b.created_at
}
) || []
)
text: ''
}
},
methods: {
@ -170,23 +146,7 @@ export default {
const duration = 350
scrollArea.setScrollPosition(scrollTarget.scrollHeight, duration)
},
async failed() {
var messages = this.$q.localStorage.getItem(
`messages.${this.$route.params.pubkey}`
)
if (messages) {
for (var i = 0; i < messages.length; i++) {
if (messages[i].loading === true) {
messages[i].failed = true
messages[i].loading = false
this.$q.localStorage.set(
`messages.${this.$route.params.pubkey}`,
messages
)
}
}
}
},
async submitMessage() {
await this.$store.dispatch('sendChatMessage', {
pubkey: this.$route.params.pubkey,
@ -194,39 +154,7 @@ export default {
})
this.text = ''
this.$store.commit('chatUpdated')
this.scroll()
setTimeout(() => {
this.$store.commit('chatUpdated') // another hack if post fails
this.failed()
}, 2000)
},
clickMessageAction(ev, id, text) {
ev.preventDefault()
var action = ev.target
for (let i = 0; i < 5; i++) {
if (action.classList.contains('retry')) {
this.$store.dispatch('deleteChatMessage', {
pubkey: this.$route.params.pubkey,
id
})
this.text = text
this.submitMessage()
} else if (action.classList.contains('delete')) {
this.$store.dispatch('deleteChatMessage', {
pubkey: this.$route.params.pubkey,
id
})
this.$store.commit('chatUpdated')
}
action = action.parentNode
}
},
resetMessage() {
this.text = ''
}
}
}

View File

@ -1,116 +0,0 @@
<template>
<q-page>
<center>
<strong class="text-h6 q-pa-lg">Help</strong>
</center>
<br />
<q-btn
v-go-back.single
flat
color="white"
icon="arrow_back"
label="back"
class="small-screen-only fixed-top-left"
/>
<br /><br />
<strong>What is Nostr (Notes and other stuff relays)?</strong><br /><br />
<p>
Nostr is a decentralised collection of relays passing data between
clients. Anyone can run a client or relay. This particular 240char limited
client is just one way to send data through Nostr.
</p>
<center>
<img
class="q-px-auto"
style="width: 100%"
src="https://i.imgur.com/NsnaiBP.png"
/>
<br />
</center>
<p>
Nostr uses public key cryptography. Posts are signed with your private key
and people can follow your posts using your public key. Direct messages in
this client are encrypted before being sent through nostr network.
</p>
<p>
<a
href="https://github.com/fiatjaf/nostr"
target="_blank"
style="color: #26a69a"
>Learn more about the Nostr protocol</a
>
</p>
<p>
<a
href="https://github.com/arcbtc/nostr"
target="_blank"
style="color: #26a69a"
>https://github.com/arcbtc/nostr</a
>
</p>
<center>
<q-btn
v-if="$store.getters.disabled"
dense
flat
class="small-screen-only q-pa-lg"
color="primary"
size="md"
label="Generate or Restore User Account"
@click="dialogGenerate = true"
></q-btn>
</center>
<q-dialog v-model="dialogGenerate" position="top">
<Generate />
</q-dialog>
<q-dialog v-if="$store.getters.disabled" v-model="warningPrompt" persistent>
<q-card style="min-width: 350px">
<q-card-section>
<div class="text-h6">Warning!</div>
</q-card-section>
<q-card-section class="q-pt-none">
<p>
This is buggy and experimental software running for testing purposes
ONLY, any data you put on here will be lost!<br />
</p>
</q-card-section>
<q-card-actions align="right" class="text-primary">
<q-btn flat label="Cancel" v-close-popup />
<q-btn
flat
label="Proceed"
v-close-popup
@click="warningPrompt = false"
/>
</q-card-actions>
</q-card>
</q-dialog>
</q-page>
</template>
<script>
import helpersMixin from '../utils/mixin'
export default {
name: 'PageHelp',
mixins: [helpersMixin],
data() {
return {
dialogGenerate: false,
warningPrompt: false
}
},
created() {
setTimeout(() => {
this.warningPrompt = true
}, 400)
}
}
</script>

View File

@ -1,8 +1,8 @@
<template>
<q-page>
<center>
<div class="text-center">
<strong class="text-h6 q-pa-lg">Encrypted Messages</strong>
</center>
</div>
<q-btn
v-go-back.single
flat
@ -20,7 +20,7 @@
<div class="q-mx-auto q-px-md">
<q-list>
<q-item
v-for="(_, followedKey) in $store.state.theirProfile"
v-for="(_, followedKey) in $store.state.following"
:key="followedKey"
v-ripple
clickable
@ -33,7 +33,7 @@
</q-item-section>
<q-item-section>{{
$store.getters.handle(followedKey)
$store.getters.displayName(followedKey)
}}</q-item-section>
</q-item>
</q-list>

View File

@ -9,7 +9,9 @@
class="small-screen-only fixed-top-left q-ma-xs"
/>
<center><strong class="text-h6 q-ma-sm">Profile</strong></center>
<div class="text-center">
<strong class="text-h6 q-ma-sm">Profile</strong>
</div>
<br />
<br />
@ -81,7 +83,6 @@
</template>
<script>
import 'md-gum-polyfill'
import helpersMixin from '../utils/mixin'
import {pool} from '../global'
@ -100,7 +101,7 @@ export default {
computed: {
isFollowing() {
return this.$route.params.pubkey in this.$store.state.theirProfile
return this.$store.state.following.includes(this.$route.params.pubkey)
}
},
@ -129,12 +130,12 @@ export default {
methods: {
unFollow() {
this.$store.dispatch('stopFollowing', this.$route.params.pubkey)
this.$store.commit('unfollow', this.$route.params.pubkey)
this.$router.push('/')
},
addPubFollow() {
this.$store.dispatch('startFollowing', this.$route.params.pubkey)
this.$store.commit('follow', this.$route.params.pubkey)
}
}
}

View File

@ -73,10 +73,10 @@
/>
<q-input
disable
v-model="imagetemp"
v-model="picture"
filled
type="text"
hint="Profile picture (imgur url)"
hint="Picture URL"
maxlength="150"
/>
<q-btn
@ -97,7 +97,7 @@
<div class="row">
<div class="col-9">
<q-input
v-model="relaya"
v-model="addingRelay"
filled
type="textarea"
autogrow
@ -120,7 +120,7 @@
<div class="row">
<div class="col-9">
<q-select
v-model="relayr"
v-model="removingRelay"
filled
:options="$store.state.myProfile.relays"
hint="Remove a relay"
@ -219,7 +219,7 @@
<q-card-actions align="right" class="text-primary">
<q-btn flat label="Cancel" v-close-popup />
<q-btn flat label="Yes, delete storage" @click="deletels" />
<q-btn flat label="Yes, delete storage" @click="hardReset" />
</q-card-actions>
</q-card>
</q-dialog>
@ -229,44 +229,31 @@
<script>
import helpersMixin from '../utils/mixin'
import {copyToClipboard} from 'quasar'
const bip39 = require('bip39')
export default {
name: 'Settings',
mixins: [helpersMixin],
data() {
const {imagetemp, handle, about} = this.$store.state.myProfile
const {name, picture, about} = this.$store.state.me
return {
privatekey: null,
publickey: null,
deleteAccDialog: false,
privateKeyDialog: false,
imagetemp,
handle,
about,
relayr: '',
relaya: '',
removingRelay: '',
addingRelay: '',
isPrivPwd: true,
isPwd: true,
addPubKey: ''
}
},
methods: {
addPubFollow() {
if (this.addPubKey.trim() !== this.$store.state.myProfile.pubkey) {
this.$store.dispatch('startFollowing', this.addPubKey.trim())
} else {
this.$q.notify({color: 'pink', message: 'You cant follow yourself!'})
}
this.addPubKey = ''
},
setProfile() {
this.$store.dispatch('saveMeta', {
image: this.imagetemp,
handle: this.handle,
about: this.about
this.$store.dispatch('setMetadata', {
name: this.handle,
about: this.about,
picture: this.picture
})
},
privateKey() {
@ -285,15 +272,15 @@ export default {
this.$q.notify({type: 'negative', message: 'FAILED'})
})
},
relayAdd() {
this.$store.dispatch('relayPush', this.relaya)
this.relaya = ''
addRelay() {
this.$store.commit('addRelay', this.addingRelay)
this.addingRelay = ''
},
relayRem() {
this.$store.dispatch('relayRemove', this.relayr)
this.relayr = ''
removeRelay() {
this.$store.commit('removeRelat', this.removingRelay)
this.removingRelay = ''
},
deletels() {
hardReset() {
this.$q.localStorage.clear()
window.location.reload()
}

View File

@ -4,29 +4,24 @@ const routes = [
component: () => import('layouts/MainLayout.vue'),
children: [
{path: '', component: () => import('pages/Home.vue'), name: 'home'},
{
path: '/messages',
component: () => import('pages/Messages.vue'),
name: 'messages'
},
{path: '/chat/:pubkey', component: () => import('pages/Chat.vue')},
{
path: '/user/:pubkey',
component: () => import('pages/Profile.vue')
},
{
path: '/notifications',
component: () => import('pages/Notifications.vue')
},
{
path: '/settings',
component: () => import('pages/Settings.vue'),
name: 'settings'
},
{
path: '/help',
component: () => import('pages/Help.vue'),
name: 'help'
path: '/messages',
component: () => import('pages/Messages.vue'),
name: 'messages'
},
{path: '/messages/:pubkey', component: () => import('pages/Chat.vue')},
{
path: '/notifications',
component: () => import('pages/Notifications.vue')
},
{
path: '/:pubkey',
component: () => import('pages/Profile.vue')
}
]
},

View File

@ -1,17 +1,30 @@
import {getEventHash} from 'nostr-tools'
import {encrypt, decrypt} from 'nostr-tools/nip04'
import {LocalStorage, Notify} from 'quasar'
import 'md-gum-polyfill'
import {pool} from '../global'
export function launch(store) {
pool.setPrivateKey(store.state.myProfile.privkey)
if (!!store.state.keys.pub) {
store.commit('setKey') // passing no arguments will cause a new seed to be generated
}
store.state.myProfile.relays.forEach(relay => {
pool.addRelay(relay)
})
// now we already have a key
if (!!store.state.keys.priv) {
pool.setPrivateKey(store.state.keys.priv)
}
// add default relays
if (Object.keys(store.state.relays).length === 0) {
store.commit('addRelay', 'wss://freedom-relay.herokuapp.com/ws')
store.commit('addRelay', 'wss://relayer.fiatjaf.com')
store.commit('addRelay', 'wss://nostr-relay.freeberty.net')
}
// setup pool
for (let url in store.state.relays) {
pool.addRelay(url)
}
pool.onNotice((notice, relay) => {
Notify.create({
message: `Relay ${relay.url} says: ${notice}`,
@ -28,153 +41,65 @@ export function restartHomeFeed(store) {
homeSubscription = homeSubscription.sub({
filter: [
{
authors: Object.keys(store.state.theirProfile).length
? Object.keys(store.state.theirProfile)
: null
authors: store.state.following.length ? store.state.following : null
},
{
author: store.state.myProfile.pubkey
author: store.state.keys.pub
},
{
'#p': store.state.myProfile.pubkey
'#p': store.state.keys.pub
}
],
cb: (event, relay) => {
switch (event.kind) {
case 0:
store.commit('addKind0', event)
try {
event.metadata = JSON.parse(event.content)
} catch (err) {}
break
case 1:
for (let i = 0; i < store.state.kind1.length; i++) {
if (
(store.state.kind1[i].loading === true ||
store.state.kind1[i].retry === true) &&
store.state.kind1[i].id === event.id
) {
event.retry = false
event.loading = false
store.commit('replaceKind1', {index: i, event})
return
} else if (store.state.kind1[i].id === event.id) {
return
}
}
store.commit('addKind1', event)
break
case 4:
// a direct encrypted message
if (
event.tags.find(
tag => tag[0] === 'p' && tag[1] === store.state.myProfile.pubkey
([tag, value]) => tag === 'p' && value === store.state.keys.pub
)
) {
// it is addressed to us
let lsKey = `messages.${event.pubkey}`
var messages = LocalStorage.getItem(lsKey) || []
if (messages.find(({id}) => id === event.id)) {
// we already have this one, discard
return
}
// decrypt it
let [ciphertext, iv] = event.content.split('?iv=')
let text = decrypt(
store.state.myProfile.privkey,
event.plaintext = decrypt(
store.state.keys.priv,
event.pubkey,
ciphertext,
iv
)
// store it locally push
messages.push({
text,
from: event.pubkey,
id: event.id,
created_at: event.created_at,
tags: event.tags,
loading: false,
retry: false
})
LocalStorage.set(lsKey, messages)
// a hack to update the UI
store.commit('chatUpdated')
} else if (
event.pubkey === store.state.myProfile.pubkey &&
event.tags[0][1] in store.state.theirProfile
) {
} else if (event.pubkey === store.state.keys.pub) {
// it is coming from us
let p = event.tags.find(tag => tag[0] === 'p')
let lsKey = `messages.${p[1]}`
var messagesS = LocalStorage.getItem(lsKey)
if (messagesS.length > 0) {
for (var i = 0; i < messagesS.length; i++) {
if (
messagesS[i].id === event.id &&
messagesS[i].loading === true
) {
messagesS[i].loading = false
LocalStorage.set(lsKey, messagesS)
return
}
}
if (messagesS.find(({id}) => id === event.id)) {
// we already have this one, discard
return
}
// decrypt it
let [ciphertext, iv] = event.content.split('?iv=')
let text = decrypt(
store.state.myProfile.privkey,
p[1],
ciphertext,
iv
)
messagesS.push({
text,
from: event.pubkey,
id: event.id,
created_at: event.created_at,
tags: event.tags,
loading: false,
retry: false
})
LocalStorage.set(lsKey, messagesS)
}
let [_, target] = event.tags.find(([tag]) => tag === 'p')
// decrypt it
let [ciphertext, iv] = event.content.split('?iv=')
event.plaintext = decrypt(
store.state.keys.priv,
target,
ciphertext,
iv
)
}
break
}
store.commit('addEvent', event)
}
})
}
export function relayPush(store, url) {
store.commit('relayPush', url)
pool.addRelay(url, {
read: true,
write: true
})
}
export async function relayRemove(store, url) {
store.commit('relaySplice', url)
pool.removeRelay(url)
}
export async function sendPost(store, {message, tags = [], kind = 1}) {
if (message.length === 0) return
let event = {
pubkey: store.state.myProfile.pubkey,
pubkey: store.state.keys.pub,
created_at: Math.floor(Date.now() / 1000),
kind,
tags,
@ -184,164 +109,41 @@ export async function sendPost(store, {message, tags = [], kind = 1}) {
event.id = await getEventHash(event)
pool.publish(event)
store.commit('addKind1', {
...event,
loading: true,
retry: false
})
store.commit('addEvent', event)
}
export function postAgain(store, event) {
for (let i = 0; i < store.state.kind1.length; i++) {
if (store.state.kind1[i].id === event.id) {
store.commit('replaceKind1', {
index: i,
event: {
...event,
loading: true,
retry: false
}
})
}
}
pool.publish(event)
}
export async function saveMeta(store, {image, handle, about}) {
store.commit('setProfile', {
...store.state.myProfile,
picture: image,
name: handle,
about
})
export async function setMetadata(store, metadata) {
store.commit('setProfile', metadata)
var event = {
pubkey: store.state.myProfile.pubkey,
pubkey: store.state.keys.pub,
created_at: Math.floor(Date.now() / 1000),
kind: 0,
tags: [],
content: JSON.stringify({
name: store.state.myProfile.name,
about: store.state.myProfile.about,
picture: store.state.myProfile.picture
})
content: JSON.stringify(metadata)
}
event.id = await getEventHash(event)
pool.publish(event)
}
export function deletePost(store, postId) {
store.commit('deleteKind1', postId)
}
export function startFollowing(store, key) {
if (key in store.state.theirProfile) {
Notify.create({
message: 'Already following',
color: 'pink'
})
return
}
if (!key.match(/^[0-9a-fA-F]{64}$/)) {
Notify.create({
message:
'Invalid public key. Must be 32 bytes hex-encoded (64 characters).',
color: 'pink'
})
return
}
LocalStorage.set(`messages.${key}`, [])
store.commit('startFollowing', key)
store.dispatch('restartHomeFeed')
}
export async function stopFollowing(store, key) {
if (!(key in store.state.theirProfile)) {
Notify.create({
message: 'No such user',
color: 'pink'
})
return
}
store.commit('stopFollowing', key)
store.dispatch('restartHomeFeed')
}
export function finalGenerate(store, {keystoreoption, publickey, privatekey}) {
var profile = {
pubkey: publickey,
privkey: privatekey,
relays: [
'wss://nostr-relay.herokuapp.com/ws',
'wss://nostr-relay.bigsun.xyz/ws',
'wss://freedom-relay.herokuapp.com/ws'
// 'wss://relay.nostr.org',
// 'wss://nodestr-relay.dolu.dev/ws'
],
avatar: null,
handle: null,
about: null
}
if (keystoreoption === 'external') {
profile.privkey = null
}
store.commit('setProfile', profile)
LocalStorage.set('theirProfile', {})
LocalStorage.set('kind1', [])
store.dispatch('launch')
}
export async function sendChatMessage(store, {pubkey, text}) {
export async function sendChatMessage(store, {pubkey, text, replyTo}) {
if (text.length === 0) return
let [ciphertext, iv] = encrypt(store.state.myProfile.privkey, pubkey, text)
let [ciphertext, iv] = encrypt(store.state.keys.priv, pubkey, text)
// make event
let event = {
pubkey: store.state.myProfile.pubkey,
pubkey: store.state.keys.pub,
created_at: Math.floor(Date.now() / 1000),
kind: 4,
tags: [['p', pubkey]],
content: ciphertext + '?iv=' + iv
}
let lsKey = `messages.${pubkey}`
var messages = LocalStorage.getItem(lsKey) || []
if (messages.length > 0) {
event.tags.push(['e', messages[messages.length - 1].id])
if (replyTo) {
event.tags.push(['e', replyTo])
}
event.id = await getEventHash(event)
let message = {
text,
from: store.state.myProfile.pubkey,
id: event.id,
created_at: event.created_at,
tags: event.tags,
loading: true,
failed: false
}
messages.push(message)
LocalStorage.set(lsKey, messages)
pool.publish(event)
}
export function deleteChatMessage(store, {pubkey, id}) {
let lsKey = `messages.${pubkey}`
var messages = LocalStorage.getItem(lsKey) || []
let index = messages.findIndex(message => message.id === id)
if (index === -1) return
messages.splice(index, 1)
LocalStorage.set(lsKey, messages)
}

View File

@ -1,29 +1,21 @@
import Identicon from 'identicon.js'
export function disabled(state) {
return !state.myProfile
return !state.keys.pub
}
export function handle(state, pubkey) {
export function displayName(state) {
return pubkey => {
let profile = state.theirProfile[pubkey]
if (profile && profile.name) return profile.name
let kind0 = state.kind0[pubkey]
if (kind0 && kind0.name) return profile.name
return pubkey.slice(0, 20) + '...'
let {metadata = {}} = state.events.kind0[pubkey]
if (metadata.name) return metadata.name
return pubkey.slice(0, 3) + '...' + pubkey.slice(-4)
}
}
export function avatar(state) {
return pubkey => {
let profile = state.theirProfile[pubkey]
if (profile && profile.picture) return profile.picture
let kind0 = state.kind0[pubkey]
if (kind0 && kind0.picture) return profile.picture
let {metadata = {}} = state.events.kind0[pubkey]
if (metadata.picture) return metadata.picture
let data = new Identicon(pubkey, 40).toString()
return 'data:image/png;base64,' + data
}

View File

@ -1,61 +1,71 @@
export function setProfile(state, profile) {
state.myProfile = profile
import {LocalStorage} from 'quasar'
import {getPublicKey} from 'nostr-tools'
import bip32 from 'bip32'
import * as bip39 from 'bip39'
export function setKey(state, {seed, priv, pub} = {}) {
if (!seed && !priv && !pub) {
// generate
let randomBytes = crypto.randomBytes(16)
let mnemonic = bip39.entropyToMnemonic(randomBytes.toString('hex'))
seed = bip39.mnemonicToSeedSync(mnemonic)
}
if (seed) {
let root = bip32.fromSeed(seed)
priv = root.privateKey.toString('hex')
}
if (priv) {
pub = getPublicKey(priv)
}
state.keys = {seed, priv, pub}
}
export function relayPush(state, url) {
state.myProfile.relays.push(url)
export function setMetadata(state, {name, picture, about}) {
state.me = {name, picture, about}
}
export function relaySplice(state, url) {
let index = state.myProfile.relays.indexOf(url)
if (index === -1) return
state.myProfile.relays.splice(index, 1)
}
export function startFollowing(state, key) {
// use metadata from kind0 or leave everything blank
state.theirProfile = {
[key]: state.kind0[key] || {name: null, about: null, picture: null},
...state.theirProfile
export function addRelay(state, url) {
state.relays[url] = {
read: true,
write: true
}
}
export function stopFollowing(state, key) {
delete state.theirProfile[key]
export function removeRelay(state, url) {
delete state.relays[url]
}
export function addKind1(state, event) {
state.kind1.unshift(event)
}
export function replaceKind1(state, {index, event}) {
state.kind1 = [
...state.kind1.slice(0, index),
event,
...state.kind1.slice(index + 1)
]
}
export function deleteKind1(state, id) {
console.log(state.kind1)
console.log(id)
let index = state.kind1.findIndex(event => event.id === id)
console.log(index)
if (index !== -1) state.kind1.splice(index, 1)
}
export function addKind0(state, event) {
// increment theirProfile with this or store it temporarily
try {
let {name, about, picture} = JSON.parse(event.content)
if (event.pubkey in state.theirProfile) {
state.theirProfile[event.pubkey] = {name, about, picture}
return
}
state.kind0[event.pubkey] = {name, about, picture}
} catch (err) {
export function follow(state, key) {
if (state.following.includes(key)) {
return
}
state.following.push(key)
}
export function unfollow(state, key) {
delete state.following[key]
}
export function addEvent(state, event) {
switch (event.kind) {
case 0:
state.events.kind0[event.pubkey] = event
break
case 1:
if (state.events.kind1.find(e => e.id === event.id)) return
state.events.kind1.push(event)
break
case 4:
let peerTag = event.tags.find(([tag]) => tag === 'p')
if (!peerTag) return
let peer = event.pubkey === state.key.pub ? peerTag[1] : event.pubkey
if (state.events.kind4[peer].find(e => e.id === event.id)) return
state.events.kind4[peer].push(event)
break
}
}
export function chatUpdated(state) {

View File

@ -2,11 +2,15 @@ import {LocalStorage} from 'quasar'
export default function () {
return {
myProfile: LocalStorage.getItem('myProfile'),
theirProfile: LocalStorage.getItem('theirProfile') || {},
kind0: {}, // temporary, will be merged with theirProfile or erased at the end
kind1: LocalStorage.getItem('kind1') || [],
me: LocalStorage.getItem('me') || {}, // { name, picture, about, ... }
keys: LocalStorage.getItem('keys') || {}, // { seed, priv, pub }
relays: LocalStorage.getItem('relays') || {}, // { [url]: {} }
following: LocalStorage.getItem('following') || [], // [ pubkeys... ]
events: {
kind0: LocalStorage.getItem('events.kind0') || {}, // { [pubkey]: event }
kind1: LocalStorage.getItem('events.kind1') || [], // [ events... ]
kind4: LocalStorage.getItem('events.kind4') || {} // { [pubkey]: [events...] }
},
chatUpdated: 1 // hack
}

View File

@ -4,20 +4,19 @@ export default function (store) {
store.subscribe(({type, payload}, state) => {
switch (type) {
case 'setProfile':
case 'relayPush':
case 'relaySplice':
console.log('storing', state.myProfile)
LocalStorage.set('myProfile', state.myProfile)
LocalStorage.set('me', state.me)
case 'addRelay':
case 'removeRelay':
LocalStorage.set('relays', state.relays)
break
case 'startFollowing':
case 'stopFollowing':
case 'addKind0':
LocalStorage.set('theirProfile', state.theirProfile)
break
case 'addKind1':
case 'replaceKind1':
case 'deleteKind1':
LocalStorage.set('kind1', state.kind1)
case 'follow':
case 'unfollow':
LocalStorage.set('following', state.following)
case 'addEvent':
LocalStorage.set(
`events.${payload.kind}`,
state.events[`kind${payload.kind}`]
)
break
}
})

View File

@ -11,7 +11,7 @@ export default {
},
methods: {
toProfile(pubkey) {
this.$router.push('/user/' + pubkey)
this.$router.push('/' + pubkey)
},
niceDate(value) {

View File

@ -992,6 +992,11 @@
resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.3.tgz#0300943770e04231041a51bd39f0439b5c7ab4f0"
integrity sha512-nkalE/f1RvRGChwBnEIoBfSEYOXnCRdleKuv6+lePbMDrMZXeDQnqak5XDOeBgrPPyPfAdcCu/B5z+v3VhplGg==
"@noble/secp256k1@^1.3.0":
version "1.3.0"
resolved "https://registry.yarnpkg.com/@noble/secp256k1/-/secp256k1-1.3.0.tgz#426880cf0355b24d81c129af1ec31dfa6eee8b9c"
integrity sha512-wuFthUc6Ul4xflhY5u1+p1bDILPzVEisekxt5M+iLg4R+gG6+k2jnRR19sC9fMtzlsN5sKloBwprziDS0XlmyQ==
"@nodelib/fs.scandir@2.1.5":
version "2.1.5"
resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
@ -1270,6 +1275,11 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.12.tgz#ac7fb693ac587ee182c3780c26eb65546a1a3c10"
integrity sha512-+2Iggwg7PxoO5Kyhvsq9VarmPbIelXP070HMImEpbtGCoyWNINQj4wzjbQCXzdHTRXnqufutJb5KAURZANNBAw==
"@types/node@10.12.18":
version "10.12.18"
resolved "https://registry.yarnpkg.com/@types/node/-/node-10.12.18.tgz#1d3ca764718915584fcd9f6344621b7672665c67"
integrity sha512-fh+pAqt4xRzPfqA6eh3Z2y6fyZavRIumvjhaCL753+TVkGKGhpPeyrJG2JftD0T9q4GF00KjefsQ+PQNDdWQaQ==
"@types/node@11.11.6":
version "11.11.6"
resolved "https://registry.yarnpkg.com/@types/node/-/node-11.11.6.tgz#df929d1bb2eee5afdda598a41930fe50b43eaa6a"
@ -1850,6 +1860,13 @@ balanced-match@^1.0.0:
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
base-x@^3.0.2:
version "3.0.9"
resolved "https://registry.yarnpkg.com/base-x/-/base-x-3.0.9.tgz#6349aaabb58526332de9f60995e548a53fe21320"
integrity sha512-H7JU6iBHTal1gp56aKoaa//YUxEaAOUiydvrV/pILqIHXTtqxSkATOnDA2u+jZ/61sD+L/412+7kzXRtWukhpQ==
dependencies:
safe-buffer "^5.0.1"
base64-js@^1.3.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
@ -1870,6 +1887,18 @@ binary-extensions@^2.0.0:
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
bip32@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/bip32/-/bip32-3.0.1.tgz#1d1121469cce6e910e0ec3a5a1990dd62687e2a3"
integrity sha512-Uhpp9aEx3iyiO7CpbNGFxv9WcMIVdGoHG04doQ5Ln0u60uwDah7jUSc3QMV/fSZGm/Oo01/OeAmYevXV+Gz5jQ==
dependencies:
"@types/node" "10.12.18"
bs58check "^2.1.1"
create-hash "^1.2.0"
create-hmac "^1.1.7"
typeforce "^1.11.5"
wif "^2.0.6"
bip39@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/bip39/-/bip39-3.0.4.tgz#5b11fed966840b5e1b8539f0f54ab6392969b2a0"
@ -1948,6 +1977,22 @@ browserslist@^4.0.0, browserslist@^4.12.0, browserslist@^4.14.5, browserslist@^4
node-releases "^2.0.1"
picocolors "^1.0.0"
bs58@^4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/bs58/-/bs58-4.0.1.tgz#be161e76c354f6f788ae4071f63f34e8c4f0a42a"
integrity sha1-vhYedsNU9veIrkBx9j806MTwpCo=
dependencies:
base-x "^3.0.2"
bs58check@<3.0.0, bs58check@^2.1.1:
version "2.1.2"
resolved "https://registry.yarnpkg.com/bs58check/-/bs58check-2.1.2.tgz#53b018291228d82a5aa08e7d796fdafda54aebfc"
integrity sha512-0TS1jicxdU09dwJMNZtVAfzPi6Q6QeN0pM1Fkzrjn+XYHvzMKPU3pHVpva+769iNVSfIYWf7LJ6WR+BuuMf8cA==
dependencies:
bs58 "^4.0.0"
create-hash "^1.1.0"
safe-buffer "^5.1.2"
buffer-crc32@^0.2.1, buffer-crc32@^0.2.13:
version "0.2.13"
resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242"
@ -2344,7 +2389,7 @@ crc32-stream@^4.0.2:
crc-32 "^1.2.0"
readable-stream "^3.4.0"
create-hash@^1.1.0, create-hash@^1.1.2:
create-hash@^1.1.0, create-hash@^1.1.2, create-hash@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.2.0.tgz#889078af11a63756bcfb59bd221996be3a9ef196"
integrity sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==
@ -2355,7 +2400,7 @@ create-hash@^1.1.0, create-hash@^1.1.2:
ripemd160 "^2.0.1"
sha.js "^2.4.0"
create-hmac@^1.1.4:
create-hmac@^1.1.4, create-hmac@^1.1.7:
version "1.1.7"
resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.7.tgz#69170c78b3ab957147b2b8b04572e47ead2243ff"
integrity sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==
@ -4168,11 +4213,6 @@ make-dir@^3.0.2, make-dir@^3.1.0:
dependencies:
semver "^6.0.0"
md-gum-polyfill@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/md-gum-polyfill/-/md-gum-polyfill-1.0.0.tgz#829a512d25ed0318c0c49a961048b505f670a2db"
integrity sha1-gppRLSXtAxjAxJqWEEi1BfZwots=
md5.js@^1.3.4:
version "1.3.5"
resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f"
@ -4373,11 +4413,6 @@ no-case@^3.0.4:
lower-case "^2.0.2"
tslib "^2.0.3"
noble-secp256k1@^1.1.1:
version "1.2.14"
resolved "https://registry.yarnpkg.com/noble-secp256k1/-/noble-secp256k1-1.2.14.tgz#39429c941d51211ca40161569cee03e61d72599e"
integrity sha512-GSCXyoZBUaaPwVWdYncMEmzlSUjF9J/YeEHpklYJCyg8wPuJP3NzDx0BkiwArzINkdX2HJHvUJhL6vVWPOQQcg==
node-forge@^0.10.0:
version "0.10.0"
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.10.0.tgz#32dea2afb3e9926f02ee5ce8794902691a676bf3"
@ -4415,14 +4450,14 @@ normalize-url@^6.0.1:
resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-6.1.0.tgz#40d0885b535deffe3f3147bec877d05fe4c5668a"
integrity sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==
nostr-tools@^0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/nostr-tools/-/nostr-tools-0.5.0.tgz#cb641ff21d035169590cc7185eca6aed6a9a101c"
integrity sha512-JxLOP8psDx3Ye08ar8pbY7u08zJUIo9sS3CqpPqfvgV86Blv9oImKP/GfODP0H3MbfOKk4zdYQt6+qZie0k8uA==
nostr-tools@^0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/nostr-tools/-/nostr-tools-0.6.0.tgz#03f6ac3bd72ed7ead9b2472b655228ebc011bb92"
integrity sha512-XBi+tuRQo9prL8n1Wxu3/O1NYGc4tQaIExaLfebj3eg0um8xDnKzgpmbRL+dTXqaZmWKnTtTAbZvea2uluXIPw==
dependencies:
"@noble/secp256k1" "^1.3.0"
buffer "^6.0.3"
dns-packet "^5.2.4"
noble-secp256k1 "^1.1.1"
websocket-polyfill "^0.0.3"
npm-run-path@^4.0.1:
@ -5920,6 +5955,11 @@ typedarray-to-buffer@^3.1.5:
dependencies:
is-typedarray "^1.0.0"
typeforce@^1.11.5:
version "1.18.0"
resolved "https://registry.yarnpkg.com/typeforce/-/typeforce-1.18.0.tgz#d7416a2c5845e085034d70fcc5b6cc4a90edbfdc"
integrity sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g==
typescript@4.4.2:
version "4.4.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.4.2.tgz#6d618640d430e3569a1dfb44f7d7e600ced3ee86"
@ -6268,6 +6308,13 @@ which@^2.0.1:
dependencies:
isexe "^2.0.0"
wif@^2.0.6:
version "2.0.6"
resolved "https://registry.yarnpkg.com/wif/-/wif-2.0.6.tgz#08d3f52056c66679299726fade0d432ae74b4704"
integrity sha1-CNP1IFbGZnkplyb63g1DKudLRwQ=
dependencies:
bs58check "<3.0.0"
wildcard@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.0.tgz#a77d20e5200c6faaac979e4b3aadc7b3dd7f8fec"