From 3338075c6c438e96099687d57ff5c0fcaaf91fe2 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Thu, 21 Nov 2024 16:16:16 +0100 Subject: [PATCH] payment requests with seed phrase --- src/components/ReceiveTokenDialog.vue | 76 +++------------------- src/components/SendPaymentRequest.vue | 70 ++++++++++++++++----- src/components/SettingsView.vue | 24 +++++-- src/stores/mints.ts | 8 +++ src/stores/nostr.ts | 91 ++++++++++++++++++--------- src/stores/payment-request.ts | 13 ++-- src/stores/receiveTokensStore.js | 63 ++++++++++++++++++- src/stores/wallet.ts | 6 +- 8 files changed, 225 insertions(+), 126 deletions(-) diff --git a/src/components/ReceiveTokenDialog.vue b/src/components/ReceiveTokenDialog.vue index 3e773979..c1982112 100644 --- a/src/components/ReceiveTokenDialog.vue +++ b/src/components/ReceiveTokenDialog.vue @@ -208,64 +208,12 @@ export default defineComponent({ "showLastKey", ]), ...mapActions(useMintsStore, ["addMint"]), - knowThisMintOfTokenJson: function (tokenJson) { - const mintStore = useMintsStore(); - return mintStore.mints - .map((m) => m.url) - .includes(token.getMint(tokenJson)); - }, - receiveToken: async function (encodedToken) { - const mintStore = useMintsStore(); - const receiveStore = useReceiveTokensStore(); - const uIStore = useUiStore(); - receiveStore.showReceiveTokens = false; - console.log("### receive tokens", receiveStore.receiveData.tokensBase64); - - if (receiveStore.receiveData.tokensBase64.length == 0) { - throw new Error("no tokens provided."); - } - - // get the private key for the token we want to receive if it is locked with P2PK - receiveStore.receiveData.p2pkPrivateKey = - this.getPrivateKeyForP2PKEncodedToken( - receiveStore.receiveData.tokensBase64 - ); - - const tokenJson = token.decode(receiveStore.receiveData.tokensBase64); - if (tokenJson == undefined) { - throw new Error("no tokens provided."); - } - // check if we have all mints - if (!this.knowThisMintOfTokenJson(tokenJson)) { - // // pop up add mint dialog warning - // // hack! The "add mint" component is in SettingsView which may now - // // have been loaded yet. We switch the tab to settings to make sure - // // that it loads. Remove this code when the TrustMintComnent is refactored! - // uIStore.setTab("mints"); - // // hide the receive dialog - // receiveStore.showReceiveTokens = false; - // // set the mint to add - // this.addMintData = { url: token.getMint(tokenJson) }; - // // show the add mint dialog - // this.showAddMintDialog = true; - // // show the token receive dialog again for the next attempt - // receiveStore.showReceiveTokens = true; - // return; - - // add the mint - await this.addMint({ url: token.getMint(tokenJson) }); - } - // redeem the token - await this.redeem(receiveStore.receiveData.tokensBase64); - }, + ...mapActions(useReceiveTokensStore, [ + "receiveIfDecodes", + "decodeToken", + "knowThisMintOfTokenJson", + ]), // TOKEN METHODS - decodeToken: function (encoded_token) { - let decodedToken = undefined; - try { - decodedToken = token.decode(encoded_token); - } catch (error) {} - return decodedToken; - }, getProofs: function (decoded_token) { return token.getProofs(decoded_token); }, @@ -286,16 +234,6 @@ export default defineComponent({ prStore.newPaymentRequest(); } }, - receiveIfDecodes: function () { - try { - const decodedToken = this.decodeToken(this.receiveData.tokensBase64); - if (decodedToken) { - this.receiveToken(this.receiveData.tokensBase64); - } - } catch (error) { - console.error(error); - } - }, tokenAlreadyInHistory: function (tokenStr) { const tokensStore = useTokensStore(); return ( @@ -310,7 +248,7 @@ export default defineComponent({ return; } const tokensStore = useTokensStore(); - const decodedToken = this.decodeToken(token); + const decodedToken = tokensStore.decodeToken(token); // get amount from decodedToken.token.proofs[..].amount const amount = this.getProofs(decodedToken).reduce( (sum, el) => (sum += el.amount), @@ -323,7 +261,7 @@ export default defineComponent({ }); this.showReceiveTokens = false; // show success notification - this.notifySuccess("Ecash added to history."); + this.notifySuccess("Incoming payment added to history."); }, pasteToParseDialog: function () { console.log("pasteToParseDialog"); diff --git a/src/components/SendPaymentRequest.vue b/src/components/SendPaymentRequest.vue index c0273ba4..8dc6178b 100644 --- a/src/components/SendPaymentRequest.vue +++ b/src/components/SendPaymentRequest.vue @@ -1,15 +1,32 @@ + diff --git a/src/components/SettingsView.vue b/src/components/SettingsView.vue index ac2ca269..273600d0 100644 --- a/src/components/SettingsView.vue +++ b/src/components/SettingsView.vue @@ -217,7 +217,6 @@ -
@@ -225,8 +224,9 @@ >Payment requests Payment requests allow you to receive payments via - nostr.Payment requests allow you to receive payments via nostr. If you + enable this, your wallet will subscribe to your nostr + relays. @@ -238,6 +238,19 @@ />
+
+ + + If enabled, the wallet will automatically receive incoming payments. + + +
@@ -1083,7 +1096,10 @@ export default defineComponent({ "showRemoveMintDialog", ]), ...mapWritableState(useNWCStore, ["nwcEnabled", "connections", "relays"]), - ...mapWritableState(usePRStore, ["enablePaymentRequest"]), + ...mapWritableState(usePRStore, [ + "enablePaymentRequest", + "receivePaymentRequestsAutomatically", + ]), keysetCountersByMint() { const mints = this.mints; const keysetCountersByMint = {}; // {mintUrl: [keysetCounter: {id: string, count: number}, ...]} diff --git a/src/stores/mints.ts b/src/stores/mints.ts index 8d9a03f9..5f97beb6 100644 --- a/src/stores/mints.ts +++ b/src/stores/mints.ts @@ -179,6 +179,14 @@ export const useMintsStore = defineStore("mints", { units[(units.indexOf(this.activeUnit) + 1) % units.length]; return this.activeUnit; }, + selectUnit: function (unit: string) { + const units = this.activeMint().units; + if (units.includes(unit)) { + this.activeUnit = unit; + } else { + notifyError(`Unit ${unit} not supported by mint`, "Unit selection failed"); + } + }, proofsToWalletProofs(proofs: Proof[], quote?: string): WalletProof[] { return proofs.map((p) => { return { diff --git a/src/stores/nostr.ts b/src/stores/nostr.ts index b76d1eed..857ba77f 100644 --- a/src/stores/nostr.ts +++ b/src/stores/nostr.ts @@ -44,6 +44,7 @@ export const useNostrStore = defineStore("nostr", { nip46signer: {} as NDKNip46Signer, privateKeySignerPrivateKey: useLocalStorage("cashu.ndk.privateKeySignerPrivateKey", ""), seedSignerPrivateKey: useLocalStorage("cashu.ndk.seedSignerPrivateKey", ""), + seedSignerPublicKey: useLocalStorage("cashu.ndk.seedSignerPublicKey", ""), seedSignerPrivateKeyNsec: "", privateKeySigner: {} as NDKPrivateKeySigner, signer: {} as NDKSigner, @@ -64,6 +65,13 @@ export const useNostrStore = defineStore("nostr", { }; return nip19.nprofileEncode(profile); }, + seedSignerNprofile: (state) => { + const profile: ProfilePointer = { + pubkey: state.seedSignerPublicKey, + relays: state.relays, + }; + return nip19.nprofileEncode(profile); + }, }, actions: { initNdkReadOnly: function () { @@ -183,16 +191,20 @@ export const useNostrStore = defineStore("nostr", { this.privateKeySignerPrivateKey = ""; await this.initWalletSeedPrivateKeySigner(); }, - initWalletSeedPrivateKeySigner: async function () { + walletSeedGenerateKeyPair: async function () { const walletStore = useWalletStore(); const sk = walletStore.seed.slice(0, 32) const walletPublicKeyHex = getPublicKey(sk) // `pk` is a hex string const walletPrivateKeyHex = bytesToHex(sk) this.seedSignerPrivateKey = walletPrivateKeyHex; - this.privateKeySigner = new NDKPrivateKeySigner(walletPrivateKeyHex) + this.seedSignerPublicKey = walletPublicKeyHex; + }, + initWalletSeedPrivateKeySigner: async function () { + await this.walletSeedGenerateKeyPair(); + this.privateKeySigner = new NDKPrivateKeySigner(this.seedSignerPrivateKey) this.signerType = SignerType.SEED; this.setSigner(this.privateKeySigner); - this.setPubkey(walletPublicKeyHex); + this.setPubkey(this.seedSignerPublicKey); }, fetchEventsFromUser: async function () { const filter: NDKFilter = { kinds: [1], authors: [this.pubkey] }; @@ -222,10 +234,10 @@ export const useNostrStore = defineStore("nostr", { return mintUrlsCounted; }, sendNip04DirectMessage: async function (recipient: string, message: string) { - // const randomPrivateKey = generateSecretKey(); - // const randomPublicKey = getPublicKey(randomPrivateKey); - const randomPrivateKey = hexToBytes(this.seedSignerPrivateKey); - const randomPublicKey = this.pubkey; + const randomPrivateKey = generateSecretKey(); + const randomPublicKey = getPublicKey(randomPrivateKey); + // const randomPrivateKey = hexToBytes(this.seedSignerPrivateKey); + // const randomPublicKey = this.pubkey; const ndk = new NDK({ explicitRelayUrls: this.relays, signer: new NDKPrivateKeySigner(bytesToHex(randomPrivateKey)) }); const event = new NDKEvent(ndk); ndk.connect(); @@ -242,17 +254,19 @@ export const useNostrStore = defineStore("nostr", { } }, subscribeToNip04DirectMessages: async function () { + await this.walletSeedGenerateKeyPair(); + await this.initNdkReadOnly(); let nip04DirectMessageEvents: Set = new Set(); const fetchEventsPromise = new Promise>(resolve => { if (!this.lastEventTimestamp) { this.lastEventTimestamp = Math.floor(Date.now() / 1000); } - console.log(`### Subscribing to NIP-04 direct messages to ${this.pubkey} since ${this.lastEventTimestamp}`); + console.log(`### Subscribing to NIP-04 direct messages to ${this.seedSignerPublicKey} since ${this.lastEventTimestamp}`); this.ndk.connect(); const sub = this.ndk.subscribe( { kinds: [NDKKind.EncryptedDirectMessage], - "#p": [this.pubkey], + "#p": [this.seedSignerPublicKey], since: this.lastEventTimestamp, } as NDKFilter, { closeOnEose: false, groupable: false }, @@ -293,7 +307,7 @@ export const useNostrStore = defineStore("nostr", { dmEvent.content = message; dmEvent.tags = [['p', recipient]]; dmEvent.created_at = Math.floor(Date.now() / 1000); - dmEvent.pubkey = this.pubkey; + dmEvent.pubkey = this.seedSignerPublicKey; dmEvent.id = dmEvent.getEventHash(); const dmEventString = JSON.stringify(await dmEvent.toNostrEvent()); @@ -301,7 +315,7 @@ export const useNostrStore = defineStore("nostr", { sealEvent.kind = 13; sealEvent.content = nip44.v2.encrypt(dmEventString, nip44.v2.utils.getConversationKey(this.seedSignerPrivateKey, recipient)); sealEvent.created_at = this.randomTimeUpTo2DaysInThePast(); - sealEvent.pubkey = this.pubkey; + sealEvent.pubkey = this.seedSignerPublicKey; sealEvent.id = sealEvent.getEventHash(); sealEvent.sig = await sealEvent.sign(); const sealEventString = JSON.stringify(await sealEvent.toNostrEvent()); @@ -318,25 +332,26 @@ export const useNostrStore = defineStore("nostr", { try { ndk.connect(); await wrapEvent.publish(); - notifySuccess("NIP-17 event published"); } catch (e) { console.error(e); notifyError("Could not publish NIP-17 event"); } }, subscribeToNip17DirectMessages: async function () { + await this.walletSeedGenerateKeyPair(); + await this.initNdkReadOnly(); let nip17DirectMessageEvents: Set = new Set(); const fetchEventsPromise = new Promise>(resolve => { if (!this.lastEventTimestamp) { this.lastEventTimestamp = Math.floor(Date.now() / 1000); } const since = this.lastEventTimestamp - 172800; // last 2 days - console.log(`### Subscribing to NIP-17 direct messages to ${this.pubkey} since ${since}`); + console.log(`### Subscribing to NIP-17 direct messages to ${this.seedSignerPublicKey} since ${since}`); this.ndk.connect(); const sub = this.ndk.subscribe( { kinds: [1059 as NDKKind], - "#p": [this.pubkey], + "#p": [this.seedSignerPublicKey], since: since, } as NDKFilter, { closeOnEose: false, groupable: false }, @@ -345,19 +360,29 @@ export const useNostrStore = defineStore("nostr", { sub.on('event', (wrapEvent: NDKEvent) => { const eventLog = { id: wrapEvent.id, created_at: wrapEvent.created_at } as NostrEventLog; if (this.nip17EventIdsWeHaveSeen.find((e) => e.id === wrapEvent.id)) { - console.log(`### Already seen NIP-17 event ${wrapEvent.id}`); + // console.log(`### Already seen NIP-17 event ${wrapEvent.id} (time: ${wrapEvent.created_at})`); return; } else { console.log(`### New event ${wrapEvent.id}`); this.nip17EventIdsWeHaveSeen.push(eventLog); - // remove all events older than 4 days to keep the list small - this.nip17EventIdsWeHaveSeen = this.nip17EventIdsWeHaveSeen.filter((e) => e.created_at > Math.floor(Date.now() / 1000) - 345600); + // remove all events older than 10 days to keep the list small + const fourDaysAgo = Math.floor(Date.now() / 1000) - 10 * 24 * 60 * 60; + this.nip17EventIdsWeHaveSeen = this.nip17EventIdsWeHaveSeen.filter((e) => e.created_at > fourDaysAgo); + } + let dmEvent: NDKEvent; + let content: string; + try { + const wappedContent = nip44.v2.decrypt(wrapEvent.content, nip44.v2.utils.getConversationKey(this.seedSignerPrivateKey, wrapEvent.pubkey)) + const sealEvent = JSON.parse(wappedContent) as NostrEvent; + const dmEventString = nip44.v2.decrypt(sealEvent.content, nip44.v2.utils.getConversationKey(this.seedSignerPrivateKey, sealEvent.pubkey)); + dmEvent = JSON.parse(dmEventString) as NDKEvent; + content = dmEvent.content; + console.log("### NIP-17 DM from", dmEvent.pubkey); + console.log("Content:", content); + } catch (e) { + console.error(e); + return; } - const wappedContent = nip44.v2.decrypt(wrapEvent.content, nip44.v2.utils.getConversationKey(this.seedSignerPrivateKey, wrapEvent.pubkey)) - const sealEvent = JSON.parse(wappedContent) as NostrEvent; - const dmEventString = nip44.v2.decrypt(sealEvent.content, nip44.v2.utils.getConversationKey(this.seedSignerPrivateKey, sealEvent.pubkey)); - const dmEvent = JSON.parse(dmEventString) as NDKEvent; - const content = dmEvent.content; nip17DirectMessageEvents.add(dmEvent) this.lastEventTimestamp = Math.floor(Date.now() / 1000); this.parseMessageForEcash(content); @@ -391,15 +416,23 @@ export const useNostrStore = defineStore("nostr", { console.log("### incoming token already in history"); return; } - await this.addPendingTokenToHistory(tokenStr); + await this.addPendingTokenToHistory(tokenStr, false); receiveStore.receiveData.tokensBase64 = tokenStr; const sendTokensStore = useSendTokensStore(); sendTokensStore.showSendTokens = false; const prStore = usePRStore(); - prStore.showPRDialog = false; - - receiveStore.showReceiveTokens = true; + if (prStore.receivePaymentRequestsAutomatically) { + const success = receiveStore.receiveIfDecodes(); + if (success) { + prStore.showPRDialog = false; + } else { + notifyWarning("Could not receive incoming payment"); + } + } else { + prStore.showPRDialog = false; + receiveStore.showReceiveTokens = true; + } return } } catch (e) { @@ -423,7 +456,7 @@ export const useNostrStore = defineStore("nostr", { const tokensStore = useTokensStore(); return tokensStore.historyTokens.find((t) => t.token === tokenStr); }, - addPendingTokenToHistory: function (tokenStr: string) { + addPendingTokenToHistory: function (tokenStr: string, verbose = true) { const receiveStore = useReceiveTokensStore(); if (this.tokenAlreadyInHistory(tokenStr)) { notifySuccess("Ecash already in history"); @@ -449,7 +482,9 @@ export const useNostrStore = defineStore("nostr", { }); receiveStore.showReceiveTokens = false; // show success notification - notifySuccess("Ecash added to history."); + if (verbose) { + notifySuccess("Ecash added to history."); + } }, }, }); diff --git a/src/stores/payment-request.ts b/src/stores/payment-request.ts index d3097d92..d3b4d3ad 100644 --- a/src/stores/payment-request.ts +++ b/src/stores/payment-request.ts @@ -15,6 +15,7 @@ export const usePRStore = defineStore("payment-request", { showPRDialog: false, showPRKData: "" as string, enablePaymentRequest: useLocalStorage("cashu.pr.enable", false), + receivePaymentRequestsAutomatically: useLocalStorage("cashu.pr.receive", false), }), getters: { }, @@ -23,18 +24,17 @@ export const usePRStore = defineStore("payment-request", { const walletStore = useWalletStore(); this.showPRKData = walletStore.createPaymentRequest(amount, memo); }, - decodePaymentRequest(pr: string) { + async decodePaymentRequest(pr: string) { console.log("decodePaymentRequest", pr); const request: PaymentRequest = decodePaymentRequest(pr) console.log("decodePaymentRequest", request); + const mintsStore = useMintsStore(); // activate the mint in the payment request if (request.mints && request.mints.length > 0) { - const walletStore = useWalletStore(); - const mintsStore = useMintsStore(); let foundMint = false; for (const mint of request.mints) { if (mintsStore.mints.find((m) => m.url == mint)) { - mintsStore.activateMintUrl(mint); + await mintsStore.activateMintUrl(mint); foundMint = true; break; } @@ -45,6 +45,11 @@ export const usePRStore = defineStore("payment-request", { } } + // activate the unit in the payment request + if (request.unit) { + mintsStore.activateUnit(request.unit); + } + const sendTokenStore = useSendTokensStore(); if (!sendTokenStore.showSendTokens) { // if the sendtokendialog is not currently open, clear all data and then show the send dialog diff --git a/src/stores/receiveTokensStore.js b/src/stores/receiveTokensStore.js index 10cc3d7a..a8673674 100644 --- a/src/stores/receiveTokensStore.js +++ b/src/stores/receiveTokensStore.js @@ -1,4 +1,9 @@ import { defineStore } from "pinia"; +import { useMintsStore } from "./mints"; +import { useUiStore } from "./ui"; +import { useP2PKStore } from "./p2pk"; +import { useWalletStore } from "./wallet"; +import token from "src/js/token"; export const useReceiveTokensStore = defineStore("receiveTokensStore", { state: () => ({ @@ -8,5 +13,61 @@ export const useReceiveTokensStore = defineStore("receiveTokensStore", { p2pkPrivateKey: "", }, }), - actions: {}, + actions: { + decodeToken: function (encoded_token) { + let decodedToken = undefined; + try { + decodedToken = token.decode(encoded_token); + } catch (error) {} + return decodedToken; + }, + knowThisMintOfTokenJson: function (tokenJson) { + const mintStore = useMintsStore(); + return mintStore.mints + .map((m) => m.url) + .includes(token.getMint(tokenJson)); + }, + receiveToken: async function (encodedToken) { + const mintStore = useMintsStore(); + const walletStore = useWalletStore(); + const receiveStore = useReceiveTokensStore(); + const uIStore = useUiStore(); + receiveStore.showReceiveTokens = false; + console.log("### receive tokens", receiveStore.receiveData.tokensBase64); + + if (receiveStore.receiveData.tokensBase64.length == 0) { + throw new Error("no tokens provided."); + } + + // get the private key for the token we want to receive if it is locked with P2PK + receiveStore.receiveData.p2pkPrivateKey = + useP2PKStore().getPrivateKeyForP2PKEncodedToken( + receiveStore.receiveData.tokensBase64 + ); + + const tokenJson = token.decode(receiveStore.receiveData.tokensBase64); + if (tokenJson == undefined) { + throw new Error("no tokens provided."); + } + // check if we have all mints + if (!this.knowThisMintOfTokenJson(tokenJson)) { + // add the mint + await mintStore.addMint({ url: token.getMint(tokenJson) }); + } + // redeem the token + await walletStore.redeem(receiveStore.receiveData.tokensBase64); + }, + receiveIfDecodes: function () { + try { + const decodedToken = this.decodeToken(this.receiveData.tokensBase64); + if (decodedToken) { + this.receiveToken(this.receiveData.tokensBase64); + return true + } + } catch (error) { + console.error(error); + return false + } + }, + }, }); diff --git a/src/stores/wallet.ts b/src/stores/wallet.ts index 65744e33..3b1406ac 100644 --- a/src/stores/wallet.ts +++ b/src/stores/wallet.ts @@ -1059,9 +1059,9 @@ export const useWalletStore = defineStore("wallet", { sendTokenStore.showSendTokens = true sendTokenStore.showLockInput = true }, - handlePaymentRequest: function (req: string) { + handlePaymentRequest: async function (req: string) { const prStore = usePRStore() - prStore.decodePaymentRequest(req) + await prStore.decodePaymentRequest(req) }, decodeRequest: async function (req: string) { const p2pkStore = useP2PKStore() @@ -1222,7 +1222,7 @@ export const useWalletStore = defineStore("wallet", { const tags = [["n", "17"]]; const transport = [{ type: PaymentRequestTransportType.NOSTR, - target: nostrStore.nprofile, + target: nostrStore.seedSignerNprofile, tags: tags, }] as PaymentRequestTransport[]; const uuid = uuidv4().split("-")[0];