@1sat/actions 0.0.1

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 (53) hide show
  1. package/dist/balance/index.d.ts +41 -0
  2. package/dist/balance/index.d.ts.map +1 -0
  3. package/dist/balance/index.js +111 -0
  4. package/dist/balance/index.js.map +1 -0
  5. package/dist/constants.d.ts +2 -0
  6. package/dist/constants.d.ts.map +1 -0
  7. package/dist/constants.js +2 -0
  8. package/dist/constants.js.map +1 -0
  9. package/dist/index.d.ts +19 -0
  10. package/dist/index.d.ts.map +1 -0
  11. package/dist/index.js +42 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/inscriptions/index.d.ts +26 -0
  14. package/dist/inscriptions/index.d.ts.map +1 -0
  15. package/dist/inscriptions/index.js +116 -0
  16. package/dist/inscriptions/index.js.map +1 -0
  17. package/dist/locks/index.d.ts +48 -0
  18. package/dist/locks/index.d.ts.map +1 -0
  19. package/dist/locks/index.js +296 -0
  20. package/dist/locks/index.js.map +1 -0
  21. package/dist/ordinals/index.d.ts +118 -0
  22. package/dist/ordinals/index.d.ts.map +1 -0
  23. package/dist/ordinals/index.js +850 -0
  24. package/dist/ordinals/index.js.map +1 -0
  25. package/dist/payments/index.d.ts +49 -0
  26. package/dist/payments/index.d.ts.map +1 -0
  27. package/dist/payments/index.js +194 -0
  28. package/dist/payments/index.js.map +1 -0
  29. package/dist/registry.d.ts +62 -0
  30. package/dist/registry.d.ts.map +1 -0
  31. package/dist/registry.js +75 -0
  32. package/dist/registry.js.map +1 -0
  33. package/dist/signing/index.d.ts +36 -0
  34. package/dist/signing/index.d.ts.map +1 -0
  35. package/dist/signing/index.js +82 -0
  36. package/dist/signing/index.js.map +1 -0
  37. package/dist/sweep/index.d.ts +38 -0
  38. package/dist/sweep/index.d.ts.map +1 -0
  39. package/dist/sweep/index.js +748 -0
  40. package/dist/sweep/index.js.map +1 -0
  41. package/dist/sweep/types.d.ts +79 -0
  42. package/dist/sweep/types.d.ts.map +1 -0
  43. package/dist/sweep/types.js +5 -0
  44. package/dist/sweep/types.js.map +1 -0
  45. package/dist/tokens/index.d.ts +88 -0
  46. package/dist/tokens/index.d.ts.map +1 -0
  47. package/dist/tokens/index.js +548 -0
  48. package/dist/tokens/index.js.map +1 -0
  49. package/dist/types.d.ts +72 -0
  50. package/dist/types.d.ts.map +1 -0
  51. package/dist/types.js +15 -0
  52. package/dist/types.js.map +1 -0
  53. package/package.json +34 -0
@@ -0,0 +1,748 @@
1
+ /**
2
+ * Sweep Module
3
+ *
4
+ * Functions for sweeping assets from external wallets into a BRC-100 wallet.
5
+ */
6
+ import { BSV21 } from '@bopen-io/templates';
7
+ import { P2PKH, PrivateKey, PublicKey, Transaction, Utils, } from '@bsv/sdk';
8
+ import { BSV21_BASKET, BSV21_PROTOCOL, ONESAT_PROTOCOL, ORDINALS_BASKET, } from '../constants';
9
+ export * from './types';
10
+ /**
11
+ * Prepare sweep inputs from IndexedOutput objects by fetching locking scripts.
12
+ * This extracts locking scripts from the raw transactions in BEEF format.
13
+ */
14
+ export async function prepareSweepInputs(ctx, utxos) {
15
+ if (!ctx.services) {
16
+ throw new Error('Services required for prepareSweepInputs');
17
+ }
18
+ // Group UTXOs by txid to minimize BEEF fetches
19
+ const byTxid = new Map();
20
+ for (const utxo of utxos) {
21
+ const [txid, voutStr] = utxo.outpoint.split('_');
22
+ const vout = Number.parseInt(voutStr, 10);
23
+ const existing = byTxid.get(txid) ?? [];
24
+ existing.push({ vout, utxo });
25
+ byTxid.set(txid, existing);
26
+ }
27
+ const results = [];
28
+ // Fetch BEEF for each txid and extract locking scripts
29
+ for (const [txid, outputs] of byTxid) {
30
+ const beef = await ctx.services.getBeefForTxid(txid);
31
+ const beefTx = beef.findTxid(txid);
32
+ if (!beefTx?.tx) {
33
+ throw new Error(`Transaction ${txid} not found in BEEF`);
34
+ }
35
+ for (const { vout, utxo } of outputs) {
36
+ const output = beefTx.tx.outputs[vout];
37
+ if (!output) {
38
+ throw new Error(`Output ${vout} not found in transaction ${txid}`);
39
+ }
40
+ results.push({
41
+ outpoint: utxo.outpoint,
42
+ satoshis: utxo.satoshis ?? output.satoshis ?? 0,
43
+ lockingScript: output.lockingScript?.toHex() ?? '',
44
+ });
45
+ }
46
+ }
47
+ return results;
48
+ }
49
+ /**
50
+ * Sweep BSV from external inputs into the destination wallet.
51
+ *
52
+ * If amount is specified, only that amount is swept and the remainder
53
+ * is returned to the source address. If amount is omitted, all input
54
+ * value is swept (minus fees).
55
+ */
56
+ export const sweepBsv = {
57
+ meta: {
58
+ name: 'sweepBsv',
59
+ description: 'Sweep BSV from external wallet (via WIF) into the connected wallet',
60
+ category: 'sweep',
61
+ requiresServices: true,
62
+ inputSchema: {
63
+ type: 'object',
64
+ properties: {
65
+ inputs: {
66
+ type: 'array',
67
+ description: 'UTXOs to sweep (use prepareSweepInputs to build these)',
68
+ items: {
69
+ type: 'object',
70
+ properties: {
71
+ outpoint: { type: 'string', description: 'Outpoint (txid_vout)' },
72
+ satoshis: { type: 'integer', description: 'Satoshis in output' },
73
+ lockingScript: {
74
+ type: 'string',
75
+ description: 'Locking script hex',
76
+ },
77
+ },
78
+ required: ['outpoint', 'satoshis', 'lockingScript'],
79
+ },
80
+ },
81
+ wif: {
82
+ type: 'string',
83
+ description: 'WIF private key controlling the inputs',
84
+ },
85
+ amount: {
86
+ type: 'integer',
87
+ description: 'Amount to sweep (satoshis). If omitted, sweeps all input value.',
88
+ },
89
+ },
90
+ required: ['inputs', 'wif'],
91
+ },
92
+ },
93
+ async execute(ctx, request) {
94
+ if (!ctx.services) {
95
+ return { error: 'services-required' };
96
+ }
97
+ try {
98
+ const { inputs, wif, amount } = request;
99
+ if (!inputs || inputs.length === 0) {
100
+ return { error: 'no-inputs' };
101
+ }
102
+ // Parse WIF and derive source address
103
+ const privateKey = PrivateKey.fromWif(wif);
104
+ const sourceAddress = privateKey.toPublicKey().toAddress();
105
+ // Calculate totals
106
+ const inputTotal = inputs.reduce((sum, i) => sum + i.satoshis, 0);
107
+ // Validate amount if specified
108
+ if (amount !== undefined) {
109
+ if (amount <= 0) {
110
+ return { error: 'invalid-amount' };
111
+ }
112
+ if (amount > inputTotal) {
113
+ return { error: 'insufficient-funds' };
114
+ }
115
+ }
116
+ // Fetch BEEF for all input transactions and merge them
117
+ const txids = [...new Set(inputs.map((i) => i.outpoint.split('_')[0]))];
118
+ console.log(`[sweep] Fetching BEEF for ${txids.length} transactions`);
119
+ // Get first BEEF, then merge others into it
120
+ const firstBeef = await ctx.services.getBeefForTxid(txids[0]);
121
+ for (let i = 1; i < txids.length; i++) {
122
+ const additionalBeef = await ctx.services.getBeefForTxid(txids[i]);
123
+ firstBeef.mergeBeef(additionalBeef);
124
+ }
125
+ console.log(`[sweep] Merged BEEF valid=${firstBeef.isValid()}, txs=${firstBeef.txs.length}`);
126
+ console.log(`[sweep] BEEF structure:\n${firstBeef.toLogString()}`);
127
+ // Build input descriptors (we'll sign after getting the final transaction)
128
+ const inputDescriptors = inputs.map((input) => {
129
+ const [txid, voutStr] = input.outpoint.split('_');
130
+ // Convert outpoint format: our format uses "_" but SDK expects "."
131
+ return {
132
+ outpoint: `${txid}.${voutStr}`,
133
+ inputDescription: 'Sweep input',
134
+ unlockingScriptLength: 108, // P2PKH unlocking script length
135
+ sequenceNumber: 0xffffffff,
136
+ };
137
+ });
138
+ const beefData = firstBeef.toBinary();
139
+ // Build outputs array
140
+ const outputs = [];
141
+ // If amount specified, create return output for the difference
142
+ if (amount !== undefined) {
143
+ const returnAmount = inputTotal - amount;
144
+ if (returnAmount > 0) {
145
+ outputs.push({
146
+ lockingScript: new P2PKH().lock(sourceAddress).toHex(),
147
+ satoshis: returnAmount,
148
+ outputDescription: 'Return to source',
149
+ });
150
+ }
151
+ }
152
+ // If no amount specified, no outputs - wallet creates change for everything
153
+ // Step 1: Create action to get the signable transaction
154
+ const createResult = await ctx.wallet.createAction({
155
+ description: amount
156
+ ? `Sweep ${amount} sats`
157
+ : `Sweep ${inputTotal} sats`,
158
+ inputBEEF: beefData,
159
+ inputs: inputDescriptors,
160
+ outputs,
161
+ options: { signAndProcess: false },
162
+ });
163
+ if ('error' in createResult && createResult.error) {
164
+ return { error: String(createResult.error) };
165
+ }
166
+ if (!createResult.signableTransaction) {
167
+ return { error: 'no-signable-transaction' };
168
+ }
169
+ // Step 2: Sign each input with our external key
170
+ const tx = Transaction.fromBEEF(createResult.signableTransaction.tx);
171
+ console.log(`[sweep] Transaction has ${tx.inputs.length} inputs, ${tx.outputs.length} outputs`);
172
+ // Build a set of outpoints we control (using SDK format with ".")
173
+ const ourOutpoints = new Set(inputs.map((input) => {
174
+ const [txid, vout] = input.outpoint.split('_');
175
+ return `${txid}.${vout}`;
176
+ }));
177
+ // Find and set up P2PKH unlocker on each input we control
178
+ for (let i = 0; i < tx.inputs.length; i++) {
179
+ const txInput = tx.inputs[i];
180
+ const inputOutpoint = `${txInput.sourceTXID}.${txInput.sourceOutputIndex}`;
181
+ const hasSourceTx = !!txInput.sourceTransaction;
182
+ const sourceSatoshis = txInput.sourceTransaction?.outputs[txInput.sourceOutputIndex]
183
+ ?.satoshis;
184
+ console.log(`[sweep] Input ${i}: ${inputOutpoint}, hasSourceTx=${hasSourceTx}, satoshis=${sourceSatoshis}, isOurs=${ourOutpoints.has(inputOutpoint)}`);
185
+ if (ourOutpoints.has(inputOutpoint)) {
186
+ const p2pkh = new P2PKH();
187
+ txInput.unlockingScriptTemplate = p2pkh.unlock(privateKey, 'all', // SIGHASH_ALL - commit to outputs (we know them now)
188
+ true);
189
+ }
190
+ }
191
+ // Sign all inputs
192
+ await tx.sign();
193
+ // Extract unlocking scripts for signAction (only for our inputs)
194
+ const spends = {};
195
+ for (let i = 0; i < tx.inputs.length; i++) {
196
+ const txInput = tx.inputs[i];
197
+ const inputOutpoint = `${txInput.sourceTXID}.${txInput.sourceOutputIndex}`;
198
+ if (ourOutpoints.has(inputOutpoint)) {
199
+ spends[i] = {
200
+ unlockingScript: txInput.unlockingScript?.toHex() ?? '',
201
+ };
202
+ }
203
+ }
204
+ // Step 3: Complete the action with our signatures
205
+ const signResult = await ctx.wallet.signAction({
206
+ reference: createResult.signableTransaction.reference,
207
+ spends,
208
+ options: { acceptDelayedBroadcast: false },
209
+ });
210
+ if ('error' in signResult) {
211
+ return { error: String(signResult.error) };
212
+ }
213
+ return {
214
+ txid: signResult.txid,
215
+ beef: signResult.tx ? Array.from(signResult.tx) : undefined,
216
+ };
217
+ }
218
+ catch (error) {
219
+ console.error('[sweepBsv]', error);
220
+ // Log detailed error info for WERR_REVIEW_ACTIONS
221
+ if (error && typeof error === 'object' && 'sendWithResults' in error) {
222
+ const werr = error;
223
+ console.error('[sweepBsv] WERR_REVIEW_ACTIONS details:', {
224
+ message: werr.message,
225
+ txid: werr.txid,
226
+ sendWithResults: JSON.stringify(werr.sendWithResults, null, 2),
227
+ });
228
+ }
229
+ return {
230
+ error: error instanceof Error ? error.message : 'unknown-error',
231
+ };
232
+ }
233
+ },
234
+ };
235
+ /**
236
+ * Sweep ordinals from external inputs into the destination wallet.
237
+ *
238
+ * Each input is expected to be a 1-sat ordinal output. Each ordinal is
239
+ * transferred to a derived address using the wallet's key derivation.
240
+ */
241
+ export const sweepOrdinals = {
242
+ meta: {
243
+ name: 'sweepOrdinals',
244
+ description: 'Sweep ordinals from external wallet (via WIF) into the connected wallet',
245
+ category: 'sweep',
246
+ requiresServices: true,
247
+ inputSchema: {
248
+ type: 'object',
249
+ properties: {
250
+ inputs: {
251
+ type: 'array',
252
+ description: 'Ordinal UTXOs to sweep',
253
+ items: {
254
+ type: 'object',
255
+ properties: {
256
+ outpoint: {
257
+ type: 'string',
258
+ description: 'Outpoint (txid_vout)',
259
+ },
260
+ satoshis: {
261
+ type: 'integer',
262
+ description: 'Satoshis (should be 1)',
263
+ },
264
+ lockingScript: {
265
+ type: 'string',
266
+ description: 'Locking script hex',
267
+ },
268
+ contentType: {
269
+ type: 'string',
270
+ description: 'Content type from metadata',
271
+ },
272
+ origin: { type: 'string', description: 'Origin outpoint' },
273
+ name: { type: 'string', description: 'Name from MAP metadata' },
274
+ },
275
+ required: ['outpoint', 'satoshis', 'lockingScript'],
276
+ },
277
+ },
278
+ wif: {
279
+ type: 'string',
280
+ description: 'WIF private key controlling the inputs',
281
+ },
282
+ },
283
+ required: ['inputs', 'wif'],
284
+ },
285
+ },
286
+ async execute(ctx, request) {
287
+ if (!ctx.services) {
288
+ return { error: 'services-required' };
289
+ }
290
+ try {
291
+ const { inputs, wif } = request;
292
+ if (!inputs || inputs.length === 0) {
293
+ return { error: 'no-inputs' };
294
+ }
295
+ // Parse WIF
296
+ const privateKey = PrivateKey.fromWif(wif);
297
+ // Fetch BEEF for all input transactions and merge them
298
+ const txids = [...new Set(inputs.map((i) => i.outpoint.split('_')[0]))];
299
+ console.log(`[sweepOrdinals] Fetching BEEF for ${txids.length} transactions`);
300
+ const firstBeef = await ctx.services.getBeefForTxid(txids[0]);
301
+ for (let i = 1; i < txids.length; i++) {
302
+ const additionalBeef = await ctx.services.getBeefForTxid(txids[i]);
303
+ firstBeef.mergeBeef(additionalBeef);
304
+ }
305
+ console.log(`[sweepOrdinals] Merged BEEF valid=${firstBeef.isValid()}, txs=${firstBeef.txs.length}`);
306
+ // Build input descriptors
307
+ const inputDescriptors = inputs.map((input) => {
308
+ const [txid, voutStr] = input.outpoint.split('_');
309
+ return {
310
+ outpoint: `${txid}.${voutStr}`,
311
+ inputDescription: `Ordinal ${input.origin ?? input.outpoint}`,
312
+ unlockingScriptLength: 108,
313
+ sequenceNumber: 0xffffffff,
314
+ };
315
+ });
316
+ // Build outputs - one per ordinal, each 1 sat to derived address
317
+ const outputs = [];
318
+ for (const input of inputs) {
319
+ // Derive a unique public key for this ordinal using the input outpoint as keyID
320
+ const pubKeyResult = await ctx.wallet.getPublicKey({
321
+ protocolID: ONESAT_PROTOCOL,
322
+ keyID: input.outpoint,
323
+ forSelf: true,
324
+ });
325
+ if (!pubKeyResult.publicKey) {
326
+ return { error: `Failed to derive key for ${input.outpoint}` };
327
+ }
328
+ // Create P2PKH locking script from derived public key
329
+ const derivedAddress = PublicKey.fromString(pubKeyResult.publicKey).toAddress();
330
+ const lockingScript = new P2PKH().lock(derivedAddress);
331
+ // Build tags following ordinals API pattern
332
+ const tags = [];
333
+ if (input.contentType)
334
+ tags.push(`type:${input.contentType}`);
335
+ if (input.origin)
336
+ tags.push(`origin:${input.origin}`);
337
+ const customInstructions = JSON.stringify({
338
+ protocolID: ONESAT_PROTOCOL,
339
+ keyID: input.outpoint,
340
+ ...(input.name && { name: input.name.slice(0, 64) }),
341
+ });
342
+ console.log(`[sweepOrdinals] Output for ${input.outpoint}: keyID=${input.outpoint}, customInstructions=${customInstructions}`);
343
+ outputs.push({
344
+ lockingScript: lockingScript.toHex(),
345
+ satoshis: 1,
346
+ outputDescription: `Ordinal ${input.origin ?? input.outpoint}`,
347
+ basket: ORDINALS_BASKET,
348
+ tags,
349
+ customInstructions,
350
+ });
351
+ }
352
+ const beefData = firstBeef.toBinary();
353
+ // Create action to get signable transaction
354
+ // CRITICAL: randomizeOutputs must be false to preserve ordinal satoshi positions
355
+ const createActionArgs = {
356
+ description: `Sweep ${inputs.length} ordinal${inputs.length !== 1 ? 's' : ''}`,
357
+ inputBEEF: beefData,
358
+ inputs: inputDescriptors,
359
+ outputs,
360
+ options: { signAndProcess: false, randomizeOutputs: false },
361
+ };
362
+ console.log('[sweepOrdinals] === CREATE ACTION ARGS ===');
363
+ console.log(`[sweepOrdinals] description: ${createActionArgs.description}`);
364
+ console.log(`[sweepOrdinals] inputBEEF length: ${beefData.length} bytes`);
365
+ console.log(`[sweepOrdinals] inputs count: ${inputDescriptors.length}`);
366
+ console.log(`[sweepOrdinals] outputs count: ${outputs.length}`);
367
+ console.log('[sweepOrdinals] inputs:', JSON.stringify(inputDescriptors, null, 2));
368
+ console.log('[sweepOrdinals] outputs:', JSON.stringify(outputs, null, 2));
369
+ console.log('[sweepOrdinals] options:', JSON.stringify(createActionArgs.options));
370
+ console.log('[sweepOrdinals] Calling createAction...');
371
+ let createResult;
372
+ try {
373
+ createResult = await ctx.wallet.createAction(createActionArgs);
374
+ console.log('[sweepOrdinals] createAction returned:', JSON.stringify(createResult, (key, value) => {
375
+ // Don't stringify large binary data
376
+ if (key === 'tx' && value instanceof Uint8Array) {
377
+ return `<Uint8Array ${value.length} bytes>`;
378
+ }
379
+ if (key === 'tx' && Array.isArray(value)) {
380
+ return `<Array ${value.length} bytes>`;
381
+ }
382
+ return value;
383
+ }, 2));
384
+ }
385
+ catch (createError) {
386
+ console.error('[sweepOrdinals] createAction threw:', createError);
387
+ const errorMsg = createError instanceof Error
388
+ ? createError.message
389
+ : String(createError);
390
+ const errorStack = createError instanceof Error ? createError.stack : undefined;
391
+ console.error('[sweepOrdinals] Stack:', errorStack);
392
+ return { error: `createAction failed: ${errorMsg}` };
393
+ }
394
+ if ('error' in createResult && createResult.error) {
395
+ return { error: String(createResult.error) };
396
+ }
397
+ if (!createResult.signableTransaction) {
398
+ return { error: 'no-signable-transaction' };
399
+ }
400
+ // Sign each input with our external key
401
+ const tx = Transaction.fromBEEF(createResult.signableTransaction.tx);
402
+ // Log transaction structure for debugging
403
+ console.log('[sweepOrdinals] === Transaction Structure ===');
404
+ console.log(`[sweepOrdinals] Inputs (${tx.inputs.length}):`);
405
+ let totalInputSats = 0;
406
+ for (let i = 0; i < tx.inputs.length; i++) {
407
+ const inp = tx.inputs[i];
408
+ const sats = inp.sourceTransaction?.outputs[inp.sourceOutputIndex]?.satoshis ?? 0;
409
+ totalInputSats += sats;
410
+ console.log(` [${i}] ${inp.sourceTXID}:${inp.sourceOutputIndex} = ${sats} sats`);
411
+ }
412
+ console.log(`[sweepOrdinals] Outputs (${tx.outputs.length}):`);
413
+ let totalOutputSats = 0;
414
+ for (let i = 0; i < tx.outputs.length; i++) {
415
+ const out = tx.outputs[i];
416
+ totalOutputSats += out.satoshis ?? 0;
417
+ console.log(` [${i}] ${out.satoshis} sats, script len=${out.lockingScript?.toHex().length ?? 0}`);
418
+ }
419
+ console.log(`[sweepOrdinals] Total in: ${totalInputSats}, Total out: ${totalOutputSats}, Fee: ${totalInputSats - totalOutputSats}`);
420
+ console.log(`[sweepOrdinals] Signable tx hex: ${Utils.toHex(createResult.signableTransaction.tx)}`);
421
+ console.log('[sweepOrdinals] ==============================');
422
+ // Build a set of outpoints we control
423
+ const ourOutpoints = new Set(inputs.map((input) => {
424
+ const [txid, vout] = input.outpoint.split('_');
425
+ return `${txid}.${vout}`;
426
+ }));
427
+ // Set up P2PKH unlocker on each input we control
428
+ for (let i = 0; i < tx.inputs.length; i++) {
429
+ const txInput = tx.inputs[i];
430
+ const inputOutpoint = `${txInput.sourceTXID}.${txInput.sourceOutputIndex}`;
431
+ if (ourOutpoints.has(inputOutpoint)) {
432
+ const p2pkh = new P2PKH();
433
+ txInput.unlockingScriptTemplate = p2pkh.unlock(privateKey, 'all', true);
434
+ }
435
+ }
436
+ await tx.sign();
437
+ // Log signed transaction details for debugging
438
+ const localTxid = tx.id('hex');
439
+ console.log('[sweepOrdinals] === LOCAL SIGNED TX ===');
440
+ console.log(`[sweepOrdinals] Local txid: ${localTxid}`);
441
+ console.log(`[sweepOrdinals] Signed tx hex: ${tx.toHex()}`);
442
+ // Extract unlocking scripts for signAction
443
+ const spends = {};
444
+ console.log('[sweepOrdinals] === UNLOCKING SCRIPTS FOR SIGNACTION ===');
445
+ for (let i = 0; i < tx.inputs.length; i++) {
446
+ const txInput = tx.inputs[i];
447
+ const inputOutpoint = `${txInput.sourceTXID}.${txInput.sourceOutputIndex}`;
448
+ if (ourOutpoints.has(inputOutpoint)) {
449
+ const unlockHex = txInput.unlockingScript?.toHex() ?? '';
450
+ spends[i] = { unlockingScript: unlockHex };
451
+ console.log(` [${i}] ${inputOutpoint}: ${unlockHex.length} chars`);
452
+ }
453
+ else {
454
+ console.log(` [${i}] ${inputOutpoint}: (wallet input - not ours)`);
455
+ }
456
+ }
457
+ // Complete the action with our signatures
458
+ const signResult = await ctx.wallet.signAction({
459
+ reference: createResult.signableTransaction.reference,
460
+ spends,
461
+ options: { acceptDelayedBroadcast: false },
462
+ });
463
+ if ('error' in signResult) {
464
+ return { error: String(signResult.error) };
465
+ }
466
+ // Debug: compare local vs signAction result
467
+ console.log('[sweepOrdinals] === SIGN ACTION RESULT ===');
468
+ console.log(`[sweepOrdinals] signAction txid: ${signResult.txid}`);
469
+ // Log broadcast results if available
470
+ if ('sendWithResults' in signResult) {
471
+ console.log('[sweepOrdinals] sendWithResults:', JSON.stringify(signResult.sendWithResults));
472
+ }
473
+ console.log(`[sweepOrdinals] Local txid (partial): ${localTxid}`);
474
+ console.log('[sweepOrdinals] Note: TXIDs differ because local is partial (wallet input unsigned)');
475
+ if (signResult.tx) {
476
+ // Parse returned BEEF to show final transaction structure
477
+ const returnedTx = Transaction.fromBEEF(signResult.tx);
478
+ console.log('[sweepOrdinals] === FINAL TX STRUCTURE (broadcast) ===');
479
+ console.log(`[sweepOrdinals] Final inputs (${returnedTx.inputs.length}):`);
480
+ let returnedInputSats = 0;
481
+ for (let i = 0; i < returnedTx.inputs.length; i++) {
482
+ const inp = returnedTx.inputs[i];
483
+ const sats = inp.sourceTransaction?.outputs[inp.sourceOutputIndex]?.satoshis ?? 0;
484
+ returnedInputSats += sats;
485
+ const isOurs = ourOutpoints.has(`${inp.sourceTXID}.${inp.sourceOutputIndex}`);
486
+ console.log(` [${i}] ${inp.sourceTXID?.slice(0, 8)}...:${inp.sourceOutputIndex} = ${sats} sats, unlock=${inp.unlockingScript?.toHex().length ?? 0} chars ${isOurs ? '(ours)' : '(wallet fee)'}`);
487
+ }
488
+ console.log(`[sweepOrdinals] Final outputs (${returnedTx.outputs.length}):`);
489
+ let returnedOutputSats = 0;
490
+ for (let i = 0; i < returnedTx.outputs.length; i++) {
491
+ const out = returnedTx.outputs[i];
492
+ returnedOutputSats += out.satoshis ?? 0;
493
+ console.log(` [${i}] ${out.satoshis} sats, script=${out.lockingScript?.toHex().length ?? 0} chars`);
494
+ }
495
+ const finalFee = returnedInputSats - returnedOutputSats;
496
+ console.log(`[sweepOrdinals] Final: Total in=${returnedInputSats}, Total out=${returnedOutputSats}, Fee=${finalFee} sats`);
497
+ console.log(`[sweepOrdinals] Final tx hex: ${returnedTx.toHex()}`);
498
+ console.log(`[sweepOrdinals] Final tx computed id: ${returnedTx.id('hex')}`);
499
+ // Check if fee seems too low (less than 1 sat/byte)
500
+ const txSize = returnedTx.toHex().length / 2;
501
+ const satPerByte = finalFee / txSize;
502
+ console.log(`[sweepOrdinals] Tx size: ${txSize} bytes, Fee rate: ${satPerByte.toFixed(2)} sat/byte`);
503
+ if (satPerByte < 0.5) {
504
+ console.warn('[sweepOrdinals] WARNING: Fee rate seems very low!');
505
+ }
506
+ }
507
+ return {
508
+ txid: signResult.txid,
509
+ beef: signResult.tx ? Array.from(signResult.tx) : undefined,
510
+ };
511
+ }
512
+ catch (error) {
513
+ console.error('[sweepOrdinals]', error);
514
+ return {
515
+ error: error instanceof Error ? error.message : 'unknown-error',
516
+ };
517
+ }
518
+ },
519
+ };
520
+ /**
521
+ * Sweep BSV-21 tokens from external inputs into the destination wallet.
522
+ *
523
+ * Consolidates all token inputs into a single output. All inputs must be
524
+ * for the same tokenId. Creates a fee output to the overlay fund address.
525
+ */
526
+ export const sweepBsv21 = {
527
+ meta: {
528
+ name: 'sweepBsv21',
529
+ description: 'Sweep BSV-21 tokens from external wallet (via WIF) into the connected wallet',
530
+ category: 'sweep',
531
+ requiresServices: true,
532
+ inputSchema: {
533
+ type: 'object',
534
+ properties: {
535
+ inputs: {
536
+ type: 'array',
537
+ description: 'Token UTXOs to sweep (must all be same tokenId)',
538
+ items: {
539
+ type: 'object',
540
+ properties: {
541
+ outpoint: { type: 'string', description: 'Outpoint (txid_vout)' },
542
+ satoshis: {
543
+ type: 'integer',
544
+ description: 'Satoshis (should be 1)',
545
+ },
546
+ lockingScript: {
547
+ type: 'string',
548
+ description: 'Locking script hex',
549
+ },
550
+ tokenId: {
551
+ type: 'string',
552
+ description: 'Token ID (txid_vout format)',
553
+ },
554
+ amount: { type: 'string', description: 'Token amount as string' },
555
+ },
556
+ required: [
557
+ 'outpoint',
558
+ 'satoshis',
559
+ 'lockingScript',
560
+ 'tokenId',
561
+ 'amount',
562
+ ],
563
+ },
564
+ },
565
+ wif: {
566
+ type: 'string',
567
+ description: 'WIF private key controlling the inputs',
568
+ },
569
+ },
570
+ required: ['inputs', 'wif'],
571
+ },
572
+ },
573
+ async execute(ctx, request) {
574
+ if (!ctx.services) {
575
+ return { error: 'services-required' };
576
+ }
577
+ try {
578
+ const { inputs, wif } = request;
579
+ if (!inputs || inputs.length === 0) {
580
+ return { error: 'no-inputs' };
581
+ }
582
+ // Validate all inputs have the same tokenId
583
+ const tokenId = inputs[0].tokenId;
584
+ if (!inputs.every((i) => i.tokenId === tokenId)) {
585
+ return { error: 'mixed-token-ids' };
586
+ }
587
+ // Lookup token details to verify it's active and get fee info
588
+ const tokenDetails = await ctx.services.bsv21.getTokenDetails(tokenId);
589
+ if (!tokenDetails.status.is_active) {
590
+ return { error: 'token-not-active' };
591
+ }
592
+ const { fee_address, fee_per_output } = tokenDetails.status;
593
+ // Validate all input outpoints exist in the overlay
594
+ const candidateOutpoints = inputs.map((i) => i.outpoint);
595
+ const validated = await ctx.services.bsv21.validateOutputs(tokenId, candidateOutpoints, { unspent: true });
596
+ const validSet = new Set(validated.map((v) => v.outpoint));
597
+ const invalidInputs = inputs.filter((i) => !validSet.has(i.outpoint));
598
+ if (invalidInputs.length > 0) {
599
+ return {
600
+ error: `unvalidated-inputs: ${invalidInputs.map((i) => i.outpoint).join(', ')}`,
601
+ };
602
+ }
603
+ // Parse WIF
604
+ const privateKey = PrivateKey.fromWif(wif);
605
+ // Sum all input amounts
606
+ const totalAmount = inputs.reduce((sum, i) => sum + BigInt(i.amount), 0n);
607
+ if (totalAmount <= 0n) {
608
+ return { error: 'no-token-amount' };
609
+ }
610
+ // Fetch BEEF for all input transactions and merge them
611
+ const txids = [...new Set(inputs.map((i) => i.outpoint.split('_')[0]))];
612
+ console.log(`[sweepBsv21] Fetching BEEF for ${txids.length} transactions`);
613
+ const firstBeef = await ctx.services.getBeefForTxid(txids[0]);
614
+ for (let i = 1; i < txids.length; i++) {
615
+ const additionalBeef = await ctx.services.getBeefForTxid(txids[i]);
616
+ firstBeef.mergeBeef(additionalBeef);
617
+ }
618
+ console.log(`[sweepBsv21] Merged BEEF valid=${firstBeef.isValid()}, txs=${firstBeef.txs.length}`);
619
+ // Build input descriptors
620
+ const inputDescriptors = inputs.map((input) => {
621
+ const [txid, voutStr] = input.outpoint.split('_');
622
+ return {
623
+ outpoint: `${txid}.${voutStr}`,
624
+ inputDescription: `Token input ${input.outpoint}`,
625
+ unlockingScriptLength: 108,
626
+ sequenceNumber: 0xffffffff,
627
+ };
628
+ });
629
+ // Build outputs
630
+ const outputs = [];
631
+ // 1. Token output (1 sat) - derive key for this token
632
+ const keyID = `${tokenId}-${Date.now()}`;
633
+ const pubKeyResult = await ctx.wallet.getPublicKey({
634
+ protocolID: BSV21_PROTOCOL,
635
+ keyID,
636
+ forSelf: true,
637
+ });
638
+ if (!pubKeyResult.publicKey) {
639
+ return { error: 'failed-to-derive-key' };
640
+ }
641
+ const derivedAddress = PublicKey.fromString(pubKeyResult.publicKey).toAddress();
642
+ const p2pkh = new P2PKH();
643
+ const destinationLockingScript = p2pkh.lock(derivedAddress);
644
+ const transferScript = BSV21.transfer(tokenId, totalAmount).lock(destinationLockingScript);
645
+ outputs.push({
646
+ lockingScript: transferScript.toHex(),
647
+ satoshis: 1,
648
+ outputDescription: `Sweep ${totalAmount} tokens`,
649
+ basket: BSV21_BASKET,
650
+ tags: [
651
+ `id:${tokenId}`,
652
+ `amt:${totalAmount}`,
653
+ `dec:${tokenDetails.token.dec}`,
654
+ ...(tokenDetails.token.sym ? [`sym:${tokenDetails.token.sym}`] : []),
655
+ ...(tokenDetails.token.icon
656
+ ? [`icon:${tokenDetails.token.icon}`]
657
+ : []),
658
+ ],
659
+ customInstructions: JSON.stringify({
660
+ protocolID: BSV21_PROTOCOL,
661
+ keyID,
662
+ }),
663
+ });
664
+ // 2. Fee output to overlay fund address
665
+ outputs.push({
666
+ lockingScript: p2pkh.lock(fee_address).toHex(),
667
+ satoshis: fee_per_output,
668
+ outputDescription: 'Overlay processing fee',
669
+ tags: [],
670
+ });
671
+ const beefData = firstBeef.toBinary();
672
+ // Create action to get signable transaction
673
+ const createResult = await ctx.wallet.createAction({
674
+ description: `Sweep ${inputs.length} token UTXO${inputs.length !== 1 ? 's' : ''}`,
675
+ inputBEEF: beefData,
676
+ inputs: inputDescriptors,
677
+ outputs,
678
+ options: { signAndProcess: false, randomizeOutputs: false },
679
+ });
680
+ if ('error' in createResult && createResult.error) {
681
+ return { error: String(createResult.error) };
682
+ }
683
+ if (!createResult.signableTransaction) {
684
+ return { error: 'no-signable-transaction' };
685
+ }
686
+ // Sign each input with our external key
687
+ const tx = Transaction.fromBEEF(createResult.signableTransaction.tx);
688
+ // Build a set of outpoints we control
689
+ const ourOutpoints = new Set(inputs.map((input) => {
690
+ const [txid, vout] = input.outpoint.split('_');
691
+ return `${txid}.${vout}`;
692
+ }));
693
+ // Set up P2PKH unlocker on each input we control
694
+ for (let i = 0; i < tx.inputs.length; i++) {
695
+ const txInput = tx.inputs[i];
696
+ const inputOutpoint = `${txInput.sourceTXID}.${txInput.sourceOutputIndex}`;
697
+ if (ourOutpoints.has(inputOutpoint)) {
698
+ txInput.unlockingScriptTemplate = p2pkh.unlock(privateKey, 'all', true);
699
+ }
700
+ }
701
+ await tx.sign();
702
+ // Extract unlocking scripts for signAction
703
+ const spends = {};
704
+ for (let i = 0; i < tx.inputs.length; i++) {
705
+ const txInput = tx.inputs[i];
706
+ const inputOutpoint = `${txInput.sourceTXID}.${txInput.sourceOutputIndex}`;
707
+ if (ourOutpoints.has(inputOutpoint)) {
708
+ spends[i] = {
709
+ unlockingScript: txInput.unlockingScript?.toHex() ?? '',
710
+ };
711
+ }
712
+ }
713
+ // Complete the action with our signatures
714
+ const signResult = await ctx.wallet.signAction({
715
+ reference: createResult.signableTransaction.reference,
716
+ spends,
717
+ options: { acceptDelayedBroadcast: false },
718
+ });
719
+ if ('error' in signResult) {
720
+ return { error: String(signResult.error) };
721
+ }
722
+ // Submit to overlay service for indexing
723
+ if (signResult.tx) {
724
+ try {
725
+ const overlayResult = await ctx.services.overlay.submitBsv21(signResult.tx, tokenId);
726
+ console.log('[sweepBsv21] Overlay submission result:', overlayResult);
727
+ }
728
+ catch (overlayError) {
729
+ // Log but don't fail the sweep - tx is already broadcast
730
+ console.warn('[sweepBsv21] Overlay submission failed:', overlayError);
731
+ }
732
+ }
733
+ return {
734
+ txid: signResult.txid,
735
+ beef: signResult.tx ? Array.from(signResult.tx) : undefined,
736
+ };
737
+ }
738
+ catch (error) {
739
+ console.error('[sweepBsv21]', error);
740
+ return {
741
+ error: error instanceof Error ? error.message : 'unknown-error',
742
+ };
743
+ }
744
+ },
745
+ };
746
+ // Export actions array for registry
747
+ export const sweepActions = [sweepBsv, sweepOrdinals, sweepBsv21];
748
+ //# sourceMappingURL=index.js.map