applesauce-wallet 0.0.0-next-20250314133024 → 0.0.0-next-20250314151125
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/actions/tokens.d.ts
CHANGED
|
@@ -11,3 +11,4 @@ export declare function ReceiveToken(token: Token, redeemed?: string[], fee?: nu
|
|
|
11
11
|
export declare function RolloverTokens(tokens: NostrEvent[], token: Token): Action;
|
|
12
12
|
/** An action that deletes old token events and adds a spend history item */
|
|
13
13
|
export declare function CompleteSpend(spent: NostrEvent[], change: Token): Action;
|
|
14
|
+
/** combines all unlocked existing token events into a single event per mint */
|
package/dist/actions/tokens.js
CHANGED
|
@@ -62,3 +62,32 @@ export function CompleteSpend(spent, change) {
|
|
|
62
62
|
yield signedHistory;
|
|
63
63
|
};
|
|
64
64
|
}
|
|
65
|
+
/** combines all unlocked existing token events into a single event per mint */
|
|
66
|
+
// export function ConsolidateTokens({ ignoreLocked = true }): Action {
|
|
67
|
+
// return async function* ({ events, factory, self }) {
|
|
68
|
+
// const tokens = Array.from(events.getAll({ kinds: [WALLET_TOKEN_KIND], authors: [self] })).filter((token) => {
|
|
69
|
+
// if (isTokenContentLocked(token)) {
|
|
70
|
+
// if (ignoreLocked) return false;
|
|
71
|
+
// else throw new Error("Token is locked");
|
|
72
|
+
// } else return true;
|
|
73
|
+
// });
|
|
74
|
+
// const byMint = tokens.reduce((map, token) => {
|
|
75
|
+
// const mint = getTokenContent(token)!.mint;
|
|
76
|
+
// if (!map.has(mint)) map.set(mint, []);
|
|
77
|
+
// map.get(mint)!.push(token);
|
|
78
|
+
// return map;
|
|
79
|
+
// }, new Map<string, NostrEvent[]>());
|
|
80
|
+
// for (const [mint, tokens] of byMint) {
|
|
81
|
+
// const cashuMint = new CashuMint(mint);
|
|
82
|
+
// const cashuWallet = new CashuWallet(cashuMint);
|
|
83
|
+
// const proofs = tokens
|
|
84
|
+
// .map((t) => getTokenContent(t)!.proofs)
|
|
85
|
+
// .flat()
|
|
86
|
+
// .filter(ignoreDuplicateProofs());
|
|
87
|
+
// // NOTE: this assumes that the states array is the same length and order as the proofs array
|
|
88
|
+
// const states = await cashuWallet.checkProofsStates(proofs);
|
|
89
|
+
// const notSpent = proofs.filter((_, i) => states[i].state !== CheckStateEnum.SPENT);
|
|
90
|
+
// // delete old
|
|
91
|
+
// }
|
|
92
|
+
// };
|
|
93
|
+
// }
|
|
@@ -3,7 +3,7 @@ import { EventFactory } from "applesauce-factory";
|
|
|
3
3
|
import { FakeUser } from "../../__tests__/fake-user.js";
|
|
4
4
|
import { WalletTokenBlueprint } from "../../blueprints/tokens.js";
|
|
5
5
|
import { decodeTokenFromEmojiString, dumbTokenSelection, encodeTokenToEmoji, unlockTokenContent } from "../tokens.js";
|
|
6
|
-
import { HiddenContentSymbol } from "applesauce-core/helpers";
|
|
6
|
+
import { HiddenContentSymbol, unixNow } from "applesauce-core/helpers";
|
|
7
7
|
const user = new FakeUser();
|
|
8
8
|
const factory = new EventFactory({ signer: user });
|
|
9
9
|
describe("dumbTokenSelection", () => {
|
|
@@ -20,7 +20,7 @@ describe("dumbTokenSelection", () => {
|
|
|
20
20
|
bDraft.created_at -= 60 * 60 * 7;
|
|
21
21
|
const b = await user.signEvent(bDraft);
|
|
22
22
|
await unlockTokenContent(b, user);
|
|
23
|
-
expect(dumbTokenSelection([a, b], 40)).toEqual([b]);
|
|
23
|
+
expect(dumbTokenSelection([a, b], 40).events).toEqual([b]);
|
|
24
24
|
});
|
|
25
25
|
it("should select enough tokens to total min amount", async () => {
|
|
26
26
|
const a = await user.signEvent(await factory.create(WalletTokenBlueprint, {
|
|
@@ -35,7 +35,7 @@ describe("dumbTokenSelection", () => {
|
|
|
35
35
|
bDraft.created_at -= 60 * 60 * 7;
|
|
36
36
|
const b = await user.signEvent(bDraft);
|
|
37
37
|
await unlockTokenContent(b, user);
|
|
38
|
-
expect(dumbTokenSelection([a, b], 120)).toEqual(expect.arrayContaining([a, b]));
|
|
38
|
+
expect(dumbTokenSelection([a, b], 120).events).toEqual(expect.arrayContaining([a, b]));
|
|
39
39
|
});
|
|
40
40
|
it("should throw if not enough funds", async () => {
|
|
41
41
|
const a = await user.signEvent(await factory.create(WalletTokenBlueprint, {
|
|
@@ -59,7 +59,47 @@ describe("dumbTokenSelection", () => {
|
|
|
59
59
|
const b = await user.signEvent(bDraft);
|
|
60
60
|
// manually remove the hidden content to lock it again
|
|
61
61
|
Reflect.deleteProperty(b, HiddenContentSymbol);
|
|
62
|
-
expect(dumbTokenSelection([a, b], 20)).toEqual([a]);
|
|
62
|
+
expect(dumbTokenSelection([a, b], 20).events).toEqual([a]);
|
|
63
|
+
});
|
|
64
|
+
it("should ignore duplicate proofs", async () => {
|
|
65
|
+
const a = await user.signEvent(await factory.create(WalletTokenBlueprint, {
|
|
66
|
+
mint: "https://money.com",
|
|
67
|
+
proofs: [{ secret: "A", C: "A", id: "A", amount: 100 }],
|
|
68
|
+
}));
|
|
69
|
+
// create a second event with the same proofs
|
|
70
|
+
const b = await user.signEvent(await factory.create(WalletTokenBlueprint, {
|
|
71
|
+
mint: "https://money.com",
|
|
72
|
+
proofs: [{ secret: "A", C: "A", id: "A", amount: 100 }],
|
|
73
|
+
}));
|
|
74
|
+
expect(() => dumbTokenSelection([a, b], 150)).toThrow();
|
|
75
|
+
});
|
|
76
|
+
it("should include duplicate token events and ignore duplicate proofs", async () => {
|
|
77
|
+
const A = { secret: "A", C: "A", id: "A", amount: 100 };
|
|
78
|
+
const a = await user.signEvent({
|
|
79
|
+
...(await factory.create(WalletTokenBlueprint, {
|
|
80
|
+
mint: "https://money.com",
|
|
81
|
+
proofs: [A],
|
|
82
|
+
})),
|
|
83
|
+
// make event older
|
|
84
|
+
created_at: unixNow() - 100,
|
|
85
|
+
});
|
|
86
|
+
// create a second event with the same proofs
|
|
87
|
+
const a2 = await user.signEvent({
|
|
88
|
+
...(await factory.create(WalletTokenBlueprint, {
|
|
89
|
+
mint: "https://money.com",
|
|
90
|
+
proofs: [A],
|
|
91
|
+
})),
|
|
92
|
+
// make event older
|
|
93
|
+
created_at: a.created_at - 200,
|
|
94
|
+
});
|
|
95
|
+
const B = { secret: "B", C: "B", id: "B", amount: 50 };
|
|
96
|
+
const b = await user.signEvent(await factory.create(WalletTokenBlueprint, {
|
|
97
|
+
mint: "https://money.com",
|
|
98
|
+
proofs: [B],
|
|
99
|
+
}));
|
|
100
|
+
const result = dumbTokenSelection([a, a2, b], 150);
|
|
101
|
+
expect(result.events.map((e) => e.id)).toEqual(expect.arrayContaining([a.id, a2.id, b.id]));
|
|
102
|
+
expect(result.proofs).toEqual([A, B]);
|
|
63
103
|
});
|
|
64
104
|
});
|
|
65
105
|
describe("encodeTokenToEmoji", () => {
|
package/dist/helpers/tokens.d.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
import { Token } from "@cashu/cashu-ts";
|
|
1
|
+
import { Proof, Token } from "@cashu/cashu-ts";
|
|
2
2
|
import { HiddenContentSigner } from "applesauce-core/helpers";
|
|
3
3
|
import { NostrEvent } from "nostr-tools";
|
|
4
|
+
/** Internal method for creating a unique id for each proof */
|
|
5
|
+
export declare function getProofUID(proof: Proof): string;
|
|
6
|
+
/** Internal method to filter out duplicate proofs */
|
|
7
|
+
export declare function ignoreDuplicateProofs(seen?: Set<string>): (proof: Proof) => boolean;
|
|
4
8
|
export declare const WALLET_TOKEN_KIND = 7375;
|
|
5
9
|
export type TokenContent = {
|
|
6
10
|
/** Cashu mint for the proofs */
|
|
@@ -18,7 +22,6 @@ export type TokenContent = {
|
|
|
18
22
|
del: string[];
|
|
19
23
|
};
|
|
20
24
|
export declare const TokenContentSymbol: unique symbol;
|
|
21
|
-
export declare const TokenProofsTotalSymbol: unique symbol;
|
|
22
25
|
/**
|
|
23
26
|
* Returns the decrypted and parsed details of a 7375 token event
|
|
24
27
|
* @throws
|
|
@@ -30,13 +33,19 @@ export declare function isTokenContentLocked(token: NostrEvent): boolean;
|
|
|
30
33
|
export declare function unlockTokenContent(token: NostrEvent, signer: HiddenContentSigner): Promise<TokenContent>;
|
|
31
34
|
/** Removes the unencrypted hidden content */
|
|
32
35
|
export declare function lockTokenContent(token: NostrEvent): void;
|
|
33
|
-
/**
|
|
36
|
+
/**
|
|
37
|
+
* Gets the totaled amount of proofs in a token event
|
|
38
|
+
* @param token The token event to calculate the total
|
|
39
|
+
*/
|
|
34
40
|
export declare function getTokenProofsTotal(token: NostrEvent): number | undefined;
|
|
35
41
|
/**
|
|
36
|
-
* Selects oldest tokens that total up to more than the min amount
|
|
42
|
+
* Selects oldest tokens and proofs that total up to more than the min amount
|
|
37
43
|
* @throws
|
|
38
44
|
*/
|
|
39
|
-
export declare function dumbTokenSelection(tokens: NostrEvent[], minAmount: number, mint?: string):
|
|
45
|
+
export declare function dumbTokenSelection(tokens: NostrEvent[], minAmount: number, mint?: string): {
|
|
46
|
+
events: NostrEvent[];
|
|
47
|
+
proofs: Proof[];
|
|
48
|
+
};
|
|
40
49
|
/**
|
|
41
50
|
* Returns a decoded cashu token inside an unicode emoji
|
|
42
51
|
* @see https://github.com/cashubtc/cashu.me/blob/1194a7b9ee2f43305e38304de7bef8839601ff4d/src/components/ReceiveTokenDialog.vue#L387
|
package/dist/helpers/tokens.js
CHANGED
|
@@ -1,8 +1,23 @@
|
|
|
1
1
|
import { getDecodedToken, getEncodedToken } from "@cashu/cashu-ts";
|
|
2
2
|
import { getHiddenContent, getOrComputeCachedValue, isHiddenContentLocked, isHiddenTagsLocked, lockHiddenContent, unlockHiddenContent, } from "applesauce-core/helpers";
|
|
3
|
+
/** Internal method for creating a unique id for each proof */
|
|
4
|
+
export function getProofUID(proof) {
|
|
5
|
+
return proof.id + proof.amount + proof.C + proof.secret;
|
|
6
|
+
}
|
|
7
|
+
/** Internal method to filter out duplicate proofs */
|
|
8
|
+
export function ignoreDuplicateProofs(seen = new Set()) {
|
|
9
|
+
return (proof) => {
|
|
10
|
+
const id = getProofUID(proof);
|
|
11
|
+
if (seen.has(id))
|
|
12
|
+
return false;
|
|
13
|
+
else {
|
|
14
|
+
seen.add(id);
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
}
|
|
3
19
|
export const WALLET_TOKEN_KIND = 7375;
|
|
4
20
|
export const TokenContentSymbol = Symbol.for("token-content");
|
|
5
|
-
export const TokenProofsTotalSymbol = Symbol.for("token-proofs-total");
|
|
6
21
|
/**
|
|
7
22
|
* Returns the decrypted and parsed details of a 7375 token event
|
|
8
23
|
* @throws
|
|
@@ -37,20 +52,20 @@ export async function unlockTokenContent(token, signer) {
|
|
|
37
52
|
/** Removes the unencrypted hidden content */
|
|
38
53
|
export function lockTokenContent(token) {
|
|
39
54
|
Reflect.deleteProperty(token, TokenContentSymbol);
|
|
40
|
-
Reflect.deleteProperty(token, TokenProofsTotalSymbol);
|
|
41
55
|
lockHiddenContent(token);
|
|
42
56
|
}
|
|
43
|
-
/**
|
|
57
|
+
/**
|
|
58
|
+
* Gets the totaled amount of proofs in a token event
|
|
59
|
+
* @param token The token event to calculate the total
|
|
60
|
+
*/
|
|
44
61
|
export function getTokenProofsTotal(token) {
|
|
45
62
|
if (isTokenContentLocked(token))
|
|
46
63
|
return undefined;
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
return content.proofs.reduce((t, p) => t + p.amount, 0);
|
|
50
|
-
});
|
|
64
|
+
const content = getTokenContent(token);
|
|
65
|
+
return content.proofs.reduce((t, p) => t + p.amount, 0);
|
|
51
66
|
}
|
|
52
67
|
/**
|
|
53
|
-
* Selects oldest tokens that total up to more than the min amount
|
|
68
|
+
* Selects oldest tokens and proofs that total up to more than the min amount
|
|
54
69
|
* @throws
|
|
55
70
|
*/
|
|
56
71
|
export function dumbTokenSelection(tokens, minAmount, mint) {
|
|
@@ -58,19 +73,21 @@ export function dumbTokenSelection(tokens, minAmount, mint) {
|
|
|
58
73
|
const sorted = tokens
|
|
59
74
|
.filter((token) => !isTokenContentLocked(token) && (mint ? getTokenContent(token).mint === mint : true))
|
|
60
75
|
.sort((a, b) => b.created_at - a.created_at);
|
|
61
|
-
const total = sorted.reduce((t, token) => t + getTokenProofsTotal(token), 0);
|
|
62
|
-
if (total < minAmount)
|
|
63
|
-
throw new Error("Insufficient funds");
|
|
64
76
|
let amount = 0;
|
|
65
|
-
const
|
|
77
|
+
const seen = new Set();
|
|
78
|
+
const selectedTokens = [];
|
|
79
|
+
const selectedProofs = [];
|
|
66
80
|
while (amount < minAmount) {
|
|
67
81
|
const token = sorted.pop();
|
|
68
82
|
if (!token)
|
|
69
|
-
throw new Error("
|
|
70
|
-
|
|
71
|
-
|
|
83
|
+
throw new Error("Insufficient funds");
|
|
84
|
+
const proofs = getTokenContent(token).proofs.filter(ignoreDuplicateProofs(seen));
|
|
85
|
+
const total = proofs.reduce((t, p) => t + p.amount, 0);
|
|
86
|
+
selectedTokens.push(token);
|
|
87
|
+
selectedProofs.push(...proofs);
|
|
88
|
+
amount += total;
|
|
72
89
|
}
|
|
73
|
-
return
|
|
90
|
+
return { events: selectedTokens, proofs: selectedProofs };
|
|
74
91
|
}
|
|
75
92
|
/**
|
|
76
93
|
* Returns a decoded cashu token inside an unicode emoji
|
package/dist/queries/tokens.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { combineLatest, filter, map, startWith } from "rxjs";
|
|
2
|
-
import { getTokenContent, isTokenContentLocked, WALLET_TOKEN_KIND } from "../helpers/tokens.js";
|
|
2
|
+
import { getTokenContent, ignoreDuplicateProofs, isTokenContentLocked, WALLET_TOKEN_KIND } from "../helpers/tokens.js";
|
|
3
3
|
/** removes deleted events from sorted array */
|
|
4
4
|
function filterDeleted(tokens) {
|
|
5
5
|
const deleted = new Set();
|
|
@@ -50,11 +50,15 @@ export function WalletBalanceQuery(pubkey) {
|
|
|
50
50
|
// filter out deleted tokens
|
|
51
51
|
map(filterDeleted),
|
|
52
52
|
// map tokens to totals
|
|
53
|
-
map((tokens) =>
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
-
return
|
|
57
|
-
|
|
53
|
+
map((tokens) => {
|
|
54
|
+
// ignore duplicate proofs
|
|
55
|
+
const seen = new Set();
|
|
56
|
+
return tokens.reduce((totals, token) => {
|
|
57
|
+
const details = getTokenContent(token);
|
|
58
|
+
const total = details.proofs.filter(ignoreDuplicateProofs(seen)).reduce((t, p) => t + p.amount, 0);
|
|
59
|
+
return { ...totals, [details.mint]: (totals[details.mint] ?? 0) + total };
|
|
60
|
+
}, {});
|
|
61
|
+
}));
|
|
58
62
|
},
|
|
59
63
|
};
|
|
60
64
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "applesauce-wallet",
|
|
3
|
-
"version": "0.0.0-next-
|
|
3
|
+
"version": "0.0.0-next-20250314151125",
|
|
4
4
|
"description": "",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -80,16 +80,16 @@
|
|
|
80
80
|
"@cashu/cashu-ts": "2.0.0-rc1",
|
|
81
81
|
"@gandlaf21/bc-ur": "^1.1.12",
|
|
82
82
|
"@noble/hashes": "^1.7.1",
|
|
83
|
-
"applesauce-actions": "0.0.0-next-
|
|
84
|
-
"applesauce-core": "0.0.0-next-
|
|
85
|
-
"applesauce-factory": "0.0.0-next-
|
|
83
|
+
"applesauce-actions": "0.0.0-next-20250314151125",
|
|
84
|
+
"applesauce-core": "0.0.0-next-20250314151125",
|
|
85
|
+
"applesauce-factory": "0.0.0-next-20250314151125",
|
|
86
86
|
"nostr-tools": "^2.10.4",
|
|
87
87
|
"rxjs": "^7.8.1"
|
|
88
88
|
},
|
|
89
89
|
"devDependencies": {
|
|
90
90
|
"@hirez_io/observer-spy": "^2.2.0",
|
|
91
91
|
"@types/debug": "^4.1.12",
|
|
92
|
-
"applesauce-signers": "0.0.0-next-
|
|
92
|
+
"applesauce-signers": "0.0.0-next-20250314151125",
|
|
93
93
|
"typescript": "^5.7.3",
|
|
94
94
|
"vitest": "^3.0.5"
|
|
95
95
|
},
|