@@ -356,7 +355,11 @@ import { Buffer } from "buffer";
import { useCameraStore } from "src/stores/camera";
import { useP2PKStore } from "src/stores/p2pk";
import TokenInformation from "components/TokenInformation.vue";
-import { getDecodedToken, getEncodedTokenV4, getEncodedToken } from "@cashu/cashu-ts";
+import {
+ getDecodedToken,
+ getEncodedTokenV4,
+ getEncodedToken,
+} from "@cashu/cashu-ts";
import { mapActions, mapState, mapWritableState } from "pinia";
import ChooseMint from "components/ChooseMint.vue";
@@ -403,15 +406,23 @@ export default defineComponent({
]),
...mapWritableState(useSendTokensStore, ["sendData"]),
...mapWritableState(useCameraStore, ["camera", "hasCamera"]),
- ...mapState(useUiStore, ["tickerShort", "canPasteFromClipboard", "globalMutexLock"]),
+ ...mapState(useUiStore, [
+ "tickerShort",
+ "canPasteFromClipboard",
+ "globalMutexLock",
+ ]),
...mapState(useMintsStore, [
"activeProofs",
"activeUnit",
"activeUnitLabel",
+ "activeUnitCurrencyMultiplyer",
"activeMintUrl",
"activeMintBalance",
]),
- ...mapState(useSettingsStore, ["checkSentTokens"]),
+ ...mapState(useSettingsStore, [
+ "checkSentTokens",
+ "includeFeesInSendAmount",
+ ]),
...mapState(useWorkersStore, ["tokenWorkerRunning"]),
// TOKEN METHODS
sumProofs: function () {
@@ -450,12 +461,19 @@ export default defineComponent({
let spendableProofs = this.spendableProofs(this.activeProofs);
let selectedProofs = this.coinSelect(
spendableProofs,
- this.sendData.amount
+ this.sendData.amount * this.activeUnitCurrencyMultiplyer,
+ this.includeFeesInSendAmount
);
+ const feesToAdd = this.includeFeesInSendAmount
+ ? this.getFeesForProofs(selectedProofs)
+ : 0;
const sumSelectedProofs = selectedProofs
.flat()
.reduce((sum, el) => (sum += el.amount), 0);
- return sumSelectedProofs == this.sendData.amount;
+ return (
+ sumSelectedProofs ==
+ this.sendData.amount * this.activeUnitCurrencyMultiplyer + feesToAdd
+ );
},
},
watch: {
@@ -499,10 +517,11 @@ export default defineComponent({
"clearAllWorkers",
]),
...mapActions(useWalletStore, [
- "splitToSend",
+ "send",
"sendToLock",
"coinSelect",
"spendableProofs",
+ "getFeesForProofs",
]),
...mapActions(useProofsStore, ["serializeProofs"]),
...mapActions(useTokensStore, [
@@ -596,17 +615,13 @@ export default defineComponent({
// if it starts with 'cashuB', it is a v4 token
if (this.sendData.tokensBase64.startsWith("cashuA")) {
try {
- this.sendData.tokensBase64 = getEncodedTokenV4(decodedToken)
+ this.sendData.tokensBase64 = getEncodedTokenV4(decodedToken);
} catch {
console.log("### Could not encode token to V4");
- this.sendData.tokensBase64 = getEncodedToken(
- decodedToken
- );
+ this.sendData.tokensBase64 = getEncodedToken(decodedToken);
}
} else {
- this.sendData.tokensBase64 = getEncodedToken(
- decodedToken
- );
+ this.sendData.tokensBase64 = getEncodedToken(decodedToken);
}
},
deleteThisToken: function () {
@@ -630,7 +645,6 @@ export default defineComponent({
);
// update UI
this.sendData.tokens = sendProofs;
- console.log("### this.sendData.tokens", this.sendData.tokens);
this.sendData.tokensBase64 = this.serializeProofs(sendProofs);
this.addPendingToken({
@@ -649,7 +663,7 @@ export default defineComponent({
},
sendTokens: async function () {
/*
- calls splitToSend, displays token and kicks off the spendableWorker
+ calls send, displays token and kicks off the spendableWorker
*/
if (
this.sendData.p2pkPubkey &&
@@ -660,25 +674,22 @@ export default defineComponent({
}
try {
- let sendAmount = this.sendData.amount;
- // if unit is USD, multiply by 100
- if (this.activeUnit === "usd" || this.activeUnit == "eur") {
- sendAmount = sendAmount * 100;
- }
+ let sendAmount =
+ this.sendData.amount * this.activeUnitCurrencyMultiplyer;
// keep firstProofs, send scndProofs and delete them (invalidate=true)
- let { _, sendProofs } = await this.splitToSend(
+ let { _, sendProofs } = await this.send(
this.activeProofs,
sendAmount,
- true
+ true,
+ this.includeFeesInSendAmount
);
// update UI
this.sendData.tokens = sendProofs;
- console.log("### this.sendData.tokens", this.sendData.tokens);
this.sendData.tokensBase64 = this.serializeProofs(sendProofs);
this.addPendingToken({
- amount: -this.sendData.amount,
+ amount: -sendAmount,
serializedProofs: this.sendData.tokensBase64,
unit: this.activeUnit,
mint: this.activeMintUrl,
diff --git a/src/components/SettingsView.vue b/src/components/SettingsView.vue
index ead93e9b..a52307a2 100644
--- a/src/components/SettingsView.vue
+++ b/src/components/SettingsView.vue
@@ -10,12 +10,7 @@
>
Your seed phrase can restore your wallet. Keep it safe and
- private. Warning: this wallet does not support seed phrase
- recovery yet. Use a different Cashu wallet or
- this tool
- to recover from seed phrase.
+ private.
@@ -51,80 +46,6 @@
-
-
-
-
-
- P2PK
- Generate a key pair to receive P2PK-locked ecash. Warning: This
- feature is experimental. Only use with small amounts. If you lose
- your private keys, nobody will be able to unlock the ecash locked
- to it anymore.
-
-
-
- Generate key
-
-
-
-
-
-
-
-
-
- {{ key.publicKey }}
-
-
-
-
-
-
-
-
-
-
@@ -562,6 +483,116 @@
+
+
+
+
+
+ P2PK
+ Generate a key pair to receive P2PK-locked ecash. Warning:
+ This feature is experimental. Only use with small amounts. If
+ you lose your private keys, nobody will be able to unlock the
+ ecash locked to it anymore.
+
+
+
+ Generate key
+
+
+
+
+
+
+
+
+
+ {{ key.publicKey }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Restore ecash
+ The restore wizard lets you recover lost ecash from a
+ mnemonic seed phrase.
+
+
+
+ Restore
+
+
+
+
@@ -950,6 +981,7 @@ import { useP2PKStore } from "src/stores/p2pk";
import { useNWCStore } from "src/stores/nwc";
import { useWorkersStore } from "src/stores/workers";
import { useProofsStore } from "src/stores/proofs";
+import { useRestoreStore } from "src/stores/restore";
export default defineComponent({
name: "SettingsView",
@@ -1097,6 +1129,7 @@ export default defineComponent({
...mapActions(useWorkersStore, ["invoiceCheckWorker"]),
...mapActions(useProofsStore, ["serializeProofs"]),
...mapActions(useNPCStore, ["generateNPCConnection"]),
+ ...mapActions(useRestoreStore, ["restoreMint"]),
generateNewMnemonic: async function () {
this.newMnemonic();
await this.initSigner();
diff --git a/src/pages/Restore.vue b/src/pages/Restore.vue
new file mode 100644
index 00000000..51f2c4d7
--- /dev/null
+++ b/src/pages/Restore.vue
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
diff --git a/src/router/routes.js b/src/router/routes.js
index 55298121..93169b14 100644
--- a/src/router/routes.js
+++ b/src/router/routes.js
@@ -11,6 +11,11 @@ const routes = [
component: () => import("layouts/FullscreenLayout.vue"),
children: [{ path: "", component: () => import("src/pages/Settings.vue") }],
},
+ {
+ path: "/restore",
+ component: () => import("layouts/FullscreenLayout.vue"),
+ children: [{ path: "", component: () => import("src/pages/Restore.vue") }],
+ },
{
path: "/already-running",
component: () => import("layouts/FullscreenLayout.vue"),
diff --git a/src/stores/mints.ts b/src/stores/mints.ts
index 8eade977..90623b3f 100644
--- a/src/stores/mints.ts
+++ b/src/stores/mints.ts
@@ -2,7 +2,7 @@ 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, Proof, SerializedBlindedSignature, MintKeyset } 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;
@@ -48,7 +48,7 @@ export class MintClass {
}
unitKeysets(unit: string): MintKeyset[] {
- return this.mint.keysets.filter((k) => k.unit === unit && k.active);
+ return this.mint.keysets.filter((k) => k.unit === unit);
}
unitProofs(unit: string) {
@@ -116,6 +116,23 @@ export const useMintsStore = defineStore("mints", {
).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);
+ if (!unitKeysets) {
+ return [];
+ }
+ return unitKeysets;
+ },
+ activeKeys({ activeMintUrl, activeUnit }): MintKeys[] {
+ const unitKeys = this.mints.find((m) => m.url === activeMintUrl)?.keys?.filter((k) => k.unit === activeUnit);
+ if (!unitKeys) {
+ return [];
+ }
+ return unitKeys;
+ },
+ activeInfo({ activeMintUrl }): GetInfoResponse {
+ return this.mints.find((m) => m.url === activeMintUrl)?.info;
+ },
activeUnitLabel({ activeUnit }): string {
if (activeUnit == "sat") {
return "SAT";
@@ -128,7 +145,16 @@ export const useMintsStore = defineStore("mints", {
} else {
return activeUnit;
}
- }
+ },
+ activeUnitCurrencyMultiplyer({ activeUnit }): number {
+ if (activeUnit == "usd") {
+ return 100;
+ } else if (activeUnit == "eur") {
+ return 100;
+ } else {
+ return 1;
+ }
+ },
},
actions: {
activeMint() {
@@ -157,12 +183,9 @@ export const useMintsStore = defineStore("mints", {
proofsToWalletProofs(proofs: Proof[]): WalletProof[] {
return proofs.map((p) => {
return {
- amount: p.amount,
- secret: p.secret,
- C: p.C,
+ ...p,
reserved: false,
- id: p.id,
- };
+ } as WalletProof;
});
},
addProofs(proofs: Proof[]) {
@@ -268,6 +291,8 @@ export const useMintsStore = defineStore("mints", {
}
},
activateUnit: async function (unit: string, verbose = false) {
+ const uIStore = useUiStore();
+ await uIStore.lockMutex();
const mint = this.mints.find((m) => m.url === this.activeMintUrl);
if (!mint) {
notifyError("No active mint", "Unit activation failed");
@@ -279,6 +304,9 @@ export const useMintsStore = defineStore("mints", {
} else {
notifyError("Unit not supported by mint", "Unit activation failed");
}
+ await uIStore.unlockMutex();
+ const worker = useWorkersStore();
+ worker.clearAllWorkers();
},
activateMint: async function (mint: Mint, verbose = false, force = false) {
const workers = useWorkersStore();
diff --git a/src/stores/proofs.ts b/src/stores/proofs.ts
index c0cc9b17..2476a920 100644
--- a/src/stores/proofs.ts
+++ b/src/stores/proofs.ts
@@ -25,7 +25,7 @@ export const useProofsStore = defineStore("proofs", {
getUnreservedProofs: function (proofs: WalletProof[]) {
return proofs.filter((p) => !p.reserved);
},
- serializeProofs: function (proofs: Proof[]) {
+ serializeProofs: function (proofs: Proof[]): string {
const mintStore = useMintsStore();
// unique keyset IDs of proofs
let uniqueIds = [...new Set(proofs.map((p) => p.id))];
@@ -34,14 +34,14 @@ export const useProofsStore = defineStore("proofs", {
m.keysets.filter((k) => uniqueIds.includes(k.id))
);
if (keysets.length === 0) {
- return null;
+ 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))
);
if (mints.length === 0) {
- return null;
+ throw new Error("No mints found for proofs");
}
// unit of keysets
let unit = keysets[0].unit;
diff --git a/src/stores/restore.ts b/src/stores/restore.ts
new file mode 100644
index 00000000..a18b5d5d
--- /dev/null
+++ b/src/stores/restore.ts
@@ -0,0 +1,119 @@
+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 { useWalletStore } from "./wallet";
+import { CashuMint, CashuWallet, Proof } from "@cashu/cashu-ts";
+import { useMintsStore } from "./mints";
+import { notify, notifyError, notifySuccess } from "src/js/notify";
+import { useUiStore } from "./ui";
+
+const BATCH_SIZE = 100;
+const MAX_GAP = 2;
+
+export const useRestoreStore = defineStore("restore", {
+ state: () => ({
+ showRestoreDialog: useLocalStorage("cashu.restore.showRestoreDialog", false),
+ restoringState: false,
+ restoringMint: "",
+ mnemonicToRestore: useLocalStorage("cashu.restore.mnemonicToRestore", ""),
+ restoreProgress: 0,
+ restoreCounter: 0,
+ restoreStatus: "",
+ }),
+ getters: {
+
+ },
+ actions: {
+ restoreMint: async function (url: string) {
+ this.restoringState = true;
+ this.restoringMint = url;
+ this.restoreProgress = 0;
+ this.restoreCounter = 0;
+ this.restoreStatus = "";
+ try {
+ await this._restoreMint(url);
+ } catch (error) {
+ notifyError(`Error restoring mint: ${error}`);
+ } finally {
+ this.restoringState = false;
+ this.restoringMint = "";
+ this.restoreProgress = 0;
+ }
+ },
+ _restoreMint: async function (url: string) {
+ if (this.mnemonicToRestore.length === 0) {
+ notifyError("Please enter a mnemonic");
+ return;
+ }
+ this.restoreProgress = 0;
+ const mintStore = useMintsStore();
+ await mintStore.activateMintUrl(url);
+
+ const mnemonic = this.mnemonicToRestore;
+ this.restoreStatus = `Preparing restore process...`;
+ const mint = new CashuMint(url);
+ const keysets = (await mint.getKeySets()).keysets;
+ let restoredSomething = false;
+
+ // Calculate total steps for progress calculation
+ let totalSteps = keysets.length * MAX_GAP * 2;
+ let currentStep = 1;
+
+ for (const keyset of keysets) {
+ console.log(`Restoring keyset ${keyset.id} with unit ${keyset.unit}`);
+ const wallet = new CashuWallet(mint, { mnemonicOrSeed: mnemonic, 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;
+ 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)}`);
+ restoreProofs = restoreProofs.concat(proofs);
+ emptyBatchCount = 0;
+ totalSteps += MAX_GAP * 2;
+ this.restoreCounter += proofs.length;
+ }
+ this.restoreStatus = `Restored ${this.restoreCounter} proofs for keyset ${keyset.id}`;
+ start += BATCH_SIZE;
+
+ 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 spentProofs = await wallet.checkProofsSpent(checkRestoreProofs);
+ const spentProofsSecrets = spentProofs.map((p) => 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)}`);
+ }
+ 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);
+ if (restoredAmount > 0) {
+ notifySuccess(`Restored ${restoredAmountStr}`);
+ restoredSomething = true;
+ }
+ }
+ if (!restoredSomething) {
+ notify("No proofs found to restore");
+ }
+ },
+ },
+});
diff --git a/src/stores/settings.ts b/src/stores/settings.ts
index 2ebd7ebc..3fce78b9 100644
--- a/src/stores/settings.ts
+++ b/src/stores/settings.ts
@@ -9,6 +9,7 @@ export const useSettingsStore = defineStore("settings", {
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 5f6e351c..49f73694 100644
--- a/src/stores/tokens.ts
+++ b/src/stores/tokens.ts
@@ -14,6 +14,7 @@ type HistoryToken = {
token: string;
mint: string;
unit: string;
+ fee?: number;
};
export const useTokensStore = defineStore("tokens", {
@@ -30,11 +31,13 @@ export const useTokensStore = defineStore("tokens", {
serializedProofs,
mint,
unit,
+ fee,
}: {
amount: number;
serializedProofs: string;
mint: string;
unit: string;
+ fee?: number;
}) {
this.historyTokens.push({
status: "paid",
@@ -43,6 +46,7 @@ export const useTokensStore = defineStore("tokens", {
token: serializedProofs,
mint,
unit,
+ fee,
} as HistoryToken);
},
addPendingToken({
@@ -50,11 +54,13 @@ export const useTokensStore = defineStore("tokens", {
serializedProofs,
mint,
unit,
+ fee,
}: {
amount: number;
serializedProofs: string;
mint: string;
unit: string;
+ fee?: number;
}) {
this.historyTokens.push({
status: "pending",
@@ -63,9 +69,10 @@ export const useTokensStore = defineStore("tokens", {
token: serializedProofs,
mint,
unit,
+ fee,
});
},
- editHistoryToken(tokenToEdit: string, options?: { newAmount?: number; addAmount?: number, newStatus?: "paid" | "pending", newToken?: string, }): HistoryToken | undefined {
+ 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) {
@@ -86,6 +93,9 @@ export const useTokensStore = defineStore("tokens", {
if (options.newStatus) {
this.historyTokens[index].status = options.newStatus;
}
+ if (options.newFee) {
+ this.historyTokens[index].fee = options.newFee;
+ }
}
return this.historyTokens[index];
diff --git a/src/stores/wallet.ts b/src/stores/wallet.ts
index 2afd88a0..05f74feb 100644
--- a/src/stores/wallet.ts
+++ b/src/stores/wallet.ts
@@ -18,7 +18,8 @@ import * as bolt11Decoder from "light-bolt11-decoder";
import { bech32 } from "bech32";
import axios from "axios";
import { date } from "quasar";
-import { splitAmount } from "@cashu/cashu-ts/dist/lib/es5/utils";
+import { getKeepAmounts, splitAmount } from "@cashu/cashu-ts/dist/lib/es5/utils";
+import { KeepAlive } from "vue";
// 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
@@ -111,7 +112,7 @@ export const useWalletStore = defineStore("wallet", {
this.mnemonic = generateNewMnemonic();
}
const mnemonic: string = this.mnemonic;
- const wallet = new CashuWallet(mint, { mnemonicOrSeed: mnemonic, unit: mints.activeUnit });
+ const wallet = new CashuWallet(mint, { keys: mints.activeKeys, keysets: mints.activeKeysets, mintInfo: mints.activeInfo, mnemonicOrSeed: mnemonic, unit: mints.activeUnit });
return wallet;
},
seed(): Uint8Array {
@@ -140,11 +141,9 @@ export const useWalletStore = defineStore("wallet", {
const keysetCounter = this.keysetCounters.find((c) => c.id === id);
if (keysetCounter) {
keysetCounter.counter += by;
- console.log("### increaseKeysetCounter", keysetCounter);
} else {
const newCounter = { id, counter: by } as KeysetCounter;
this.keysetCounters.push(newCounter);
- console.log("### new keyset counter", keysetCounter);
}
},
getKeyset(): string {
@@ -176,9 +175,9 @@ export const useWalletStore = defineStore("wallet", {
}
const keyset_id = sortedKeysets[0].id;
const keys = mintStore.activeMint().mint.keys.find((k) => k.id === keyset_id);
- if (keys) {
- this.wallet.keys = keys;
- }
+ // if (keys) {
+ // this.wallet.keys = keys;
+ // }
return keyset_id;
},
/**
@@ -200,53 +199,6 @@ export const useWalletStore = defineStore("wallet", {
}
return chunks;
},
- outputAmountSelect: function (amount: number, target = 3) {
- // This function produces an amount split for outputs based on the current coins we have.
- // Its objective is to fill up the wallet so that it reaches `target` coins of each amount.
- // The coins we currently have are are this.activeProofs
- const mintStore = useMintsStore();
- const amountsWeHave = mintStore.activeProofs.map((p) => p.amount);
- /*
- # NOTE: Do not assume 2^n here. This is not a general case.
- */
- // calculate until 2^64
- const allPossibleAmounts = Array.from({ length: 64 }, (_, i) => 2 ** i);
- const amountsWeWantLL = allPossibleAmounts.map((a) => {
- const count = Math.max(0, target - amountsWeHave.filter((x) => x === a).length);
- return Array(count).fill(a);
- });
- const amountsWeWant = amountsWeWantLL.flat().sort((a, b) => a - b);
-
- let amounts: number[] = [];
- while (amounts.reduce((s, t) => (s += t), 0) < amount && amountsWeWant.length) {
- if (amounts.reduce((s, t) => (s += t), 0) + amountsWeWant[0] > amount) {
- break;
- }
- amounts.push(amountsWeWant.shift() as number);
- }
- const remainingAmount = amount - amounts.reduce((s, t) => (s += t), 0);
- if (remainingAmount > 0) {
- console.log("remaining amount", remainingAmount)
- // amount_split is the optimal 2^n split: splitAmount
- amounts = amounts.concat(this.splitAmount(remainingAmount));
- }
- if (amounts.reduce((s, t) => (s += t), 0) != amount) {
- throw new Error(`Amounts do not sum to ${amount}.`);
- }
-
- // make array of AmountPreference types which have a unique `amount` and its `count`
- const amountsWithCount: AmountPreference[] = [];
- amounts.forEach((a) => {
- const existing = amountsWithCount.find((ac) => ac.amount === a);
- if (existing) {
- existing.count += 1;
- } else {
- amountsWithCount.push({ amount: a, count: 1 });
- }
- });
-
- return amountsWithCount;
- },
coinSelectSpendBase64: function (proofs: WalletProof[], amount: number): WalletProof[] {
const base64Proofs = proofs.filter(p => !p.id.startsWith("00"))
if (base64Proofs.length > 0) {
@@ -265,55 +217,16 @@ export const useWalletStore = defineStore("wallet", {
}
return [];
},
- coinSelect: function (proofs: WalletProof[], amount: number) {
+ 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 [];
}
-
- // override: if there are proofs with a base64 id, use them
- const base64Proofs = this.coinSelectSpendBase64(proofs, amount);
- if (base64Proofs.length > 0 && base64Proofs.reduce((s, t) => (s += t.amount), 0) >= amount) {
- return base64Proofs;
- }
-
- // sort proofs by amount ascending
- proofs = proofs.slice().sort((a, b) => a.amount - b.amount);
- // remember next bigger proof as a fallback
- const nextBigger = proofs.find((p) => p.amount > amount);
-
- // go through smaller proofs until sum is bigger than amount
- const smallerProofs = proofs.filter((p) => p.amount <= amount);
- // sort by amount descending
- smallerProofs.sort((a, b) => b.amount - a.amount);
-
- let selectedProofs: WalletProof[] = [];
-
- if (smallerProofs.length == 0 && nextBigger) {
- // if there are no smaller proofs, take the next bigger proof as a fallback
- return [nextBigger];
- } else if (smallerProofs.length == 0 && !nextBigger) {
- // no proofs available
- return [];
- }
-
- // recursively select the largest proof of smallerProofs, subtract the amount from the remainder
- // and call coinSelect again with the remainder and the rest of the smallerProofs (without the largest proof)
- let remainder = amount;
- selectedProofs = [smallerProofs[0]];
- remainder -= smallerProofs[0].amount;
- if (remainder > 0) {
- selectedProofs = selectedProofs.concat(this.coinSelect(smallerProofs.slice(1), remainder));
- }
- let sum = selectedProofs.reduce((s, t) => (s += t.amount), 0);
-
- // if sum of selectedProofs is smaller than amount, take next bigger proof instead as a fallback
- if (sum < amount && nextBigger) {
- selectedProofs = [nextBigger];
- }
-
- // console.log("### selected amounts", "sum", selectedProofs.reduce((s, t) => (s += t.amount), 0), selectedProofs.map(p => p.amount));
- return selectedProofs
+ const { send: selectedProofs, keep: _ } = this.wallet.selectProofsToSend(proofs, amount, includeFees);
+ const selectedWalletProofs = selectedProofs.map((p) => {
+ return { ...p, reserved: false } as WalletProof;
+ });
+ return selectedWalletProofs;
},
spendableProofs: function (proofs: WalletProof[], amount: number) {
const uIStore = useUiStore();
@@ -331,18 +244,21 @@ export const useWalletStore = defineStore("wallet", {
}
return spendableProofs;
},
+ getFeesForProofs: function (proofs: Proof[]): number {
+ return this.wallet.getFeesForProofs(proofs);
+ },
sendToLock: async function (proofs: WalletProof[], amount: number, receiverPubkey: string) {
const spendableProofs = this.spendableProofs(proofs, amount);
- const proofsToSplit = this.coinSelect(spendableProofs, amount);
- const { returnChange: keepProofs, send: sendProofs } = await this.wallet.send(amount, proofsToSplit, { pubkey: receiverPubkey })
+ const proofsToSend = this.coinSelect(spendableProofs, amount);
+ 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
mintStore.addProofs(keepProofs);
- mintStore.removeProofs(proofsToSplit);
+ mintStore.removeProofs(proofsToSend);
return { keepProofs, sendProofs };
},
- splitToSend: async function (proofs: WalletProof[], amount: number, invalidate: 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.
@@ -352,36 +268,32 @@ export const useWalletStore = defineStore("wallet", {
const mintStore = useMintsStore();
const proofsStore = useProofsStore()
const uIStore = useUiStore();
- let proofsToSplit: WalletProof[] = [];
+ let proofsToSend: WalletProof[] = [];
const keysetId = this.getKeyset()
await uIStore.lockMutex();
try {
const spendableProofs = this.spendableProofs(proofs, amount);
- proofsToSplit = this.coinSelect(spendableProofs, amount);
- const totalAmount = proofsToSplit.reduce((s, t) => (s += t.amount), 0);
- proofsStore.setReserved(proofsToSplit, true);
+
+ 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 targetAmount = amount + fees;
+
let keepProofs: Proof[] = [];
let sendProofs: Proof[] = [];
- if (totalAmount != amount) {
+
+ if (totalAmount != targetAmount) {
const counter = this.keysetCounter(keysetId);
- ({ returnChange: keepProofs, send: sendProofs } = await this.wallet.send(amount, proofsToSplit, { counter }));
+ 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);
-
- mintStore.removeProofs(proofsToSplit);
-
-
mintStore.addProofs(keepProofs);
mintStore.addProofs(sendProofs);
- } else if (totalAmount == amount) {
+ mintStore.removeProofs(proofsToSend);
+ } else if (totalAmount == targetAmount) {
+
keepProofs = [];
- sendProofs = proofsToSplit.map((p) => {
- return {
- amount: p.amount,
- secret: p.secret,
- C: p.C,
- id: p.id,
- };
- });
+ sendProofs = proofsToSend
} else {
throw new Error("could not split proofs.");
}
@@ -389,10 +301,10 @@ export const useWalletStore = defineStore("wallet", {
if (invalidate) {
mintStore.removeProofs(sendProofs);
}
-
+ proofsStore.setReserved(sendProofs, true);
return { keepProofs, sendProofs };
} catch (error: any) {
- proofsStore.setReserved(proofsToSplit, false);
+ proofsStore.setReserved(proofsToSend, false);
console.error(error);
notifyApiError(error);
this.handleOutputsHaveAlreadyBeenSignedError(keysetId, error);
@@ -415,7 +327,6 @@ export const useWalletStore = defineStore("wallet", {
const p2pkStore = useP2PKStore();
receiveStore.showReceiveTokens = false;
- console.log("### receive tokens", receiveStore.receiveData.tokensBase64);
if (receiveStore.receiveData.tokensBase64.length == 0) {
throw new Error("no tokens provided.");
@@ -435,33 +346,36 @@ export const useWalletStore = defineStore("wallet", {
// redeem
const keysetId = this.getKeyset()
const counter = this.keysetCounter(keysetId)
- const preference = this.outputAmountSelect(amount);
+ // const preference = this.outputAmountSelect(amount);
const privkey = receiveStore.receiveData.p2pkPrivateKey;
- const decodedToken = getDecodedToken(receiveStore.receiveData.tokensBase64);
- let tokenCts: Token
+ // const decodedToken = getDecodedToken(receiveStore.receiveData.tokensBase64);
+ // let tokenCts: Token
let proofs: Proof[]
try {
- proofs = await this.wallet.receive(receiveStore.receiveData.tokensBase64, { counter, preference, privkey })
+ proofs = await this.wallet.receive(receiveStore.receiveData.tokensBase64, { counter, privkey, proofsWeHave: mintStore.activeProofs })
this.increaseKeysetCounter(keysetId, proofs.length);
} catch (error: any) {
console.error(error);
this.handleOutputsHaveAlreadyBeenSignedError(keysetId, error);
throw new Error("Error receiving tokens: " + error);
}
- // const proofs: Proof[] = tokenCts.token.map(t => t.proofs).flat();
p2pkStore.setPrivateKeyUsed(privkey);
-
mintStore.removeProofs(proofs);
- // gather all token.token[i].proofs
mintStore.addProofs(proofs);
+ const receivedAdmount = 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)) {
+ if (tokenStore.historyTokens.find((t) => t.token === receiveStore.receiveData.tokensBase64 && t.amount == receivedAdmount)) {
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 == -receivedAdmount)) {
+ tokenStore.setTokenPaid(receiveStore.receiveData.tokensBase64);
+ }
tokenStore.addPaidToken({
- amount,
+ amount: receivedAdmount,
serializedProofs: receiveStore.receiveData.tokensBase64,
unit: mintStore.activeUnit,
mint: mintStore.activeMintUrl,
@@ -470,7 +384,7 @@ export const useWalletStore = defineStore("wallet", {
if (!!window.navigator.vibrate) navigator.vibrate(200);
- notifySuccess("Received " + uIStore.formatCurrency(amount, mintStore.activeUnit));
+ notifySuccess("Received " + uIStore.formatCurrency(receivedAdmount, mintStore.activeUnit));
} catch (error: any) {
console.error(error);
notifyApiError(error);
@@ -521,6 +435,16 @@ export const useWalletStore = defineStore("wallet", {
uIStore.unlockMutex();
}
},
+ 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 {
+ // 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) {
const proofsStore = useProofsStore();
const mintStore = useMintsStore();
@@ -540,9 +464,7 @@ export const useWalletStore = defineStore("wallet", {
throw new Error("invoice not paid yet.");
}
const counter = this.keysetCounter(keysetId)
- const preference = this.outputAmountSelect(amount);
- console.log("### preference", preference);
- const { proofs } = await this.wallet.mintTokens(amount, hash, { keysetId, counter, preference: preference })
+ 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);
@@ -599,7 +521,6 @@ export const useWalletStore = defineStore("wallet", {
const data = await mintStore.activeMint().api.createMeltQuote(payload);
mintStore.assertMintError(data);
this.payInvoiceData.meltQuote.response = data;
- console.log("#### meltQuote", payload, " response:", data);
this.payInvoiceData.blocking = false;
return data;
} catch (error: any) {
@@ -639,35 +560,30 @@ export const useWalletStore = defineStore("wallet", {
throw new Error("no quote found.");
}
const amount = quote.amount + quote.fee_reserve;
-
- console.log(
- "#### amount invoice",
- amount_invoice,
- "amount with fees",
- amount
- );
let countChangeOutputs = 0;
const keysetId = this.getKeyset();
let keysetCounterIncrease = 0;
- // get right amount of proofs to send
- const { keepProofs, sendProofs } = await this.splitToSend(
- mintStore.activeMint().unitProofs(mintStore.activeUnit),
- amount
- );
- if (sendProofs.length == 0) {
- throw new Error("could not split proofs.");
- }
+
// start melt
- await uIStore.lockMutex();
+ let sendProofs: Proof[] = [];
try {
- // proof management
- const serializedSendProofs = proofsStore.serializeProofs(sendProofs);
- if (serializedSendProofs == null) {
- throw new Error("could not serialize proofs.");
+ 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.");
}
- proofsStore.setReserved(sendProofs, true);
- await this.addOutgoingPendingInvoiceToHistory(quote, serializedSendProofs);
+ } catch (error: any) {
+ console.error(error);
+ notifyApiError(error, "Payment failed");
+ throw error;
+ }
+
+
+ await uIStore.lockMutex();
+ try {
+
+ await this.addOutgoingPendingInvoiceToHistory(quote, sendProofs);
// NUT-08 blank outputs for change
const counter = this.keysetCounter(keysetId);
@@ -684,9 +600,9 @@ 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.payLnInvoice(invoice, sendProofs, quote, { keysetId, counter })
+ const data = await this.wallet.meltProofs(quote, sendProofs, { keysetId, counter })
- if (data.isPaid != true) {
+ if (data.quote.state != MeltQuoteState.PAID) {
throw new Error("Invoice not paid.");
}
let amount_paid = amount - proofsStore.sumProofs(data.change)
@@ -708,7 +624,7 @@ export const useWalletStore = defineStore("wallet", {
tokenStore.addPaidToken({
amount: -amount_paid,
- serializedProofs: serializedSendProofs,
+ serializedProofs: proofsStore.serializeProofs(sendProofs),
unit: mintStore.activeUnit,
mint: mintStore.activeMintUrl,
});
@@ -770,8 +686,6 @@ export const useWalletStore = defineStore("wallet", {
};
try {
const spentProofs = await this.wallet.checkProofsSpent(proofs);
- // const data = await mintStore.activeMint().api.check(payload);
- // mintStore.assertMintError(data);
if (spentProofs.length) {
mintStore.removeProofs(spentProofs);
@@ -860,15 +774,22 @@ export const useWalletStore = defineStore("wallet", {
checkInvoice: async function (quote: string, verbose = true) {
const uIStore = useUiStore();
const mintStore = useMintsStore();
- console.log("### checkInvoice.quote", quote);
const invoice = this.invoiceHistory.find((i) => i.quote === quote);
if (!invoice) {
throw new Error("invoice not found");
}
try {
+ // check the state first
+ const state = await this.getMintQuoteState(invoice.quote, new CashuMint(invoice.mint));
+ if (state != MintQuoteState.PAID) {
+ console.log("### mintQuote not paid yet");
+ if (verbose) {
+ notify("Invoice still pending");
+ }
+ throw new Error("invoice not paid yet.");
+ }
// activate the mint
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");
@@ -888,60 +809,46 @@ export const useWalletStore = defineStore("wallet", {
if (!invoice) {
throw new Error("invoice not found");
}
+
+ let proofs: Proof[] = [];
+ if (invoice.token) {
+ const tokenJson = token.decode(invoice.token);
+ if (tokenJson == undefined) {
+ throw new Error("no tokens provided.");
+ }
+ proofs = token.getProofs(tokenJson);
+ if (proofs.length == 0) {
+ throw new Error("no proofs found.");
+ }
+ }
+
try {
- await mintStore.activateMintUrl(invoice.mint, false, false, invoice.unit);
// this is an outgoing invoice, we first do a getMintQuote to check if the invoice is paid
- const mintQuote = await mintStore.activeMint().api.checkMeltQuote(quote);
- console.log("### mintQuote", mintQuote);
- if (mintQuote.state != MeltQuoteState.PAID) {
+ const mint = new CashuMint(invoice.mint);
+ const mintQuote = await mint.checkMeltQuote(quote);
+ if (mintQuote.state == MeltQuoteState.PENDING) {
console.log("### mintQuote not paid yet");
- if (invoice.token) {
- const tokenJson = token.decode(invoice.token);
- if (tokenJson == undefined) {
- throw new Error("no tokens provided.");
- }
- let proofs = token.getProofs(tokenJson);
- if (proofs.length == 0) {
- throw new Error("no proofs found.");
- }
- const states = await this.getProofState(proofs);
- // if all proofs are CheckStateEnum.PENDING, we notify that the invoice is still pending
- if (states.every((s) => s.state === CheckStateEnum.PENDING)) {
- if (verbose) {
- notify("Invoice still pending");
- }
- throw new Error("invoice not paid yet.");
- }
- // if all proofs are CheckStateEnum.UNSPENT, we assume that the payment failed and we unset the proofs as reserved
- // and remove the invoice from the history
- if (states.every((s) => s.state === CheckStateEnum.UNSPENT)) {
- useProofsStore().setReserved(proofs, false);
- this.removeOutgoingInvoiceFromHistory(quote);
- notifyWarning("Lightning payment failed");
- }
- //
- } else {
- throw new Error("no token in invoice.");
+ if (verbose) {
+ notify("Invoice still pending");
}
- } else {
+ throw new Error("invoice not paid yet.");
+ }
+ 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"
- if (invoice.token) {
- const tokenJson = token.decode(invoice.token);
- if (tokenJson == undefined) {
- throw new Error("no tokens provided.");
- }
- let proofs = token.getProofs(tokenJson);
- if (proofs.length == 0) {
- throw new Error("no proofs found.");
- }
- const spentProofs = await this.checkProofsSpendable(proofs, true);
- if (spentProofs != undefined && spentProofs.length == proofs.length) {
- if (!!window.navigator.vibrate) navigator.vibrate(200);
- notifySuccess("Sent " + uIStore.formatCurrency(useProofsStore().sumProofs(spentProofs), mintStore.activeUnit));
- }
- // set invoice in history to paid
- this.setInvoicePaid(quote);
+ 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));
}
+ // set invoice in history to paid
+ this.setInvoicePaid(quote);
}
} catch (error: any) {
if (verbose) {
@@ -952,7 +859,10 @@ export const useWalletStore = defineStore("wallet", {
}
},
////////////// UI HELPERS //////////////
- addOutgoingPendingInvoiceToHistory: function (quote: MeltQuoteResponse, serlializedToken?: string) {
+ addOutgoingPendingInvoiceToHistory: function (quote: MeltQuoteResponse, sendProofs: Proof[]) {
+ const proofsStore = useProofsStore();
+ const serlializedToken = proofsStore.serializeProofs(sendProofs);
+ proofsStore.setReserved(sendProofs, true);
const mintStore = useMintsStore();
this.invoiceHistory.push({
amount: -(quote.amount + quote.fee_reserve),
@@ -963,6 +873,7 @@ export const useWalletStore = defineStore("wallet", {
status: "pending",
mint: mintStore.activeMintUrl,
token: serlializedToken,
+ unit: mintStore.activeUnit,
});
},
removeOutgoingInvoiceFromHistory: function (quote: string) {
@@ -1196,7 +1107,6 @@ export const useWalletStore = defineStore("wallet", {
const satPrice = 1 / (priceUsd / 1e8);
const usdAmount = amount;
amount = Math.floor(usdAmount * satPrice);
- console.log(`converted amount: ${amount}`);
}
var { data } = await axios.get(
`${this.payInvoiceData.lnurlpay.callback}?amount=${amount * 1000}`
@@ -1206,8 +1116,6 @@ export const useWalletStore = defineStore("wallet", {
notifyError(data.reason, "LNURL Error");
return;
}
- console.log(data.pr);
- console.log(`callback: ${this.payInvoiceData.lnurlpay.callback}`);
await this.decodeRequest(data.pr);
}
},