applesauce-wallet 0.0.0-next-20250314133024 → 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,3 +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 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,3 +63,48 @@ export function CompleteSpend(spent, change) {
62
63
  yield signedHistory;
63
64
  };
64
65
  }
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
+ }
@@ -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", () => {
@@ -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
- /** Gets the totaled amount of proofs in a token event */
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): import("nostr-tools").Event[];
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
@@ -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
- /** Gets the totaled amount of proofs in a token event */
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
- return getOrComputeCachedValue(token, TokenProofsTotalSymbol, () => {
48
- const content = getTokenContent(token);
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 selected = [];
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("Ran out of tokens");
70
- selected.push(token);
71
- amount += getTokenProofsTotal(token);
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 selected;
90
+ return { events: selectedTokens, proofs: selectedProofs };
74
91
  }
75
92
  /**
76
93
  * Returns a decoded cashu token inside an unicode emoji
@@ -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,
@@ -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) => tokens.reduce((totals, token) => {
54
- const details = getTokenContent(token);
55
- const total = details.proofs.reduce((t, p) => t + p.amount, 0);
56
- return { ...totals, [details.mint]: (totals[details.mint] ?? 0) + total };
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-20250314133024",
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-20250314133024",
84
- "applesauce-core": "0.0.0-next-20250314133024",
85
- "applesauce-factory": "0.0.0-next-20250314133024",
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-20250314133024",
92
+ "applesauce-signers": "0.0.0-next-20250315131629",
93
93
  "typescript": "^5.7.3",
94
94
  "vitest": "^3.0.5"
95
95
  },