From 7bba4a4cef9e8bf91feb3877e84f7db21c44a665 Mon Sep 17 00:00:00 2001 From: Pavol Rusnak Date: Mon, 14 Oct 2024 21:39:39 +0200 Subject: [PATCH 1/3] call npm run format, add format github action --- .github/workflows/format.yaml | 22 +++++++++++++++++++++ README.md | 2 ++ docker-compose.yaml | 4 ++-- extension/embedder.html | 2 +- index.html | 2 +- src-capacitor/android/app/debug/output.json | 19 +++++++++++++++++- src-pwa/custom-service-worker.js | 4 ++-- src/boot/base.js | 4 ++-- src/components/BalanceView.vue | 16 +++++++++------ src/components/InvoiceDetailDialog.vue | 2 +- src/components/MintSettings.vue | 2 +- src/components/PayInvoiceDialog.vue | 6 +++--- src/components/QrcodeReader.vue | 2 +- src/components/ReceiveTokenDialog.vue | 4 ++-- src/components/SendTokenDialog.vue | 8 ++++---- src/components/SettingsView.vue | 2 +- src/components/TokenInformation.vue | 4 ++-- src/css/base.scss | 8 ++++++-- src/pages/WalletPage.vue | 8 ++++---- src/router/index.js | 4 ++-- 20 files changed, 87 insertions(+), 38 deletions(-) create mode 100644 .github/workflows/format.yaml diff --git a/.github/workflows/format.yaml b/.github/workflows/format.yaml new file mode 100644 index 00000000..dff334ba --- /dev/null +++ b/.github/workflows/format.yaml @@ -0,0 +1,22 @@ +name: Check format + +on: [push, pull_request] + +jobs: + format-check: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: Install dependencies + run: npm install + + - name: Run format check + run: npm run format diff --git a/README.md b/README.md index f40c8676..00570169 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,11 @@ Cashu Wallet ## One-liner build & run + ``` docker-compose up -d ``` + access at http://localhost:3000 or serve it behind a reverse proxy. ## Install the dependencies diff --git a/docker-compose.yaml b/docker-compose.yaml index 10990a9e..a92cc673 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,4 +1,4 @@ -version: '2' +version: "2" services: cashu.me: image: cashu.me @@ -6,4 +6,4 @@ services: container_name: cashu.me restart: always ports: - - "127.0.0.1:3000:80" + - "127.0.0.1:3000:80" diff --git a/extension/embedder.html b/extension/embedder.html index 54dcd14b..fc32d1e1 100644 --- a/extension/embedder.html +++ b/extension/embedder.html @@ -1,4 +1,4 @@ - + diff --git a/index.html b/index.html index 978574a0..964a36c4 100644 --- a/index.html +++ b/index.html @@ -1,4 +1,4 @@ - + <%= productName %> diff --git a/src-capacitor/android/app/debug/output.json b/src-capacitor/android/app/debug/output.json index dc2b1619..f8cd2add 100644 --- a/src-capacitor/android/app/debug/output.json +++ b/src-capacitor/android/app/debug/output.json @@ -1 +1,18 @@ -[{"outputType":{"type":"APK"},"apkData":{"type":"MAIN","splits":[],"versionCode":1,"versionName":"1.0","enabled":true,"outputFile":"app-debug.apk","fullName":"debug","baseName":"debug","dirName":""},"path":"app-debug.apk","properties":{}}] \ No newline at end of file +[ + { + "outputType": { "type": "APK" }, + "apkData": { + "type": "MAIN", + "splits": [], + "versionCode": 1, + "versionName": "1.0", + "enabled": true, + "outputFile": "app-debug.apk", + "fullName": "debug", + "baseName": "debug", + "dirName": "" + }, + "path": "app-debug.apk", + "properties": {} + } +] diff --git a/src-pwa/custom-service-worker.js b/src-pwa/custom-service-worker.js index 365ae9de..a8bb59c5 100644 --- a/src-pwa/custom-service-worker.js +++ b/src-pwa/custom-service-worker.js @@ -28,7 +28,7 @@ if (process.env.MODE !== "ssr" || process.env.PROD) { registerRoute( new NavigationRoute( createHandlerBoundToURL(process.env.PWA_FALLBACK_HTML), - { denylist: [/sw\.js$/, /workbox-(.)*\.js$/] } - ) + { denylist: [/sw\.js$/, /workbox-(.)*\.js$/] }, + ), ); } diff --git a/src/boot/base.js b/src/boot/base.js index bfb65c57..61a345af 100644 --- a/src/boot/base.js +++ b/src/boot/base.js @@ -152,7 +152,7 @@ window.windowMixin = { type = "null", position = "top", caption = null, - color = null + color = null, ) { // failure this.$q.notify({ @@ -194,7 +194,7 @@ window.windowMixin = { if (this.$q.localStorage.getItem("cashu.theme")) { document.body.setAttribute( "data-theme", - this.$q.localStorage.getItem("cashu.theme") + this.$q.localStorage.getItem("cashu.theme"), ); } else { this.changeColor("monochrome"); diff --git a/src/components/BalanceView.vue b/src/components/BalanceView.vue index 903f189e..7cce4ce6 100644 --- a/src/components/BalanceView.vue +++ b/src/components/BalanceView.vue @@ -2,17 +2,21 @@
-
+
- - + +
-
+
@@ -70,7 +74,7 @@ {{ formatCurrency( (getTotalBalance / 100 / bitcoinPrice) * 100000000, - "sat" + "sat", ) }} diff --git a/src/components/InvoiceDetailDialog.vue b/src/components/InvoiceDetailDialog.vue index 0467236e..bb601e22 100644 --- a/src/components/InvoiceDetailDialog.vue +++ b/src/components/InvoiceDetailDialog.vue @@ -188,7 +188,7 @@ export default defineComponent({ let display = this.formatCurrency( this.invoiceData.amount, this.invoiceData.unit, - true + true, ); return display; }, diff --git a/src/components/MintSettings.vue b/src/components/MintSettings.vue index 9a90a22b..51a77887 100644 --- a/src/components/MintSettings.vue +++ b/src/components/MintSettings.vue @@ -321,7 +321,7 @@ mintSwap( swapData.from_url.url, swapData.to_url.url, - swapData.amount + swapData.amount, ) " :disable=" diff --git a/src/components/PayInvoiceDialog.vue b/src/components/PayInvoiceDialog.vue index 0d3c4337..ccb88d9d 100644 --- a/src/components/PayInvoiceDialog.vue +++ b/src/components/PayInvoiceDialog.vue @@ -23,7 +23,7 @@ formatCurrency( payInvoiceData.meltQuote.response.amount, activeUnit, - true + true, ) }} @@ -59,8 +59,8 @@ payInvoiceData.meltQuote.error != '' ? 'Error' : !payInvoiceData.blocking - ? 'Pay' - : 'Processing...' + ? 'Pay' + : 'Processing...' " :loading="globalMutexLock && !payInvoiceData.blocking" class="q-px-lg" diff --git a/src/components/QrcodeReader.vue b/src/components/QrcodeReader.vue index 6347fbe8..6ba5fb29 100644 --- a/src/components/QrcodeReader.vue +++ b/src/components/QrcodeReader.vue @@ -27,7 +27,7 @@ export default { highlightScanRegion: true, highlightCodeOutline: true, onDecodeError: () => {}, - } + }, ); this.qrScanner.start(); this.urDecoder = new URDecoder(); diff --git a/src/components/ReceiveTokenDialog.vue b/src/components/ReceiveTokenDialog.vue index 3e773979..ce652407 100644 --- a/src/components/ReceiveTokenDialog.vue +++ b/src/components/ReceiveTokenDialog.vue @@ -228,7 +228,7 @@ export default defineComponent({ // 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 + receiveStore.receiveData.tokensBase64, ); const tokenJson = token.decode(receiveStore.receiveData.tokensBase64); @@ -314,7 +314,7 @@ export default defineComponent({ // get amount from decodedToken.token.proofs[..].amount const amount = this.getProofs(decodedToken).reduce( (sum, el) => (sum += el.amount), - 0 + 0, ); tokensStore.addPendingToken({ diff --git a/src/components/SendTokenDialog.vue b/src/components/SendTokenDialog.vue index 426b8a9c..dad4b9eb 100644 --- a/src/components/SendTokenDialog.vue +++ b/src/components/SendTokenDialog.vue @@ -479,7 +479,7 @@ export default defineComponent({ let selectedProofs = this.coinSelect( spendableProofs, this.sendData.amount * this.activeUnitCurrencyMultiplyer, - this.includeFeesInSendAmount + this.includeFeesInSendAmount, ); const feesToAdd = this.includeFeesInSendAmount ? this.getFeesForProofs(selectedProofs) @@ -607,7 +607,7 @@ export default defineComponent({ } console.log( "### this.currentFragmentInterval", - this.currentFragmentInterval + this.currentFragmentInterval, ); this.startQrCodeLoop(); }, @@ -658,7 +658,7 @@ export default defineComponent({ let { _, sendProofs } = await this.sendToLock( this.activeProofs, sendAmount, - this.sendData.p2pkPubkey + this.sendData.p2pkPubkey, ); // update UI this.sendData.tokens = sendProofs; @@ -698,7 +698,7 @@ export default defineComponent({ this.activeProofs, sendAmount, true, - this.includeFeesInSendAmount + this.includeFeesInSendAmount, ); // update UI diff --git a/src/components/SettingsView.vue b/src/components/SettingsView.vue index cac3e3ab..29b0c5b7 100644 --- a/src/components/SettingsView.vue +++ b/src/components/SettingsView.vue @@ -1075,7 +1075,7 @@ export default defineComponent({ for (let mint of mints) { const mintIds = mint.keysets.map((keyset) => keyset.id); const keysetCounterThisMint = this.keysetCounters.filter((entry) => - mintIds.includes(entry.id) + mintIds.includes(entry.id), ); keysetCountersByMint[mint.url] = keysetCounterThisMint; } diff --git a/src/components/TokenInformation.vue b/src/components/TokenInformation.vue index 6dc8ffbc..31246688 100644 --- a/src/components/TokenInformation.vue +++ b/src/components/TokenInformation.vue @@ -100,7 +100,7 @@ export default defineComponent({ let uniqueIds = [...new Set(proofs.map((p) => p.id))]; // mints that have any of the keyset IDs let mints_keysets = this.mints.filter((m) => - m.keysets.some((r) => uniqueIds.indexOf(r) >= 0) + m.keysets.some((r) => uniqueIds.indexOf(r) >= 0), ); // what we put into the JSON let mints = mints_keysets.map((m) => [{ url: m.url, ids: m.keysets }][0]); @@ -116,7 +116,7 @@ export default defineComponent({ // mints that have any of the keyset IDs return ( this.mints.filter((m) => - m.keysets.some((r) => uniqueIds.indexOf(r.id) >= 0) + m.keysets.some((r) => uniqueIds.indexOf(r.id) >= 0), ).length > 0 ); }, diff --git a/src/css/base.scss b/src/css/base.scss index 2eca4e93..d6a5eb45 100644 --- a/src/css/base.scss +++ b/src/css/base.scss @@ -213,14 +213,18 @@ video { } .q-card { - box-shadow: 0 1px 5px rgba(0, 0, 0, 0.2), 0 2px 2px rgba(0, 0, 0, 0.14), + box-shadow: + 0 1px 5px rgba(0, 0, 0, 0.2), + 0 2px 2px rgba(0, 0, 0, 0.14), 0 3px 1px -2px rgba(0, 0, 0, 0.12); border-radius: 4px; vertical-align: top; } .shadow-2 { - box-shadow: 0 1px 5px rgba(0, 0, 0, 0.2), 0 2px 2px rgba(0, 0, 0, 0.14), + box-shadow: + 0 1px 5px rgba(0, 0, 0, 0.2), + 0 2px 2px rgba(0, 0, 0, 0.14), 0 3px 1px -2px rgba(0, 0, 0, 0.12); } diff --git a/src/pages/WalletPage.vue b/src/pages/WalletPage.vue index b60702a6..9cd8a49d 100644 --- a/src/pages/WalletPage.vue +++ b/src/pages/WalletPage.vue @@ -498,13 +498,13 @@ export default { this.deferredPWAInstallPrompt = e; console.log( `'beforeinstallprompt' event was fired.`, - this.getPwaDisplayMode() + this.getPwaDisplayMode(), ); }); }, getPwaDisplayMode: function () { const isStandalone = window.matchMedia( - "(display-mode: standalone)" + "(display-mode: standalone)", ).matches; if (document.referrer.startsWith("android-app://")) { return "twa"; @@ -533,7 +533,7 @@ export default { sessionStorage.setItem( "tabId", Math.random().toString(36).substring(2) + - new Date().getTime().toString(36) + new Date().getTime().toString(36), ); } const tabId = sessionStorage.getItem("tabId"); @@ -625,7 +625,7 @@ export default { window.history.pushState( {}, document.title, - window.location.href.split("?")[0].split("#")[0] + window.location.href.split("?")[0].split("#")[0], ); // startup tasks diff --git a/src/router/index.js b/src/router/index.js index 544b4fee..e7424f11 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -20,8 +20,8 @@ export default route(function (/* { store, ssrContext } */) { const createHistory = process.env.SERVER ? createMemoryHistory : process.env.VUE_ROUTER_MODE === "history" - ? createWebHistory - : createWebHashHistory; + ? createWebHistory + : createWebHashHistory; const Router = createRouter({ scrollBehavior: () => ({ left: 0, top: 0 }), From 64cd350027e36ac56847d9fef5ce3f9a2b562730 Mon Sep 17 00:00:00 2001 From: Pavol Rusnak Date: Sun, 27 Oct 2024 09:12:35 +0100 Subject: [PATCH 2/3] extend format check to all files --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0882e320..c4470072 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "build:pwa": "quasar build -m pwa", "quasar": "quasar", "lint": "eslint --ext .js,.vue ./", - "format": "prettier --write \"**/*.{js,vue,scss,html,md,json}\" --ignore-path .gitignore", + "format": "prettier --write .", "test": "vitest", "test:ci": "vitest run" }, From b3eb59014ca7e39e3840a40f11e0a149b990e180 Mon Sep 17 00:00:00 2001 From: Pavol Rusnak Date: Tue, 12 Nov 2024 15:56:16 +0100 Subject: [PATCH 3/3] call npm run format again, to apply on TS files --- src/components/SendPaymentRequest.vue | 2 +- src/js/notify.ts | 25 +- src/js/token.ts | 4 +- src/stores/mints.ts | 122 +++++-- src/stores/nostr.ts | 267 +++++++++----- src/stores/npubcash.ts | 186 +++++----- src/stores/nwc.ts | 415 ++++++++++++---------- src/stores/p2pk.ts | 56 ++- src/stores/payment-request.ts | 39 ++- src/stores/proofs.ts | 18 +- src/stores/restore.ts | 56 ++- src/stores/sendTokensStore.ts | 2 +- src/stores/settings.ts | 30 +- src/stores/tokens.ts | 23 +- src/stores/ui.ts | 23 +- src/stores/wallet.ts | 483 ++++++++++++++++++-------- src/stores/workers.ts | 5 +- 17 files changed, 1125 insertions(+), 631 deletions(-) diff --git a/src/components/SendPaymentRequest.vue b/src/components/SendPaymentRequest.vue index c0273ba4..cdd6247d 100644 --- a/src/components/SendPaymentRequest.vue +++ b/src/components/SendPaymentRequest.vue @@ -48,7 +48,7 @@ export default defineComponent({ clickPaymentRequest: function () { this.parseAndPayPaymentRequest( this.sendData.paymentRequest, - this.sendData.tokensBase64 + this.sendData.tokensBase64, ); }, getPaymentRequestTransportType: function (request) { diff --git a/src/js/notify.ts b/src/js/notify.ts index 753fbb66..c7217111 100644 --- a/src/js/notify.ts +++ b/src/js/notify.ts @@ -7,7 +7,11 @@ const errorTypes = { 500: "negative", } as StatusMap; -async function notifyApiError(error: Error, caption: string = "", position = "top" as QNotifyCreateOptions["position"]) { +async function notifyApiError( + error: Error, + caption: string = "", + position = "top" as QNotifyCreateOptions["position"], +) { try { Notify.create({ timeout: 5000, @@ -20,7 +24,7 @@ async function notifyApiError(error: Error, caption: string = "", position = "to { icon: "close", color: "white", - handler: () => { }, + handler: () => {}, }, ], }); @@ -31,7 +35,7 @@ async function notifyApiError(error: Error, caption: string = "", position = "to async function notifySuccess( message: string, - position = "top" as QNotifyCreateOptions["position"] + position = "top" as QNotifyCreateOptions["position"], ) { Notify.create({ timeout: 5000, @@ -43,7 +47,7 @@ async function notifySuccess( { icon: "close", color: "white", - handler: () => { }, + handler: () => {}, }, ], }); @@ -60,7 +64,7 @@ async function notifyError(message: string, caption?: string) { { icon: "close", color: "white", - handler: () => { }, + handler: () => {}, }, ], }); @@ -69,7 +73,7 @@ async function notifyError(message: string, caption?: string) { async function notifyWarning( message: string, caption?: string, - timeout = 5000 + timeout = 5000, ) { Notify.create({ timeout: timeout, @@ -82,13 +86,16 @@ async function notifyWarning( { icon: "close", color: "black", - handler: () => { }, + handler: () => {}, }, ], }); } -async function notify(message: string, position = "top" as QNotifyCreateOptions["position"]) { +async function notify( + message: string, + position = "top" as QNotifyCreateOptions["position"], +) { // failure Notify.create({ timeout: 5000, @@ -100,7 +107,7 @@ async function notify(message: string, position = "top" as QNotifyCreateOptions[ { icon: "close", color: "white", - handler: () => { }, + handler: () => {}, }, ], }); diff --git a/src/js/token.ts b/src/js/token.ts index 5f0bc090..e58fc45b 100644 --- a/src/js/token.ts +++ b/src/js/token.ts @@ -14,9 +14,7 @@ function decode(encoded_token: string) { * Returns a list of proofs from a decoded token */ function getProofs(decoded_token: Token): WalletProof[] { - if ( - !(decoded_token.proofs.length > 0) - ) { + if (!(decoded_token.proofs.length > 0)) { throw new Error("Token format wrong"); } const proofs = decoded_token.proofs.flat(); diff --git a/src/stores/mints.ts b/src/stores/mints.ts index 80316f97..f97b2359 100644 --- a/src/stores/mints.ts +++ b/src/stores/mints.ts @@ -2,7 +2,16 @@ import { defineStore } from "pinia"; import { useLocalStorage } from "@vueuse/core"; import { useWorkersStore } from "./workers"; import { notifyApiError, notifyError, notifySuccess } from "src/js/notify"; -import { CashuMint, MintKeys, MintAllKeysets, MintActiveKeys, Proof, SerializedBlindedSignature, MintKeyset, GetInfoResponse } from "@cashu/cashu-ts"; +import { + CashuMint, + MintKeys, + MintAllKeysets, + MintActiveKeys, + Proof, + SerializedBlindedSignature, + MintKeyset, + GetInfoResponse, +} from "@cashu/cashu-ts"; import { useUiStore } from "./ui"; export type Mint = { url: string; @@ -23,7 +32,9 @@ export class MintClass { } get proofs() { const mintStore = useMintsStore(); - return mintStore.proofs.filter((p) => this.mint.keysets.map((k) => k.id).includes(p.id)); + return mintStore.proofs.filter((p) => + this.mint.keysets.map((k) => k.id).includes(p.id), + ); } // get balance() { // const proofs = this.proofs; @@ -44,7 +55,9 @@ export class MintClass { } get units() { - return this.mint.keysets.map((k) => k.unit).filter((value, index, self) => self.indexOf(value) === index); + return this.mint.keysets + .map((k) => k.unit) + .filter((value, index, self) => self.indexOf(value) === index); } unitKeysets(unit: string): MintKeyset[] { @@ -53,7 +66,9 @@ export class MintClass { unitProofs(unit: string) { const unitKeysets = this.unitKeysets(unit); - return this.proofs.filter((p) => unitKeysets.map((k) => k.id).includes(p.id)); + return this.proofs.filter((p) => + unitKeysets.map((k) => k.id).includes(p.id), + ); } unitBalance(unit: string) { @@ -69,7 +84,6 @@ export type Balances = { [unit: string]: number; }; - type BlindSignatureAudit = { signature: SerializedBlindedSignature; amount: number; @@ -90,7 +104,10 @@ export const useMintsStore = defineStore("mints", { mints: useLocalStorage("cashu.mints", [] as Mint[]), proofs: useLocalStorage("cashu.proofs", [] as WalletProof[]), spentProofs: useLocalStorage("cashu.spentProofs", [] as WalletProof[]), - blindSignatures: useLocalStorage("cashu.blindSignatures", [] as BlindSignatureAudit[]), + blindSignatures: useLocalStorage( + "cashu.blindSignatures", + [] as BlindSignatureAudit[], + ), // balances: useLocalStorage("cashu.balances", {} as Balances), showAddMintDialog: false, addMintBlocking: false, @@ -101,30 +118,39 @@ export const useMintsStore = defineStore("mints", { }, getters: { activeProofs({ activeMintUrl, activeUnit }): WalletProof[] { - const unitKeysets = this.mints.find((m) => m.url === activeMintUrl)?.keysets?.filter((k) => k.unit === activeUnit); + const unitKeysets = this.mints + .find((m) => m.url === activeMintUrl) + ?.keysets?.filter((k) => k.unit === activeUnit); if (!unitKeysets) { return []; } return this.proofs.filter((p) => - unitKeysets.map((k) => k.id).includes(p.id) + unitKeysets.map((k) => k.id).includes(p.id), ); }, activeBalance({ activeUnit }): number { - const allUnitKeysets = this.mints.map((m) => m.keysets).flat().filter((k) => k.unit === activeUnit); - const balance = this.proofs.filter((p) => - allUnitKeysets.map((k) => k.id).includes(p.id) - ).reduce((sum, p) => sum + p.amount, 0); - return balance + const allUnitKeysets = this.mints + .map((m) => m.keysets) + .flat() + .filter((k) => k.unit === activeUnit); + const balance = this.proofs + .filter((p) => allUnitKeysets.map((k) => k.id).includes(p.id)) + .reduce((sum, p) => sum + p.amount, 0); + return balance; }, activeKeysets({ activeMintUrl, activeUnit }): MintKeyset[] { - const unitKeysets = this.mints.find((m) => m.url === activeMintUrl)?.keysets?.filter((k) => k.unit === activeUnit); + const unitKeysets = this.mints + .find((m) => m.url === activeMintUrl) + ?.keysets?.filter((k) => k.unit === activeUnit); if (!unitKeysets) { return []; } return unitKeysets; }, activeKeys({ activeMintUrl, activeUnit }): MintKeys[] { - const unitKeys = this.mints.find((m) => m.url === activeMintUrl)?.keys?.filter((k) => k.unit === activeUnit); + const unitKeys = this.mints + .find((m) => m.url === activeMintUrl) + ?.keys?.filter((k) => k.unit === activeUnit); if (!unitKeys) { return []; } @@ -163,8 +189,10 @@ export const useMintsStore = defineStore("mints", { return new MintClass(mint); } else { if (this.mints.length) { - console.error("No active mint. This should not happen. switching to first one.") - this.activateMintUrl(this.mints[0].url, false, true) + console.error( + "No active mint. This should not happen. switching to first one.", + ); + this.activateMintUrl(this.mints[0].url, false, true); return new MintClass(this.mints[0]); } throw new Error("No active mint"); @@ -202,7 +230,12 @@ export const useMintsStore = defineStore("mints", { }); this.spentProofs = this.spentProofs.concat(walletProofs); }, - appendBlindSignatures(signature: SerializedBlindedSignature, amount: number, secret: Uint8Array, r: Uint8Array) { + appendBlindSignatures( + signature: SerializedBlindedSignature, + amount: number, + secret: Uint8Array, + r: Uint8Array, + ) { const audit: BlindSignatureAudit = { signature: signature, amount: amount, @@ -215,8 +248,11 @@ export const useMintsStore = defineStore("mints", { toggleActiveUnitForMint(mint: Mint) { // method to set the active unit to one that is supported by `mint` const mintClass = new MintClass(mint); - if (!this.activeUnit || mintClass.allBalances[this.activeUnit] == undefined) { - this.activeUnit = mintClass.units[0] + if ( + !this.activeUnit || + mintClass.allBalances[this.activeUnit] == undefined + ) { + this.activeUnit = mintClass.units[0]; } }, updateMint(oldMint: Mint, newMint: Mint) { @@ -236,21 +272,30 @@ export const useMintsStore = defineStore("mints", { throw new Error("Mint not found"); } }, - addMint: async function (addMintData: { url: string, nickname: string }, verbose = false) { + addMint: async function ( + addMintData: { url: string; nickname: string }, + verbose = false, + ) { let url = addMintData.url; this.addMintBlocking = true; try { // sanitize url const sanitizeUrl = (url: string): string => { - let cleanedUrl = url.trim().replace(/\/+$/, '') - if (!/^[a-z]+:\/\//.test(cleanedUrl)) { // Check for any protocol followed by "://" - cleanedUrl = 'https://' + cleanedUrl; + let cleanedUrl = url.trim().replace(/\/+$/, ""); + if (!/^[a-z]+:\/\//.test(cleanedUrl)) { + // Check for any protocol followed by "://" + cleanedUrl = "https://" + cleanedUrl; } return cleanedUrl; }; url = sanitizeUrl(url); - const mintToAdd: Mint = { url: url, keys: [], keysets: [], nickname: addMintData.nickname }; + const mintToAdd: Mint = { + url: url, + keys: [], + keysets: [], + nickname: addMintData.nickname, + }; // we have no mints at all if (this.mints.length === 0) { @@ -279,7 +324,12 @@ export const useMintsStore = defineStore("mints", { this.addMintBlocking = false; } }, - activateMintUrl: async function (url: string, verbose = false, force = false, unit: string | undefined = undefined) { + activateMintUrl: async function ( + url: string, + verbose = false, + force = false, + unit: string | undefined = undefined, + ) { const mint = this.mints.filter((m) => m.url === url)[0]; if (mint) { await this.activateMint(mint, verbose, force); @@ -312,7 +362,6 @@ export const useMintsStore = defineStore("mints", { worker.clearAllWorkers(); }, activateMint: async function (mint: Mint, verbose = false, force = false) { - if (mint.url === this.activeMintUrl && !force) { return; } @@ -334,10 +383,7 @@ export const useMintsStore = defineStore("mints", { if (verbose) { await notifySuccess("Mint activated."); } - console.log( - "### activateMint: Mint activated: ", - this.activeMintUrl, - ); + console.log("### activateMint: Mint activated: ", this.activeMintUrl); } catch (error: any) { // restore previous values because the activation errored this.activeMintUrl = previousUrl; @@ -360,7 +406,7 @@ export const useMintsStore = defineStore("mints", { console.error(error); try { notifyApiError(error, "Could not get mint info"); - } catch { } + } catch {} throw error; } }, @@ -388,7 +434,9 @@ export const useMintsStore = defineStore("mints", { if (!mint.keys.find((k) => k.id === keyset.id)) { const keys = await mintClass.api.getKeys(keyset.id); // store keys in mint and update local storage - this.mints.filter((m) => m.url === mint.url)[0].keys.push(keys.keysets[0]); + this.mints + .filter((m) => m.url === mint.url)[0] + .keys.push(keys.keysets[0]); } } @@ -398,12 +446,12 @@ export const useMintsStore = defineStore("mints", { // this.mints.filter((m) => m.url === mint.url)[0].keys = keys.keysets; // return the mint with keys set - return this.mints.filter((m) => m.url === mint.url)[0] + return this.mints.filter((m) => m.url === mint.url)[0]; } catch (error: any) { console.error(error); try { notifyApiError(error, "Could not get mint keys"); - } catch { } + } catch {} throw error; } }, @@ -417,7 +465,7 @@ export const useMintsStore = defineStore("mints", { console.error(error); try { notifyApiError(error, "Could not get mint keysets"); - } catch { } + } catch {} throw error; } }, @@ -458,5 +506,5 @@ export const useMintsStore = defineStore("mints", { // } // return null // } - } + }, }); diff --git a/src/stores/nostr.ts b/src/stores/nostr.ts index b76d1eed..a1e6efb7 100644 --- a/src/stores/nostr.ts +++ b/src/stores/nostr.ts @@ -1,15 +1,38 @@ import { defineStore } from "pinia"; -import NDK, { NDKEvent, NDKSigner, NDKNip07Signer, NDKNip46Signer, NDKFilter, NDKPrivateKeySigner, NostrEvent, NDKKind, NDKRelaySet, NDKRelay, NDKTag, ProfilePointer } from "@nostr-dev-kit/ndk"; -import { nip04, nip19, nip44 } from 'nostr-tools' -import { bytesToHex, hexToBytes } from '@noble/hashes/utils' // already an installed dependency +import NDK, { + NDKEvent, + NDKSigner, + NDKNip07Signer, + NDKNip46Signer, + NDKFilter, + NDKPrivateKeySigner, + NostrEvent, + NDKKind, + NDKRelaySet, + NDKRelay, + NDKTag, + ProfilePointer, +} from "@nostr-dev-kit/ndk"; +import { nip04, nip19, nip44 } from "nostr-tools"; +import { bytesToHex, hexToBytes } from "@noble/hashes/utils"; // already an installed dependency import { useWalletStore } from "./wallet"; -import { generateSecretKey, getPublicKey } from 'nostr-tools' +import { generateSecretKey, getPublicKey } from "nostr-tools"; import { useLocalStorage } from "@vueuse/core"; import { useSettingsStore } from "./settings"; import { useReceiveTokensStore } from "./receiveTokensStore"; -import { getEncodedTokenV4, PaymentRequestPayload, Token } from "@cashu/cashu-ts"; +import { + getEncodedTokenV4, + PaymentRequestPayload, + Token, +} from "@cashu/cashu-ts"; import { useTokensStore } from "./tokens"; -import { notifyApiError, notifyError, notifySuccess, notifyWarning, notify } from "../js/notify"; +import { + notifyApiError, + notifyError, + notifySuccess, + notifyWarning, + notify, +} from "../js/notify"; import { useSendTokensStore } from "./sendTokensStore"; import { usePRStore } from "./payment-request"; import token from "../js/token"; @@ -21,15 +44,15 @@ type MintRecommendation = { }; type NostrEventLog = { - id: string, - created_at: number, -} + id: string; + created_at: number; +}; export enum SignerType { NIP07 = "NIP07", NIP46 = "NIP46", PRIVATEKEY = "PRIVATEKEY", - SEED = "SEED" + SEED = "SEED", } export const useNostrStore = defineStore("nostr", { @@ -38,19 +61,37 @@ export const useNostrStore = defineStore("nostr", { pubkey: useLocalStorage("cashu.ndk.pubkey", ""), relays: useSettingsStore().defaultNostrRelays, ndk: {} as NDK, - signerType: useLocalStorage("cashu.ndk.signerType", SignerType.SEED), + signerType: useLocalStorage( + "cashu.ndk.signerType", + SignerType.SEED, + ), nip07signer: {} as NDKNip07Signer, nip46Token: useLocalStorage("cashu.ndk.nip46Token", ""), nip46signer: {} as NDKNip46Signer, - privateKeySignerPrivateKey: useLocalStorage("cashu.ndk.privateKeySignerPrivateKey", ""), - seedSignerPrivateKey: useLocalStorage("cashu.ndk.seedSignerPrivateKey", ""), + privateKeySignerPrivateKey: useLocalStorage( + "cashu.ndk.privateKeySignerPrivateKey", + "", + ), + seedSignerPrivateKey: useLocalStorage( + "cashu.ndk.seedSignerPrivateKey", + "", + ), seedSignerPrivateKeyNsec: "", privateKeySigner: {} as NDKPrivateKeySigner, signer: {} as NDKSigner, - mintRecommendations: useLocalStorage("cashu.ndk.mintRecommendations", []), + mintRecommendations: useLocalStorage( + "cashu.ndk.mintRecommendations", + [], + ), initialized: false, - lastEventTimestamp: useLocalStorage("cashu.ndk.lastEventTimestamp", 0), - nip17EventIdsWeHaveSeen: useLocalStorage("cashu.ndk.nip17EventIdsWeHaveSeen", []), + lastEventTimestamp: useLocalStorage( + "cashu.ndk.lastEventTimestamp", + 0, + ), + nip17EventIdsWeHaveSeen: useLocalStorage( + "cashu.ndk.nip17EventIdsWeHaveSeen", + [], + ), }), getters: { seedSignerPrivateKeyNsec: (state) => { @@ -89,14 +130,14 @@ export const useNostrStore = defineStore("nostr", { this.initialized = true; }, setSigner: async function (signer: NDKSigner) { - this.signer = signer - this.ndk = new NDK({ signer: signer, explicitRelayUrls: this.relays }) + this.signer = signer; + this.ndk = new NDK({ signer: signer, explicitRelayUrls: this.relays }); }, signDummyEvent: async function (): Promise { const ndkEvent = new NDKEvent(); ndkEvent.kind = 1; ndkEvent.content = "Hello, world!"; - const sig = await ndkEvent.sign(this.signer) + const sig = await ndkEvent.sign(this.signer); console.log(`nostr signature: ${sig})`); const eventString = JSON.stringify(ndkEvent.rawEvent()); console.log(`nostr event: ${eventString}`); @@ -107,7 +148,7 @@ export const useNostrStore = defineStore("nostr", { this.pubkey = pubkey; }, checkNip07Signer: async function (): Promise { - const signer = new NDKNip07Signer() + const signer = new NDKNip07Signer(); try { await signer.user(); return true; @@ -116,10 +157,13 @@ export const useNostrStore = defineStore("nostr", { } }, initNip07Signer: async function () { - const signer = new NDKNip07Signer() + const signer = new NDKNip07Signer(); signer.user().then(async (user) => { if (!!user.npub) { - console.log("Permission granted to read their public key:", user.npub); + console.log( + "Permission granted to read their public key:", + user.npub, + ); const me = this.ndk.getUser({ npub: user.npub, }); @@ -133,7 +177,9 @@ export const useNostrStore = defineStore("nostr", { initNip46Signer: async function (nip46Token?: string) { const ndk = new NDK({ explicitRelayUrls: this.relays }); if (!nip46Token && !this.nip46Token.length) { - nip46Token = await prompt("Enter your NIP-46 connection string") as string; + nip46Token = (await prompt( + "Enter your NIP-46 connection string", + )) as string; if (!nip46Token) { return; } @@ -143,14 +189,16 @@ export const useNostrStore = defineStore("nostr", { this.nip46Token = nip46Token; } } - const signer = new NDKNip46Signer(ndk, this.nip46Token) + const signer = new NDKNip46Signer(ndk, this.nip46Token); this.signerType = SignerType.NIP46; await this.setSigner(signer); // If the backend sends an auth_url event, open that URL as a popup so the user can authorize the app - signer.on("authUrl", (url) => { window.open(url, "auth", "width=600,height=600") }) + signer.on("authUrl", (url) => { + window.open(url, "auth", "width=600,height=600"); + }); // wait until the signer is ready - const loggedinUser = await signer.blockUntilReady() - alert("You are now logged in as " + loggedinUser.npub) + const loggedinUser = await signer.blockUntilReady(); + alert("You are now logged in as " + loggedinUser.npub); this.setPubkey(loggedinUser.pubkey); }, resetNip46Signer: async function () { @@ -160,19 +208,21 @@ export const useNostrStore = defineStore("nostr", { initPrivateKeySigner: async function (nsec?: string) { let privateKeyBytes: Uint8Array; if (!nsec && !this.privateKeySignerPrivateKey.length) { - nsec = await prompt("Enter your nsec") as string; + nsec = (await prompt("Enter your nsec")) as string; if (!nsec) { return; } - privateKeyBytes = nip19.decode(nsec).data as Uint8Array + privateKeyBytes = nip19.decode(nsec).data as Uint8Array; } else { if (nsec) { - privateKeyBytes = nip19.decode(nsec).data as Uint8Array + privateKeyBytes = nip19.decode(nsec).data as Uint8Array; } else { privateKeyBytes = hexToBytes(this.privateKeySignerPrivateKey); } } - this.privateKeySigner = new NDKPrivateKeySigner(this.privateKeySignerPrivateKey); + this.privateKeySigner = new NDKPrivateKeySigner( + this.privateKeySignerPrivateKey, + ); this.privateKeySignerPrivateKey = bytesToHex(privateKeyBytes); this.signerType = SignerType.PRIVATEKEY; await this.setSigner(this.privateKeySigner); @@ -185,11 +235,11 @@ export const useNostrStore = defineStore("nostr", { }, initWalletSeedPrivateKeySigner: 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) + 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.privateKeySigner = new NDKPrivateKeySigner(walletPrivateKeyHex); this.signerType = SignerType.SEED; this.setSigner(this.privateKeySigner); this.setPubkey(walletPublicKeyHex); @@ -205,12 +255,15 @@ export const useNostrStore = defineStore("nostr", { events.forEach((event) => { if (event.tagValue("k") == "38172" && event.tagValue("u")) { const mintUrl = event.tagValue("u"); - if (typeof mintUrl === "string" && mintUrl.length > 0 && mintUrl.startsWith("https://")) { + if ( + typeof mintUrl === "string" && + mintUrl.length > 0 && + mintUrl.startsWith("https://") + ) { mintUrls.push(mintUrl); } } - } - ); + }); // Count the number of times each mint URL appears const mintUrlsSet = new Set(mintUrls); const mintUrlsArray = Array.from(mintUrlsSet); @@ -221,18 +274,24 @@ export const useNostrStore = defineStore("nostr", { this.mintRecommendations = mintUrlsCounted; return mintUrlsCounted; }, - sendNip04DirectMessage: async function (recipient: string, message: string) { + sendNip04DirectMessage: async function ( + recipient: string, + message: string, + ) { // 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 ndk = new NDK({ + explicitRelayUrls: this.relays, + signer: new NDKPrivateKeySigner(bytesToHex(randomPrivateKey)), + }); const event = new NDKEvent(ndk); ndk.connect(); event.kind = NDKKind.EncryptedDirectMessage; event.content = await nip04.encrypt(randomPrivateKey, recipient, message); - event.tags = [['p', recipient]]; - event.sign() + event.tags = [["p", recipient]]; + event.sign(); try { await event.publish(); notifySuccess("NIP-04 event published"); @@ -243,11 +302,13 @@ export const useNostrStore = defineStore("nostr", { }, subscribeToNip04DirectMessages: async function () { let nip04DirectMessageEvents: Set = new Set(); - const fetchEventsPromise = new Promise>(resolve => { + 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.pubkey} since ${this.lastEventTimestamp}`, + ); this.ndk.connect(); const sub = this.ndk.subscribe( { @@ -257,41 +318,58 @@ export const useNostrStore = defineStore("nostr", { } as NDKFilter, { closeOnEose: false, groupable: false }, ); - sub.on('event', (event: NDKEvent) => { - console.log('event') - nip04.decrypt(hexToBytes(this.seedSignerPrivateKey), event.pubkey, event.content).then((content) => { - console.log('NIP-04 DM from', event.pubkey); - console.log("Content:", content); - nip04DirectMessageEvents.add(event) - this.lastEventTimestamp = Math.floor(Date.now() / 1000); - this.parseMessageForEcash(content); - }); + sub.on("event", (event: NDKEvent) => { + console.log("event"); + nip04 + .decrypt( + hexToBytes(this.seedSignerPrivateKey), + event.pubkey, + event.content, + ) + .then((content) => { + console.log("NIP-04 DM from", event.pubkey); + console.log("Content:", content); + nip04DirectMessageEvents.add(event); + this.lastEventTimestamp = Math.floor(Date.now() / 1000); + this.parseMessageForEcash(content); + }); }); }); try { nip04DirectMessageEvents = await fetchEventsPromise; } catch (error) { - console.error('Error fetching contact events:', error); + console.error("Error fetching contact events:", error); } }, - sendNip17DirectMessageToNprofile: async function (nprofile: string, message: string) { + sendNip17DirectMessageToNprofile: async function ( + nprofile: string, + message: string, + ) { const result = nip19.decode(nprofile); const pubkey: string = (result.data as ProfilePointer).pubkey; - const relays: string[] | undefined = (result.data as ProfilePointer).relays; - this.sendNip17DirectMessage(pubkey, message, relays) + const relays: string[] | undefined = (result.data as ProfilePointer) + .relays; + this.sendNip17DirectMessage(pubkey, message, relays); }, randomTimeUpTo2DaysInThePast: function () { return Math.floor(Date.now() / 1000) - Math.floor(Math.random() * 172800); }, - sendNip17DirectMessage: async function (recipient: string, message: string, relays?: string[]) { + sendNip17DirectMessage: async function ( + recipient: string, + message: string, + relays?: string[], + ) { const randomPrivateKey = generateSecretKey(); const randomPublicKey = getPublicKey(randomPrivateKey); - const ndk = new NDK({ explicitRelayUrls: relays ?? this.relays, signer: new NDKPrivateKeySigner(bytesToHex(randomPrivateKey)) }); + const ndk = new NDK({ + explicitRelayUrls: relays ?? this.relays, + signer: new NDKPrivateKeySigner(bytesToHex(randomPrivateKey)), + }); const dmEvent = new NDKEvent(); dmEvent.kind = 14; dmEvent.content = message; - dmEvent.tags = [['p', recipient]]; + dmEvent.tags = [["p", recipient]]; dmEvent.created_at = Math.floor(Date.now() / 1000); dmEvent.pubkey = this.pubkey; dmEvent.id = dmEvent.getEventHash(); @@ -299,7 +377,10 @@ export const useNostrStore = defineStore("nostr", { const sealEvent = new NDKEvent(this.ndk as NDK); sealEvent.kind = 13; - sealEvent.content = nip44.v2.encrypt(dmEventString, nip44.v2.utils.getConversationKey(this.seedSignerPrivateKey, recipient)); + sealEvent.content = nip44.v2.encrypt( + dmEventString, + nip44.v2.utils.getConversationKey(this.seedSignerPrivateKey, recipient), + ); sealEvent.created_at = this.randomTimeUpTo2DaysInThePast(); sealEvent.pubkey = this.pubkey; sealEvent.id = sealEvent.getEventHash(); @@ -308,8 +389,14 @@ export const useNostrStore = defineStore("nostr", { const wrapEvent = new NDKEvent(ndk); wrapEvent.kind = 1059; - wrapEvent.tags = [['p', recipient]]; - wrapEvent.content = nip44.v2.encrypt(sealEventString, nip44.v2.utils.getConversationKey(bytesToHex(randomPrivateKey), recipient)); + wrapEvent.tags = [["p", recipient]]; + wrapEvent.content = nip44.v2.encrypt( + sealEventString, + nip44.v2.utils.getConversationKey( + bytesToHex(randomPrivateKey), + recipient, + ), + ); wrapEvent.created_at = this.randomTimeUpTo2DaysInThePast(); wrapEvent.pubkey = randomPublicKey; wrapEvent.id = wrapEvent.getEventHash(); @@ -326,12 +413,14 @@ export const useNostrStore = defineStore("nostr", { }, subscribeToNip17DirectMessages: async function () { let nip17DirectMessageEvents: Set = new Set(); - const fetchEventsPromise = new Promise>(resolve => { + 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.pubkey} since ${since}`, + ); this.ndk.connect(); const sub = this.ndk.subscribe( { @@ -342,8 +431,11 @@ export const useNostrStore = defineStore("nostr", { { closeOnEose: false, groupable: false }, ); - sub.on('event', (wrapEvent: NDKEvent) => { - const eventLog = { id: wrapEvent.id, created_at: wrapEvent.created_at } as NostrEventLog; + 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}`); return; @@ -351,14 +443,28 @@ export const useNostrStore = defineStore("nostr", { 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); + this.nip17EventIdsWeHaveSeen = this.nip17EventIdsWeHaveSeen.filter( + (e) => e.created_at > Math.floor(Date.now() / 1000) - 345600, + ); } - const wappedContent = nip44.v2.decrypt(wrapEvent.content, nip44.v2.utils.getConversationKey(this.seedSignerPrivateKey, wrapEvent.pubkey)) + 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 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) + nip17DirectMessageEvents.add(dmEvent); this.lastEventTimestamp = Math.floor(Date.now() / 1000); this.parseMessageForEcash(content); }); @@ -366,7 +472,7 @@ export const useNostrStore = defineStore("nostr", { try { nip17DirectMessageEvents = await fetchEventsPromise; } catch (error) { - console.error('Error fetching contact events:', error); + console.error("Error fetching contact events:", error); } }, parseMessageForEcash: async function (message: string) { @@ -400,11 +506,11 @@ export const useNostrStore = defineStore("nostr", { prStore.showPRDialog = false; receiveStore.showReceiveTokens = true; - return + return; } } catch (e) { // console.log("### parsing message for ecash failed"); - return + return; } console.log("### parsing message for ecash", message); @@ -419,7 +525,9 @@ export const useNostrStore = defineStore("nostr", { await this.addPendingTokenToHistory(tokenStr); } }, - tokenAlreadyInHistory: function (tokenStr: string): HistoryToken | undefined { + tokenAlreadyInHistory: function ( + tokenStr: string, + ): HistoryToken | undefined { const tokensStore = useTokensStore(); return tokensStore.historyTokens.find((t) => t.token === tokenStr); }, @@ -433,13 +541,12 @@ export const useNostrStore = defineStore("nostr", { const tokensStore = useTokensStore(); const decodedToken = token.decode(tokenStr); if (decodedToken == undefined) { - throw Error('could not decode token') + throw Error("could not decode token"); } // get amount from decodedToken.token.proofs[..].amount - const amount = token.getProofs(decodedToken).reduce( - (sum, el) => (sum += el.amount), - 0 - ); + const amount = token + .getProofs(decodedToken) + .reduce((sum, el) => (sum += el.amount), 0); tokensStore.addPendingToken({ amount: amount, diff --git a/src/stores/npubcash.ts b/src/stores/npubcash.ts index 5297b866..4653bf3a 100644 --- a/src/stores/npubcash.ts +++ b/src/stores/npubcash.ts @@ -1,12 +1,18 @@ import { defineStore } from "pinia"; import NDK, { NDKEvent, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk"; import { useLocalStorage } from "@vueuse/core"; -import { bytesToHex } from '@noble/hashes/utils' // already an installed dependency -import { generateSecretKey, getPublicKey } from 'nostr-tools' -import { nip19 } from 'nostr-tools' +import { bytesToHex } from "@noble/hashes/utils"; // already an installed dependency +import { generateSecretKey, getPublicKey } from "nostr-tools"; +import { nip19 } from "nostr-tools"; import { useWalletStore } from "./wallet"; import { useReceiveTokensStore } from "./receiveTokensStore"; -import { notifyApiError, notifyError, notifySuccess, notifyWarning, notify } from "../js/notify"; +import { + notifyApiError, + notifyError, + notifySuccess, + notifyWarning, + notify, +} from "../js/notify"; import { Proof } from "@cashu/cashu-ts"; import token from "../js/token"; import { WalletProof, useMintsStore } from "./mints"; @@ -18,38 +24,38 @@ import { useNostrStore } from "../stores/nostr"; // } type NPCInfo = { - mintUrl: string, - npub: string, - username: string - error?: string -} + mintUrl: string; + npub: string; + username: string; + error?: string; +}; type NPCBalance = { - error: string, - data: number -} + error: string; + data: number; +}; type NPCClaim = { - error: string, + error: string; data: { - token: string, - } -} + token: string; + }; +}; type NPCWithdrawl = { id: number; claim_ids: number[]; created_at: number; pubkey: string; amount: number; -} +}; type NPCWithdrawals = { - error: string, + error: string; data: { - count: number, - withdrawals: Array - } -} + count: number; + withdrawals: Array; + }; +}; const NIP98Kind = 27235; @@ -65,65 +71,73 @@ export const useNPCStore = defineStore("npc", { // ndk: new NDK(), // signer: {} as NDKPrivateKeySigner, }), - getters: { - }, + getters: {}, actions: { generateNPCConnection: async function () { - const nostrStore = useNostrStore() + const nostrStore = useNostrStore(); if (!nostrStore.pubkey) { - return + return; } - const walletPublicKeyHex = nostrStore.pubkey - console.log('Lightning address for wallet:', nip19.npubEncode(walletPublicKeyHex) + '@' + this.npcDomain) - console.log('npub:', nip19.npubEncode(walletPublicKeyHex)) - this.baseURL = `https://${this.npcDomain}` - const previousAddress = this.npcAddress - this.npcAddress = nip19.npubEncode(walletPublicKeyHex) + '@' + this.npcDomain + const walletPublicKeyHex = nostrStore.pubkey; + console.log( + "Lightning address for wallet:", + nip19.npubEncode(walletPublicKeyHex) + "@" + this.npcDomain, + ); + console.log("npub:", nip19.npubEncode(walletPublicKeyHex)); + this.baseURL = `https://${this.npcDomain}`; + const previousAddress = this.npcAddress; + this.npcAddress = + nip19.npubEncode(walletPublicKeyHex) + "@" + this.npcDomain; if (!this.npcEnabled) { - return + return; } // get info this.npcLoading = true; try { - const info = await this.getInfo() + const info = await this.getInfo(); if (info.error) { - notifyError(info.error) - return + notifyError(info.error); + return; } // log info - console.log(info) + console.log(info); if (info.username) { - const usernameAddress = info.username + '@' + this.npcDomain + const usernameAddress = info.username + "@" + this.npcDomain; if (previousAddress !== usernameAddress) { - notifySuccess(`Logged in as ${info.username}`) + notifySuccess(`Logged in as ${info.username}`); } - this.npcAddress = usernameAddress + this.npcAddress = usernameAddress; } } catch (e: any) { - notifyApiError(e) + notifyApiError(e); } finally { this.npcLoading = false; } - - }, - generateNip98Event: async function (url: string, method: string, body?: string): Promise { - const nostrStore = useNostrStore() - await nostrStore.initSignerIfNotSet() + generateNip98Event: async function ( + url: string, + method: string, + body?: string, + ): Promise { + const nostrStore = useNostrStore(); + await nostrStore.initSignerIfNotSet(); const nip98Event = new NDKEvent(new NDK()); nip98Event.kind = NIP98Kind; - nip98Event.content = ''; - nip98Event.tags = [['u', url], ['method', method]]; + nip98Event.content = ""; + nip98Event.tags = [ + ["u", url], + ["method", method], + ]; // TODO: if body is set, add 'payload' tag with sha256 hash of body - const sig = await nip98Event.sign(nostrStore.signer) + const sig = await nip98Event.sign(nostrStore.signer); const eventString = JSON.stringify(nip98Event.rawEvent()); // encode the eventString to base64 - return btoa(eventString) + return btoa(eventString); }, getInfo: async function (): Promise { const authHeader = await this.generateNip98Event( `${this.baseURL}/api/v1/info`, - "GET" + "GET", ); try { const response = await fetch(`${this.baseURL}/api/v1/info`, { @@ -131,21 +145,21 @@ export const useNPCStore = defineStore("npc", { headers: { Authorization: `Nostr ${authHeader}`, }, - }) - const info: NPCInfo = await response.json() - return info + }); + const info: NPCInfo = await response.json(); + return info; } catch (e) { - console.error(e) + console.error(e); return { mintUrl: "", npub: "", - username: "" - } + username: "", + }; } }, claimAllTokens: async function () { if (!this.npcEnabled) { - return + return; } const receiveStore = useReceiveTokensStore(); const npubCashBalance = await this.getBalance(); @@ -155,13 +169,13 @@ export const useNPCStore = defineStore("npc", { const token = await this.getClaim(); if (token) { // add token to history first - this.addPendingTokenToHistory(token) + this.addPendingTokenToHistory(token); receiveStore.receiveData.tokensBase64 = token; if (this.automaticClaim) { try { // redeem token automatically - const walletStore = useWalletStore() - await walletStore.redeem() + const walletStore = useWalletStore(); + await walletStore.redeem(); } catch { // if it doesn't work, show the receive window receiveStore.showReceiveTokens = true; @@ -175,34 +189,34 @@ export const useNPCStore = defineStore("npc", { tokenAlreadyInHistory: function (tokenStr: string) { const tokensStore = useTokensStore(); return ( - tokensStore.historyTokens.find((t) => t.token === tokenStr) !== undefined + tokensStore.historyTokens.find((t) => t.token === tokenStr) !== + undefined ); }, addPendingTokenToHistory: function (tokenStr: string) { const receiveStore = useReceiveTokensStore(); if (this.tokenAlreadyInHistory(tokenStr)) { - notifySuccess("Ecash already in history") + notifySuccess("Ecash already in history"); receiveStore.showReceiveTokens = false; return; } const tokensStore = useTokensStore(); const decodedToken = token.decode(tokenStr); if (decodedToken == undefined) { - throw Error('could not decode token') + throw Error("could not decode token"); } // get amount from decodedToken.token.proofs[..].amount - const amount = token.getProofs(decodedToken).reduce( - (sum, el) => (sum += el.amount), - 0 - ); + const amount = token + .getProofs(decodedToken) + .reduce((sum, el) => (sum += el.amount), 0); - const mintUrl = token.getMint(decodedToken) - const unit = token.getUnit(decodedToken) + const mintUrl = token.getMint(decodedToken); + const unit = token.getUnit(decodedToken); tokensStore.addPendingToken({ amount: amount, serializedProofs: tokenStr, mint: mintUrl, - unit: unit + unit: unit, }); receiveStore.showReceiveTokens = false; }, @@ -210,7 +224,7 @@ export const useNPCStore = defineStore("npc", { getBalance: async function (): Promise { const authHeader = await this.generateNip98Event( `${this.baseURL}/api/v1/balance`, - "GET" + "GET", ); try { const response = await fetch(`${this.baseURL}/api/v1/balance`, { @@ -218,22 +232,22 @@ export const useNPCStore = defineStore("npc", { headers: { Authorization: `Nostr ${authHeader}`, }, - }) + }); // deserialize the response to NPCBalance - const balance: NPCBalance = await response.json() + const balance: NPCBalance = await response.json(); if (balance.error) { - return 0 + return 0; } - return balance.data + return balance.data; } catch (e) { - console.error(e) - return 0 + console.error(e); + return 0; } }, getClaim: async function (): Promise { const authHeader = await this.generateNip98Event( `${this.baseURL}/api/v1/claim`, - "GET" + "GET", ); try { const response = await fetch(`${this.baseURL}/api/v1/claim`, { @@ -241,17 +255,17 @@ export const useNPCStore = defineStore("npc", { headers: { Authorization: `Nostr ${authHeader}`, }, - }) + }); // deserialize the response to NPCClaim - const claim: NPCClaim = await response.json() + const claim: NPCClaim = await response.json(); if (claim.error) { - return "" + return ""; } - return claim.data.token + return claim.data.token; } catch (e) { - console.error(e) - return "" + console.error(e); + return ""; } }, - } + }, }); diff --git a/src/stores/nwc.ts b/src/stores/nwc.ts index 607784e2..fb64c0a0 100644 --- a/src/stores/nwc.ts +++ b/src/stores/nwc.ts @@ -1,65 +1,79 @@ import { defineStore } from "pinia"; -import NDK, { NDKEvent, NDKNip07Signer, NDKNip46Signer, NDKFilter, NDKPrivateKeySigner, NDKKind, NDKSubscription } from "@nostr-dev-kit/ndk"; +import NDK, { + NDKEvent, + NDKNip07Signer, + NDKNip46Signer, + NDKFilter, + NDKPrivateKeySigner, + NDKKind, + NDKSubscription, +} from "@nostr-dev-kit/ndk"; import { useLocalStorage } from "@vueuse/core"; -import { bytesToHex } from '@noble/hashes/utils' // already an installed dependency -import { nip04, generateSecretKey, getPublicKey } from 'nostr-tools' -import { useMintsStore } from './mints' +import { bytesToHex } from "@noble/hashes/utils"; // already an installed dependency +import { nip04, generateSecretKey, getPublicKey } from "nostr-tools"; +import { useMintsStore } from "./mints"; import { useWalletStore } from "./wallet"; import { useProofsStore } from "./proofs"; import { notify, notifyError, notifyWarning } from "../js/notify"; import { useSettingsStore } from "./settings"; type NWCConnection = { - walletPublicKey: string, - walletPrivateKey: string, - connectionSecret: string, - connectionPublicKey: string, - allowanceLeft: number -} + walletPublicKey: string; + walletPrivateKey: string; + connectionSecret: string; + connectionPublicKey: string; + allowanceLeft: number; +}; type NWCCommand = { - method: string, - params: any -} + method: string; + params: any; +}; type NWCResult = { - result_type: string, - result: any -} + result_type: string; + result: any; +}; type NWCError = { - result_type: string, + result_type: string; error: { - code: string, - message: string - } -} + code: string; + message: string; + }; +}; const NWCKind = { NWCInfo: 13194, NWCRequest: 23194, - NWCResponse: 23195 -} + NWCResponse: 23195, +}; export const useNWCStore = defineStore("nwc", { state: () => ({ nwcEnabled: useLocalStorage("cashu.nwc.enabled", false), connections: useLocalStorage("cashu.nwc.connections", []), - supportedMethods: ["pay_invoice", "get_balance", "get_info", "list_transactions"], - relays: useLocalStorage("cashu.nwc.relays", useSettingsStore().defaultNostrRelays), + supportedMethods: [ + "pay_invoice", + "get_balance", + "get_info", + "list_transactions", + ], + relays: useLocalStorage( + "cashu.nwc.relays", + useSettingsStore().defaultNostrRelays, + ), blocking: false, ndk: new NDK(), subscriptions: [] as NDKSubscription[], showNWCDialog: false, showNWCData: { connection: {} as NWCConnection, connectionString: "" }, }), - getters: { - - }, + getters: {}, actions: { // ––––---------- NWC Command Handlers ––––---------- handleGetInfo: async function (nwcCommand: NWCCommand) { - console.log("### get_info", nwcCommand.method) + console.log("### get_info", nwcCommand.method); return { result_type: "get_info", result: { @@ -69,125 +83,131 @@ export const useNWCStore = defineStore("nwc", { network: "mainnet", block_height: 1, block_hash: "blockchain disrespectoor", - methods: this.supportedMethods - } - } + methods: this.supportedMethods, + }, + }; }, handleGetBalance: async function (nwcCommand: NWCCommand) { - const mintsStore = useMintsStore() - console.log("### get_balance", nwcCommand.method) + const mintsStore = useMintsStore(); + console.log("### get_balance", nwcCommand.method); return { result_type: "get_balance", result: { - balance: mintsStore.activeBalance * 1000 - } - } + balance: mintsStore.activeBalance * 1000, + }, + }; }, handlePayInvoice: async function (nwcCommand: NWCCommand) { - const invoice = nwcCommand.params.invoice - const amountMsat = nwcCommand.params.amount - console.log("### pay_invoice", nwcCommand.method) - console.log("### invoice", invoice) - console.log("### amountMsat", amountMsat) + const invoice = nwcCommand.params.invoice; + const amountMsat = nwcCommand.params.amount; + console.log("### pay_invoice", nwcCommand.method); + console.log("### invoice", invoice); + console.log("### amountMsat", amountMsat); // pay invoice - const walletStore = useWalletStore() + const walletStore = useWalletStore(); const proofsStore = useProofsStore(); - const mintStore = useMintsStore() + const mintStore = useMintsStore(); try { - await walletStore.decodeRequest(invoice) + await walletStore.decodeRequest(invoice); } catch (e) { - console.log("### error decoding invoice", e) + console.log("### error decoding invoice", e); return { result_type: nwcCommand.method, - error: { code: "INTERNAL", message: "Invalid invoice" } - } as NWCError + error: { code: "INTERNAL", message: "Invalid invoice" }, + } as NWCError; } // expect that the melt quote was requested - if (walletStore.payInvoiceData.meltQuote.response.amount == 0 || walletStore.payInvoiceData.meltQuote.error) { - notifyWarning("NWC: Error requesting melt quote") + if ( + walletStore.payInvoiceData.meltQuote.response.amount == 0 || + walletStore.payInvoiceData.meltQuote.error + ) { + notifyWarning("NWC: Error requesting melt quote"); return { result_type: nwcCommand.method, - error: { code: "INTERNAL", message: "Error requesting melt quote" } - } as NWCError + error: { code: "INTERNAL", message: "Error requesting melt quote" }, + } as NWCError; } - const maximumAmount = walletStore.payInvoiceData.meltQuote.response.amount + walletStore.payInvoiceData.meltQuote.response.fee_reserve + const maximumAmount = + walletStore.payInvoiceData.meltQuote.response.amount + + walletStore.payInvoiceData.meltQuote.response.fee_reserve; if (mintStore.activeUnit != "sat") { - notifyWarning("NWC: Active unit must be sats") + notifyWarning("NWC: Active unit must be sats"); return { result_type: nwcCommand.method, - error: { code: "INTERNAL", message: "Your active must be sats" } - } as NWCError + error: { code: "INTERNAL", message: "Your active must be sats" }, + } as NWCError; } if (maximumAmount > this.connections[0].allowanceLeft) { - notifyWarning("NWC: Allowance exceeded") + notifyWarning("NWC: Allowance exceeded"); return { result_type: nwcCommand.method, - error: { code: "QUOTA_EXCEEDED", message: "Your quota has exceeded" } - } as NWCError + error: { code: "QUOTA_EXCEEDED", message: "Your quota has exceeded" }, + } as NWCError; } try { - const meltData = await walletStore.melt() - const paidAmount = walletStore.payInvoiceData.meltQuote.response.amount + proofsStore.sumProofs(meltData.change); + const meltData = await walletStore.melt(); + const paidAmount = + walletStore.payInvoiceData.meltQuote.response.amount + + proofsStore.sumProofs(meltData.change); this.connections[0].allowanceLeft -= paidAmount; return { result_type: nwcCommand.method, result: { preimage: meltData.preimage, - } - } + }, + }; } catch (e) { return { result_type: nwcCommand.method, - error: { code: "INTERNAL", message: "Could not pay invoice" } - } as NWCError + error: { code: "INTERNAL", message: "Could not pay invoice" }, + } as NWCError; } - }, handleListTransactions: async function (nwcCommand: NWCCommand) { - console.log("### list_transactions", nwcCommand.method) + console.log("### list_transactions", nwcCommand.method); type nwcTransaction = { - type: string, - invoice: string, - description: string | null, - preimage: string | null, - payment_hash: string | null, - amount: number, - fees_paid: number | null, - created_at: number, - settled_at: number | null - expires_at: number | null - } - const walletStore = useWalletStore() - const from = nwcCommand.params.from || 0 - const until = nwcCommand.params.until || Math.floor(Date.now() / 1000) - const limit = nwcCommand.params.limit || 10 - const offset = nwcCommand.params.offset || 0 - const unpaid = nwcCommand.params.unpaid || false - const type = nwcCommand.params.type || undefined - + type: string; + invoice: string; + description: string | null; + preimage: string | null; + payment_hash: string | null; + amount: number; + fees_paid: number | null; + created_at: number; + settled_at: number | null; + expires_at: number | null; + }; + const walletStore = useWalletStore(); + const from = nwcCommand.params.from || 0; + const until = nwcCommand.params.until || Math.floor(Date.now() / 1000); + const limit = nwcCommand.params.limit || 10; + const offset = nwcCommand.params.offset || 0; + const unpaid = nwcCommand.params.unpaid || false; + const type = nwcCommand.params.type || undefined; - const invoiceHistory = walletStore.invoiceHistory - const transactionsHistory = invoiceHistory.filter((invoice) => { - const date = new Date(invoice.date) - const created_at = Math.floor(date.getTime() / 1000) - if (from && created_at < from) { - return false - } - if (until && created_at > until) { - return false - } - if (type && type == "incoming" && invoice.amount < 0) { - return false - } - if (type && type == "outgoing" && invoice.amount > 0) { - return false - } - if (unpaid && invoice.status == "paid") { - return false - } - return true - } - ).slice(offset, offset + limit) + const invoiceHistory = walletStore.invoiceHistory; + const transactionsHistory = invoiceHistory + .filter((invoice) => { + const date = new Date(invoice.date); + const created_at = Math.floor(date.getTime() / 1000); + if (from && created_at < from) { + return false; + } + if (until && created_at > until) { + return false; + } + if (type && type == "incoming" && invoice.amount < 0) { + return false; + } + if (type && type == "outgoing" && invoice.amount > 0) { + return false; + } + if (unpaid && invoice.status == "paid") { + return false; + } + return true; + }) + .slice(offset, offset + limit); // now create an array "transactions" out of nwcTransaction from transactionsHistory // // type = "incoming" if amount > 0 else "outgoing" @@ -196,10 +216,13 @@ export const useNWCStore = defineStore("nwc", { // settled_at = unix timestamp of date if status == "paid" else null const transactions = transactionsHistory.map((invoice) => { - let type = invoice.amount > 0 ? "incoming" : "outgoing" - let amount = Math.abs(invoice.amount) * 1000 - let created_at = Math.floor(new Date(invoice.date).getTime() / 1000) - let settled_at = invoice.status == "paid" ? Math.floor(new Date(invoice.date).getTime() / 1000) : null + let type = invoice.amount > 0 ? "incoming" : "outgoing"; + let amount = Math.abs(invoice.amount) * 1000; + let created_at = Math.floor(new Date(invoice.date).getTime() / 1000); + let settled_at = + invoice.status == "paid" + ? Math.floor(new Date(invoice.date).getTime() / 1000) + : null; return { type: type, invoice: invoice.bolt11, @@ -208,138 +231,164 @@ export const useNWCStore = defineStore("nwc", { fees_paid: 0, created_at: created_at, settled_at: settled_at, - } as nwcTransaction - }) + } as nwcTransaction; + }); return { result_type: "list_transactions", result: { - transactions: transactions - } - } + transactions: transactions, + }, + }; }, // ––––---------- NWC Connection ––––---------- - replyNWC: async function (result: NWCResult | NWCError, event: NDKEvent, conn: NWCConnection) { + replyNWC: async function ( + result: NWCResult | NWCError, + event: NDKEvent, + conn: NWCConnection, + ) { // reply to NWC with result let replyEvent = new NDKEvent(event.ndk); replyEvent.kind = 23195; - console.log("### replying with", JSON.stringify(result)) - replyEvent.content = await nip04.encrypt(conn.walletPrivateKey, event.author.pubkey, JSON.stringify(result)); - replyEvent.tags = [["p", event.author.pubkey], ["e", event.id]]; - console.log("### replyEvent", replyEvent) - console.log("### replying to", event.id) + console.log("### replying with", JSON.stringify(result)); + replyEvent.content = await nip04.encrypt( + conn.walletPrivateKey, + event.author.pubkey, + JSON.stringify(result), + ); + replyEvent.tags = [ + ["p", event.author.pubkey], + ["e", event.id], + ]; + console.log("### replyEvent", replyEvent); + console.log("### replying to", event.id); // await this.ndk.publish(replyEvent); - await replyEvent.publish() + await replyEvent.publish(); }, - parseNWCCommand: async function (command: string, event: NDKEvent, conn: NWCConnection) { + parseNWCCommand: async function ( + command: string, + event: NDKEvent, + conn: NWCConnection, + ) { // parse command to JSON object {method: 'pay_invoice', params: {invoice: '1234'}} - let nwcCommand: NWCCommand = JSON.parse(command) - let result: NWCResult | NWCError - console.log("### nwcCommand", nwcCommand) + let nwcCommand: NWCCommand = JSON.parse(command); + let result: NWCResult | NWCError; + console.log("### nwcCommand", nwcCommand); // parse "get_info" without params if (nwcCommand.method == "get_info") { - result = await this.handleGetInfo(nwcCommand) + result = await this.handleGetInfo(nwcCommand); } else if (nwcCommand.method == "get_balance") { - result = await this.handleGetBalance(nwcCommand) + result = await this.handleGetBalance(nwcCommand); } else if (nwcCommand.method == "pay_invoice") { if (this.blocking) { result = { result_type: nwcCommand.method, - error: { code: "INTERNAL", message: "Already processing a payment." } - } as NWCError + error: { + code: "INTERNAL", + message: "Already processing a payment.", + }, + } as NWCError; } - this.blocking = true + this.blocking = true; try { - result = await this.handlePayInvoice(nwcCommand) + result = await this.handlePayInvoice(nwcCommand); } catch (e) { - return + return; } finally { - this.blocking = false + this.blocking = false; } } else if (nwcCommand.method == "list_transactions") { - result = await this.handleListTransactions(nwcCommand) + result = await this.handleListTransactions(nwcCommand); } else { - console.log("### method not supported", nwcCommand.method) + console.log("### method not supported", nwcCommand.method); result = { result_type: nwcCommand.method, - error: { code: "NOT_IMPLEMENTED", message: "Method not supported" } + error: { code: "NOT_IMPLEMENTED", message: "Method not supported" }, } as NWCError; } - await this.replyNWC(result, event, conn) + await this.replyNWC(result, event, conn); }, getConnectionString: function (connection: NWCConnection) { - const walletPublicKeyHex = connection.walletPublicKey - const connectionSecretHex = connection.connectionSecret - return `nostr+walletconnect://${walletPublicKeyHex}?relay=${this.relays.join('&relay=')}&secret=${connectionSecretHex}` + const walletPublicKeyHex = connection.walletPublicKey; + const connectionSecretHex = connection.connectionSecret; + return `nostr+walletconnect://${walletPublicKeyHex}?relay=${this.relays.join("&relay=")}&secret=${connectionSecretHex}`; }, generateNWCConnection: async function () { - let conn: NWCConnection + let conn: NWCConnection; // NOTE: we only support one connection for now if (!this.connections.length) { - const sk = generateSecretKey() // `sk` is a Uint8Array - const walletPublicKeyHex = getPublicKey(sk) // `pk` is a hex string - const walletPrivateKeyHex = bytesToHex(sk) + const sk = generateSecretKey(); // `sk` is a Uint8Array + const walletPublicKeyHex = getPublicKey(sk); // `pk` is a hex string + const walletPrivateKeyHex = bytesToHex(sk); - const connectionSecret = generateSecretKey() - const connectionPublicKeyHex = getPublicKey(connectionSecret) - const connectionSecretHex = bytesToHex(connectionSecret) + const connectionSecret = generateSecretKey(); + const connectionPublicKeyHex = getPublicKey(connectionSecret); + const connectionSecretHex = bytesToHex(connectionSecret); conn = { walletPublicKey: walletPublicKeyHex, walletPrivateKey: walletPrivateKeyHex, connectionSecret: connectionSecretHex, connectionPublicKey: connectionPublicKeyHex, - allowanceLeft: 1000 + allowanceLeft: 1000, } as NWCConnection; - this.connections = this.connections.concat(conn) + this.connections = this.connections.concat(conn); } else { - conn = this.connections[0] + conn = this.connections[0]; } - const walletSigner = new NDKPrivateKeySigner(conn.walletPrivateKey) + const walletSigner = new NDKPrivateKeySigner(conn.walletPrivateKey); // close and delete all old subscriptions - this.unsubscribeNWC() - this.ndk = new NDK({ explicitRelayUrls: this.relays, signer: walletSigner }); + this.unsubscribeNWC(); + this.ndk = new NDK({ + explicitRelayUrls: this.relays, + signer: walletSigner, + }); this.ndk.connect(); const nip47InfoEvent = new NDKEvent(this.ndk); nip47InfoEvent.kind = NWCKind.NWCInfo; - nip47InfoEvent.content = this.supportedMethods.join(' ') + nip47InfoEvent.content = this.supportedMethods.join(" "); try { // let's fetch the info event from the relay to see if we need to republish it // use NWCKind.NWCInfo as an integer here - let filterInfoEvent: NDKFilter = { kinds: [NWCKind.NWCInfo], authors: [conn.walletPublicKey] }; + let filterInfoEvent: NDKFilter = { + kinds: [NWCKind.NWCInfo], + authors: [conn.walletPublicKey], + }; let eventsInfoEvent = await this.ndk.fetchEvents(filterInfoEvent); if (eventsInfoEvent.size === 0) { - await nip47InfoEvent.publish() - console.log("### published nip47InfoEvent", nip47InfoEvent) + await nip47InfoEvent.publish(); + console.log("### published nip47InfoEvent", nip47InfoEvent); } else { - console.log("### nip47InfoEvent already published") + console.log("### nip47InfoEvent already published"); } } catch (e) { - console.log("### could not publish nip47InfoEvent", nip47InfoEvent) - console.log("### error", e) + console.log("### could not publish nip47InfoEvent", nip47InfoEvent); + console.log("### error", e); } }, listenToNWCCommands: async function () { // if (!this.connections.length) { // await this.generateNWCConnection() // } - await this.generateNWCConnection() + await this.generateNWCConnection(); // we only support one connection for now - const conn = this.connections[0] + const conn = this.connections[0]; - const currentUnitTime = Math.floor(Date.now() / 1000) + const currentUnitTime = Math.floor(Date.now() / 1000); let filter = { kinds: [NWCKind.NWCRequest as NDKKind], since: currentUnitTime, authors: [conn.connectionPublicKey], - "#p": [conn.walletPublicKey] + "#p": [conn.walletPublicKey], } as NDKFilter; const sub = this.ndk.subscribe(filter); - console.log("### subscribing to NWC on relays: ", this.relays) - this.subscriptions.push(sub) + console.log("### subscribing to NWC on relays: ", this.relays); + this.subscriptions.push(sub); - sub.on("eose", () => console.log("All relays have reached the end of the event stream")); + sub.on("eose", () => + console.log("All relays have reached the end of the event stream"), + ); sub.on("close", () => console.log("Subscription closed")); sub.on("event", async (event) => { @@ -351,25 +400,29 @@ export const useNWCStore = defineStore("nwc", { // console.log("### event.tagValue('e')", event.tagValue("e")) // console.log("### event.content", event.content) if (event.kind != NWCKind.NWCRequest) { - return // ignore non-NWC events + return; // ignore non-NWC events } if (!this.nwcEnabled) { - console.log("### Received NWC command but NWC is disabled") - return + console.log("### Received NWC command but NWC is disabled"); + return; } - console.log("### NWC request!") - console.log("### event", event) - const decryptedContent = await nip04.decrypt(conn.connectionSecret, conn.walletPublicKey, event.content) + console.log("### NWC request!"); + console.log("### event", event); + const decryptedContent = await nip04.decrypt( + conn.connectionSecret, + conn.walletPublicKey, + event.content, + ); // console.log("### decryptedContent", decryptedContent) - await this.parseNWCCommand(decryptedContent, event, conn) + await this.parseNWCCommand(decryptedContent, event, conn); }); }, unsubscribeNWC: function () { - console.log("### unsubscribing from NWC") + console.log("### unsubscribing from NWC"); for (let sub of this.subscriptions) { - sub.stop() + sub.stop(); } - this.subscriptions = [] - } + this.subscriptions = []; + }, }, }); diff --git a/src/stores/p2pk.ts b/src/stores/p2pk.ts index d2437ed2..b45fc9f6 100644 --- a/src/stores/p2pk.ts +++ b/src/stores/p2pk.ts @@ -1,44 +1,40 @@ import { defineStore } from "pinia"; import { useLocalStorage } from "@vueuse/core"; -import { generateSecretKey, getPublicKey } from 'nostr-tools' -import { bytesToHex } from '@noble/hashes/utils' // already an installed dependency +import { generateSecretKey, getPublicKey } from "nostr-tools"; +import { bytesToHex } from "@noble/hashes/utils"; // already an installed dependency import { WalletProof } from "stores/mints"; import token from "src/js/token"; type P2PKKey = { - publicKey: string, - privateKey: string, - used: boolean, - usedCount: number -} + publicKey: string; + privateKey: string; + used: boolean; + usedCount: number; +}; export const useP2PKStore = defineStore("p2pk", { state: () => ({ p2pkKeys: useLocalStorage("cashu.P2PKKeys", []), showP2PKDialog: false, - showP2PKData: {} as P2PKKey + showP2PKData: {} as P2PKKey, }), - getters: { - - }, + getters: {}, actions: { haveThisKey: function (key: string) { - return ( - this.p2pkKeys.filter((m) => m.publicKey == key).length > 0 - ); + return this.p2pkKeys.filter((m) => m.publicKey == key).length > 0; }, isValidPubkey: function (key: string) { return key && key.length == 66; }, setPrivateKeyUsed: function (key: string) { - const thisKeys = this.p2pkKeys.filter((k) => k.privateKey == key) + const thisKeys = this.p2pkKeys.filter((k) => k.privateKey == key); if (thisKeys.length) { thisKeys[0].used = true; thisKeys[0].usedCount += 1; } }, showKeyDetails: function (key: string) { - const thisKeys = this.p2pkKeys.filter((k) => k.publicKey == key) + const thisKeys = this.p2pkKeys.filter((k) => k.publicKey == key); if (thisKeys.length) { this.showP2PKData = JSON.parse(JSON.stringify(thisKeys[0])); this.showP2PKDialog = true; @@ -46,21 +42,23 @@ export const useP2PKStore = defineStore("p2pk", { }, showLastKey: function () { if (this.p2pkKeys.length) { - this.showP2PKData = JSON.parse(JSON.stringify(this.p2pkKeys[this.p2pkKeys.length - 1])); + this.showP2PKData = JSON.parse( + JSON.stringify(this.p2pkKeys[this.p2pkKeys.length - 1]), + ); this.showP2PKDialog = true; } }, generateKeypair: function () { - let sk = generateSecretKey() // `sk` is a Uint8Array - let pk = "02" + getPublicKey(sk) // `pk` is a hex string - let skHex = bytesToHex(sk) + let sk = generateSecretKey(); // `sk` is a Uint8Array + let pk = "02" + getPublicKey(sk); // `pk` is a hex string + let skHex = bytesToHex(sk); const keyPair: P2PKKey = { publicKey: pk, privateKey: skHex, used: false, - usedCount: 0 - } - this.p2pkKeys = this.p2pkKeys.concat(keyPair) + usedCount: 0, + }; + this.p2pkKeys = this.p2pkKeys.concat(keyPair); }, getSecretP2PKPubkey: function (secret: string) { try { @@ -68,7 +66,7 @@ export const useP2PKStore = defineStore("p2pk", { if (secretObject[0] == "P2PK" && secretObject[1]["data"] != undefined) { return secretObject[1]["data"]; } - } catch { } + } catch {} return ""; }, isLocked: function (proofs: WalletProof[]) { @@ -78,7 +76,7 @@ export const useP2PKStore = defineStore("p2pk", { if (this.getSecretP2PKPubkey(secret)) { return true; } - } catch { } + } catch {} } return false; }, @@ -106,13 +104,11 @@ export const useP2PKStore = defineStore("p2pk", { const pubkey = this.getSecretP2PKPubkey(secret); if (pubkey && this.haveThisKey(pubkey)) { // NOTE: we assume all tokens are locked to the same key here! - return ( - this.p2pkKeys.filter((m) => m.publicKey == pubkey)[0].privateKey - ); + return this.p2pkKeys.filter((m) => m.publicKey == pubkey)[0] + .privateKey; } } return ""; - - } + }, }, }); diff --git a/src/stores/payment-request.ts b/src/stores/payment-request.ts index d3097d92..384c9cf7 100644 --- a/src/stores/payment-request.ts +++ b/src/stores/payment-request.ts @@ -1,6 +1,12 @@ import { defineStore } from "pinia"; import { useWalletStore } from "./wallet"; -import { decodePaymentRequest, PaymentRequest, PaymentRequestPayload, PaymentRequestTransport, PaymentRequestTransportType } from "@cashu/cashu-ts"; +import { + decodePaymentRequest, + PaymentRequest, + PaymentRequestPayload, + PaymentRequestTransport, + PaymentRequestTransportType, +} from "@cashu/cashu-ts"; import { useMintsStore } from "./mints"; import { useSendTokensStore } from "./sendTokensStore"; import { useNostrStore } from "./nostr"; @@ -9,15 +15,13 @@ import token from "src/js/token"; import { notify, notifyError, notifySuccess } from "src/js/notify"; import { useLocalStorage } from "@vueuse/core"; - export const usePRStore = defineStore("payment-request", { state: () => ({ showPRDialog: false, showPRKData: "" as string, enablePaymentRequest: useLocalStorage("cashu.pr.enable", false), }), - getters: { - }, + getters: {}, actions: { newPaymentRequest(amount?: number, memo?: string) { const walletStore = useWalletStore(); @@ -25,7 +29,7 @@ export const usePRStore = defineStore("payment-request", { }, decodePaymentRequest(pr: string) { console.log("decodePaymentRequest", pr); - const request: PaymentRequest = decodePaymentRequest(pr) + const request: PaymentRequest = decodePaymentRequest(pr); console.log("decodePaymentRequest", request); // activate the mint in the payment request if (request.mints && request.mints.length > 0) { @@ -41,7 +45,9 @@ export const usePRStore = defineStore("payment-request", { } if (!foundMint) { notifyError("We do not know the mint in the payment request"); - throw new Error(`We do not know the mint in the payment request: ${request.mints}`); + throw new Error( + `We do not know the mint in the payment request: ${request.mints}`, + ); } } @@ -73,7 +79,11 @@ export const usePRStore = defineStore("payment-request", { } } }, - async payNostrPaymentRequest(request: PaymentRequest, transport: PaymentRequestTransport, tokenStr: string) { + async payNostrPaymentRequest( + request: PaymentRequest, + transport: PaymentRequestTransport, + tokenStr: string, + ) { console.log("payNostrPaymentRequest", request, tokenStr); console.log("transport", transport); const nostrStore = useNostrStore(); @@ -92,14 +102,21 @@ export const usePRStore = defineStore("payment-request", { }; const paymentPayloadString = JSON.stringify(paymentPayload); try { - await nostrStore.sendNip17DirectMessageToNprofile(transport.target, paymentPayloadString); + await nostrStore.sendNip17DirectMessageToNprofile( + transport.target, + paymentPayloadString, + ); } catch (error) { console.error("Error paying payment request:", error); notifyError("Could not pay request"); } notifySuccess("Payment sent"); }, - async payPostPaymentRequest(request: PaymentRequest, transport: PaymentRequestTransport, tokenStr: string) { + async payPostPaymentRequest( + request: PaymentRequest, + transport: PaymentRequestTransport, + tokenStr: string, + ) { console.log("payPostPaymentRequest", request, tokenStr); // get the endpoint from the transport target and make an HTTP POST request with the paymentPayload as the body const decodedToken = token.decode(tokenStr); @@ -118,18 +135,16 @@ export const usePRStore = defineStore("payment-request", { try { const response = await fetch(transport.target, { headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", }, method: "POST", body: paymentPayloadString, }); notifySuccess("Payment sent"); - } catch (error) { console.error("Error paying payment request:", error); notifyError("Could not pay request"); } - }, }, }); diff --git a/src/stores/proofs.ts b/src/stores/proofs.ts index 72d88ce8..1fda1ba2 100644 --- a/src/stores/proofs.ts +++ b/src/stores/proofs.ts @@ -1,6 +1,12 @@ import { defineStore } from "pinia"; import { useMintsStore, WalletProof } from "./mints"; -import { Proof, getEncodedToken, getEncodedTokenV4, Token, TokenEntry } from "@cashu/cashu-ts"; +import { + Proof, + getEncodedToken, + getEncodedTokenV4, + Token, + TokenEntry, +} from "@cashu/cashu-ts"; export const useProofsStore = defineStore("proofs", { state: () => ({}), @@ -19,8 +25,7 @@ export const useProofsStore = defineStore("proofs", { mintStore.proofs .filter((pr) => pr.secret === p.secret) .forEach((pr) => (pr.reserved = reserved)); - } - ); + }); }, getUnreservedProofs: function (proofs: WalletProof[]) { return proofs.filter((p) => !p.reserved); @@ -31,14 +36,14 @@ export const useProofsStore = defineStore("proofs", { let uniqueIds = [...new Set(proofs.map((p) => p.id))]; // keysets with these uniqueIds let keysets = mintStore.mints.flatMap((m) => - m.keysets.filter((k) => uniqueIds.includes(k.id)) + m.keysets.filter((k) => uniqueIds.includes(k.id)), ); if (keysets.length === 0) { throw new Error("No keysets found for proofs"); } // mints that have any of the keyset.id let mints = mintStore.mints.filter((m) => - m.keysets.some((k) => uniqueIds.includes(k.id)) + m.keysets.some((k) => uniqueIds.includes(k.id)), ); if (mints.length === 0) { throw new Error("No mints found for proofs"); @@ -57,7 +62,6 @@ export const useProofsStore = defineStore("proofs", { return getEncodedToken(token); } - // // what we put into the JSON // let mintsJson = mints.map((m) => [{ url: m.url, ids: m.keysets }][0]); // let tokenV3 = { @@ -72,7 +76,7 @@ export const useProofsStore = defineStore("proofs", { let uniqueIds = [...new Set(proofs.map((p) => p.id))]; // mints that have any of the keyset IDs let mints_keysets = mintStore.mints.filter((m) => - m.keysets.some((k) => uniqueIds.includes(k.id)) + m.keysets.some((k) => uniqueIds.includes(k.id)), ); // what we put into the JSON let mints = mints_keysets.map((m) => [{ url: m.url, ids: m.keysets }][0]); diff --git a/src/stores/restore.ts b/src/stores/restore.ts index 12c8e341..a8876e0f 100644 --- a/src/stores/restore.ts +++ b/src/stores/restore.ts @@ -1,7 +1,7 @@ import { defineStore } from "pinia"; import { useLocalStorage } from "@vueuse/core"; -import { generateSecretKey, getPublicKey } from 'nostr-tools' -import { bytesToHex } from '@noble/hashes/utils' // already an installed dependency +import { generateSecretKey, getPublicKey } from "nostr-tools"; +import { bytesToHex } from "@noble/hashes/utils"; // already an installed dependency import { useWalletStore } from "./wallet"; import { CashuMint, CashuWallet, CheckStateEnum, Proof } from "@cashu/cashu-ts"; import { useMintsStore } from "./mints"; @@ -13,17 +13,21 @@ const MAX_GAP = 2; export const useRestoreStore = defineStore("restore", { state: () => ({ - showRestoreDialog: useLocalStorage("cashu.restore.showRestoreDialog", false), + showRestoreDialog: useLocalStorage( + "cashu.restore.showRestoreDialog", + false, + ), restoringState: false, restoringMint: "", - mnemonicToRestore: useLocalStorage("cashu.restore.mnemonicToRestore", ""), + mnemonicToRestore: useLocalStorage( + "cashu.restore.mnemonicToRestore", + "", + ), restoreProgress: 0, restoreCounter: 0, restoreStatus: "", }), - getters: { - - }, + getters: {}, actions: { restoreMint: async function (url: string) { this.restoringState = true; @@ -64,19 +68,26 @@ export const useRestoreStore = defineStore("restore", { for (const keyset of keysets) { console.log(`Restoring keyset ${keyset.id} with unit ${keyset.unit}`); const bip39Seed = walletStore.mnemonicToSeedSync(mnemonic); - const wallet = new CashuWallet(mint, { bip39seed: bip39Seed, unit: keyset.unit }); + const wallet = new CashuWallet(mint, { + bip39seed: bip39Seed, + unit: keyset.unit, + }); let start = 0; let emptyBatchCount = 0; let restoreProofs: Proof[] = []; while (emptyBatchCount < MAX_GAP) { console.log(`Restoring proofs ${start} to ${start + BATCH_SIZE}`); - const proofs = (await wallet.restore(start, BATCH_SIZE, { keysetId: keyset.id })).proofs; + const proofs = ( + await wallet.restore(start, BATCH_SIZE, { keysetId: keyset.id }) + ).proofs; if (proofs.length === 0) { console.log(`No proofs found for keyset ${keyset.id}`); emptyBatchCount++; } else { - console.log(`> Restored ${proofs.length} proofs with sum ${proofs.reduce((s, p) => s + p.amount, 0)}`); + console.log( + `> Restored ${proofs.length} proofs with sum ${proofs.reduce((s, p) => s + p.amount, 0)}`, + ); restoreProofs = restoreProofs.concat(proofs); emptyBatchCount = 0; totalSteps += MAX_GAP * 2; @@ -87,28 +98,39 @@ export const useRestoreStore = defineStore("restore", { currentStep++; this.restoreProgress = currentStep / totalSteps; - } let restoredProofs: Proof[] = []; for (let i = 0; i < restoreProofs.length; i += BATCH_SIZE) { this.restoreStatus = `Checking proofs ${i} to ${i + BATCH_SIZE} for keyset ${keyset.id}`; const checkRestoreProofs = restoreProofs.slice(i, i + BATCH_SIZE); - const proofStates = await wallet.checkProofsStates(checkRestoreProofs); - const spentProofs = checkRestoreProofs.filter((p, i) => proofStates[i].state === CheckStateEnum.SPENT); + const proofStates = + await wallet.checkProofsStates(checkRestoreProofs); + const spentProofs = checkRestoreProofs.filter( + (p, i) => proofStates[i].state === CheckStateEnum.SPENT, + ); const spentProofsSecrets = spentProofs.map((p) => p.secret); - const unspentProofs = checkRestoreProofs.filter((p) => !spentProofsSecrets.includes(p.secret)); + const unspentProofs = checkRestoreProofs.filter( + (p) => !spentProofsSecrets.includes(p.secret), + ); if (unspentProofs.length > 0) { - console.log(`Found ${unspentProofs.length} unspent proofs with sum ${unspentProofs.reduce((s, p) => s + p.amount, 0)}`); + console.log( + `Found ${unspentProofs.length} unspent proofs with sum ${unspentProofs.reduce((s, p) => s + p.amount, 0)}`, + ); } - const newProofs = unspentProofs.filter((p) => !mintStore.proofs.some((pr) => pr.secret === p.secret)); + const newProofs = unspentProofs.filter( + (p) => !mintStore.proofs.some((pr) => pr.secret === p.secret), + ); mintStore.addProofs(newProofs); restoredProofs = restoredProofs.concat(newProofs); currentStep++; this.restoreProgress = currentStep / totalSteps; } const restoredAmount = restoredProofs.reduce((s, p) => s + p.amount, 0); - const restoredAmountStr = useUiStore().formatCurrency(restoredAmount, keyset.unit); + const restoredAmountStr = useUiStore().formatCurrency( + restoredAmount, + keyset.unit, + ); if (restoredAmount > 0) { notifySuccess(`Restored ${restoredAmountStr}`); restoredSomething = true; diff --git a/src/stores/sendTokensStore.ts b/src/stores/sendTokensStore.ts index 8f94c817..affe75b0 100644 --- a/src/stores/sendTokensStore.ts +++ b/src/stores/sendTokensStore.ts @@ -29,6 +29,6 @@ export const useSendTokensStore = defineStore("sendTokensStore", { this.sendData.tokensBase64 = ""; this.sendData.p2pkPubkey = ""; this.sendData.paymentRequest = undefined; - } + }, }, }); diff --git a/src/stores/settings.ts b/src/stores/settings.ts index 1f2e0a5a..83fe9282 100644 --- a/src/stores/settings.ts +++ b/src/stores/settings.ts @@ -1,15 +1,31 @@ import { defineStore } from "pinia"; import { useLocalStorage } from "@vueuse/core"; -const defaultNostrRelays = ["wss://relay.damus.io", "wss://relay.8333.space/", "wss://nos.lol"] +const defaultNostrRelays = [ + "wss://relay.damus.io", + "wss://relay.8333.space/", + "wss://nos.lol", +]; export const useSettingsStore = defineStore("settings", { state: () => { return { - getBitcoinPrice: useLocalStorage("cashu.settings.getBitcoinPrice", false), - checkSentTokens: useLocalStorage("cashu.settings.checkSentTokens", true), - defaultNostrRelays: useLocalStorage("cashu.settings.defaultNostrRelays", defaultNostrRelays), - includeFeesInSendAmount: useLocalStorage("cashu.settings.includeFeesInSendAmount", false), - } - } + getBitcoinPrice: useLocalStorage( + "cashu.settings.getBitcoinPrice", + false, + ), + checkSentTokens: useLocalStorage( + "cashu.settings.checkSentTokens", + true, + ), + defaultNostrRelays: useLocalStorage( + "cashu.settings.defaultNostrRelays", + defaultNostrRelays, + ), + includeFeesInSendAmount: useLocalStorage( + "cashu.settings.includeFeesInSendAmount", + false, + ), + }; + }, }); diff --git a/src/stores/tokens.ts b/src/stores/tokens.ts index 9564b51c..b87326f4 100644 --- a/src/stores/tokens.ts +++ b/src/stores/tokens.ts @@ -40,7 +40,7 @@ export const useTokensStore = defineStore("tokens", { mint: string; unit: string; fee?: number; - paymentRequest?: PaymentRequest + paymentRequest?: PaymentRequest; }) { this.historyTokens.push({ status: "paid", @@ -79,15 +79,27 @@ export const useTokensStore = defineStore("tokens", { paymentRequest, }); }, - editHistoryToken(tokenToEdit: string, options?: { newAmount?: number; addAmount?: number, newStatus?: "paid" | "pending", newToken?: string, newFee?: number }): HistoryToken | undefined { - const index = this.historyTokens.findIndex((t) => t.token === tokenToEdit); + editHistoryToken( + tokenToEdit: string, + options?: { + newAmount?: number; + addAmount?: number; + newStatus?: "paid" | "pending"; + newToken?: string; + newFee?: number; + }, + ): HistoryToken | undefined { + const index = this.historyTokens.findIndex( + (t) => t.token === tokenToEdit, + ); if (index >= 0) { if (options) { if (options.newToken) { this.historyTokens[index].token = options.newToken; } if (options.newAmount) { - this.historyTokens[index].amount = options.newAmount * Math.sign(this.historyTokens[index].amount); + this.historyTokens[index].amount = + options.newAmount * Math.sign(this.historyTokens[index].amount); } if (options.addAmount) { if (this.historyTokens[index].amount > 0) { @@ -95,7 +107,6 @@ export const useTokensStore = defineStore("tokens", { } else { this.historyTokens[index].amount -= options.addAmount; } - } if (options.newStatus) { this.historyTokens[index].status = options.newStatus; @@ -121,7 +132,7 @@ export const useTokensStore = defineStore("tokens", { if (index >= 0) { this.historyTokens.splice(index, 1); } - } + }, }, }); diff --git a/src/stores/ui.ts b/src/stores/ui.ts index cf02305f..0df91b64 100644 --- a/src/stores/ui.ts +++ b/src/stores/ui.ts @@ -1,13 +1,19 @@ import { defineStore } from "pinia"; import { useMintsStore } from "./mints"; import { useLocalStorage } from "@vueuse/core"; -import { notifyApiError, notifyError, notifySuccess, notifyWarning, notify } from "../js/notify"; +import { + notifyApiError, + notifyError, + notifySuccess, + notifyWarning, + notify, +} from "../js/notify"; const unitTickerShortMap = { sat: "sats", usd: "USD", eur: "EUR", - msat: "msats" + msat: "msats", }; export const useUiStore = defineStore("ui", { @@ -18,8 +24,7 @@ export const useUiStore = defineStore("ui", { showSendDialog: false, showReceiveDialog: false, tab: useLocalStorage("cashu.ui.tab", "history" as string), - expandHistory: - useLocalStorage("cashu.ui.expandHistory", true as boolean), + expandHistory: useLocalStorage("cashu.ui.expandHistory", true as boolean), globalMutexLock: false, }), actions: { @@ -30,11 +35,11 @@ export const useUiStore = defineStore("ui", { while (this.globalMutexLock) { if (retries >= nRetries) { - notify("Please try again.") + notify("Please try again."); throw new Error("Failed to acquire global mutex lock"); } retries++; - await new Promise(resolve => setTimeout(resolve, retryInterval)); + await new Promise((resolve) => setTimeout(resolve, retryInterval)); } this.globalMutexLock = true; @@ -52,7 +57,11 @@ export const useUiStore = defineStore("ui", { fromMsat: function (value: number) { return new Intl.NumberFormat(navigator.language).format(value) + " msat"; }, - formatCurrency: function (value: number, currency: string, showBalance = false) { + formatCurrency: function ( + value: number, + currency: string, + showBalance = false, + ) { if (currency == undefined) { currency = "sat"; } diff --git a/src/stores/wallet.ts b/src/stores/wallet.ts index 71df679e..ee056a86 100644 --- a/src/stores/wallet.ts +++ b/src/stores/wallet.ts @@ -6,33 +6,53 @@ import { useProofsStore } from "./proofs"; import { useTokensStore } from "./tokens"; import { useReceiveTokensStore } from "./receiveTokensStore"; import { useUiStore } from "src/stores/ui"; -import { useP2PKStore } from "src/stores/p2pk" -import { useSendTokensStore } from "src/stores/sendTokensStore" +import { useP2PKStore } from "src/stores/p2pk"; +import { useSendTokensStore } from "src/stores/sendTokensStore"; import { usePRStore } from "./payment-request"; import * as _ from "underscore"; import token from "src/js/token"; -import { notifyApiError, notifyError, notifySuccess, notifyWarning, notify } from "src/js/notify"; -import { CashuMint, CashuWallet, Proof, MintQuotePayload, MeltQuotePayload, MeltQuoteResponse, CheckStateEnum, MeltQuoteState, MintQuoteState, PaymentRequest, PaymentRequestTransportType, PaymentRequestTransport, decodePaymentRequest } from "@cashu/cashu-ts"; -import { hashToCurve } from '@cashu/crypto/modules/common'; +import { + notifyApiError, + notifyError, + notifySuccess, + notifyWarning, + notify, +} from "src/js/notify"; +import { + CashuMint, + CashuWallet, + Proof, + MintQuotePayload, + MeltQuotePayload, + MeltQuoteResponse, + CheckStateEnum, + MeltQuoteState, + MintQuoteState, + PaymentRequest, + PaymentRequestTransportType, + PaymentRequestTransport, + decodePaymentRequest, +} from "@cashu/cashu-ts"; +import { hashToCurve } from "@cashu/crypto/modules/common"; import * as bolt11Decoder from "light-bolt11-decoder"; import { bech32 } from "bech32"; import axios from "axios"; import { date } from "quasar"; import { useNostrStore } from "./nostr"; -import { v4 as uuidv4 } from 'uuid'; +import { v4 as uuidv4 } from "uuid"; // bip39 requires Buffer // import { Buffer } from 'buffer'; // window.Buffer = Buffer; import { generateMnemonic, mnemonicToSeedSync } from "@scure/bip39"; -import { wordlist } from '@scure/bip39/wordlists/english'; +import { wordlist } from "@scure/bip39/wordlists/english"; // HACK: this is a workaround so that the catch block in the melt function does not throw an error when the user exits the app // before the payment is completed. This is necessary because the catch block in the melt function would otherwise remove all // quotes from the invoiceHistory and the user would not be able to pay the invoice again after reopening the app. let isUnloading = false; -window.addEventListener('beforeunload', () => { +window.addEventListener("beforeunload", () => { isUnloading = true; }); @@ -65,10 +85,16 @@ export const useWalletStore = defineStore("wallet", { mnemonic: useLocalStorage("cashu.mnemonic", ""), invoiceHistory: useLocalStorage( "cashu.invoiceHistory", - [] as InvoiceHistory[] + [] as InvoiceHistory[], + ), + keysetCounters: useLocalStorage( + "cashu.keysetCounters", + [] as KeysetCounter[], + ), + oldMnemonicCounters: useLocalStorage( + "cashu.oldMnemonicCounters", + [] as { mnemonic: string; keysetCounters: KeysetCounter[] }[], ), - keysetCounters: useLocalStorage("cashu.keysetCounters", [] as KeysetCounter[]), - oldMnemonicCounters: useLocalStorage("cashu.oldMnemonicCounters", [] as { mnemonic: string, keysetCounters: KeysetCounter[] }[]), invoiceData: {} as InvoiceHistory, payInvoiceData: { blocking: false, @@ -90,7 +116,7 @@ export const useWalletStore = defineStore("wallet", { sat: 0, memo: "", bolt11: "", - } as { sat: number, memo: string, bolt11: string } | null, + } as { sat: number; memo: string; bolt11: string } | null, lnurlpay: { domain: "", callback: "", @@ -107,7 +133,12 @@ export const useWalletStore = defineStore("wallet", { amount: null, comment: "", quote: "", - } as { request: string, amount: number | null, comment: string, quote: string }, + } as { + request: string; + amount: number | null; + comment: string; + quote: string; + }, }, }; }, @@ -120,12 +151,18 @@ export const useWalletStore = defineStore("wallet", { } const mnemonic: string = this.mnemonic; const bip39Seed = mnemonicToSeedSync(mnemonic); - const wallet = new CashuWallet(mint, { keys: mints.activeKeys, keysets: mints.activeKeysets, mintInfo: mints.activeInfo, bip39seed: bip39Seed, unit: mints.activeUnit }); + const wallet = new CashuWallet(mint, { + keys: mints.activeKeys, + keysets: mints.activeKeysets, + mintInfo: mints.activeInfo, + bip39seed: bip39Seed, + unit: mints.activeUnit, + }); return wallet; }, seed(): Uint8Array { return mnemonicToSeedSync(this.mnemonic); - } + }, }, actions: { mnemonicToSeedSync: function (mnemonic: string): Uint8Array { @@ -163,7 +200,9 @@ export const useWalletStore = defineStore("wallet", { if (keysets == null || keysets.length == 0) { throw new Error("no keysets found."); } - const unitKeysets = mintStore.activeMint().unitKeysets(mintStore.activeUnit) + const unitKeysets = mintStore + .activeMint() + .unitKeysets(mintStore.activeUnit); if (unitKeysets == null || unitKeysets.length == 0) { console.error("no keysets found for unit", mintStore.activeUnit); throw new Error("no keysets found for unit"); @@ -175,17 +214,19 @@ export const useWalletStore = defineStore("wallet", { // - order by id (whether it is hex or base64) // - order by input_fee_ppk (ascending) TODO: this is not implemented yet // - select the first one - const activeKeysets = unitKeysets.filter(k => k.active) - const hexKeysets = activeKeysets.filter(k => k.id.startsWith("00")) - const base64Keysets = activeKeysets.filter(k => !k.id.startsWith("00")) - const sortedKeysets = hexKeysets.concat(base64Keysets) + const activeKeysets = unitKeysets.filter((k) => k.active); + const hexKeysets = activeKeysets.filter((k) => k.id.startsWith("00")); + const base64Keysets = activeKeysets.filter((k) => !k.id.startsWith("00")); + const sortedKeysets = hexKeysets.concat(base64Keysets); // const sortedKeysets = _.sortBy(activeKeysets, k => [k.id, k.input_fee_ppk]) if (sortedKeysets.length == 0) { console.error("no active keysets found for unit", mintStore.activeUnit); throw new Error("no active keysets found for unit"); } const keyset_id = sortedKeysets[0].id; - const keys = mintStore.activeMint().mint.keys.find((k) => k.id === keyset_id); + const keys = mintStore + .activeMint() + .mint.keys.find((k) => k.id === keyset_id); // if (keys) { // this.wallet.keys = keys; // } @@ -210,8 +251,11 @@ export const useWalletStore = defineStore("wallet", { } return chunks; }, - coinSelectSpendBase64: function (proofs: WalletProof[], amount: number): WalletProof[] { - const base64Proofs = proofs.filter(p => !p.id.startsWith("00")) + coinSelectSpendBase64: function ( + proofs: WalletProof[], + amount: number, + ): WalletProof[] { + const base64Proofs = proofs.filter((p) => !p.id.startsWith("00")); if (base64Proofs.length > 0) { base64Proofs.sort((a, b) => b.amount - a.amount); let sum = 0; @@ -228,12 +272,20 @@ export const useWalletStore = defineStore("wallet", { } return []; }, - coinSelect: function (proofs: WalletProof[], amount: number, includeFees: boolean = false): WalletProof[] { + coinSelect: function ( + proofs: WalletProof[], + amount: number, + includeFees: boolean = false, + ): WalletProof[] { if (proofs.reduce((s, t) => (s += t.amount), 0) < amount) { // there are not enough proofs to pay the amount return []; } - const { send: selectedProofs, keep: _ } = this.wallet.selectProofsToSend(proofs, amount, includeFees); + const { send: selectedProofs, keep: _ } = this.wallet.selectProofsToSend( + proofs, + amount, + includeFees, + ); const selectedWalletProofs = selectedProofs.map((p) => { return { ...p, reserved: false } as WalletProof; }); @@ -246,10 +298,10 @@ export const useWalletStore = defineStore("wallet", { const spendableProofs = proofsStore.getUnreservedProofs(proofs); if (proofsStore.sumProofs(spendableProofs) < amount) { const balance = mintStore.activeMintBalance(); - const unit = mintStore.activeUnit + const unit = mintStore.activeUnit; notifyWarning( "Balance is too low", - `${uIStore.formatCurrency(balance, unit)} is not enough to pay ${uIStore.formatCurrency(amount, unit)}.` + `${uIStore.formatCurrency(balance, unit)} is not enough to pay ${uIStore.formatCurrency(amount, unit)}.`, ); throw Error("Balance too low"); } @@ -258,10 +310,18 @@ export const useWalletStore = defineStore("wallet", { getFeesForProofs: function (proofs: Proof[]): number { return this.wallet.getFeesForProofs(proofs); }, - sendToLock: async function (proofs: WalletProof[], amount: number, receiverPubkey: string) { + sendToLock: async function ( + proofs: WalletProof[], + amount: number, + receiverPubkey: string, + ) { const spendableProofs = this.spendableProofs(proofs, amount); const proofsToSend = this.coinSelect(spendableProofs, amount); - const { keep: keepProofs, send: sendProofs } = await this.wallet.send(amount, proofsToSend, { pubkey: receiverPubkey }) + const { keep: keepProofs, send: sendProofs } = await this.wallet.send( + amount, + proofsToSend, + { pubkey: receiverPubkey }, + ); const mintStore = useMintsStore(); // note: we do not store sendProofs in the proofs store but // expect from the caller to store it in the history @@ -269,7 +329,12 @@ export const useWalletStore = defineStore("wallet", { mintStore.removeProofs(proofsToSend); return { keepProofs, sendProofs }; }, - send: async function (proofs: WalletProof[], amount: number, invalidate: boolean = false, includeFees: boolean = false): Promise<{ keepProofs: Proof[], sendProofs: Proof[] }> { + send: async function ( + proofs: WalletProof[], + amount: number, + invalidate: boolean = false, + includeFees: boolean = false, + ): Promise<{ keepProofs: Proof[]; sendProofs: Proof[] }> { /* splits proofs so the user can keep firstProofs, send scndProofs. then sets scndProofs as reserved. @@ -277,17 +342,19 @@ export const useWalletStore = defineStore("wallet", { if invalidate, scndProofs (the one to send) are invalidated */ const mintStore = useMintsStore(); - const proofsStore = useProofsStore() + const proofsStore = useProofsStore(); const uIStore = useUiStore(); let proofsToSend: WalletProof[] = []; - const keysetId = this.getKeyset() + const keysetId = this.getKeyset(); await uIStore.lockMutex(); try { const spendableProofs = this.spendableProofs(proofs, amount); proofsToSend = this.coinSelect(spendableProofs, amount, includeFees); const totalAmount = proofsToSend.reduce((s, t) => (s += t.amount), 0); - const fees = includeFees ? this.wallet.getFeesForProofs(proofsToSend) : 0; + const fees = includeFees + ? this.wallet.getFeesForProofs(proofsToSend) + : 0; const targetAmount = amount + fees; let keepProofs: Proof[] = []; @@ -296,15 +363,21 @@ export const useWalletStore = defineStore("wallet", { if (totalAmount != targetAmount) { const counter = this.keysetCounter(keysetId); proofsToSend = this.coinSelect(spendableProofs, targetAmount, true); - ({ keep: keepProofs, send: sendProofs } = await this.wallet.send(targetAmount, proofsToSend, { counter, proofsWeHave: spendableProofs })); - this.increaseKeysetCounter(keysetId, keepProofs.length + sendProofs.length); + ({ keep: keepProofs, send: sendProofs } = await this.wallet.send( + targetAmount, + proofsToSend, + { counter, proofsWeHave: spendableProofs }, + )); + this.increaseKeysetCounter( + keysetId, + keepProofs.length + sendProofs.length, + ); mintStore.addProofs(keepProofs); mintStore.addProofs(sendProofs); mintStore.removeProofs(proofsToSend); } else if (totalAmount == targetAmount) { - keepProofs = []; - sendProofs = proofsToSend + sendProofs = proofsToSend; } else { throw new Error("could not split proofs."); } @@ -349,21 +422,29 @@ export const useWalletStore = defineStore("wallet", { let proofs = token.getProofs(tokenJson); // activate the mint and the unit - await mintStore.activateMintUrl(token.getMint(tokenJson), false, false, tokenJson.unit); + await mintStore.activateMintUrl( + token.getMint(tokenJson), + false, + false, + tokenJson.unit, + ); const inputAmount = proofs.reduce((s, t) => (s += t.amount), 0); await uIStore.lockMutex(); try { // redeem - const keysetId = this.getKeyset() - const counter = this.keysetCounter(keysetId) + const keysetId = this.getKeyset(); + const counter = this.keysetCounter(keysetId); // const preference = this.outputAmountSelect(amount); const privkey = receiveStore.receiveData.p2pkPrivateKey; // const decodedToken = getDecodedToken(receiveStore.receiveData.tokensBase64); // let tokenCts: Token - let proofs: Proof[] + let proofs: Proof[]; try { - proofs = await this.wallet.receive(receiveStore.receiveData.tokensBase64, { counter, privkey, proofsWeHave: mintStore.activeProofs }) + proofs = await this.wallet.receive( + receiveStore.receiveData.tokensBase64, + { counter, privkey, proofsWeHave: mintStore.activeProofs }, + ); this.increaseKeysetCounter(keysetId, proofs.length); } catch (error: any) { console.error(error); @@ -378,11 +459,22 @@ export const useWalletStore = defineStore("wallet", { const outputAmount = proofs.reduce((s, t) => (s += t.amount), 0); // if token is already in history, set to paid, else add to history - if (tokenStore.historyTokens.find((t) => t.token === receiveStore.receiveData.tokensBase64 && t.amount > 0)) { + if ( + tokenStore.historyTokens.find( + (t) => + t.token === receiveStore.receiveData.tokensBase64 && t.amount > 0, + ) + ) { tokenStore.setTokenPaid(receiveStore.receiveData.tokensBase64); } else { // if this is a self-sent token, we will find an outgoing token with the inverse amount - if (tokenStore.historyTokens.find((t) => t.token === receiveStore.receiveData.tokensBase64 && t.amount < 0)) { + if ( + tokenStore.historyTokens.find( + (t) => + t.token === receiveStore.receiveData.tokensBase64 && + t.amount < 0, + ) + ) { tokenStore.setTokenPaid(receiveStore.receiveData.tokensBase64); } const fee = inputAmount - outputAmount; @@ -391,13 +483,15 @@ export const useWalletStore = defineStore("wallet", { serializedProofs: receiveStore.receiveData.tokensBase64, unit: mintStore.activeUnit, mint: mintStore.activeMintUrl, - fee: fee + fee: fee, }); } - if (!!window.navigator.vibrate) navigator.vibrate(200); - notifySuccess("Received " + uIStore.formatCurrency(outputAmount, mintStore.activeUnit)); + notifySuccess( + "Received " + + uIStore.formatCurrency(outputAmount, mintStore.activeUnit), + ); } catch (error: any) { console.error(error); notifyApiError(error); @@ -408,7 +502,6 @@ export const useWalletStore = defineStore("wallet", { // } }, - // /mint /** * Ask the mint to generate an invoice for the given amount @@ -426,11 +519,10 @@ export const useWalletStore = defineStore("wallet", { try { // create MintQuotePayload(this.invoiceData.amount) payload const payload: MintQuotePayload = { - amount: this.invoiceData.amount, unit: mintStore.activeUnit + amount: this.invoiceData.amount, + unit: mintStore.activeUnit, }; - const data = await mintStore.activeMint().api.createMintQuote( - payload - ); + const data = await mintStore.activeMint().api.createMintQuote(payload); this.invoiceData.bolt11 = data.request; this.invoiceData.quote = data.quote; this.invoiceData.date = currentDateStr(); @@ -448,22 +540,32 @@ export const useWalletStore = defineStore("wallet", { uIStore.unlockMutex(); } }, - getMintQuoteState: async function (quote: string, mint: CashuMint): Promise { + getMintQuoteState: async function ( + quote: string, + mint: CashuMint, + ): Promise { // stateless function to check the state of a mint quote const resp = await mint.checkMintQuote(quote); return resp.state; }, - getMeltQuoteState: async function (quote: string, mint: CashuMint): Promise { + getMeltQuoteState: async function ( + quote: string, + mint: CashuMint, + ): Promise { // stateless function to check the state of a melt quote const resp = await mint.checkMeltQuote(quote); return resp.state; }, - mint: async function (amount: number, hash: string, verbose: boolean = true) { + mint: async function ( + amount: number, + hash: string, + verbose: boolean = true, + ) { const proofsStore = useProofsStore(); const mintStore = useMintsStore(); const tokenStore = useTokensStore(); const uIStore = useUiStore(); - const keysetId = this.getKeyset() + const keysetId = this.getKeyset(); await uIStore.lockMutex(); try { // first we check if the mint quote is paid @@ -476,8 +578,12 @@ export const useWalletStore = defineStore("wallet", { } throw new Error("invoice not paid yet."); } - const counter = this.keysetCounter(keysetId) - const { proofs } = await this.wallet.mintProofs(amount, hash, { keysetId, counter, proofsWeHave: mintStore.activeProofs }) + const counter = this.keysetCounter(keysetId); + const { proofs } = await this.wallet.mintProofs(amount, hash, { + keysetId, + counter, + proofsWeHave: mintStore.activeProofs, + }); this.increaseKeysetCounter(keysetId, proofs.length); // const proofs = await this.mintApi(split, hash, verbose); @@ -562,7 +668,11 @@ export const useWalletStore = defineStore("wallet", { } const invoice = this.payInvoiceData.invoice.bolt11; // throw an error if the invoice is already in invoiceHistory - if (this.invoiceHistory.find((i) => i.bolt11 === invoice && i.amount < 0 && i.status === "paid")) { + if ( + this.invoiceHistory.find( + (i) => i.bolt11 === invoice && i.amount < 0 && i.status === "paid", + ) + ) { notifyError("Invoice already paid."); throw new Error("invoice already paid."); } @@ -577,11 +687,11 @@ export const useWalletStore = defineStore("wallet", { const keysetId = this.getKeyset(); let keysetCounterIncrease = 0; - // start melt let sendProofs: Proof[] = []; try { - const { keepProofs: keepProofs, sendProofs: _sendProofs } = await this.send(mintStore.activeProofs, amount, false, true) + const { keepProofs: keepProofs, sendProofs: _sendProofs } = + await this.send(mintStore.activeProofs, amount, false, true); sendProofs = _sendProofs; if (sendProofs.length == 0) { throw new Error("could not split proofs."); @@ -592,10 +702,8 @@ export const useWalletStore = defineStore("wallet", { throw error; } - await uIStore.lockMutex(); try { - await this.addOutgoingPendingInvoiceToHistory(quote, sendProofs); // NUT-08 blank outputs for change @@ -613,15 +721,22 @@ export const useWalletStore = defineStore("wallet", { // NOTE: if the user exits the app while we're in the API call, JS will emit an error that we would catch below! // We have to handle that case in the catch block below - const data = await this.wallet.meltProofs(quote, sendProofs, { keysetId, counter }) + const data = await this.wallet.meltProofs(quote, sendProofs, { + keysetId, + counter, + }); if (data.quote.state != MeltQuoteState.PAID) { throw new Error("Invoice not paid."); } - let amount_paid = amount - proofsStore.sumProofs(data.change) + let amount_paid = amount - proofsStore.sumProofs(data.change); if (!!window.navigator.vibrate) navigator.vibrate(200); - notifySuccess("Paid " + uIStore.formatCurrency(amount_paid, mintStore.activeUnit) + " via Lightning"); + notifySuccess( + "Paid " + + uIStore.formatCurrency(amount_paid, mintStore.activeUnit) + + " via Lightning", + ); console.log("#### pay lightning: token paid"); // delete spent tokens from db mintStore.removeProofs(sendProofs); @@ -630,7 +745,7 @@ export const useWalletStore = defineStore("wallet", { if (data.change != null) { const changeProofs = data.change; console.log( - "## Received change: " + proofsStore.sumProofs(changeProofs) + "## Received change: " + proofsStore.sumProofs(changeProofs), ); mintStore.addProofs(changeProofs); } @@ -642,7 +757,10 @@ export const useWalletStore = defineStore("wallet", { mint: mintStore.activeMintUrl, }); - this.updateInvoiceInHistory(quote, { status: "paid", amount: -amount_paid }) + this.updateInvoiceInHistory(quote, { + status: "paid", + amount: -amount_paid, + }); this.payInvoiceData.invoice = { sat: 0, memo: "", bolt11: "" }; this.payInvoiceData.show = false; @@ -654,15 +772,22 @@ export const useWalletStore = defineStore("wallet", { throw error; } // get quote and check state - const mintQuote = await mintStore.activeMint().api.checkMeltQuote(quote.quote); - if (mintQuote.state == MeltQuoteState.PAID || mintQuote.state == MeltQuoteState.PENDING) { - console.log("### melt: error, but quote is paid or pending. not rolling back."); + const mintQuote = await mintStore + .activeMint() + .api.checkMeltQuote(quote.quote); + if ( + mintQuote.state == MeltQuoteState.PAID || + mintQuote.state == MeltQuoteState.PENDING + ) { + console.log( + "### melt: error, but quote is paid or pending. not rolling back.", + ); throw error; } // roll back proof management and keyset counter proofsStore.setReserved(sendProofs, false); this.increaseKeysetCounter(keysetId, -keysetCounterIncrease); - this.removeOutgoingInvoiceFromHistory(quote.quote) + this.removeOutgoingInvoiceFromHistory(quote.quote); console.error(error); this.handleOutputsHaveAlreadyBeenSignedError(keysetId, error); @@ -675,13 +800,20 @@ export const useWalletStore = defineStore("wallet", { getProofState: async function (proofs: Proof[]) { const mintStore = useMintsStore(); const enc = new TextEncoder(); - const Ys = proofs.map((p) => hashToCurve(enc.encode(p.secret)).toHex(true)); + const Ys = proofs.map((p) => + hashToCurve(enc.encode(p.secret)).toHex(true), + ); const payload = { Ys: Ys }; - const { states } = await new CashuMint(mintStore.activeMintUrl).check(payload); + const { states } = await new CashuMint(mintStore.activeMintUrl).check( + payload, + ); return states; }, // /check - checkProofsSpendable: async function (proofs: Proof[], update_history = false) { + checkProofsSpendable: async function ( + proofs: Proof[], + update_history = false, + ) { /* checks with the mint whether an array of proofs is still spendable or already invalidated @@ -695,8 +827,14 @@ export const useWalletStore = defineStore("wallet", { const enc = new TextEncoder(); try { const proofStates = await this.wallet.checkProofsStates(proofs); - const spentProofsStates = proofStates.filter((p) => p.state == CheckStateEnum.SPENT) - const spentProofs = proofs.filter((p) => spentProofsStates.find((s) => s.Y == hashToCurve(enc.encode(p.secret)).toHex(true))) + const spentProofsStates = proofStates.filter( + (p) => p.state == CheckStateEnum.SPENT, + ); + const spentProofs = proofs.filter((p) => + spentProofsStates.find( + (s) => s.Y == hashToCurve(enc.encode(p.secret)).toHex(true), + ), + ); if (spentProofs.length) { mintStore.removeProofs(spentProofs); @@ -722,7 +860,10 @@ export const useWalletStore = defineStore("wallet", { throw error; } }, - checkTokenSpendable: async function (tokenStr: string, verbose: boolean = true) { + checkTokenSpendable: async function ( + tokenStr: string, + verbose: boolean = true, + ) { /* checks whether a base64-encoded token (from the history table) has been spent already. if it is spent, the appropraite entry in the history table is set to paid. @@ -746,16 +887,27 @@ export const useWalletStore = defineStore("wallet", { if (spentProofs != undefined && spentProofs.length == proofs.length) { // all proofs are spent, set token to paid tokenStore.setTokenPaid(tokenStr); - } else if (spentProofs != undefined && spentProofs.length && spentProofs.length < proofs.length) { + } else if ( + spentProofs != undefined && + spentProofs.length && + spentProofs.length < proofs.length + ) { // not all proofs are spent, we remove the spent part of the token from the history const spentAmount = proofsStore.sumProofs(spentProofs); const serializedSpentProofs = proofsStore.serializeProofs(spentProofs); - const unspentProofs = proofs.filter(p => !spentProofs.find(sp => sp.secret === p.secret)) + const unspentProofs = proofs.filter( + (p) => !spentProofs.find((sp) => sp.secret === p.secret), + ); const unspentAmount = proofsStore.sumProofs(unspentProofs); - const serializedUnspentProofs = proofsStore.serializeProofs(unspentProofs); + const serializedUnspentProofs = + proofsStore.serializeProofs(unspentProofs); if (serializedSpentProofs && serializedUnspentProofs) { - const historyToken = tokenStore.editHistoryToken(tokenStr, { newAmount: spentAmount, newStatus: "paid", newToken: serializedSpentProofs }) + const historyToken = tokenStore.editHistoryToken(tokenStr, { + newAmount: spentAmount, + newStatus: "paid", + newToken: serializedSpentProofs, + }); // add all unspent proofs back to the history // QUICK: we use the historyToken object here because we don't know if the transaction is incoming or outgoing (we don't know the sign of the amount) if (historyToken) { @@ -767,12 +919,17 @@ export const useWalletStore = defineStore("wallet", { }); } } - } if (spentProofs != undefined && spentProofs.length) { if (!!window.navigator.vibrate) navigator.vibrate(200); const proofStore = useProofsStore(); - notifySuccess("Sent " + uIStore.formatCurrency(proofStore.sumProofs(spentProofs), mintStore.activeUnit)); + notifySuccess( + "Sent " + + uIStore.formatCurrency( + proofStore.sumProofs(spentProofs), + mintStore.activeUnit, + ), + ); } else { console.log("### token not paid yet"); if (verbose) { @@ -791,7 +948,10 @@ export const useWalletStore = defineStore("wallet", { } try { // check the state first - const state = await this.getMintQuoteState(invoice.quote, new CashuMint(invoice.mint)); + const state = await this.getMintQuoteState( + invoice.quote, + new CashuMint(invoice.mint), + ); if (state != MintQuoteState.PAID) { console.log("### mintQuote not paid yet"); if (verbose) { @@ -800,10 +960,19 @@ export const useWalletStore = defineStore("wallet", { throw new Error("invoice not paid yet."); } // activate the mint - await mintStore.activateMintUrl(invoice.mint, false, false, invoice.unit); + await mintStore.activateMintUrl( + invoice.mint, + false, + false, + invoice.unit, + ); const proofs = await this.mint(invoice.amount, invoice.quote, verbose); if (!!window.navigator.vibrate) navigator.vibrate(200); - notifySuccess("Received " + uIStore.formatCurrency(invoice.amount, mintStore.activeUnit) + " via Lightning"); + notifySuccess( + "Received " + + uIStore.formatCurrency(invoice.amount, mintStore.activeUnit) + + " via Lightning", + ); return proofs; } catch (error) { if (verbose) { @@ -843,20 +1012,30 @@ export const useWalletStore = defineStore("wallet", { notify("Invoice still pending"); } throw new Error("invoice not paid yet."); - } - else if (mintQuote.state == MeltQuoteState.UNPAID) { + } else if (mintQuote.state == MeltQuoteState.UNPAID) { // we assume that the payment failed and we unset the proofs as reserved useProofsStore().setReserved(proofs, false); this.removeOutgoingInvoiceFromHistory(quote); notifyWarning("Lightning payment failed"); } else if (mintQuote.state == MeltQuoteState.PAID) { // if the invoice is paid, we check if all proofs are spent and if so, we invalidate them and set the invoice state in the history to "paid" - await mintStore.activateMintUrl(invoice.mint, false, false, invoice.unit); + await mintStore.activateMintUrl( + invoice.mint, + false, + false, + invoice.unit, + ); const spentProofs = await this.checkProofsSpendable(proofs, true); if (spentProofs != undefined && spentProofs.length == proofs.length) { mintStore.removeProofs(proofs); if (!!window.navigator.vibrate) navigator.vibrate(200); - notifySuccess("Sent " + uIStore.formatCurrency(useProofsStore().sumProofs(proofs), mintStore.activeUnit)); + notifySuccess( + "Sent " + + uIStore.formatCurrency( + useProofsStore().sumProofs(proofs), + mintStore.activeUnit, + ), + ); } // set invoice in history to paid this.setInvoicePaid(quote); @@ -870,7 +1049,10 @@ export const useWalletStore = defineStore("wallet", { } }, ////////////// UI HELPERS ////////////// - addOutgoingPendingInvoiceToHistory: function (quote: MeltQuoteResponse, sendProofs: Proof[]) { + addOutgoingPendingInvoiceToHistory: function ( + quote: MeltQuoteResponse, + sendProofs: Proof[], + ) { const proofsStore = useProofsStore(); const serlializedToken = proofsStore.serializeProofs(sendProofs); proofsStore.setReserved(sendProofs, true); @@ -893,18 +1075,22 @@ export const useWalletStore = defineStore("wallet", { this.invoiceHistory.splice(index, 1); } }, - updateInvoiceInHistory: function (quote: MeltQuoteResponse, options?: { status?: "pending" | "paid", amount?: number }) { - this.invoiceHistory.filter((i) => i.quote === quote.quote).forEach((i) => { - if (options) { - if (options.status) { - i.status = options.status; - } - if (options.amount) { - i.amount = options.amount; + updateInvoiceInHistory: function ( + quote: MeltQuoteResponse, + options?: { status?: "pending" | "paid"; amount?: number }, + ) { + this.invoiceHistory + .filter((i) => i.quote === quote.quote) + .forEach((i) => { + if (options) { + if (options.status) { + i.status = options.status; + } + if (options.amount) { + i.amount = options.amount; + } } - } - } - ); + }); }, // checkPendingInvoices: async function (verbose: boolean = true) { // const last_n = 10; @@ -935,7 +1121,7 @@ export const useWalletStore = defineStore("wallet", { break; } if (t.status === "pending" && t.amount < 0) { - console.log("### checkPendingTokens", t.token) + console.log("### checkPendingTokens", t.token); this.checkTokenSpendable(t.token, verbose); i += 1; } @@ -977,11 +1163,11 @@ export const useWalletStore = defineStore("wallet", { cleanInvoice.timestamp = tag.value; } else if (tag.name === "expiry") { var expireDate = new Date( - (cleanInvoice.timestamp + tag.value) * 1000 + (cleanInvoice.timestamp + tag.value) * 1000, ); cleanInvoice.expireDate = date.formatDate( expireDate, - "YYYY-MM-DDTHH:mm:ss.SSSZ" + "YYYY-MM-DDTHH:mm:ss.SSSZ", ); cleanInvoice.expired = false; // TODO } @@ -990,31 +1176,31 @@ export const useWalletStore = defineStore("wallet", { this.payInvoiceData.invoice = Object.freeze(cleanInvoice); // get quote for this request - await this.meltQuote() + await this.meltQuote(); }, handleCashuToken: function () { this.payInvoiceData.show = false; receiveStore.showReceiveTokens = true; }, handleP2PK: function (req: string) { - const sendTokenStore = useSendTokensStore() - sendTokenStore.sendData.p2pkPubkey = req - sendTokenStore.showSendTokens = true - sendTokenStore.showLockInput = true + const sendTokenStore = useSendTokensStore(); + sendTokenStore.sendData.p2pkPubkey = req; + sendTokenStore.showSendTokens = true; + sendTokenStore.showLockInput = true; }, handlePaymentRequest: function (req: string) { - const prStore = usePRStore() - prStore.decodePaymentRequest(req) + const prStore = usePRStore(); + prStore.decodePaymentRequest(req); }, decodeRequest: async function (req: string) { - const p2pkStore = useP2PKStore() - this.payInvoiceData.input.request = req + const p2pkStore = useP2PKStore(); + this.payInvoiceData.input.request = req; if (req.toLowerCase().startsWith("lnbc")) { this.payInvoiceData.input.request = req; - await this.handleBolt11Invoice() + await this.handleBolt11Invoice(); } else if (req.toLowerCase().startsWith("lightning:")) { this.payInvoiceData.input.request = req.slice(10); - await this.handleBolt11Invoice() + await this.handleBolt11Invoice(); } else if (req.toLowerCase().startsWith("lnurl:")) { this.payInvoiceData.input.request = req.slice(6); await this.lnurlPayFirst(this.payInvoiceData.input.request); @@ -1027,35 +1213,37 @@ export const useWalletStore = defineStore("wallet", { req.toLowerCase().startsWith("lnurl1") || req.match(/[\w.+-~_]+@[\w.+-~_]/) ) { - this.payInvoiceData.input.request = req + this.payInvoiceData.input.request = req; await this.lnurlPayFirst(this.payInvoiceData.input.request); } else if (req.indexOf("cashuA") !== -1) { // very dirty way of parsing cashu tokens from either a pasted token or a URL like https://host.com?token=eyJwcm - receiveStore.receiveData.tokensBase64 = req.slice(req.indexOf("cashuA")); - this.handleCashuToken() + receiveStore.receiveData.tokensBase64 = req.slice( + req.indexOf("cashuA"), + ); + this.handleCashuToken(); } else if (req.indexOf("cashuB") !== -1) { - receiveStore.receiveData.tokensBase64 = req.slice(req.indexOf("cashuB")); - this.handleCashuToken() + receiveStore.receiveData.tokensBase64 = req.slice( + req.indexOf("cashuB"), + ); + this.handleCashuToken(); } else if (p2pkStore.isValidPubkey(req)) { - this.handleP2PK(req) - } else if ( - req.startsWith("http") - ) { + this.handleP2PK(req); + } else if (req.startsWith("http")) { const mintStore = useMintsStore(); - mintStore.addMintData = { url: req, nickname: "" } + mintStore.addMintData = { url: req, nickname: "" }; } else if (req.startsWith("creqA")) { - await this.handlePaymentRequest(req) + await this.handlePaymentRequest(req); } }, fetchBitcoinPriceUSD: async function () { var { data } = await axios.get( - "https://api.coinbase.com/v2/exchange-rates?currency=BTC" + "https://api.coinbase.com/v2/exchange-rates?currency=BTC", ); return data.data.rates.USD; }, lnurlPayFirst: async function (address: string) { var host; - var data + var data; if (address.split("@").length == 2) { let [user, lnaddresshost] = address.split("@"); host = `https://${lnaddresshost}/.well-known/lnurlp/${user}`; @@ -1076,7 +1264,9 @@ export const useWalletStore = defineStore("wallet", { } if (data.tag == "payRequest") { this.payInvoiceData.lnurlpay = data; - this.payInvoiceData.lnurlpay.domain = host.split("https://")[1].split("/")[0]; + this.payInvoiceData.lnurlpay.domain = host + .split("https://")[1] + .split("/")[0]; if ( this.payInvoiceData.lnurlpay.maxSendable == this.payInvoiceData.lnurlpay.minSendable @@ -1116,7 +1306,7 @@ export const useWalletStore = defineStore("wallet", { } catch (e) { notifyError( "Couldn't get Bitcoin price", - "fetchBitcoinPriceUSD Error" + "fetchBitcoinPriceUSD Error", ); return; } @@ -1126,7 +1316,7 @@ export const useWalletStore = defineStore("wallet", { amount = Math.floor(usdAmount * satPrice); } var { data } = await axios.get( - `${this.payInvoiceData.lnurlpay.callback}?amount=${amount * 1000}` + `${this.payInvoiceData.lnurlpay.callback}?amount=${amount * 1000}`, ); // check http error if (data.status == "ERROR") { @@ -1140,9 +1330,12 @@ export const useWalletStore = defineStore("wallet", { if (this.mnemonic == "") { this.mnemonic = generateMnemonic(wordlist); } - return this.mnemonic + return this.mnemonic; }, - handleOutputsHaveAlreadyBeenSignedError: function (keysetId: string, error: any) { + handleOutputsHaveAlreadyBeenSignedError: function ( + keysetId: string, + error: any, + ) { if (error.message.includes("outputs have already been signed")) { this.increaseKeysetCounter(keysetId, 10); notify("Please try again."); @@ -1154,11 +1347,13 @@ export const useWalletStore = defineStore("wallet", { const nostrStore = useNostrStore(); const mintStore = useMintsStore(); const tags = [["n", "17"]]; - const transport = [{ - type: PaymentRequestTransportType.NOSTR, - target: nostrStore.nprofile, - tags: tags, - }] as PaymentRequestTransport[]; + const transport = [ + { + type: PaymentRequestTransportType.NOSTR, + target: nostrStore.nprofile, + tags: tags, + }, + ] as PaymentRequestTransport[]; const uuid = uuidv4().split("-")[0]; const paymentRequest = new PaymentRequest( transport, @@ -1171,10 +1366,12 @@ export const useWalletStore = defineStore("wallet", { // TMP console.log("### paymentRequest", paymentRequest.toEncodedRequest()); - const request: PaymentRequest = decodePaymentRequest(paymentRequest.toEncodedRequest()) - console.log('### decoded paymentRequest', request); + const request: PaymentRequest = decodePaymentRequest( + paymentRequest.toEncodedRequest(), + ); + console.log("### decoded paymentRequest", request); return paymentRequest.toEncodedRequest(); - } + }, }, }); diff --git a/src/stores/workers.ts b/src/stores/workers.ts index b5ae0f7a..cdd7ee32 100644 --- a/src/stores/workers.ts +++ b/src/stores/workers.ts @@ -78,10 +78,7 @@ export const useWorkersStore = defineStore("workers", { this.clearAllWorkers(); } console.log("### checkTokenSpendableWorker setInterval", nInterval); - let paid = await walletStore.checkTokenSpendable( - tokensBase64, - false - ); + let paid = await walletStore.checkTokenSpendable(tokensBase64, false); if (paid) { console.log("### stopping token check worker"); this.clearAllWorkers();