@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.
- package/dist/balance/index.d.ts +41 -0
- package/dist/balance/index.d.ts.map +1 -0
- package/dist/balance/index.js +111 -0
- package/dist/balance/index.js.map +1 -0
- package/dist/constants.d.ts +2 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +2 -0
- package/dist/constants.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +42 -0
- package/dist/index.js.map +1 -0
- package/dist/inscriptions/index.d.ts +26 -0
- package/dist/inscriptions/index.d.ts.map +1 -0
- package/dist/inscriptions/index.js +116 -0
- package/dist/inscriptions/index.js.map +1 -0
- package/dist/locks/index.d.ts +48 -0
- package/dist/locks/index.d.ts.map +1 -0
- package/dist/locks/index.js +296 -0
- package/dist/locks/index.js.map +1 -0
- package/dist/ordinals/index.d.ts +118 -0
- package/dist/ordinals/index.d.ts.map +1 -0
- package/dist/ordinals/index.js +850 -0
- package/dist/ordinals/index.js.map +1 -0
- package/dist/payments/index.d.ts +49 -0
- package/dist/payments/index.d.ts.map +1 -0
- package/dist/payments/index.js +194 -0
- package/dist/payments/index.js.map +1 -0
- package/dist/registry.d.ts +62 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +75 -0
- package/dist/registry.js.map +1 -0
- package/dist/signing/index.d.ts +36 -0
- package/dist/signing/index.d.ts.map +1 -0
- package/dist/signing/index.js +82 -0
- package/dist/signing/index.js.map +1 -0
- package/dist/sweep/index.d.ts +38 -0
- package/dist/sweep/index.d.ts.map +1 -0
- package/dist/sweep/index.js +748 -0
- package/dist/sweep/index.js.map +1 -0
- package/dist/sweep/types.d.ts +79 -0
- package/dist/sweep/types.d.ts.map +1 -0
- package/dist/sweep/types.js +5 -0
- package/dist/sweep/types.js.map +1 -0
- package/dist/tokens/index.d.ts +88 -0
- package/dist/tokens/index.d.ts.map +1 -0
- package/dist/tokens/index.js +548 -0
- package/dist/tokens/index.js.map +1 -0
- package/dist/types.d.ts +72 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +15 -0
- package/dist/types.js.map +1 -0
- 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
|