applesauce-wallet 0.0.0-next-20250314151125 → 0.0.0-next-20250315131629

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.
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,139 @@
1
+ import { describe, it, expect, beforeEach, vitest } from "vitest";
2
+ import { EventStore } from "applesauce-core";
3
+ import { EventFactory } from "applesauce-factory";
4
+ import { ActionHub } from "applesauce-actions";
5
+ import { CheckStateEnum } from "@cashu/cashu-ts";
6
+ import { subscribeSpyTo } from "@hirez_io/observer-spy";
7
+ import { FakeUser } from "../../__tests__/fake-user.js";
8
+ import { ConsolidateTokens } from "../tokens.js";
9
+ import { WalletTokenBlueprint } from "../../blueprints/tokens.js";
10
+ import { unlockTokenContent, WALLET_TOKEN_KIND } from "../../helpers/tokens.js";
11
+ // Update the mock to allow controlling the states
12
+ const mockCheckProofsStates = vitest.fn();
13
+ vitest.mock("@cashu/cashu-ts", () => ({
14
+ CashuMint: vitest.fn(),
15
+ CashuWallet: vitest.fn().mockImplementation(() => ({
16
+ checkProofsStates: mockCheckProofsStates,
17
+ })),
18
+ CheckStateEnum: { UNSPENT: "UNSPENT", SPENT: "SPENT" },
19
+ }));
20
+ const user = new FakeUser();
21
+ const testMint = "https://mint.test.com";
22
+ let events;
23
+ let factory;
24
+ let hub;
25
+ beforeEach(() => {
26
+ events = new EventStore();
27
+ factory = new EventFactory({ signer: user });
28
+ hub = new ActionHub(events, factory);
29
+ // Reset the mock before each test
30
+ mockCheckProofsStates.mockReset();
31
+ });
32
+ describe("ConsolidateTokens", () => {
33
+ it("should combine multiple token events into a single event", async () => {
34
+ // Set all proofs to be unspent
35
+ mockCheckProofsStates.mockResolvedValue([{ state: CheckStateEnum.UNSPENT }, { state: CheckStateEnum.UNSPENT }]);
36
+ // Create two token events with different proofs
37
+ const token1 = await factory.sign(await factory.create(WalletTokenBlueprint, {
38
+ mint: testMint,
39
+ proofs: [{ amount: 10, secret: "secret1", C: "C", id: "id" }],
40
+ }));
41
+ const token2 = await factory.sign(await factory.create(WalletTokenBlueprint, {
42
+ mint: testMint,
43
+ proofs: [{ amount: 20, secret: "secret2", C: "C", id: "id" }],
44
+ }));
45
+ // Add tokens to event store
46
+ events.add(token1);
47
+ events.add(token2);
48
+ // Run consolidate action
49
+ const spy = subscribeSpyTo(hub.exec(ConsolidateTokens));
50
+ await spy.onComplete();
51
+ // First event should be the new consolidated token
52
+ expect(spy.getValueAt(0).kind).toBe(WALLET_TOKEN_KIND);
53
+ // Extract token content and verify proofs were combined
54
+ const content = await unlockTokenContent(spy.getValueAt(0), user);
55
+ expect(content.proofs).toHaveLength(2);
56
+ expect(content.proofs).toEqual(expect.arrayContaining([
57
+ expect.objectContaining({ amount: 10, secret: "secret1" }),
58
+ expect.objectContaining({ amount: 20, secret: "secret2" }),
59
+ ]));
60
+ expect(content.mint).toBe(testMint);
61
+ });
62
+ it("should handle duplicate proofs", async () => {
63
+ // Set all proofs to be unspent
64
+ mockCheckProofsStates.mockResolvedValue([{ state: CheckStateEnum.UNSPENT }, { state: CheckStateEnum.UNSPENT }]);
65
+ // Create two token events with different proofs
66
+ const token1 = await factory.sign(await factory.create(WalletTokenBlueprint, {
67
+ mint: testMint,
68
+ proofs: [{ amount: 10, secret: "secret1", C: "C", id: "id" }],
69
+ }));
70
+ const token2 = await factory.sign(await factory.create(WalletTokenBlueprint, {
71
+ mint: testMint,
72
+ proofs: [
73
+ { amount: 20, secret: "secret2", C: "C", id: "id" },
74
+ { amount: 10, secret: "secret1", C: "C", id: "id" },
75
+ ],
76
+ }));
77
+ // Add tokens to event store
78
+ events.add(token1);
79
+ events.add(token2);
80
+ // Run consolidate action
81
+ const spy = subscribeSpyTo(hub.exec(ConsolidateTokens));
82
+ await spy.onComplete();
83
+ // First event should be the new consolidated token
84
+ expect(spy.getValueAt(0).kind).toBe(WALLET_TOKEN_KIND);
85
+ // Extract token content and verify proofs were combined
86
+ const content = await unlockTokenContent(spy.getValueAt(0), user);
87
+ expect(content.proofs).toHaveLength(2);
88
+ expect(content.proofs).toEqual(expect.arrayContaining([
89
+ expect.objectContaining({ amount: 10, secret: "secret1" }),
90
+ expect.objectContaining({ amount: 20, secret: "secret2" }),
91
+ ]));
92
+ expect(content.mint).toBe(testMint);
93
+ });
94
+ it("should filter out spent proofs", async () => {
95
+ // Create token events with multiple proofs
96
+ const token1 = await factory.sign(await factory.create(WalletTokenBlueprint, {
97
+ mint: testMint,
98
+ proofs: [
99
+ { amount: 10, secret: "secret1", C: "C", id: "id" },
100
+ { amount: 20, secret: "secret2", C: "C", id: "id" },
101
+ ],
102
+ }, []));
103
+ const token2 = await factory.sign(await factory.create(WalletTokenBlueprint, {
104
+ mint: testMint,
105
+ proofs: [
106
+ { amount: 30, secret: "secret3", C: "C", id: "id" },
107
+ { amount: 40, secret: "secret4", C: "C", id: "id" },
108
+ ],
109
+ }, []));
110
+ // Add tokens to event store
111
+ events.add(token1);
112
+ events.add(token2);
113
+ // Mock some proofs as spent
114
+ mockCheckProofsStates.mockResolvedValue([
115
+ { state: CheckStateEnum.UNSPENT }, // secret1
116
+ { state: CheckStateEnum.SPENT }, // secret2
117
+ { state: CheckStateEnum.SPENT }, // secret3
118
+ { state: CheckStateEnum.UNSPENT }, // secret4
119
+ ]);
120
+ // Run consolidate action
121
+ const spy = subscribeSpyTo(hub.exec(ConsolidateTokens));
122
+ await spy.onComplete();
123
+ // Verify the consolidated token only contains unspent proofs
124
+ const content = await unlockTokenContent(spy.getValueAt(0), user);
125
+ expect(content.proofs).toHaveLength(2);
126
+ expect(content.proofs).toEqual(expect.arrayContaining([
127
+ expect.objectContaining({ amount: 10, secret: "secret1" }),
128
+ expect.objectContaining({ amount: 40, secret: "secret4" }),
129
+ ]));
130
+ expect(content.mint).toBe(testMint);
131
+ // Verify checkProofsStates was called with all proofs
132
+ expect(mockCheckProofsStates).toHaveBeenCalledWith(expect.arrayContaining([
133
+ expect.objectContaining({ amount: 10, secret: "secret1" }),
134
+ expect.objectContaining({ amount: 20, secret: "secret2" }),
135
+ expect.objectContaining({ amount: 30, secret: "secret3" }),
136
+ expect.objectContaining({ amount: 40, secret: "secret4" }),
137
+ ]));
138
+ });
139
+ });
@@ -11,4 +11,7 @@ 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 */
14
+ /** Combines all unlocked token events into a single event per mint */
15
+ export declare function ConsolidateTokens(opts?: {
16
+ ignoreLocked?: boolean;
17
+ }): Action;
@@ -1,5 +1,6 @@
1
+ import { CashuMint, CashuWallet, CheckStateEnum } from "@cashu/cashu-ts";
1
2
  import { DeleteBlueprint } from "applesauce-factory/blueprints";
2
- import { getTokenContent, isTokenContentLocked } from "../helpers/tokens.js";
3
+ import { getTokenContent, ignoreDuplicateProofs, isTokenContentLocked, WALLET_TOKEN_KIND } from "../helpers/tokens.js";
3
4
  import { WalletTokenBlueprint } from "../blueprints/tokens.js";
4
5
  import { WalletHistoryBlueprint } from "../blueprints/history.js";
5
6
  /**
@@ -62,32 +63,48 @@ export function CompleteSpend(spent, change) {
62
63
  yield signedHistory;
63
64
  };
64
65
  }
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
- // }
66
+ /** Combines all unlocked token events into a single event per mint */
67
+ export function ConsolidateTokens(opts) {
68
+ return async function* ({ events, factory, self }) {
69
+ const tokens = Array.from(events.getAll({ kinds: [WALLET_TOKEN_KIND], authors: [self] })).filter((token) => {
70
+ if (isTokenContentLocked(token)) {
71
+ if (opts?.ignoreLocked)
72
+ return false;
73
+ else
74
+ throw new Error("Token is locked");
75
+ }
76
+ else
77
+ return true;
78
+ });
79
+ const byMint = tokens.reduce((map, token) => {
80
+ const mint = getTokenContent(token).mint;
81
+ if (!map.has(mint))
82
+ map.set(mint, []);
83
+ map.get(mint).push(token);
84
+ return map;
85
+ }, new Map());
86
+ // loop over each mint and consolidate proofs
87
+ for (const [mint, tokens] of byMint) {
88
+ const cashuMint = new CashuMint(mint);
89
+ const cashuWallet = new CashuWallet(cashuMint);
90
+ // get all tokens proofs
91
+ const proofs = tokens
92
+ .map((t) => getTokenContent(t).proofs)
93
+ .flat()
94
+ // filter out duplicate proofs
95
+ .filter(ignoreDuplicateProofs());
96
+ // NOTE: this assumes that the states array is the same length and order as the proofs array
97
+ const states = await cashuWallet.checkProofsStates(proofs);
98
+ const notSpent = proofs.filter((_, i) => states[i].state !== CheckStateEnum.SPENT);
99
+ // create delete and token event
100
+ const deleteDraft = await factory.create(DeleteBlueprint, tokens);
101
+ const tokenDraft = await factory.create(WalletTokenBlueprint, { mint, proofs: notSpent }, tokens.map((t) => t.id));
102
+ // sign events
103
+ const signedToken = await factory.sign(tokenDraft);
104
+ const signedDelete = await factory.sign(deleteDraft);
105
+ // publish events for mint
106
+ yield signedToken;
107
+ yield signedDelete;
108
+ }
109
+ };
110
+ }
@@ -9,6 +9,10 @@ export function setTokenContent(token, del = []) {
9
9
  const method = EventContentEncryptionMethod[draft.kind];
10
10
  if (!method)
11
11
  throw new Error("Failed to find encryption method");
12
+ if (!token.mint)
13
+ throw new Error("Token mint is required");
14
+ if (!token.proofs || token.proofs.length === 0)
15
+ throw new Error("Token proofs are required");
12
16
  const content = {
13
17
  mint: token.mint,
14
18
  proofs: token.proofs,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "applesauce-wallet",
3
- "version": "0.0.0-next-20250314151125",
3
+ "version": "0.0.0-next-20250315131629",
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-20250314151125",
84
- "applesauce-core": "0.0.0-next-20250314151125",
85
- "applesauce-factory": "0.0.0-next-20250314151125",
83
+ "applesauce-actions": "0.0.0-next-20250315131629",
84
+ "applesauce-core": "0.0.0-next-20250315131629",
85
+ "applesauce-factory": "0.0.0-next-20250315131629",
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-20250314151125",
92
+ "applesauce-signers": "0.0.0-next-20250315131629",
93
93
  "typescript": "^5.7.3",
94
94
  "vitest": "^3.0.5"
95
95
  },