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.
- package/dist/actions/common.d.ts +4 -0
- package/dist/actions/common.js +15 -0
- package/dist/actions/index.d.ts +2 -2
- package/dist/actions/index.js +2 -2
- package/dist/actions/{zap-info.d.ts → nutzap-info.d.ts} +20 -3
- package/dist/actions/nutzap-info.js +117 -0
- package/dist/actions/nutzaps.d.ts +24 -0
- package/dist/actions/nutzaps.js +154 -0
- package/dist/actions/tokens.d.ts +77 -7
- package/dist/actions/tokens.js +332 -69
- package/dist/actions/wallet.d.ts +18 -3
- package/dist/actions/wallet.js +74 -32
- package/dist/blueprints/history.d.ts +1 -1
- package/dist/blueprints/history.js +1 -1
- package/dist/blueprints/wallet.d.ts +5 -1
- package/dist/blueprints/wallet.js +6 -3
- package/dist/casts/__register__.d.ts +20 -0
- package/dist/casts/__register__.js +41 -0
- package/dist/casts/index.d.ts +6 -0
- package/dist/casts/index.js +6 -0
- package/dist/casts/nutzap-info.d.ts +14 -0
- package/dist/casts/nutzap-info.js +22 -0
- package/dist/casts/nutzap.d.ts +16 -0
- package/dist/casts/nutzap.js +37 -0
- package/dist/casts/wallet-history.d.ts +16 -0
- package/dist/casts/wallet-history.js +40 -0
- package/dist/casts/wallet-token.d.ts +29 -0
- package/dist/casts/wallet-token.js +52 -0
- package/dist/casts/wallet.d.ts +27 -0
- package/dist/casts/wallet.js +62 -0
- package/dist/helpers/cashu.d.ts +21 -0
- package/dist/helpers/cashu.js +105 -0
- package/dist/helpers/couch.d.ts +11 -0
- package/dist/helpers/couch.js +1 -0
- package/dist/helpers/history.d.ts +5 -1
- package/dist/helpers/history.js +13 -4
- package/dist/helpers/index.d.ts +5 -1
- package/dist/helpers/index.js +5 -1
- package/dist/helpers/indexed-db-couch.d.ts +34 -0
- package/dist/helpers/indexed-db-couch.js +119 -0
- package/dist/helpers/local-storage-couch.d.ts +29 -0
- package/dist/helpers/local-storage-couch.js +78 -0
- package/dist/helpers/{zap-info.d.ts → nutzap-info.d.ts} +10 -1
- package/dist/helpers/{zap-info.js → nutzap-info.js} +22 -10
- package/dist/helpers/nutzap.d.ts +15 -0
- package/dist/helpers/nutzap.js +57 -3
- package/dist/helpers/tokens.d.ts +9 -18
- package/dist/helpers/tokens.js +64 -94
- package/dist/helpers/wallet.d.ts +16 -6
- package/dist/helpers/wallet.js +40 -14
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/models/history.d.ts +1 -1
- package/dist/models/history.js +7 -10
- package/dist/models/index.d.ts +0 -1
- package/dist/models/index.js +0 -1
- package/dist/models/nutzap.d.ts +2 -0
- package/dist/models/nutzap.js +8 -0
- package/dist/models/tokens.d.ts +2 -2
- package/dist/models/tokens.js +14 -17
- package/dist/operations/history.js +1 -1
- package/dist/operations/index.d.ts +1 -1
- package/dist/operations/index.js +1 -1
- package/dist/operations/nutzap-info.d.ts +21 -0
- package/dist/operations/nutzap-info.js +71 -0
- package/dist/operations/wallet.d.ts +10 -1
- package/dist/operations/wallet.js +33 -3
- package/package.json +15 -6
- package/dist/actions/zap-info.js +0 -83
- package/dist/actions/zaps.d.ts +0 -8
- package/dist/actions/zaps.js +0 -30
- package/dist/models/wallet.d.ts +0 -13
- package/dist/models/wallet.js +0 -21
- package/dist/operations/zap-info.d.ts +0 -10
- package/dist/operations/zap-info.js +0 -17
package/dist/actions/tokens.js
CHANGED
|
@@ -1,82 +1,143 @@
|
|
|
1
|
-
import { CheckStateEnum,
|
|
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 {
|
|
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
|
|
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
|
|
13
|
+
* @param redeemed an array of event ids to mark as redeemed
|
|
10
14
|
*/
|
|
11
|
-
export function
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
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
|
|
23
|
-
|
|
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
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
81
|
+
const unlocked = spent.filter(isTokenContentUnlocked);
|
|
82
|
+
if (unlocked.length !== spent.length)
|
|
41
83
|
throw new Error("Cant complete spend with locked tokens");
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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(
|
|
68
|
-
return async
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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((
|
|
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
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
//
|
|
106
|
-
|
|
107
|
-
|
|
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
|
}
|
package/dist/actions/wallet.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
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;
|
package/dist/actions/wallet.js
CHANGED
|
@@ -1,64 +1,106 @@
|
|
|
1
1
|
import { WalletBackupBlueprint, WalletBlueprint } from "../blueprints/wallet.js";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
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
|
|
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
|
-
|
|
12
|
-
const
|
|
13
|
-
//
|
|
14
|
-
|
|
15
|
-
|
|
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
|
|
42
|
+
* @throws if the wallet does not exist or cannot be unlocked
|
|
21
43
|
*/
|
|
22
|
-
export function WalletAddPrivateKey(privateKey) {
|
|
23
|
-
return async
|
|
24
|
-
const wallet =
|
|
25
|
-
if (
|
|
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
|
|
32
|
-
|
|
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.
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
4
|
+
export declare function WalletHistoryBlueprint(content: HistoryContent, redeemed?: (string | EventPointer)[]): import("applesauce-core").EventBlueprint;
|
|
@@ -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
|
|
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;
|