@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,850 @@
1
+ /**
2
+ * Ordinals Module
3
+ *
4
+ * Actions for managing ordinals/inscriptions.
5
+ * Returns WalletOutput[] directly from the SDK - no custom mapping needed.
6
+ */
7
+ import { OrdLock } from '@bopen-io/templates';
8
+ import { Beef, BigNumber, Hash, LockingScript, OP, P2PKH, PublicKey, Script, Transaction, TransactionSignature, UnlockingScript, Utils, } from '@bsv/sdk';
9
+ import { ONESAT_PROTOCOL, ORDINALS_BASKET, ORD_LOCK_PREFIX, ORD_LOCK_SUFFIX, } from '../constants';
10
+ // ============================================================================
11
+ // Helpers
12
+ // ============================================================================
13
+ function extractName(customInstructions) {
14
+ if (!customInstructions)
15
+ return undefined;
16
+ try {
17
+ const parsed = JSON.parse(customInstructions);
18
+ return parsed.name;
19
+ }
20
+ catch {
21
+ return undefined;
22
+ }
23
+ }
24
+ /**
25
+ * Sign a P2PKH input using the wallet's key derivation.
26
+ * Returns the unlocking script hex for the input.
27
+ */
28
+ async function signP2PKHInput(ctx, tx, inputIndex, protocolID, keyID) {
29
+ const txInput = tx.inputs[inputIndex];
30
+ const sourceLockingScript = txInput.sourceTransaction?.outputs[txInput.sourceOutputIndex]?.lockingScript;
31
+ if (!sourceLockingScript) {
32
+ return { error: `missing-source-locking-script-for-input-${inputIndex}` };
33
+ }
34
+ const sourceTXID = txInput.sourceTXID ?? txInput.sourceTransaction?.id('hex');
35
+ if (!sourceTXID) {
36
+ return { error: `missing-source-txid-for-input-${inputIndex}` };
37
+ }
38
+ const preimage = TransactionSignature.format({
39
+ sourceTXID,
40
+ sourceOutputIndex: txInput.sourceOutputIndex,
41
+ sourceSatoshis: 1,
42
+ transactionVersion: tx.version,
43
+ otherInputs: tx.inputs
44
+ .filter((_, idx) => idx !== inputIndex)
45
+ .map((inp) => ({
46
+ sourceTXID: inp.sourceTXID ?? inp.sourceTransaction?.id('hex') ?? '',
47
+ sourceOutputIndex: inp.sourceOutputIndex,
48
+ sequence: inp.sequence ?? 0xffffffff,
49
+ })),
50
+ inputIndex,
51
+ outputs: tx.outputs,
52
+ inputSequence: txInput.sequence ?? 0xffffffff,
53
+ subscript: sourceLockingScript,
54
+ lockTime: tx.lockTime,
55
+ scope: TransactionSignature.SIGHASH_ALL | TransactionSignature.SIGHASH_FORKID,
56
+ });
57
+ const sighash = Hash.sha256(Hash.sha256(preimage));
58
+ const { signature } = await ctx.wallet.createSignature({
59
+ protocolID,
60
+ keyID,
61
+ counterparty: 'self',
62
+ hashToDirectlySign: Array.from(sighash),
63
+ });
64
+ const { publicKey } = await ctx.wallet.getPublicKey({
65
+ protocolID,
66
+ keyID,
67
+ forSelf: true,
68
+ });
69
+ const sigWithHashtype = [
70
+ ...signature,
71
+ TransactionSignature.SIGHASH_ALL | TransactionSignature.SIGHASH_FORKID,
72
+ ];
73
+ return new UnlockingScript()
74
+ .writeBin(sigWithHashtype)
75
+ .writeBin(Utils.toArray(publicKey, 'hex'))
76
+ .toHex();
77
+ }
78
+ // ============================================================================
79
+ // Internal helpers
80
+ // ============================================================================
81
+ async function deriveCancelAddressInternal(ctx, outpoint) {
82
+ const result = await ctx.wallet.getPublicKey({
83
+ protocolID: ONESAT_PROTOCOL,
84
+ keyID: outpoint,
85
+ forSelf: true,
86
+ });
87
+ return PublicKey.fromString(result.publicKey).toAddress();
88
+ }
89
+ function buildOrdLockScript(ordAddress, payAddress, price) {
90
+ const cancelPkh = Utils.fromBase58Check(ordAddress).data;
91
+ const payPkh = Utils.fromBase58Check(payAddress).data;
92
+ const payoutScript = new P2PKH().lock(payPkh).toBinary();
93
+ const writer = new Utils.Writer();
94
+ writer.writeUInt64LEBn(new BigNumber(price));
95
+ writer.writeVarIntNum(payoutScript.length);
96
+ writer.write(payoutScript);
97
+ const payoutOutput = writer.toArray();
98
+ return new Script()
99
+ .writeScript(Script.fromHex(ORD_LOCK_PREFIX))
100
+ .writeBin(cancelPkh)
101
+ .writeBin(payoutOutput)
102
+ .writeScript(Script.fromHex(ORD_LOCK_SUFFIX));
103
+ }
104
+ function buildSerializedOutput(satoshis, script) {
105
+ const writer = new Utils.Writer();
106
+ writer.writeUInt64LEBn(new BigNumber(satoshis));
107
+ writer.writeVarIntNum(script.length);
108
+ writer.write(script);
109
+ return writer.toArray();
110
+ }
111
+ async function buildPurchaseUnlockingScript(tx, inputIndex, sourceSatoshis, lockingScript) {
112
+ if (tx.outputs.length < 2) {
113
+ throw new Error('Malformed transaction: requires at least 2 outputs');
114
+ }
115
+ const script = new UnlockingScript().writeBin(buildSerializedOutput(tx.outputs[0].satoshis ?? 0, tx.outputs[0].lockingScript.toBinary()));
116
+ if (tx.outputs.length > 2) {
117
+ const writer = new Utils.Writer();
118
+ for (const output of tx.outputs.slice(2)) {
119
+ writer.write(buildSerializedOutput(output.satoshis ?? 0, output.lockingScript.toBinary()));
120
+ }
121
+ script.writeBin(writer.toArray());
122
+ }
123
+ else {
124
+ script.writeOpCode(OP.OP_0);
125
+ }
126
+ const input = tx.inputs[inputIndex];
127
+ const sourceTXID = input.sourceTXID ?? input.sourceTransaction?.id('hex');
128
+ if (!sourceTXID) {
129
+ throw new Error('sourceTXID is required');
130
+ }
131
+ const preimage = TransactionSignature.format({
132
+ sourceTXID,
133
+ sourceOutputIndex: input.sourceOutputIndex,
134
+ sourceSatoshis,
135
+ transactionVersion: tx.version,
136
+ otherInputs: [],
137
+ inputIndex,
138
+ outputs: tx.outputs,
139
+ inputSequence: input.sequence ?? 0xffffffff,
140
+ subscript: lockingScript,
141
+ lockTime: tx.lockTime,
142
+ scope: TransactionSignature.SIGHASH_ALL |
143
+ TransactionSignature.SIGHASH_ANYONECANPAY |
144
+ TransactionSignature.SIGHASH_FORKID,
145
+ });
146
+ return script.writeBin(preimage).writeOpCode(OP.OP_0);
147
+ }
148
+ // ============================================================================
149
+ // Builder functions (utilities for advanced use)
150
+ // ============================================================================
151
+ /**
152
+ * Build CreateActionArgs for transferring one or more ordinals.
153
+ * Does NOT execute - returns params for createAction.
154
+ */
155
+ export async function buildTransferOrdinals(ctx, request) {
156
+ const { transfers, inputBEEF } = request;
157
+ if (!transfers.length) {
158
+ return { error: 'no-transfers' };
159
+ }
160
+ const inputs = [];
161
+ const outputs = [];
162
+ for (const { ordinal, counterparty, address } of transfers) {
163
+ if (!counterparty && !address) {
164
+ return { error: 'must-provide-counterparty-or-address' };
165
+ }
166
+ const outpoint = ordinal.outpoint;
167
+ let recipientAddress;
168
+ if (counterparty) {
169
+ const { publicKey } = await ctx.wallet.getPublicKey({
170
+ protocolID: ONESAT_PROTOCOL,
171
+ keyID: outpoint,
172
+ counterparty,
173
+ forSelf: false,
174
+ });
175
+ recipientAddress = PublicKey.fromString(publicKey).toAddress();
176
+ }
177
+ else if (address) {
178
+ recipientAddress = address;
179
+ }
180
+ else {
181
+ return { error: 'must-provide-counterparty-or-address' };
182
+ }
183
+ // Preserve important tags from source output
184
+ const tags = [];
185
+ for (const tag of ordinal.tags ?? []) {
186
+ if (tag.startsWith('type:') ||
187
+ tag.startsWith('origin:') ||
188
+ tag.startsWith('name:')) {
189
+ tags.push(tag);
190
+ }
191
+ }
192
+ const sourceName = extractName(ordinal.customInstructions);
193
+ inputs?.push({
194
+ outpoint,
195
+ inputDescription: 'Ordinal to transfer',
196
+ unlockingScriptLength: 108,
197
+ });
198
+ // Only track output in wallet when transferring to a counterparty (wallet can derive keys to spend it)
199
+ // External address transfers are NOT tracked since the wallet cannot spend them
200
+ if (counterparty) {
201
+ outputs?.push({
202
+ lockingScript: new P2PKH().lock(recipientAddress).toHex(),
203
+ satoshis: 1,
204
+ outputDescription: 'Ordinal transfer',
205
+ basket: ORDINALS_BASKET,
206
+ tags,
207
+ customInstructions: JSON.stringify({
208
+ protocolID: ONESAT_PROTOCOL,
209
+ keyID: outpoint,
210
+ ...(sourceName && { name: sourceName }),
211
+ }),
212
+ });
213
+ }
214
+ else {
215
+ // External address - output is not tracked in wallet
216
+ outputs?.push({
217
+ lockingScript: new P2PKH().lock(recipientAddress).toHex(),
218
+ satoshis: 1,
219
+ outputDescription: 'Ordinal transfer to external address',
220
+ tags: [],
221
+ });
222
+ }
223
+ }
224
+ return {
225
+ description: transfers.length === 1
226
+ ? 'Transfer ordinal'
227
+ : `Transfer ${transfers.length} ordinals`,
228
+ inputBEEF,
229
+ inputs,
230
+ outputs,
231
+ };
232
+ }
233
+ /**
234
+ * Build CreateActionArgs for listing an ordinal for sale.
235
+ * Does NOT execute - returns params for createAction.
236
+ */
237
+ export async function buildListOrdinal(ctx, request) {
238
+ const { ordinal, inputBEEF, price, payAddress } = request;
239
+ if (!payAddress)
240
+ return { error: 'missing-pay-address' };
241
+ if (price <= 0)
242
+ return { error: 'invalid-price' };
243
+ const outpoint = ordinal.outpoint;
244
+ const typeTag = ordinal.tags?.find((t) => t.startsWith('type:'));
245
+ const originTag = ordinal.tags?.find((t) => t.startsWith('origin:'));
246
+ const nameTag = ordinal.tags?.find((t) => t.startsWith('name:'));
247
+ const originOutpoint = originTag ? originTag.slice(7) : outpoint;
248
+ const sourceName = extractName(ordinal.customInstructions);
249
+ const cancelAddress = await deriveCancelAddressInternal(ctx, outpoint);
250
+ const lockingScript = buildOrdLockScript(cancelAddress, payAddress, price);
251
+ const tags = [
252
+ 'ordlock',
253
+ `origin:${originOutpoint}`,
254
+ `price:${price}`,
255
+ ];
256
+ if (typeTag)
257
+ tags.push(typeTag);
258
+ if (nameTag)
259
+ tags.push(nameTag);
260
+ return {
261
+ description: `List ordinal for ${price} sats`,
262
+ inputBEEF,
263
+ inputs: [
264
+ {
265
+ outpoint,
266
+ inputDescription: 'Ordinal to list',
267
+ unlockingScriptLength: 108,
268
+ },
269
+ ],
270
+ outputs: [
271
+ {
272
+ lockingScript: lockingScript.toHex(),
273
+ satoshis: 1,
274
+ outputDescription: `List ordinal for ${price} sats`,
275
+ basket: ORDINALS_BASKET,
276
+ tags,
277
+ customInstructions: JSON.stringify({
278
+ protocolID: ONESAT_PROTOCOL,
279
+ keyID: outpoint,
280
+ ...(sourceName && { name: sourceName }),
281
+ }),
282
+ },
283
+ ],
284
+ };
285
+ }
286
+ /**
287
+ * Get ordinals from the wallet with BEEF for spending.
288
+ */
289
+ export const getOrdinals = {
290
+ meta: {
291
+ name: 'getOrdinals',
292
+ description: 'Get ordinals/inscriptions from the wallet with BEEF for spending',
293
+ category: 'ordinals',
294
+ inputSchema: {
295
+ type: 'object',
296
+ properties: {
297
+ limit: {
298
+ type: 'integer',
299
+ description: 'Max ordinals to return (default: 100)',
300
+ },
301
+ offset: {
302
+ type: 'integer',
303
+ description: 'Offset for pagination (default: 0)',
304
+ },
305
+ },
306
+ },
307
+ },
308
+ async execute(ctx, input) {
309
+ const result = await ctx.wallet.listOutputs({
310
+ basket: ORDINALS_BASKET,
311
+ includeTags: true,
312
+ includeCustomInstructions: true,
313
+ include: 'entire transactions',
314
+ limit: input.limit ?? 100,
315
+ offset: input.offset ?? 0,
316
+ });
317
+ return {
318
+ outputs: result.outputs,
319
+ BEEF: result.BEEF,
320
+ };
321
+ },
322
+ };
323
+ /**
324
+ * Derive a cancel address for an ordinal listing.
325
+ */
326
+ export const deriveCancelAddress = {
327
+ meta: {
328
+ name: 'deriveCancelAddress',
329
+ description: 'Derive the cancel address for an ordinal listing',
330
+ category: 'ordinals',
331
+ inputSchema: {
332
+ type: 'object',
333
+ properties: {
334
+ outpoint: {
335
+ type: 'string',
336
+ description: 'Outpoint of the ordinal listing',
337
+ },
338
+ },
339
+ required: ['outpoint'],
340
+ },
341
+ },
342
+ async execute(ctx, input) {
343
+ return deriveCancelAddressInternal(ctx, input.outpoint);
344
+ },
345
+ };
346
+ /**
347
+ * Transfer an ordinal to a new owner.
348
+ */
349
+ export const transferOrdinals = {
350
+ meta: {
351
+ name: 'transferOrdinals',
352
+ description: 'Transfer one or more ordinals to new owners',
353
+ category: 'ordinals',
354
+ inputSchema: {
355
+ type: 'object',
356
+ properties: {
357
+ transfers: {
358
+ type: 'array',
359
+ description: 'Ordinals to transfer with destinations',
360
+ items: {
361
+ type: 'object',
362
+ properties: {
363
+ ordinal: {
364
+ type: 'object',
365
+ description: 'WalletOutput from listOutputs',
366
+ },
367
+ counterparty: {
368
+ type: 'string',
369
+ description: 'Recipient identity public key (hex)',
370
+ },
371
+ address: {
372
+ type: 'string',
373
+ description: 'Recipient P2PKH address',
374
+ },
375
+ },
376
+ required: ['ordinal'],
377
+ },
378
+ },
379
+ inputBEEF: {
380
+ type: 'array',
381
+ description: "BEEF from listOutputs with include: 'entire transactions'",
382
+ },
383
+ },
384
+ required: ['transfers', 'inputBEEF'],
385
+ },
386
+ },
387
+ async execute(ctx, input) {
388
+ try {
389
+ const params = await buildTransferOrdinals(ctx, input);
390
+ if ('error' in params) {
391
+ return params;
392
+ }
393
+ console.log('[transferOrdinals] params:', JSON.stringify({
394
+ description: params.description,
395
+ inputBEEF: params.inputBEEF
396
+ ? `[${params.inputBEEF.length} bytes]`
397
+ : 'undefined',
398
+ inputs: params.inputs,
399
+ outputs: params.outputs?.map((o) => ({
400
+ ...o,
401
+ lockingScript: `${o.lockingScript?.slice(0, 20)}...`,
402
+ })),
403
+ }, null, 2));
404
+ // Debug: Check if BEEF contains the source transactions
405
+ try {
406
+ const beef = Beef.fromBinary(params.inputBEEF);
407
+ console.log('[transferOrdinals] BEEF tx count:', beef.txs.length);
408
+ for (const inp of params.inputs ?? []) {
409
+ const [txid] = inp.outpoint.split('.');
410
+ const sourceTx = beef.findTxid(txid);
411
+ console.log(`[transferOrdinals] Source tx for ${inp.outpoint}: ${sourceTx ? 'FOUND' : 'MISSING'}`);
412
+ }
413
+ }
414
+ catch (e) {
415
+ console.log('[transferOrdinals] BEEF parse error:', e);
416
+ }
417
+ const createResult = await ctx.wallet.createAction({
418
+ ...params,
419
+ options: { signAndProcess: false, randomizeOutputs: false },
420
+ });
421
+ if (!createResult.signableTransaction) {
422
+ return { error: 'no-signable-transaction' };
423
+ }
424
+ const tx = Transaction.fromBEEF(createResult.signableTransaction.tx);
425
+ const spends = {};
426
+ for (let i = 0; i < input.transfers.length; i++) {
427
+ const { ordinal } = input.transfers[i];
428
+ console.log(`[transferOrdinals] Input ${i}: outpoint=${ordinal.outpoint}, customInstructions=${ordinal.customInstructions}`);
429
+ if (!ordinal.customInstructions) {
430
+ return {
431
+ error: `missing-custom-instructions-for-${ordinal.outpoint}`,
432
+ };
433
+ }
434
+ const { protocolID, keyID } = JSON.parse(ordinal.customInstructions);
435
+ console.log(`[transferOrdinals] Input ${i}: protocolID=${JSON.stringify(protocolID)}, keyID=${keyID}`);
436
+ const unlocking = await signP2PKHInput(ctx, tx, i, protocolID, keyID);
437
+ if (typeof unlocking !== 'string')
438
+ return unlocking;
439
+ spends[i] = { unlockingScript: unlocking };
440
+ }
441
+ const signResult = await ctx.wallet.signAction({
442
+ reference: createResult.signableTransaction.reference,
443
+ spends,
444
+ options: { acceptDelayedBroadcast: false },
445
+ });
446
+ if ('error' in signResult) {
447
+ return { error: String(signResult.error) };
448
+ }
449
+ return {
450
+ txid: signResult.txid,
451
+ rawtx: signResult.tx ? Utils.toHex(signResult.tx) : undefined,
452
+ };
453
+ }
454
+ catch (error) {
455
+ console.error('[transferOrdinals]', error);
456
+ return {
457
+ error: error instanceof Error ? error.message : 'unknown-error',
458
+ };
459
+ }
460
+ },
461
+ };
462
+ /**
463
+ * List an ordinal for sale on the global orderbook.
464
+ */
465
+ export const listOrdinal = {
466
+ meta: {
467
+ name: 'listOrdinal',
468
+ description: 'List an ordinal for sale on the global orderbook',
469
+ category: 'ordinals',
470
+ inputSchema: {
471
+ type: 'object',
472
+ properties: {
473
+ ordinal: {
474
+ type: 'object',
475
+ description: 'WalletOutput from listOutputs',
476
+ },
477
+ inputBEEF: {
478
+ type: 'array',
479
+ description: "BEEF from listOutputs with include: 'entire transactions'",
480
+ },
481
+ price: { type: 'integer', description: 'Price in satoshis' },
482
+ payAddress: {
483
+ type: 'string',
484
+ description: 'Address to receive payment on purchase',
485
+ },
486
+ },
487
+ required: ['ordinal', 'inputBEEF', 'price', 'payAddress'],
488
+ },
489
+ },
490
+ async execute(ctx, input) {
491
+ try {
492
+ const params = await buildListOrdinal(ctx, input);
493
+ if ('error' in params) {
494
+ return params;
495
+ }
496
+ const createResult = await ctx.wallet.createAction({
497
+ ...params,
498
+ options: { signAndProcess: false, randomizeOutputs: false },
499
+ });
500
+ if (!createResult.signableTransaction) {
501
+ return { error: 'no-signable-transaction' };
502
+ }
503
+ if (!input.ordinal.customInstructions) {
504
+ return { error: 'missing-custom-instructions' };
505
+ }
506
+ const { protocolID, keyID } = JSON.parse(input.ordinal.customInstructions);
507
+ const tx = Transaction.fromBEEF(createResult.signableTransaction.tx);
508
+ const unlocking = await signP2PKHInput(ctx, tx, 0, protocolID, keyID);
509
+ if (typeof unlocking !== 'string')
510
+ return unlocking;
511
+ const signResult = await ctx.wallet.signAction({
512
+ reference: createResult.signableTransaction.reference,
513
+ spends: { 0: { unlockingScript: unlocking } },
514
+ options: { acceptDelayedBroadcast: false },
515
+ });
516
+ if ('error' in signResult) {
517
+ return { error: String(signResult.error) };
518
+ }
519
+ return {
520
+ txid: signResult.txid,
521
+ rawtx: signResult.tx ? Utils.toHex(signResult.tx) : undefined,
522
+ };
523
+ }
524
+ catch (error) {
525
+ console.error('[listOrdinal]', error);
526
+ return {
527
+ error: error instanceof Error ? error.message : 'unknown-error',
528
+ };
529
+ }
530
+ },
531
+ };
532
+ /**
533
+ * Cancel an ordinal listing.
534
+ */
535
+ export const cancelListing = {
536
+ meta: {
537
+ name: 'cancelListing',
538
+ description: 'Cancel an ordinal listing and return the ordinal to the wallet',
539
+ category: 'ordinals',
540
+ inputSchema: {
541
+ type: 'object',
542
+ properties: {
543
+ listing: {
544
+ type: 'object',
545
+ description: 'WalletOutput of the listing (must include lockingScript)',
546
+ },
547
+ inputBEEF: {
548
+ type: 'array',
549
+ description: "BEEF from listOutputs with include: 'entire transactions'",
550
+ },
551
+ },
552
+ required: ['listing', 'inputBEEF'],
553
+ },
554
+ },
555
+ async execute(ctx, input) {
556
+ try {
557
+ const { listing, inputBEEF } = input;
558
+ const outpoint = listing.outpoint;
559
+ if (!listing.customInstructions) {
560
+ return { error: 'missing-custom-instructions' };
561
+ }
562
+ const { protocolID, keyID, name: listingName, } = JSON.parse(listing.customInstructions);
563
+ const typeTag = listing.tags?.find((t) => t.startsWith('type:'));
564
+ const originTag = listing.tags?.find((t) => t.startsWith('origin:'));
565
+ const nameTag = listing.tags?.find((t) => t.startsWith('name:'));
566
+ const cancelAddress = await deriveCancelAddressInternal(ctx, keyID);
567
+ const tags = [];
568
+ if (typeTag)
569
+ tags.push(typeTag);
570
+ if (originTag)
571
+ tags.push(originTag);
572
+ if (nameTag)
573
+ tags.push(nameTag);
574
+ const createResult = await ctx.wallet.createAction({
575
+ description: 'Cancel ordinal listing',
576
+ inputBEEF,
577
+ inputs: [
578
+ {
579
+ outpoint,
580
+ inputDescription: 'Listed ordinal',
581
+ unlockingScriptLength: 108,
582
+ },
583
+ ],
584
+ outputs: [
585
+ {
586
+ lockingScript: new P2PKH().lock(cancelAddress).toHex(),
587
+ satoshis: 1,
588
+ outputDescription: 'Cancelled listing',
589
+ basket: ORDINALS_BASKET,
590
+ tags,
591
+ customInstructions: JSON.stringify({
592
+ protocolID,
593
+ keyID,
594
+ ...(listingName && { name: listingName }),
595
+ }),
596
+ },
597
+ ],
598
+ options: { signAndProcess: false, randomizeOutputs: false },
599
+ });
600
+ if ('error' in createResult && createResult.error) {
601
+ return { error: String(createResult.error) };
602
+ }
603
+ if (!createResult.signableTransaction) {
604
+ return { error: 'no-signable-transaction' };
605
+ }
606
+ const tx = Transaction.fromBEEF(createResult.signableTransaction.tx);
607
+ const txInput = tx.inputs[0];
608
+ const lockingScript = txInput.sourceTransaction?.outputs[txInput.sourceOutputIndex]
609
+ ?.lockingScript;
610
+ if (!lockingScript) {
611
+ return { error: 'missing-locking-script' };
612
+ }
613
+ const sourceTXID = txInput.sourceTXID ?? txInput.sourceTransaction?.id('hex');
614
+ if (!sourceTXID) {
615
+ return { error: 'missing-source-txid' };
616
+ }
617
+ const preimage = TransactionSignature.format({
618
+ sourceTXID,
619
+ sourceOutputIndex: txInput.sourceOutputIndex,
620
+ sourceSatoshis: listing.satoshis,
621
+ transactionVersion: tx.version,
622
+ otherInputs: [],
623
+ inputIndex: 0,
624
+ outputs: tx.outputs,
625
+ inputSequence: txInput.sequence ?? 0xffffffff,
626
+ subscript: lockingScript,
627
+ lockTime: tx.lockTime,
628
+ scope: TransactionSignature.SIGHASH_ALL |
629
+ TransactionSignature.SIGHASH_ANYONECANPAY |
630
+ TransactionSignature.SIGHASH_FORKID,
631
+ });
632
+ const sighash = Hash.sha256(Hash.sha256(preimage));
633
+ const { signature } = await ctx.wallet.createSignature({
634
+ protocolID,
635
+ keyID,
636
+ counterparty: 'self',
637
+ hashToDirectlySign: Array.from(sighash),
638
+ });
639
+ const { publicKey } = await ctx.wallet.getPublicKey({
640
+ protocolID,
641
+ keyID,
642
+ forSelf: true,
643
+ });
644
+ const unlockingScript = new UnlockingScript()
645
+ .writeBin(signature)
646
+ .writeBin(Utils.toArray(publicKey, 'hex'))
647
+ .writeOpCode(OP.OP_1);
648
+ const signResult = await ctx.wallet.signAction({
649
+ reference: createResult.signableTransaction.reference,
650
+ spends: {
651
+ 0: { unlockingScript: unlockingScript.toHex() },
652
+ },
653
+ options: { acceptDelayedBroadcast: false },
654
+ });
655
+ if ('error' in signResult) {
656
+ return { error: String(signResult.error) };
657
+ }
658
+ return {
659
+ txid: signResult.txid,
660
+ rawtx: signResult.tx ? Utils.toHex(signResult.tx) : undefined,
661
+ };
662
+ }
663
+ catch (error) {
664
+ console.error('[cancelListing]', error);
665
+ return {
666
+ error: error instanceof Error ? error.message : 'unknown-error',
667
+ };
668
+ }
669
+ },
670
+ };
671
+ /**
672
+ * Purchase an ordinal from the global orderbook.
673
+ */
674
+ export const purchaseOrdinal = {
675
+ meta: {
676
+ name: 'purchaseOrdinal',
677
+ description: 'Purchase an ordinal from the global orderbook',
678
+ category: 'ordinals',
679
+ requiresServices: true,
680
+ inputSchema: {
681
+ type: 'object',
682
+ properties: {
683
+ outpoint: {
684
+ type: 'string',
685
+ description: 'Outpoint of the listing to purchase',
686
+ },
687
+ marketplaceAddress: {
688
+ type: 'string',
689
+ description: 'Marketplace address for fees',
690
+ },
691
+ marketplaceRate: {
692
+ type: 'number',
693
+ description: 'Marketplace fee rate (0-1)',
694
+ },
695
+ contentType: {
696
+ type: 'string',
697
+ description: 'Content type (auto-detected if not provided)',
698
+ },
699
+ origin: {
700
+ type: 'string',
701
+ description: 'Origin outpoint (auto-detected if not provided)',
702
+ },
703
+ },
704
+ required: ['outpoint'],
705
+ },
706
+ },
707
+ async execute(ctx, input) {
708
+ try {
709
+ const { outpoint, marketplaceAddress, marketplaceRate } = input;
710
+ if (!ctx.services) {
711
+ return { error: 'services-required-for-purchase' };
712
+ }
713
+ const parts = outpoint.split('_');
714
+ if (parts.length !== 2) {
715
+ return { error: 'invalid-outpoint-format' };
716
+ }
717
+ const [txid, voutStr] = parts;
718
+ const vout = Number.parseInt(voutStr, 10);
719
+ let { contentType, origin, name } = input;
720
+ if (!contentType || !origin || name === undefined) {
721
+ const metadata = await ctx.services.ordfs.getMetadata(outpoint);
722
+ contentType = contentType ?? metadata.contentType;
723
+ origin = origin ?? metadata.origin ?? outpoint;
724
+ // Extract name from map.name or map.subTypeData.name
725
+ if (name === undefined && metadata.map) {
726
+ const mapName = metadata.map.name;
727
+ const subTypeData = metadata.map.subTypeData;
728
+ name =
729
+ (typeof mapName === 'string' ? mapName : undefined) ??
730
+ (typeof subTypeData?.name === 'string'
731
+ ? subTypeData.name
732
+ : undefined);
733
+ }
734
+ }
735
+ const beef = await ctx.services.getBeefForTxid(txid);
736
+ const listingBeefTx = beef.findTxid(txid);
737
+ if (!listingBeefTx?.tx) {
738
+ return { error: 'listing-transaction-not-found' };
739
+ }
740
+ const listingOutput = listingBeefTx.tx.outputs[vout];
741
+ if (!listingOutput) {
742
+ return { error: 'listing-output-not-found' };
743
+ }
744
+ const ordLockData = OrdLock.decode(listingOutput.lockingScript);
745
+ if (!ordLockData) {
746
+ return { error: 'not-an-ordlock-listing' };
747
+ }
748
+ const { publicKey } = await ctx.wallet.getPublicKey({
749
+ protocolID: ONESAT_PROTOCOL,
750
+ keyID: outpoint,
751
+ counterparty: 'self',
752
+ forSelf: true,
753
+ });
754
+ const ourOrdAddress = PublicKey.fromString(publicKey).toAddress();
755
+ const outputs = [];
756
+ const p2pkh = new P2PKH();
757
+ const purchaseTags = [`type:${contentType}`, `origin:${origin}`];
758
+ if (name)
759
+ purchaseTags.push(`name:${name}`);
760
+ outputs.push({
761
+ lockingScript: p2pkh.lock(ourOrdAddress).toHex(),
762
+ satoshis: 1,
763
+ outputDescription: 'Purchased ordinal',
764
+ basket: ORDINALS_BASKET,
765
+ tags: purchaseTags,
766
+ customInstructions: JSON.stringify({
767
+ protocolID: ONESAT_PROTOCOL,
768
+ keyID: outpoint,
769
+ ...(name && { name: name.slice(0, 64) }),
770
+ }),
771
+ });
772
+ const payoutReader = new Utils.Reader(ordLockData.payout);
773
+ const payoutSatoshis = payoutReader.readUInt64LEBn().toNumber();
774
+ const payoutScriptLen = payoutReader.readVarIntNum();
775
+ const payoutScriptBin = payoutReader.read(payoutScriptLen);
776
+ const payoutLockingScript = LockingScript.fromBinary(payoutScriptBin);
777
+ outputs.push({
778
+ lockingScript: payoutLockingScript.toHex(),
779
+ satoshis: payoutSatoshis,
780
+ outputDescription: 'Payment to seller',
781
+ tags: [],
782
+ });
783
+ if (marketplaceAddress && marketplaceRate && marketplaceRate > 0) {
784
+ const marketFee = Math.ceil(payoutSatoshis * marketplaceRate);
785
+ if (marketFee > 0) {
786
+ outputs.push({
787
+ lockingScript: p2pkh.lock(marketplaceAddress).toHex(),
788
+ satoshis: marketFee,
789
+ outputDescription: 'Marketplace fee',
790
+ tags: [],
791
+ });
792
+ }
793
+ }
794
+ const createResult = await ctx.wallet.createAction({
795
+ description: `Purchase ordinal for ${payoutSatoshis} sats`,
796
+ inputBEEF: beef.toBinary(),
797
+ inputs: [
798
+ {
799
+ outpoint,
800
+ inputDescription: 'Listed ordinal',
801
+ unlockingScriptLength: 500,
802
+ },
803
+ ],
804
+ outputs,
805
+ options: { signAndProcess: false, randomizeOutputs: false },
806
+ });
807
+ if ('error' in createResult && createResult.error) {
808
+ return { error: String(createResult.error) };
809
+ }
810
+ if (!createResult.signableTransaction) {
811
+ return { error: 'no-signable-transaction' };
812
+ }
813
+ const tx = Transaction.fromBEEF(createResult.signableTransaction.tx);
814
+ const unlockingScript = await buildPurchaseUnlockingScript(tx, 0, listingOutput.satoshis ?? 1, listingOutput.lockingScript);
815
+ const signResult = await ctx.wallet.signAction({
816
+ reference: createResult.signableTransaction.reference,
817
+ spends: {
818
+ 0: { unlockingScript: unlockingScript.toHex() },
819
+ },
820
+ options: { acceptDelayedBroadcast: false },
821
+ });
822
+ if ('error' in signResult) {
823
+ return { error: String(signResult.error) };
824
+ }
825
+ return {
826
+ txid: signResult.txid,
827
+ rawtx: signResult.tx ? Utils.toHex(signResult.tx) : undefined,
828
+ };
829
+ }
830
+ catch (error) {
831
+ console.error('[purchaseOrdinal]', error);
832
+ return {
833
+ error: error instanceof Error ? error.message : 'unknown-error',
834
+ };
835
+ }
836
+ },
837
+ };
838
+ // ============================================================================
839
+ // Module exports
840
+ // ============================================================================
841
+ /** All ordinals actions for registry */
842
+ export const ordinalsActions = [
843
+ getOrdinals,
844
+ deriveCancelAddress,
845
+ transferOrdinals,
846
+ listOrdinal,
847
+ cancelListing,
848
+ purchaseOrdinal,
849
+ ];
850
+ //# sourceMappingURL=index.js.map