applesauce-wallet 0.0.0-next-20251209200210 → 0.0.0-next-20251220152312

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.
Files changed (75) hide show
  1. package/dist/actions/common.d.ts +4 -0
  2. package/dist/actions/common.js +15 -0
  3. package/dist/actions/index.d.ts +2 -2
  4. package/dist/actions/index.js +2 -2
  5. package/dist/actions/{zap-info.d.ts → nutzap-info.d.ts} +20 -3
  6. package/dist/actions/nutzap-info.js +117 -0
  7. package/dist/actions/nutzaps.d.ts +24 -0
  8. package/dist/actions/nutzaps.js +154 -0
  9. package/dist/actions/tokens.d.ts +77 -7
  10. package/dist/actions/tokens.js +332 -69
  11. package/dist/actions/wallet.d.ts +18 -3
  12. package/dist/actions/wallet.js +74 -32
  13. package/dist/blueprints/history.d.ts +1 -1
  14. package/dist/blueprints/history.js +1 -1
  15. package/dist/blueprints/wallet.d.ts +5 -1
  16. package/dist/blueprints/wallet.js +6 -3
  17. package/dist/casts/__register__.d.ts +20 -0
  18. package/dist/casts/__register__.js +41 -0
  19. package/dist/casts/index.d.ts +6 -0
  20. package/dist/casts/index.js +6 -0
  21. package/dist/casts/nutzap-info.d.ts +14 -0
  22. package/dist/casts/nutzap-info.js +22 -0
  23. package/dist/casts/nutzap.d.ts +16 -0
  24. package/dist/casts/nutzap.js +37 -0
  25. package/dist/casts/wallet-history.d.ts +16 -0
  26. package/dist/casts/wallet-history.js +40 -0
  27. package/dist/casts/wallet-token.d.ts +29 -0
  28. package/dist/casts/wallet-token.js +52 -0
  29. package/dist/casts/wallet.d.ts +27 -0
  30. package/dist/casts/wallet.js +62 -0
  31. package/dist/helpers/cashu.d.ts +21 -0
  32. package/dist/helpers/cashu.js +105 -0
  33. package/dist/helpers/couch.d.ts +11 -0
  34. package/dist/helpers/couch.js +1 -0
  35. package/dist/helpers/history.d.ts +5 -1
  36. package/dist/helpers/history.js +13 -4
  37. package/dist/helpers/index.d.ts +5 -1
  38. package/dist/helpers/index.js +5 -1
  39. package/dist/helpers/indexed-db-couch.d.ts +34 -0
  40. package/dist/helpers/indexed-db-couch.js +119 -0
  41. package/dist/helpers/local-storage-couch.d.ts +29 -0
  42. package/dist/helpers/local-storage-couch.js +78 -0
  43. package/dist/helpers/{zap-info.d.ts → nutzap-info.d.ts} +10 -1
  44. package/dist/helpers/{zap-info.js → nutzap-info.js} +22 -10
  45. package/dist/helpers/nutzap.d.ts +15 -0
  46. package/dist/helpers/nutzap.js +57 -3
  47. package/dist/helpers/tokens.d.ts +9 -18
  48. package/dist/helpers/tokens.js +64 -94
  49. package/dist/helpers/wallet.d.ts +16 -6
  50. package/dist/helpers/wallet.js +40 -14
  51. package/dist/index.d.ts +1 -0
  52. package/dist/index.js +1 -0
  53. package/dist/models/history.d.ts +1 -1
  54. package/dist/models/history.js +7 -10
  55. package/dist/models/index.d.ts +0 -1
  56. package/dist/models/index.js +0 -1
  57. package/dist/models/nutzap.d.ts +2 -0
  58. package/dist/models/nutzap.js +8 -0
  59. package/dist/models/tokens.d.ts +2 -2
  60. package/dist/models/tokens.js +14 -17
  61. package/dist/operations/history.js +1 -1
  62. package/dist/operations/index.d.ts +1 -1
  63. package/dist/operations/index.js +1 -1
  64. package/dist/operations/nutzap-info.d.ts +21 -0
  65. package/dist/operations/nutzap-info.js +71 -0
  66. package/dist/operations/wallet.d.ts +10 -1
  67. package/dist/operations/wallet.js +33 -3
  68. package/package.json +15 -6
  69. package/dist/actions/zap-info.js +0 -83
  70. package/dist/actions/zaps.d.ts +0 -8
  71. package/dist/actions/zaps.js +0 -30
  72. package/dist/models/wallet.d.ts +0 -13
  73. package/dist/models/wallet.js +0 -21
  74. package/dist/operations/zap-info.d.ts +0 -10
  75. package/dist/operations/zap-info.js +0 -17
@@ -1,82 +1,143 @@
1
- import { CheckStateEnum, Wallet, Mint } from "@cashu/cashu-ts";
1
+ import { CheckStateEnum, sumProofs, Wallet } from "@cashu/cashu-ts";
2
2
  import { DeleteBlueprint } from "applesauce-common/blueprints/delete";
3
3
  import { WalletHistoryBlueprint } from "../blueprints/history.js";
4
4
  import { WalletTokenBlueprint } from "../blueprints/tokens.js";
5
- import { getTokenContent, ignoreDuplicateProofs, isTokenContentUnlocked, WALLET_TOKEN_KIND, } from "../helpers/tokens.js";
5
+ import { getProofUID, ignoreDuplicateProofs } from "../helpers/cashu.js";
6
+ import { dumbTokenSelection, getTokenContent, isTokenContentUnlocked, unlockTokenContent, WALLET_TOKEN_KIND, } from "../helpers/tokens.js";
7
+ import { getUnlockedWallet } from "./common.js";
8
+ // Make sure the wallet$ is registered on the user class
9
+ import "../casts/__register__.js";
6
10
  /**
7
- * Adds a cashu token to the wallet and marks a list of nutzaps as redeemed
11
+ * Adds a cashu token to the wallet and creates a history event
8
12
  * @param token the cashu token to add
9
- * @param redeemed an array of nutzap event ids to mark as redeemed
13
+ * @param redeemed an array of event ids to mark as redeemed
10
14
  */
11
- export function ReceiveToken(token, redeemed, fee) {
12
- return async function* ({ factory }) {
13
- const amount = token.proofs.reduce((t, p) => t + p.amount, 0);
14
- const tokenEvent = await factory.sign(await factory.create(WalletTokenBlueprint, token, []));
15
- const history = await factory.sign(await factory.create(WalletHistoryBlueprint, { direction: "in", amount, mint: token.mint, created: [tokenEvent.id], fee }, redeemed ?? []));
16
- yield tokenEvent;
17
- yield history;
15
+ export function AddToken(token, options) {
16
+ const { redeemed, fee, addHistory = true } = options ?? {};
17
+ return async ({ factory, user, publish, signer, sign }) => {
18
+ const wallet = await getUnlockedWallet(user, signer);
19
+ const amount = sumProofs(token.proofs);
20
+ // Create the token and history events
21
+ const tokenEvent = await factory.create(WalletTokenBlueprint, token).then(sign);
22
+ let history;
23
+ if (addHistory || redeemed?.length) {
24
+ history = await factory
25
+ .create(WalletHistoryBlueprint, { direction: "in", amount, mint: token.mint, created: [tokenEvent.id], fee }, redeemed)
26
+ .then(sign);
27
+ }
28
+ // Publish the events
29
+ await publish([tokenEvent, history].filter((e) => !!e), wallet.relays);
30
+ };
31
+ }
32
+ /** Similar to the AddToken action but swaps the tokens before receiving them */
33
+ export function ReceiveToken(token, options) {
34
+ return async ({ run }) => {
35
+ const { couch, ...restOptions } = options ?? {};
36
+ // Get the cashu wallet
37
+ const cashuWallet = new Wallet(token.mint);
38
+ await cashuWallet.loadMint();
39
+ const amount = sumProofs(token.proofs);
40
+ // Swap cashu tokens
41
+ const receivedProofs = await cashuWallet.ops.receive(token).run();
42
+ const fee = amount - sumProofs(receivedProofs);
43
+ // Create a new token with the received proofs
44
+ const receivedToken = {
45
+ ...token,
46
+ proofs: receivedProofs,
47
+ };
48
+ // Store token in couch immediately after receiving it
49
+ const clearStoredToken = await couch?.store(receivedToken);
50
+ try {
51
+ // Run the add token action
52
+ await run(AddToken, receivedToken, { ...restOptions, fee });
53
+ // Clear the stored token from the couch after successful completion
54
+ await clearStoredToken?.();
55
+ }
56
+ catch (error) {
57
+ // If an error occurs, don't clear the couch (tokens remain for recovery)
58
+ throw error;
59
+ }
18
60
  };
19
61
  }
20
62
  /** An action that deletes old tokens and creates a new one but does not add a history event */
21
63
  export function RolloverTokens(tokens, token) {
22
- return async function* ({ factory }) {
23
- // create a delete event for old tokens
24
- const deleteDraft = await factory.create(DeleteBlueprint, tokens);
64
+ return async ({ factory, user, publish, signer, sign }) => {
65
+ const wallet = await getUnlockedWallet(user, signer);
25
66
  // create a new token event
26
- const tokenDraft = await factory.create(WalletTokenBlueprint, token, tokens.map((e) => e.id));
27
- // sign events
28
- const signedDelete = await factory.sign(deleteDraft);
29
- const signedToken = await factory.sign(tokenDraft);
67
+ const tokenEvent = await factory
68
+ .create(WalletTokenBlueprint, token, tokens.map((e) => e.id))
69
+ .then(sign);
70
+ // create a delete event for old tokens
71
+ const deleteDraft = await factory.create(DeleteBlueprint, tokens).then(sign);
30
72
  // publish events
31
- yield signedDelete;
32
- yield signedToken;
73
+ await publish([tokenEvent, deleteDraft], wallet.relays);
33
74
  };
34
75
  }
35
76
  /** An action that deletes old token events and adds a spend history item */
36
- export function CompleteSpend(spent, change) {
37
- return async function* ({ factory }) {
77
+ export function CompleteSpend(spent, change, couch) {
78
+ return async ({ factory, user, publish, signer, sign }) => {
38
79
  if (spent.length === 0)
39
80
  throw new Error("Cant complete spent with no token events");
40
- if (spent.some((s) => isTokenContentUnlocked(s)))
81
+ const unlocked = spent.filter(isTokenContentUnlocked);
82
+ if (unlocked.length !== spent.length)
41
83
  throw new Error("Cant complete spend with locked tokens");
42
- // create the nip-09 delete event for previous events
43
- const deleteDraft = await factory.create(DeleteBlueprint, spent);
44
- const changeAmount = change.proofs.reduce((t, p) => t + p.amount, 0);
45
- // create a new token event if needed
46
- const changeDraft = changeAmount > 0
47
- ? await factory.create(WalletTokenBlueprint, change, spent.map((e) => e.id))
48
- : undefined;
49
- const total = spent.reduce((total, token) => total + getTokenContent(token).proofs.reduce((t, p) => t + p.amount, 0), 0);
50
- // calculate the amount that was spent
51
- const diff = total - changeAmount;
52
- // sign delete and token
53
- const signedDelete = await factory.sign(deleteDraft);
54
- const signedToken = changeDraft && (await factory.sign(changeDraft));
55
- // create a history entry
56
- const history = await factory.create(WalletHistoryBlueprint, { direction: "out", mint: change.mint, amount: diff, created: signedToken ? [signedToken.id] : [] }, []);
57
- // sign history
58
- const signedHistory = await factory.sign(history);
59
- // publish events
60
- yield signedDelete;
61
- if (signedToken)
62
- yield signedToken;
63
- yield signedHistory;
84
+ const wallet = await getUnlockedWallet(user, signer);
85
+ const changeAmount = sumProofs(change.proofs);
86
+ // Store change token in couch before creating token event
87
+ let clearStoredToken;
88
+ if (couch && changeAmount > 0) {
89
+ clearStoredToken = await couch.store(change);
90
+ }
91
+ try {
92
+ // create a new token event if needed
93
+ const tokenEvent = changeAmount > 0
94
+ ? await factory
95
+ .create(WalletTokenBlueprint, change, spent.map((e) => e.id))
96
+ .then(sign)
97
+ : undefined;
98
+ // Get tokens total amount
99
+ const total = sumProofs(unlocked.map((s) => getTokenContent(s).proofs).flat());
100
+ // calculate the amount that was spent
101
+ const diff = total - changeAmount;
102
+ // sign delete and token
103
+ const deleteEvent = await factory.create(DeleteBlueprint, spent).then(sign);
104
+ // create a history entry
105
+ const history = await factory
106
+ .create(WalletHistoryBlueprint, { direction: "out", mint: change.mint, amount: diff, created: tokenEvent ? [tokenEvent.id] : [] }, [])
107
+ .then(sign);
108
+ // publish events
109
+ await publish([tokenEvent, deleteEvent, history].filter((e) => !!e), wallet.relays);
110
+ // Clear the stored token from the couch after successful completion
111
+ await clearStoredToken?.();
112
+ }
113
+ catch (error) {
114
+ // If an error occurs, don't clear the couch (tokens remain for recovery)
115
+ throw error;
116
+ }
64
117
  };
65
118
  }
66
119
  /** 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.getByFilters({ kinds: [WALLET_TOKEN_KIND], authors: [self] })).filter((token) => {
70
- if (isTokenContentUnlocked(token)) {
71
- if (opts?.ignoreLocked)
72
- return false;
73
- else
74
- throw new Error("Token is locked");
120
+ export function ConsolidateTokens(options) {
121
+ return async ({ events, factory, self, sign, user, signer, publish }) => {
122
+ const wallet = await getUnlockedWallet(user, signer);
123
+ const tokens = Array.from(events.getByFilters({ kinds: [WALLET_TOKEN_KIND], authors: [self] }));
124
+ // Unlock tokens if requested
125
+ if (options?.unlockTokens) {
126
+ if (!signer)
127
+ throw new Error("Missing signer");
128
+ for (const token of tokens) {
129
+ if (!isTokenContentUnlocked(token)) {
130
+ try {
131
+ await unlockTokenContent(token, signer);
132
+ }
133
+ catch { }
134
+ }
75
135
  }
76
- else
77
- return true;
78
- });
79
- const byMint = tokens.reduce((map, token) => {
136
+ }
137
+ // Collect unlocked tokens
138
+ const unlockedTokens = tokens.filter(isTokenContentUnlocked);
139
+ // group tokens by mint
140
+ const byMint = unlockedTokens.reduce((map, token) => {
80
141
  const mint = getTokenContent(token).mint;
81
142
  if (!map.has(mint))
82
143
  map.set(mint, []);
@@ -85,26 +146,228 @@ export function ConsolidateTokens(opts) {
85
146
  }, new Map());
86
147
  // loop over each mint and consolidate proofs
87
148
  for (const [mint, tokens] of byMint) {
88
- const cashuMint = new Mint(mint);
89
- const cashuWallet = new Wallet(cashuMint);
90
149
  // get all tokens proofs
91
150
  const proofs = tokens
92
- .map((t) => getTokenContent(t).proofs)
151
+ .map((token) => getTokenContent(token).proofs)
93
152
  .flat()
94
153
  // filter out duplicate proofs
95
154
  .filter(ignoreDuplicateProofs());
155
+ // If there are no proofs, just delete the old tokens without interacting with the mint
156
+ if (proofs.length === 0) {
157
+ const deleteEvent = await factory.create(DeleteBlueprint, tokens).then(sign);
158
+ await publish(deleteEvent, wallet.relays);
159
+ continue;
160
+ }
161
+ // Only interact with the mint if there are proofs to check
162
+ const cashuWallet = new Wallet(mint);
163
+ await cashuWallet.loadMint();
96
164
  // NOTE: this assumes that the states array is the same length and order as the proofs array
97
165
  const states = await cashuWallet.checkProofsStates(proofs);
98
166
  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;
167
+ // Only create a token event if there are unspent proofs
168
+ const tokenEvent = notSpent.length > 0
169
+ ? await factory
170
+ .create(WalletTokenBlueprint, { mint, proofs: notSpent }, tokens.map((t) => t.id))
171
+ .then(sign)
172
+ : undefined;
173
+ // create delete event
174
+ const deleteEvent = await factory.create(DeleteBlueprint, tokens).then(sign);
175
+ // Publish events
176
+ await publish([tokenEvent, deleteEvent].filter((e) => !!e), wallet.relays);
177
+ }
178
+ };
179
+ }
180
+ /**
181
+ * Recovers tokens from a couch by checking if they exist in the wallet,
182
+ * verifying they are unspent, and creating token events for any recoverable tokens
183
+ * @param couch the couch interface to recover tokens from
184
+ */
185
+ export function RecoverFromCouch(couch) {
186
+ return async ({ events, factory, self, sign, user, signer, publish }) => {
187
+ const wallet = await getUnlockedWallet(user, signer);
188
+ // Get all tokens from the couch
189
+ const couchTokens = await couch.getAll();
190
+ if (couchTokens.length === 0)
191
+ return; // No tokens to recover
192
+ // Get all token events from the wallet
193
+ const walletTokens = Array.from(events.getByFilters({ kinds: [WALLET_TOKEN_KIND], authors: [self] }));
194
+ // Unlock wallet tokens if needed
195
+ if (signer) {
196
+ for (const token of walletTokens) {
197
+ if (!isTokenContentUnlocked(token)) {
198
+ try {
199
+ await unlockTokenContent(token, signer);
200
+ }
201
+ catch { }
202
+ }
203
+ }
204
+ }
205
+ // Collect all proofs from wallet tokens
206
+ const walletProofs = walletTokens
207
+ .filter(isTokenContentUnlocked)
208
+ .map((token) => getTokenContent(token).proofs)
209
+ .flat();
210
+ // Create a set of seen proof UIDs from wallet
211
+ const seenProofUIDs = new Set();
212
+ walletProofs.forEach((proof) => {
213
+ seenProofUIDs.add(getProofUID(proof));
214
+ });
215
+ // Group couch tokens by mint
216
+ const couchTokensByMint = new Map();
217
+ for (const token of couchTokens) {
218
+ if (!couchTokensByMint.has(token.mint)) {
219
+ couchTokensByMint.set(token.mint, []);
220
+ }
221
+ couchTokensByMint.get(token.mint).push(token);
222
+ }
223
+ // Process each mint group
224
+ for (const [mint, tokens] of couchTokensByMint) {
225
+ // Get all proofs from couch tokens for this mint
226
+ const couchProofs = tokens.flatMap((token) => token.proofs);
227
+ // Filter out proofs that are already in the wallet
228
+ const newProofs = couchProofs.filter((proof) => {
229
+ const uid = getProofUID(proof);
230
+ if (seenProofUIDs.has(uid))
231
+ return false;
232
+ seenProofUIDs.add(uid);
233
+ return true;
234
+ });
235
+ if (newProofs.length === 0)
236
+ continue; // No new proofs to recover
237
+ // Check if proofs are unspent from the mint
238
+ const cashuWallet = new Wallet(mint);
239
+ await cashuWallet.loadMint();
240
+ const states = await cashuWallet.checkProofsStates(newProofs);
241
+ const unspentProofs = newProofs.filter((_, i) => states[i].state !== CheckStateEnum.SPENT);
242
+ if (unspentProofs.length === 0)
243
+ continue; // No unspent proofs to recover
244
+ // Create a token event with the recovered proofs
245
+ const recoveredToken = {
246
+ mint,
247
+ proofs: unspentProofs,
248
+ unit: tokens[0]?.unit,
249
+ };
250
+ const tokenEvent = await factory.create(WalletTokenBlueprint, recoveredToken).then(sign);
251
+ // Publish the token event
252
+ await publish(tokenEvent, wallet.relays);
253
+ // Clear the token from the couch
254
+ await couch.clear();
255
+ }
256
+ };
257
+ }
258
+ /**
259
+ * A generic action that safely selects tokens, performs an async operation, and handles change.
260
+ * This action requires a couch for safety - tokens are stored in the couch before the operation
261
+ * and can be recovered if something goes wrong.
262
+ *
263
+ * @param minAmount The minimum amount of tokens to select (in sats)
264
+ * @param operation An async function that receives selected proofs and performs the operation.
265
+ * Should return any change proofs. All selected proofs are considered used.
266
+ * @param options Configuration options including mint filter, required couch, and optional custom token selection
267
+ *
268
+ * @example
269
+ * // Use with NutzapProfile
270
+ * await run(TokensOperation, 100, async ({ selectedProofs, mint, cashuWallet }) => {
271
+ * const { keep, send } = await cashuWallet.ops.send(100, selectedProofs).asP2PK({ pubkey }).run();
272
+ * await run(NutzapProfile, recipient, { mint, proofs: send, unit: "sat" });
273
+ * return { change: keep };
274
+ * }, { couch });
275
+ *
276
+ * @example
277
+ * // Use with melt
278
+ * await run(TokensOperation, meltAmount + feeReserve, async ({ selectedProofs, mint, cashuWallet }) => {
279
+ * const meltQuote = await cashuWallet.createMeltQuoteBolt11(invoice);
280
+ * const { keep, send } = await cashuWallet.send(meltAmount + meltQuote.fee_reserve, selectedProofs, { includeFees: true });
281
+ * const meltResponse = await cashuWallet.meltProofs(meltQuote, send);
282
+ * return { change: meltResponse.change };
283
+ * }, { couch });
284
+ *
285
+ * @example
286
+ * // Use with custom token selection
287
+ * await run(TokensOperation, 100, async ({ selectedProofs, mint, cashuWallet }) => {
288
+ * // ... operation
289
+ * }, { couch, tokenSelection: myCustomSelectionFunction });
290
+ */
291
+ export function TokensOperation(minAmount, operation, options) {
292
+ const { mint, couch, tokenSelection = dumbTokenSelection } = options;
293
+ return async ({ events, self, user, signer, run }) => {
294
+ if (!signer)
295
+ throw new Error("Missing signer");
296
+ if (!couch)
297
+ throw new Error("Couch is required for TokensOperation");
298
+ await getUnlockedWallet(user, signer);
299
+ // Get all unlocked token events
300
+ const allTokens = Array.from(events.getByFilters({ kinds: [WALLET_TOKEN_KIND], authors: [self] }));
301
+ // Unlock tokens if needed
302
+ for (const token of allTokens) {
303
+ if (!isTokenContentUnlocked(token)) {
304
+ try {
305
+ await unlockTokenContent(token, signer);
306
+ }
307
+ catch { }
308
+ }
309
+ }
310
+ // Filter to unlocked tokens
311
+ const unlockedTokens = allTokens.filter(isTokenContentUnlocked);
312
+ if (unlockedTokens.length === 0)
313
+ throw new Error("No unlocked tokens available");
314
+ // Select tokens using the provided token selection function (defaults to dumbTokenSelection)
315
+ // The selection function will find a mint with sufficient balance if mint is undefined
316
+ // and ensures all selected tokens are from the same mint
317
+ const { events: selectedTokenEvents, proofs: selectedProofs } = tokenSelection(unlockedTokens, minAmount, mint);
318
+ if (selectedProofs.length === 0)
319
+ throw new Error("No proofs selected");
320
+ // Get the mint from the first selected token
321
+ // The token selection function guarantees all tokens are from the same mint
322
+ const firstTokenContent = getTokenContent(selectedTokenEvents[0]);
323
+ if (!firstTokenContent)
324
+ throw new Error("Unable to get content from selected token");
325
+ const selectedMint = firstTokenContent.mint;
326
+ if (!selectedMint)
327
+ throw new Error("Unable to determine mint from selected tokens");
328
+ // Safety check: Verify all selected tokens are from the same mint
329
+ // (The token selection function should have already ensured this, but verify for safety)
330
+ for (const tokenEvent of selectedTokenEvents) {
331
+ const tokenContent = getTokenContent(tokenEvent);
332
+ if (!tokenContent)
333
+ throw new Error("Unable to get content from selected token");
334
+ const tokenMint = tokenContent.mint;
335
+ if (tokenMint !== selectedMint)
336
+ throw new Error(`Selected tokens must be from the same mint. Found ${tokenMint} and ${selectedMint}`);
337
+ }
338
+ // Store selected tokens in couch for safety
339
+ const selectedToken = {
340
+ mint: selectedMint,
341
+ proofs: selectedProofs,
342
+ unit: "sat",
343
+ };
344
+ const clearStoredToken = await couch.store(selectedToken);
345
+ try {
346
+ // Create cashu wallet for the mint
347
+ const cashuWallet = new Wallet(selectedMint);
348
+ await cashuWallet.loadMint();
349
+ // Perform the async operation
350
+ // All selected proofs are considered used - the operation only needs to return change (if any)
351
+ const { change } = await operation({
352
+ selectedProofs,
353
+ mint: selectedMint,
354
+ cashuWallet,
355
+ });
356
+ // Create change token from the change proofs returned by the operation (if any)
357
+ const changeToken = {
358
+ mint: selectedMint,
359
+ proofs: change ? change.filter(ignoreDuplicateProofs()) : [],
360
+ unit: "sat",
361
+ };
362
+ // Complete the spend with change (if any)
363
+ // If there's no change, all selected proofs were spent
364
+ await run(CompleteSpend, selectedTokenEvents, changeToken, couch);
365
+ // Clear the stored token from the couch after successful completion
366
+ await clearStoredToken();
367
+ }
368
+ catch (error) {
369
+ // If an error occurs, don't clear the couch (tokens remain for recovery)
370
+ throw error;
108
371
  }
109
372
  };
110
373
  }
@@ -1,13 +1,28 @@
1
1
  import { Action } from "applesauce-actions";
2
+ import "../casts/__register__.js";
2
3
  /** An action that creates a new 17375 wallet event and 375 wallet backup */
3
- export declare function CreateWallet(mints: string[], privateKey?: Uint8Array): Action;
4
+ export declare function CreateWallet({ mints, privateKey, relays, }: {
5
+ mints: string[];
6
+ privateKey?: Uint8Array;
7
+ relays?: string[];
8
+ }): Action;
4
9
  /**
5
10
  * Adds a private key to a wallet event
6
- * @throws if the wallet does not exist or is locked
11
+ * @throws if the wallet does not exist or cannot be unlocked
7
12
  */
8
- export declare function WalletAddPrivateKey(privateKey: Uint8Array): Action;
13
+ export declare function WalletAddPrivateKey(privateKey: Uint8Array, override?: boolean): Action;
9
14
  /** Unlocks the wallet event and optionally the tokens and history events */
10
15
  export declare function UnlockWallet(unlock?: {
11
16
  history?: boolean;
12
17
  tokens?: boolean;
13
18
  }): Action;
19
+ /**
20
+ * Sets the mints on a wallet event
21
+ * @throws if the wallet does not exist or cannot be unlocked
22
+ */
23
+ export declare function SetWalletMints(mints: string[]): Action;
24
+ /**
25
+ * Sets the relays on a wallet event
26
+ * @throws if the wallet does not exist or cannot be unlocked
27
+ */
28
+ export declare function SetWalletRelays(relays: (string | URL)[]): Action;
@@ -1,64 +1,106 @@
1
1
  import { WalletBackupBlueprint, WalletBlueprint } from "../blueprints/wallet.js";
2
- import { isHistoryContentUnlocked, unlockHistoryContent, WALLET_HISTORY_KIND } from "../helpers/history.js";
3
- import { isTokenContentUnlocked, unlockTokenContent, WALLET_TOKEN_KIND } from "../helpers/tokens.js";
4
- import { getWalletMints, getWalletPrivateKey, isWalletUnlocked, unlockWallet, WALLET_KIND } from "../helpers/wallet.js";
2
+ import { unlockHistoryContent, WALLET_HISTORY_KIND } from "../helpers/history.js";
3
+ import { NUTZAP_INFO_KIND } from "../helpers/nutzap-info.js";
4
+ import { unlockTokenContent, WALLET_TOKEN_KIND } from "../helpers/tokens.js";
5
+ import { getWalletMints, unlockWallet, WALLET_KIND } from "../helpers/wallet.js";
6
+ import { setNutzapInfoPubkey } from "../operations/nutzap-info.js";
7
+ import { setMints, setRelays } from "../operations/wallet.js";
8
+ import { getUnlockedWallet } from "./common.js";
9
+ // Make sure the wallet$ is registered on the user class
10
+ import "../casts/__register__.js";
5
11
  /** An action that creates a new 17375 wallet event and 375 wallet backup */
6
- export function CreateWallet(mints, privateKey) {
7
- return async function* ({ events, factory, self }) {
12
+ export function CreateWallet({ mints, privateKey, relays, }) {
13
+ return async ({ events, factory, self, publish }) => {
14
+ if (mints.length === 0)
15
+ throw new Error("At least one mint is required");
8
16
  const existing = events.getReplaceable(WALLET_KIND, self);
9
17
  if (existing)
10
18
  throw new Error("Wallet already exists");
11
- const wallet = await factory.sign(await factory.create(WalletBlueprint, mints, privateKey));
12
- const backup = await factory.sign(await factory.create(WalletBackupBlueprint, wallet));
13
- // publish the backup first
14
- yield backup;
15
- yield wallet;
19
+ // Create new wallet event
20
+ const wallet = await factory.sign(await factory.create(WalletBlueprint, { mints, privateKey, relays }));
21
+ // Setup nutzap info event
22
+ if (privateKey) {
23
+ // Create a backup event if a private key is provided
24
+ const backup = await factory.sign(await factory.create(WalletBackupBlueprint, wallet));
25
+ const nutzapInfo = events.getReplaceable(NUTZAP_INFO_KIND, self);
26
+ // Always set pubkey if private key is provided (create or update)
27
+ const nutzapInfoDraft = nutzapInfo
28
+ ? await factory.modify(nutzapInfo, setNutzapInfoPubkey(privateKey))
29
+ : await factory.build({ kind: NUTZAP_INFO_KIND }, setNutzapInfoPubkey(privateKey));
30
+ const info = await factory.sign(nutzapInfoDraft);
31
+ // Publish all events at the same time
32
+ await publish([wallet, backup, info], relays);
33
+ }
34
+ else {
35
+ // Just publish the wallet event
36
+ await publish(wallet, relays);
37
+ }
16
38
  };
17
39
  }
18
40
  /**
19
41
  * Adds a private key to a wallet event
20
- * @throws if the wallet does not exist or is locked
42
+ * @throws if the wallet does not exist or cannot be unlocked
21
43
  */
22
- export function WalletAddPrivateKey(privateKey) {
23
- return async function* ({ events, self, factory }) {
24
- const wallet = events.getReplaceable(WALLET_KIND, self);
25
- if (!wallet)
26
- throw new Error("Wallet does not exist");
27
- if (isWalletUnlocked(wallet))
28
- throw new Error("Wallet is locked");
29
- if (getWalletPrivateKey(wallet))
44
+ export function WalletAddPrivateKey(privateKey, override = false) {
45
+ return async ({ events, self, factory, user, signer, sign, publish }) => {
46
+ const wallet = await getUnlockedWallet(user, signer);
47
+ if (wallet.privateKey && override !== true)
30
48
  throw new Error("Wallet already has a private key");
31
- const draft = await factory.create(WalletBlueprint, getWalletMints(wallet), privateKey);
32
- const signed = await factory.sign(draft);
49
+ const signed = await factory
50
+ .create(WalletBlueprint, { mints: getWalletMints(wallet.event), privateKey })
51
+ .then(sign);
33
52
  // create backup event for wallet
34
- const backup = await factory.sign(await factory.create(WalletBackupBlueprint, signed));
35
- // publish events
36
- yield backup;
37
- yield signed;
53
+ const backup = await factory.create(WalletBackupBlueprint, signed).then(sign);
54
+ // set nutzap info pubkey for receiving nutzaps
55
+ const nutzapInfo = events.getReplaceable(NUTZAP_INFO_KIND, self);
56
+ const info = nutzapInfo
57
+ ? await factory.modify(nutzapInfo, setNutzapInfoPubkey(privateKey)).then(sign)
58
+ : await factory.build({ kind: NUTZAP_INFO_KIND }, setNutzapInfoPubkey(privateKey)).then(sign);
59
+ // publish all events at the same time
60
+ await publish([signed, backup, info], wallet.relays);
38
61
  };
39
62
  }
40
63
  /** Unlocks the wallet event and optionally the tokens and history events */
41
64
  export function UnlockWallet(unlock) {
42
- return async function* ({ events, self, factory }) {
65
+ return async ({ events, self, factory }) => {
43
66
  const signer = factory.context.signer;
44
67
  if (!signer)
45
68
  throw new Error("Missing signer");
46
69
  const wallet = events.getReplaceable(WALLET_KIND, self);
47
70
  if (!wallet)
48
71
  throw new Error("Wallet does not exist");
49
- if (isWalletUnlocked(wallet))
50
- await unlockWallet(wallet, signer);
72
+ await unlockWallet(wallet, signer);
51
73
  if (unlock?.tokens) {
52
74
  const tokens = events.getTimeline({ kinds: [WALLET_TOKEN_KIND], authors: [self] });
53
75
  for (const token of tokens)
54
- if (isTokenContentUnlocked(token))
55
- await unlockTokenContent(token, signer);
76
+ await unlockTokenContent(token, signer);
56
77
  }
57
78
  if (unlock?.history) {
58
79
  const history = events.getTimeline({ kinds: [WALLET_HISTORY_KIND], authors: [self] });
59
80
  for (const entry of history)
60
- if (isHistoryContentUnlocked(entry))
61
- await unlockHistoryContent(entry, signer);
81
+ await unlockHistoryContent(entry, signer);
62
82
  }
63
83
  };
64
84
  }
85
+ /**
86
+ * Sets the mints on a wallet event
87
+ * @throws if the wallet does not exist or cannot be unlocked
88
+ */
89
+ export function SetWalletMints(mints) {
90
+ return async ({ user, signer, factory, sign, publish }) => {
91
+ const wallet = await getUnlockedWallet(user, signer);
92
+ const signed = await factory.modify(wallet.event, setMints(mints)).then(sign);
93
+ await publish(signed, wallet.relays);
94
+ };
95
+ }
96
+ /**
97
+ * Sets the relays on a wallet event
98
+ * @throws if the wallet does not exist or cannot be unlocked
99
+ */
100
+ export function SetWalletRelays(relays) {
101
+ return async ({ user, signer, factory, sign, publish }) => {
102
+ const wallet = await getUnlockedWallet(user, signer);
103
+ const signed = await factory.modify(wallet.event, setRelays(relays)).then(sign);
104
+ await publish(signed, relays.map(String));
105
+ };
106
+ }
@@ -1,4 +1,4 @@
1
1
  import { EventPointer } from "applesauce-core/helpers/pointers";
2
2
  import { HistoryContent } from "../helpers/history.js";
3
3
  /** A blueprint that creates a wallet history event */
4
- export declare function WalletHistoryBlueprint(content: HistoryContent, redeemed: (string | EventPointer)[]): import("applesauce-core").EventBlueprint;
4
+ export declare function WalletHistoryBlueprint(content: HistoryContent, redeemed?: (string | EventPointer)[]): import("applesauce-core").EventBlueprint;
@@ -7,5 +7,5 @@ export function WalletHistoryBlueprint(content, redeemed) {
7
7
  // set the encrypted tags on the event
8
8
  setHistoryContent(content),
9
9
  // set the public redeemed tags
10
- setHistoryRedeemed(redeemed));
10
+ redeemed ? setHistoryRedeemed(redeemed) : undefined);
11
11
  }
@@ -1,5 +1,9 @@
1
1
  import { NostrEvent } from "applesauce-core/helpers/event";
2
2
  /** A blueprint to create a new 17375 wallet */
3
- export declare function WalletBlueprint(mints: string[], privateKey?: Uint8Array): import("applesauce-core").EventBlueprint;
3
+ export declare function WalletBlueprint({ mints, privateKey, relays, }: {
4
+ mints: string[];
5
+ privateKey?: Uint8Array;
6
+ relays?: string[];
7
+ }): import("applesauce-core").EventBlueprint;
4
8
  /** A blueprint that creates a new 375 wallet backup event */
5
9
  export declare function WalletBackupBlueprint(wallet: NostrEvent): import("applesauce-core").EventBlueprint;