@1sat/wallet-toolbox 0.0.9 → 0.0.11

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 (39) hide show
  1. package/dist/api/balance/index.d.ts +20 -8
  2. package/dist/api/balance/index.js +104 -51
  3. package/dist/api/broadcast/index.d.ts +5 -3
  4. package/dist/api/broadcast/index.js +65 -37
  5. package/dist/api/index.d.ts +16 -15
  6. package/dist/api/index.js +42 -17
  7. package/dist/api/inscriptions/index.d.ts +9 -9
  8. package/dist/api/inscriptions/index.js +79 -31
  9. package/dist/api/locks/index.d.ts +15 -12
  10. package/dist/api/locks/index.js +252 -194
  11. package/dist/api/ordinals/index.d.ts +50 -35
  12. package/dist/api/ordinals/index.js +469 -349
  13. package/dist/api/payments/index.d.ts +15 -4
  14. package/dist/api/payments/index.js +147 -92
  15. package/dist/api/signing/index.d.ts +8 -5
  16. package/dist/api/signing/index.js +70 -33
  17. package/dist/api/skills/registry.d.ts +61 -0
  18. package/dist/api/skills/registry.js +74 -0
  19. package/dist/api/skills/types.d.ts +71 -0
  20. package/dist/api/skills/types.js +14 -0
  21. package/dist/api/sweep/index.d.ts +23 -0
  22. package/dist/api/sweep/index.js +221 -0
  23. package/dist/api/sweep/types.d.ts +30 -0
  24. package/dist/api/sweep/types.js +4 -0
  25. package/dist/api/tokens/index.d.ts +37 -38
  26. package/dist/api/tokens/index.js +398 -341
  27. package/dist/index.d.ts +2 -1
  28. package/dist/index.js +2 -0
  29. package/dist/services/client/OwnerClient.js +4 -0
  30. package/dist/wallet/factory.d.ts +64 -0
  31. package/dist/wallet/factory.js +163 -0
  32. package/dist/wallet/index.d.ts +1 -0
  33. package/dist/wallet/index.js +1 -0
  34. package/package.json +13 -4
  35. package/dist/OneSatWallet.d.ts +0 -316
  36. package/dist/OneSatWallet.js +0 -956
  37. package/dist/api/OneSatApi.d.ts +0 -100
  38. package/dist/api/OneSatApi.js +0 -156
  39. package/dist/indexers/TransactionParser.d.ts +0 -53
@@ -1,36 +1,28 @@
1
1
  /**
2
2
  * Ordinals Module
3
3
  *
4
- * Functions for managing ordinals/inscriptions.
4
+ * Skills for managing ordinals/inscriptions.
5
5
  * Returns WalletOutput[] directly from the SDK - no custom mapping needed.
6
6
  */
7
7
  import { BigNumber, Hash, LockingScript, OP, P2PKH, PublicKey, Script, Transaction, TransactionSignature, UnlockingScript, Utils, } from "@bsv/sdk";
8
8
  import { OrdLock } from "@bopen-io/templates";
9
9
  import { ORDINALS_BASKET, ORDLOCK_PREFIX, ORDLOCK_SUFFIX } from "../constants";
10
- // Protocol for ordinal listing key derivation (security level 1 = low, self-only)
10
+ // ============================================================================
11
+ // Constants
12
+ // ============================================================================
13
+ const ORDINAL_PROTOCOL = [1, "ordinal"];
11
14
  const ORDINAL_LISTING_PROTOCOL = [1, "ordinal listing"];
12
- /**
13
- * Check if address is a paymail.
14
- */
15
- function isPaymail(address) {
16
- return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(address);
17
- }
18
- /**
19
- * Derive a cancel address for an ordinal listing.
20
- * Uses the outpoint as keyID with security level 1 (self-only).
21
- */
22
- export async function deriveCancelAddress(cwi, outpoint) {
23
- const result = await cwi.getPublicKey({
15
+ // ============================================================================
16
+ // Internal helpers
17
+ // ============================================================================
18
+ async function deriveCancelAddressInternal(ctx, outpoint) {
19
+ const result = await ctx.wallet.getPublicKey({
24
20
  protocolID: ORDINAL_LISTING_PROTOCOL,
25
21
  keyID: outpoint,
26
22
  forSelf: true,
27
23
  });
28
- const publicKey = PublicKey.fromString(result.publicKey);
29
- return publicKey.toAddress();
24
+ return PublicKey.fromString(result.publicKey).toAddress();
30
25
  }
31
- /**
32
- * Build OrdLock script for listing an ordinal.
33
- */
34
26
  function buildOrdLockScript(ordAddress, payAddress, price) {
35
27
  const cancelPkh = Utils.fromBase58Check(ordAddress).data;
36
28
  const payPkh = Utils.fromBase58Check(payAddress).data;
@@ -46,31 +38,79 @@ function buildOrdLockScript(ordAddress, payAddress, price) {
46
38
  .writeBin(payoutOutput)
47
39
  .writeScript(Script.fromHex(ORDLOCK_SUFFIX));
48
40
  }
49
- /**
50
- * List ordinals from the 1sat basket.
51
- * Returns WalletOutput[] directly - use tags for metadata (origin:, type:, name:, own:, list:).
52
- */
53
- export async function listOrdinals(cwi, options = {}) {
54
- const result = await cwi.listOutputs({
55
- basket: ORDINALS_BASKET,
56
- includeTags: true,
57
- includeCustomInstructions: true,
58
- limit: options.limit ?? 100,
59
- offset: options.offset ?? 0,
60
- ...options,
41
+ function buildSerializedOutput(satoshis, script) {
42
+ const writer = new Utils.Writer();
43
+ writer.writeUInt64LEBn(new BigNumber(satoshis));
44
+ writer.writeVarIntNum(script.length);
45
+ writer.write(script);
46
+ return writer.toArray();
47
+ }
48
+ async function buildPurchaseUnlockingScript(tx, inputIndex, sourceSatoshis, lockingScript) {
49
+ if (tx.outputs.length < 2) {
50
+ throw new Error("Malformed transaction: requires at least 2 outputs");
51
+ }
52
+ const script = new UnlockingScript().writeBin(buildSerializedOutput(tx.outputs[0].satoshis ?? 0, tx.outputs[0].lockingScript.toBinary()));
53
+ if (tx.outputs.length > 2) {
54
+ const writer = new Utils.Writer();
55
+ for (const output of tx.outputs.slice(2)) {
56
+ writer.write(buildSerializedOutput(output.satoshis ?? 0, output.lockingScript.toBinary()));
57
+ }
58
+ script.writeBin(writer.toArray());
59
+ }
60
+ else {
61
+ script.writeOpCode(OP.OP_0);
62
+ }
63
+ const input = tx.inputs[inputIndex];
64
+ const sourceTXID = input.sourceTXID ?? input.sourceTransaction?.id("hex");
65
+ if (!sourceTXID) {
66
+ throw new Error("sourceTXID is required");
67
+ }
68
+ const preimage = TransactionSignature.format({
69
+ sourceTXID,
70
+ sourceOutputIndex: input.sourceOutputIndex,
71
+ sourceSatoshis,
72
+ transactionVersion: tx.version,
73
+ otherInputs: [],
74
+ inputIndex,
75
+ outputs: tx.outputs,
76
+ inputSequence: input.sequence ?? 0xffffffff,
77
+ subscript: lockingScript,
78
+ lockTime: tx.lockTime,
79
+ scope: TransactionSignature.SIGHASH_ALL |
80
+ TransactionSignature.SIGHASH_ANYONECANPAY |
81
+ TransactionSignature.SIGHASH_FORKID,
61
82
  });
62
- return result.outputs;
83
+ return script.writeBin(preimage).writeOpCode(OP.OP_0);
63
84
  }
85
+ // ============================================================================
86
+ // Builder functions (utilities for advanced use)
87
+ // ============================================================================
64
88
  /**
65
89
  * Build CreateActionArgs for transferring an ordinal.
66
90
  * Does NOT execute - returns params for createAction.
67
91
  */
68
- export async function buildTransferOrdinal(cwi, request) {
69
- const { outpoint, destination } = request;
70
- if (isPaymail(destination)) {
92
+ export async function buildTransferOrdinal(ctx, request) {
93
+ const { outpoint, counterparty, address, paymail } = request;
94
+ if (!counterparty && !address && !paymail) {
95
+ return { error: "must-provide-counterparty-address-or-paymail" };
96
+ }
97
+ let recipientAddress;
98
+ if (counterparty) {
99
+ const { publicKey } = await ctx.wallet.getPublicKey({
100
+ protocolID: ORDINAL_PROTOCOL,
101
+ keyID: outpoint,
102
+ counterparty,
103
+ forSelf: false,
104
+ });
105
+ recipientAddress = PublicKey.fromString(publicKey).toAddress();
106
+ }
107
+ else if (paymail) {
71
108
  return { error: "paymail-not-yet-implemented" };
72
109
  }
73
- const result = await cwi.listOutputs({
110
+ else {
111
+ recipientAddress = address;
112
+ }
113
+ const result = await ctx.wallet.listOutputs({
74
114
  basket: ORDINALS_BASKET,
75
115
  include: "locking scripts",
76
116
  limit: 10000,
@@ -81,366 +121,446 @@ export async function buildTransferOrdinal(cwi, request) {
81
121
  return {
82
122
  description: "Transfer ordinal",
83
123
  inputs: [{ outpoint, inputDescription: "Ordinal to transfer" }],
84
- outputs: [{
85
- lockingScript: new P2PKH().lock(destination).toHex(),
124
+ outputs: [
125
+ {
126
+ lockingScript: new P2PKH().lock(recipientAddress).toHex(),
86
127
  satoshis: 1,
87
128
  outputDescription: "Ordinal transfer",
88
- }],
129
+ },
130
+ ],
89
131
  };
90
132
  }
91
133
  /**
92
134
  * Build CreateActionArgs for listing an ordinal for sale.
93
135
  * Does NOT execute - returns params for createAction.
94
- * If cancelAddress is not provided, it will be derived from the CWI.
95
136
  */
96
- export async function buildListOrdinal(cwi, request) {
137
+ export async function buildListOrdinal(ctx, request) {
97
138
  const { outpoint, price, payAddress } = request;
98
139
  if (!payAddress)
99
140
  return { error: "missing-pay-address" };
100
141
  if (price <= 0)
101
142
  return { error: "invalid-price" };
102
- const result = await cwi.listOutputs({
143
+ const result = await ctx.wallet.listOutputs({
103
144
  basket: ORDINALS_BASKET,
145
+ includeTags: true,
104
146
  include: "locking scripts",
105
147
  limit: 10000,
106
148
  });
107
- if (!result.outputs.find((o) => o.outpoint === outpoint)) {
149
+ const sourceOutput = result.outputs.find((o) => o.outpoint === outpoint);
150
+ if (!sourceOutput) {
108
151
  return { error: "ordinal-not-found" };
109
152
  }
110
- // Derive cancel address if not provided
111
- const cancelAddress = request.cancelAddress ?? await deriveCancelAddress(cwi, outpoint);
153
+ const typeTag = sourceOutput.tags?.find((t) => t.startsWith("type:"));
154
+ const originTag = sourceOutput.tags?.find((t) => t.startsWith("origin:"));
155
+ const originOutpoint = originTag ? originTag.slice(7) : outpoint;
156
+ const cancelAddress = await deriveCancelAddressInternal(ctx, outpoint);
112
157
  const lockingScript = buildOrdLockScript(cancelAddress, payAddress, price);
158
+ const tags = ["ordlock", `origin:${originOutpoint}`, `price:${price}`];
159
+ if (typeTag)
160
+ tags.push(typeTag);
113
161
  return {
114
162
  description: `List ordinal for ${price} sats`,
115
163
  inputs: [{ outpoint, inputDescription: "Ordinal to list" }],
116
- outputs: [{
164
+ outputs: [
165
+ {
117
166
  lockingScript: lockingScript.toHex(),
118
167
  satoshis: 1,
119
168
  outputDescription: `List ordinal for ${price} sats`,
120
169
  basket: ORDINALS_BASKET,
121
- tags: [`origin:${outpoint}`, `price:${price}`],
122
- }],
170
+ tags,
171
+ customInstructions: JSON.stringify({
172
+ protocolID: ORDINAL_LISTING_PROTOCOL,
173
+ keyID: outpoint,
174
+ }),
175
+ },
176
+ ],
123
177
  };
124
178
  }
125
179
  /**
126
- * Transfer an ordinal to a new address.
180
+ * List ordinals from the wallet.
181
+ */
182
+ export const listOrdinals = {
183
+ meta: {
184
+ name: "listOrdinals",
185
+ description: "List ordinals/inscriptions from the wallet",
186
+ category: "ordinals",
187
+ inputSchema: {
188
+ type: "object",
189
+ properties: {
190
+ limit: { type: "integer", description: "Max number of ordinals to return (default: 100)" },
191
+ offset: { type: "integer", description: "Offset for pagination (default: 0)" },
192
+ },
193
+ },
194
+ },
195
+ async execute(ctx, input) {
196
+ const result = await ctx.wallet.listOutputs({
197
+ basket: ORDINALS_BASKET,
198
+ includeTags: true,
199
+ includeCustomInstructions: true,
200
+ limit: input.limit ?? 100,
201
+ offset: input.offset ?? 0,
202
+ });
203
+ return result.outputs;
204
+ },
205
+ };
206
+ /**
207
+ * Derive a cancel address for an ordinal listing.
208
+ */
209
+ export const deriveCancelAddress = {
210
+ meta: {
211
+ name: "deriveCancelAddress",
212
+ description: "Derive the cancel address for an ordinal listing",
213
+ category: "ordinals",
214
+ inputSchema: {
215
+ type: "object",
216
+ properties: {
217
+ outpoint: { type: "string", description: "Outpoint of the ordinal listing" },
218
+ },
219
+ required: ["outpoint"],
220
+ },
221
+ },
222
+ async execute(ctx, input) {
223
+ return deriveCancelAddressInternal(ctx, input.outpoint);
224
+ },
225
+ };
226
+ /**
227
+ * Transfer an ordinal to a new owner.
127
228
  */
128
- export async function transferOrdinal(cwi, request) {
129
- try {
130
- const params = await buildTransferOrdinal(cwi, request);
131
- if ("error" in params) {
132
- return params;
229
+ export const transferOrdinal = {
230
+ meta: {
231
+ name: "transferOrdinal",
232
+ description: "Transfer an ordinal to a new owner via counterparty pubkey, address, or paymail",
233
+ category: "ordinals",
234
+ inputSchema: {
235
+ type: "object",
236
+ properties: {
237
+ outpoint: { type: "string", description: "Outpoint of the ordinal (txid_vout format)" },
238
+ counterparty: { type: "string", description: "Recipient identity public key (hex)" },
239
+ address: { type: "string", description: "Recipient P2PKH address" },
240
+ paymail: { type: "string", description: "Recipient paymail address" },
241
+ },
242
+ required: ["outpoint"],
243
+ },
244
+ },
245
+ async execute(ctx, input) {
246
+ try {
247
+ const params = await buildTransferOrdinal(ctx, input);
248
+ if ("error" in params) {
249
+ return params;
250
+ }
251
+ const result = await ctx.wallet.createAction(params);
252
+ if (!result.txid) {
253
+ return { error: "no-txid-returned" };
254
+ }
255
+ return { txid: result.txid, rawtx: result.tx ? Utils.toHex(result.tx) : undefined };
133
256
  }
134
- const result = await cwi.createAction(params);
135
- if (!result.txid) {
136
- return { error: "no-txid-returned" };
257
+ catch (error) {
258
+ return { error: error instanceof Error ? error.message : "unknown-error" };
137
259
  }
138
- return { txid: result.txid, rawtx: result.tx ? Utils.toHex(result.tx) : undefined };
139
- }
140
- catch (error) {
141
- return { error: error instanceof Error ? error.message : "unknown-error" };
142
- }
143
- }
260
+ },
261
+ };
144
262
  /**
145
263
  * List an ordinal for sale on the global orderbook.
146
264
  */
147
- export async function listOrdinal(cwi, request) {
148
- try {
149
- const params = await buildListOrdinal(cwi, request);
150
- if ("error" in params) {
151
- return params;
265
+ export const listOrdinal = {
266
+ meta: {
267
+ name: "listOrdinal",
268
+ description: "List an ordinal for sale on the global orderbook",
269
+ category: "ordinals",
270
+ inputSchema: {
271
+ type: "object",
272
+ properties: {
273
+ outpoint: { type: "string", description: "Outpoint of the ordinal to list" },
274
+ price: { type: "integer", description: "Price in satoshis" },
275
+ payAddress: { type: "string", description: "Address to receive payment on purchase" },
276
+ },
277
+ required: ["outpoint", "price", "payAddress"],
278
+ },
279
+ },
280
+ async execute(ctx, input) {
281
+ try {
282
+ const params = await buildListOrdinal(ctx, input);
283
+ if ("error" in params) {
284
+ return params;
285
+ }
286
+ const result = await ctx.wallet.createAction(params);
287
+ if (!result.txid) {
288
+ return { error: "no-txid-returned" };
289
+ }
290
+ return { txid: result.txid, rawtx: result.tx ? Utils.toHex(result.tx) : undefined };
152
291
  }
153
- const result = await cwi.createAction(params);
154
- if (!result.txid) {
155
- return { error: "no-txid-returned" };
292
+ catch (error) {
293
+ return { error: error instanceof Error ? error.message : "unknown-error" };
156
294
  }
157
- return { txid: result.txid, rawtx: result.tx ? Utils.toHex(result.tx) : undefined };
158
- }
159
- catch (error) {
160
- return { error: error instanceof Error ? error.message : "unknown-error" };
161
- }
162
- }
295
+ },
296
+ };
163
297
  /**
164
298
  * Cancel an ordinal listing.
165
- * Uses the origin tag to recover the keyID for signing.
166
- * Cancel unlock script: <sig> <pubkey> OP_1
167
299
  */
168
- export async function cancelListing(cwi, outpoint) {
169
- try {
170
- // Find the listing in our wallet
171
- const result = await cwi.listOutputs({
172
- basket: ORDINALS_BASKET,
173
- includeTags: true,
174
- include: "locking scripts",
175
- limit: 10000,
176
- });
177
- const listing = result.outputs.find((o) => o.outpoint === outpoint);
178
- if (!listing) {
179
- return { error: "listing-not-found" };
180
- }
181
- // Get the origin tag to recover the keyID
182
- const originTag = listing.tags?.find((t) => t.startsWith("origin:"));
183
- if (!originTag) {
184
- return { error: "missing-origin-tag" };
185
- }
186
- const originOutpoint = originTag.slice(7); // Remove "origin:" prefix
187
- // Derive the cancel address to get output destination
188
- const cancelAddress = await deriveCancelAddress(cwi, originOutpoint);
189
- // Create transaction with signAndProcess: false
190
- const createResult = await cwi.createAction({
191
- description: "Cancel ordinal listing",
192
- inputs: [{
193
- outpoint,
194
- inputDescription: "Listed ordinal",
195
- unlockingScriptLength: 108, // sig (73) + pubkey (34) + OP_1 (1)
196
- }],
197
- outputs: [{
198
- lockingScript: new P2PKH().lock(cancelAddress).toHex(),
199
- satoshis: 1,
200
- outputDescription: "Cancelled listing",
201
- basket: ORDINALS_BASKET,
202
- }],
203
- options: { signAndProcess: false },
204
- });
205
- if ("error" in createResult && createResult.error) {
206
- return { error: String(createResult.error) };
207
- }
208
- if (!createResult.signableTransaction) {
209
- return { error: "no-signable-transaction" };
210
- }
211
- // Parse transaction for signing
212
- const tx = Transaction.fromBEEF(createResult.signableTransaction.tx);
213
- const input = tx.inputs[0];
214
- const lockingScript = Script.fromHex(listing.lockingScript);
215
- // Build preimage for signature
216
- const sourceTXID = input.sourceTXID ?? input.sourceTransaction?.id("hex");
217
- if (!sourceTXID) {
218
- return { error: "missing-source-txid" };
219
- }
220
- const preimage = TransactionSignature.format({
221
- sourceTXID,
222
- sourceOutputIndex: input.sourceOutputIndex,
223
- sourceSatoshis: listing.satoshis,
224
- transactionVersion: tx.version,
225
- otherInputs: [],
226
- inputIndex: 0,
227
- outputs: tx.outputs,
228
- inputSequence: input.sequence ?? 0xffffffff,
229
- subscript: lockingScript,
230
- lockTime: tx.lockTime,
231
- scope: TransactionSignature.SIGHASH_ALL |
232
- TransactionSignature.SIGHASH_ANYONECANPAY |
233
- TransactionSignature.SIGHASH_FORKID,
234
- });
235
- // Hash preimage for signing
236
- const sighash = Hash.sha256(Hash.sha256(preimage));
237
- // Get signature via createSignature using origin outpoint as keyID
238
- const { signature } = await cwi.createSignature({
239
- protocolID: ORDINAL_LISTING_PROTOCOL,
240
- keyID: originOutpoint,
241
- counterparty: "self",
242
- hashToDirectlySign: Array.from(sighash),
243
- });
244
- // Get public key
245
- const { publicKey } = await cwi.getPublicKey({
246
- protocolID: ORDINAL_LISTING_PROTOCOL,
247
- keyID: originOutpoint,
248
- forSelf: true,
249
- });
250
- // Build cancel unlocking script: <sig> <pubkey> OP_1
251
- const unlockingScript = new UnlockingScript()
252
- .writeBin(signature)
253
- .writeBin(Utils.toArray(publicKey, "hex"))
254
- .writeOpCode(OP.OP_1);
255
- // Sign and broadcast
256
- const signResult = await cwi.signAction({
257
- reference: createResult.signableTransaction.reference,
258
- spends: {
259
- 0: { unlockingScript: unlockingScript.toHex() },
300
+ export const cancelListing = {
301
+ meta: {
302
+ name: "cancelListing",
303
+ description: "Cancel an ordinal listing and return the ordinal to the wallet",
304
+ category: "ordinals",
305
+ inputSchema: {
306
+ type: "object",
307
+ properties: {
308
+ outpoint: { type: "string", description: "Outpoint of the listing to cancel" },
260
309
  },
261
- });
262
- if ("error" in signResult) {
263
- return { error: String(signResult.error) };
310
+ required: ["outpoint"],
311
+ },
312
+ },
313
+ async execute(ctx, input) {
314
+ try {
315
+ const { outpoint } = input;
316
+ const result = await ctx.wallet.listOutputs({
317
+ basket: ORDINALS_BASKET,
318
+ includeTags: true,
319
+ includeCustomInstructions: true,
320
+ include: "locking scripts",
321
+ limit: 10000,
322
+ });
323
+ const listing = result.outputs.find((o) => o.outpoint === outpoint);
324
+ if (!listing) {
325
+ return { error: "listing-not-found" };
326
+ }
327
+ if (!listing.customInstructions) {
328
+ return { error: "missing-custom-instructions" };
329
+ }
330
+ const { protocolID, keyID } = JSON.parse(listing.customInstructions);
331
+ const typeTag = listing.tags?.find((t) => t.startsWith("type:"));
332
+ const originTag = listing.tags?.find((t) => t.startsWith("origin:"));
333
+ const cancelAddress = await deriveCancelAddressInternal(ctx, keyID);
334
+ const tags = [];
335
+ if (typeTag)
336
+ tags.push(typeTag);
337
+ if (originTag)
338
+ tags.push(originTag);
339
+ const createResult = await ctx.wallet.createAction({
340
+ description: "Cancel ordinal listing",
341
+ inputs: [
342
+ {
343
+ outpoint,
344
+ inputDescription: "Listed ordinal",
345
+ unlockingScriptLength: 108,
346
+ },
347
+ ],
348
+ outputs: [
349
+ {
350
+ lockingScript: new P2PKH().lock(cancelAddress).toHex(),
351
+ satoshis: 1,
352
+ outputDescription: "Cancelled listing",
353
+ basket: ORDINALS_BASKET,
354
+ tags,
355
+ customInstructions: JSON.stringify({ protocolID, keyID }),
356
+ },
357
+ ],
358
+ options: { signAndProcess: false },
359
+ });
360
+ if ("error" in createResult && createResult.error) {
361
+ return { error: String(createResult.error) };
362
+ }
363
+ if (!createResult.signableTransaction) {
364
+ return { error: "no-signable-transaction" };
365
+ }
366
+ const tx = Transaction.fromBEEF(createResult.signableTransaction.tx);
367
+ const txInput = tx.inputs[0];
368
+ const lockingScript = Script.fromHex(listing.lockingScript);
369
+ const sourceTXID = txInput.sourceTXID ?? txInput.sourceTransaction?.id("hex");
370
+ if (!sourceTXID) {
371
+ return { error: "missing-source-txid" };
372
+ }
373
+ const preimage = TransactionSignature.format({
374
+ sourceTXID,
375
+ sourceOutputIndex: txInput.sourceOutputIndex,
376
+ sourceSatoshis: listing.satoshis,
377
+ transactionVersion: tx.version,
378
+ otherInputs: [],
379
+ inputIndex: 0,
380
+ outputs: tx.outputs,
381
+ inputSequence: txInput.sequence ?? 0xffffffff,
382
+ subscript: lockingScript,
383
+ lockTime: tx.lockTime,
384
+ scope: TransactionSignature.SIGHASH_ALL |
385
+ TransactionSignature.SIGHASH_ANYONECANPAY |
386
+ TransactionSignature.SIGHASH_FORKID,
387
+ });
388
+ const sighash = Hash.sha256(Hash.sha256(preimage));
389
+ const { signature } = await ctx.wallet.createSignature({
390
+ protocolID,
391
+ keyID,
392
+ counterparty: "self",
393
+ hashToDirectlySign: Array.from(sighash),
394
+ });
395
+ const { publicKey } = await ctx.wallet.getPublicKey({
396
+ protocolID,
397
+ keyID,
398
+ forSelf: true,
399
+ });
400
+ const unlockingScript = new UnlockingScript()
401
+ .writeBin(signature)
402
+ .writeBin(Utils.toArray(publicKey, "hex"))
403
+ .writeOpCode(OP.OP_1);
404
+ const signResult = await ctx.wallet.signAction({
405
+ reference: createResult.signableTransaction.reference,
406
+ spends: {
407
+ 0: { unlockingScript: unlockingScript.toHex() },
408
+ },
409
+ });
410
+ if ("error" in signResult) {
411
+ return { error: String(signResult.error) };
412
+ }
413
+ return {
414
+ txid: signResult.txid,
415
+ rawtx: signResult.tx ? Utils.toHex(signResult.tx) : undefined,
416
+ };
264
417
  }
265
- return {
266
- txid: signResult.txid,
267
- rawtx: signResult.tx ? Utils.toHex(signResult.tx) : undefined,
268
- };
269
- }
270
- catch (error) {
271
- return { error: error instanceof Error ? error.message : "unknown-error" };
272
- }
273
- }
274
- /**
275
- * Build serialized transaction output (satoshis + script) for OrdLock unlock.
276
- */
277
- function buildSerializedOutput(satoshis, script) {
278
- const writer = new Utils.Writer();
279
- writer.writeUInt64LEBn(new BigNumber(satoshis));
280
- writer.writeVarIntNum(script.length);
281
- writer.write(script);
282
- return writer.toArray();
283
- }
284
- /**
285
- * Build OrdLock purchase unlocking script.
286
- * The purchase path requires no signature - just preimage and output data.
287
- */
288
- async function buildPurchaseUnlockingScript(tx, inputIndex, sourceSatoshis, lockingScript) {
289
- if (tx.outputs.length < 2) {
290
- throw new Error("Malformed transaction: requires at least 2 outputs");
291
- }
292
- const script = new UnlockingScript()
293
- .writeBin(buildSerializedOutput(tx.outputs[0].satoshis ?? 0, tx.outputs[0].lockingScript.toBinary()));
294
- if (tx.outputs.length > 2) {
295
- const writer = new Utils.Writer();
296
- for (const output of tx.outputs.slice(2)) {
297
- writer.write(buildSerializedOutput(output.satoshis ?? 0, output.lockingScript.toBinary()));
418
+ catch (error) {
419
+ return { error: error instanceof Error ? error.message : "unknown-error" };
298
420
  }
299
- script.writeBin(writer.toArray());
300
- }
301
- else {
302
- script.writeOpCode(OP.OP_0);
303
- }
304
- const input = tx.inputs[inputIndex];
305
- const sourceTXID = input.sourceTXID ?? input.sourceTransaction?.id("hex");
306
- if (!sourceTXID) {
307
- throw new Error("sourceTXID is required");
308
- }
309
- const preimage = TransactionSignature.format({
310
- sourceTXID,
311
- sourceOutputIndex: input.sourceOutputIndex,
312
- sourceSatoshis,
313
- transactionVersion: tx.version,
314
- otherInputs: [],
315
- inputIndex,
316
- outputs: tx.outputs,
317
- inputSequence: input.sequence ?? 0xffffffff,
318
- subscript: lockingScript,
319
- lockTime: tx.lockTime,
320
- scope: TransactionSignature.SIGHASH_ALL |
321
- TransactionSignature.SIGHASH_ANYONECANPAY |
322
- TransactionSignature.SIGHASH_FORKID
323
- });
324
- return script.writeBin(preimage).writeOpCode(OP.OP_0);
325
- }
421
+ },
422
+ };
326
423
  /**
327
424
  * Purchase an ordinal from the global orderbook.
328
- *
329
- * Flow:
330
- * 1. Fetch listing BEEF to get the locking script
331
- * 2. Decode OrdLock to get price and payout
332
- * 3. Build P2PKH output for buyer
333
- * 4. Build payment output for seller
334
- * 5. Build custom OrdLock purchase unlock (preimage only, no signature)
335
425
  */
336
- export async function purchaseOrdinal(cwi, request, services) {
337
- try {
338
- const { outpoint, marketplaceAddress, marketplaceRate } = request;
339
- if (!services) {
340
- return { error: "services-required-for-purchase" };
341
- }
342
- // Parse outpoint
343
- const parts = outpoint.split("_");
344
- if (parts.length !== 2) {
345
- return { error: "invalid-outpoint-format" };
346
- }
347
- const [txid, voutStr] = parts;
348
- const vout = parseInt(voutStr, 10);
349
- // Fetch listing BEEF to get the locking script
350
- const beef = await services.getBeefForTxid(txid);
351
- const listingBeefTx = beef.findTxid(txid);
352
- if (!listingBeefTx?.tx) {
353
- return { error: "listing-transaction-not-found" };
354
- }
355
- const listingOutput = listingBeefTx.tx.outputs[vout];
356
- if (!listingOutput) {
357
- return { error: "listing-output-not-found" };
358
- }
359
- // Decode OrdLock from listing script
360
- const ordLockData = OrdLock.decode(listingOutput.lockingScript);
361
- if (!ordLockData) {
362
- return { error: "not-an-ordlock-listing" };
363
- }
364
- // Derive our ordinal receive address
365
- const { publicKey } = await cwi.getPublicKey({
366
- protocolID: [1, "ordinals"],
367
- keyID: outpoint,
368
- counterparty: "self",
369
- forSelf: true,
370
- });
371
- const ourOrdAddress = PublicKey.fromString(publicKey).toAddress();
372
- // Build outputs
373
- const outputs = [];
374
- // Output 0: Ordinal to buyer (P2PKH)
375
- const p2pkh = new P2PKH();
376
- outputs.push({
377
- lockingScript: p2pkh.lock(ourOrdAddress).toHex(),
378
- satoshis: 1,
379
- outputDescription: "Purchased ordinal",
380
- basket: ORDINALS_BASKET,
381
- });
382
- // Output 1: Payment to seller (from payout in OrdLock)
383
- // Parse payout: 8-byte LE satoshis + varint script length + script
384
- const payoutReader = new Utils.Reader(ordLockData.payout);
385
- const payoutSatoshis = payoutReader.readUInt64LEBn().toNumber();
386
- const payoutScriptLen = payoutReader.readVarIntNum();
387
- const payoutScriptBin = payoutReader.read(payoutScriptLen);
388
- const payoutLockingScript = LockingScript.fromBinary(payoutScriptBin);
389
- outputs.push({
390
- lockingScript: payoutLockingScript.toHex(),
391
- satoshis: payoutSatoshis,
392
- outputDescription: "Payment to seller",
393
- });
394
- // Output 2+ (optional): Marketplace fee
395
- if (marketplaceAddress && marketplaceRate && marketplaceRate > 0) {
396
- const marketFee = Math.ceil(payoutSatoshis * marketplaceRate);
397
- if (marketFee > 0) {
398
- outputs.push({
399
- lockingScript: p2pkh.lock(marketplaceAddress).toHex(),
400
- satoshis: marketFee,
401
- outputDescription: "Marketplace fee",
402
- });
426
+ export const purchaseOrdinal = {
427
+ meta: {
428
+ name: "purchaseOrdinal",
429
+ description: "Purchase an ordinal from the global orderbook",
430
+ category: "ordinals",
431
+ requiresServices: true,
432
+ inputSchema: {
433
+ type: "object",
434
+ properties: {
435
+ outpoint: { type: "string", description: "Outpoint of the listing to purchase" },
436
+ marketplaceAddress: { type: "string", description: "Marketplace address for fees" },
437
+ marketplaceRate: { type: "number", description: "Marketplace fee rate (0-1)" },
438
+ contentType: { type: "string", description: "Content type (auto-detected if not provided)" },
439
+ origin: { type: "string", description: "Origin outpoint (auto-detected if not provided)" },
440
+ },
441
+ required: ["outpoint"],
442
+ },
443
+ },
444
+ async execute(ctx, input) {
445
+ try {
446
+ const { outpoint, marketplaceAddress, marketplaceRate } = input;
447
+ if (!ctx.services) {
448
+ return { error: "services-required-for-purchase" };
403
449
  }
450
+ const parts = outpoint.split("_");
451
+ if (parts.length !== 2) {
452
+ return { error: "invalid-outpoint-format" };
453
+ }
454
+ const [txid, voutStr] = parts;
455
+ const vout = Number.parseInt(voutStr, 10);
456
+ let { contentType, origin } = input;
457
+ if (!contentType || !origin) {
458
+ const metadata = await ctx.services.ordfs.getMetadata(outpoint);
459
+ contentType = contentType ?? metadata.contentType;
460
+ origin = origin ?? metadata.origin ?? outpoint;
461
+ }
462
+ const beef = await ctx.services.getBeefForTxid(txid);
463
+ const listingBeefTx = beef.findTxid(txid);
464
+ if (!listingBeefTx?.tx) {
465
+ return { error: "listing-transaction-not-found" };
466
+ }
467
+ const listingOutput = listingBeefTx.tx.outputs[vout];
468
+ if (!listingOutput) {
469
+ return { error: "listing-output-not-found" };
470
+ }
471
+ const ordLockData = OrdLock.decode(listingOutput.lockingScript);
472
+ if (!ordLockData) {
473
+ return { error: "not-an-ordlock-listing" };
474
+ }
475
+ const { publicKey } = await ctx.wallet.getPublicKey({
476
+ protocolID: ORDINAL_PROTOCOL,
477
+ keyID: outpoint,
478
+ counterparty: "self",
479
+ forSelf: true,
480
+ });
481
+ const ourOrdAddress = PublicKey.fromString(publicKey).toAddress();
482
+ const outputs = [];
483
+ const p2pkh = new P2PKH();
484
+ outputs.push({
485
+ lockingScript: p2pkh.lock(ourOrdAddress).toHex(),
486
+ satoshis: 1,
487
+ outputDescription: "Purchased ordinal",
488
+ basket: ORDINALS_BASKET,
489
+ tags: [`type:${contentType}`, `origin:${origin}`],
490
+ customInstructions: JSON.stringify({
491
+ protocolID: ORDINAL_PROTOCOL,
492
+ keyID: outpoint,
493
+ }),
494
+ });
495
+ const payoutReader = new Utils.Reader(ordLockData.payout);
496
+ const payoutSatoshis = payoutReader.readUInt64LEBn().toNumber();
497
+ const payoutScriptLen = payoutReader.readVarIntNum();
498
+ const payoutScriptBin = payoutReader.read(payoutScriptLen);
499
+ const payoutLockingScript = LockingScript.fromBinary(payoutScriptBin);
500
+ outputs.push({
501
+ lockingScript: payoutLockingScript.toHex(),
502
+ satoshis: payoutSatoshis,
503
+ outputDescription: "Payment to seller",
504
+ });
505
+ if (marketplaceAddress && marketplaceRate && marketplaceRate > 0) {
506
+ const marketFee = Math.ceil(payoutSatoshis * marketplaceRate);
507
+ if (marketFee > 0) {
508
+ outputs.push({
509
+ lockingScript: p2pkh.lock(marketplaceAddress).toHex(),
510
+ satoshis: marketFee,
511
+ outputDescription: "Marketplace fee",
512
+ });
513
+ }
514
+ }
515
+ const createResult = await ctx.wallet.createAction({
516
+ description: `Purchase ordinal for ${payoutSatoshis} sats`,
517
+ inputBEEF: beef.toBinary(),
518
+ inputs: [
519
+ {
520
+ outpoint,
521
+ inputDescription: "Listed ordinal",
522
+ unlockingScriptLength: 500,
523
+ },
524
+ ],
525
+ outputs,
526
+ options: { signAndProcess: false },
527
+ });
528
+ if ("error" in createResult && createResult.error) {
529
+ return { error: String(createResult.error) };
530
+ }
531
+ if (!createResult.signableTransaction) {
532
+ return { error: "no-signable-transaction" };
533
+ }
534
+ const tx = Transaction.fromBEEF(createResult.signableTransaction.tx);
535
+ const unlockingScript = await buildPurchaseUnlockingScript(tx, 0, listingOutput.satoshis ?? 1, listingOutput.lockingScript);
536
+ const signResult = await ctx.wallet.signAction({
537
+ reference: createResult.signableTransaction.reference,
538
+ spends: {
539
+ 0: { unlockingScript: unlockingScript.toHex() },
540
+ },
541
+ });
542
+ if ("error" in signResult) {
543
+ return { error: String(signResult.error) };
544
+ }
545
+ return {
546
+ txid: signResult.txid,
547
+ rawtx: signResult.tx ? Utils.toHex(signResult.tx) : undefined,
548
+ };
404
549
  }
405
- // Create the transaction with signAndProcess: false
406
- // The listing input needs custom unlocking script
407
- const createResult = await cwi.createAction({
408
- description: `Purchase ordinal for ${payoutSatoshis} sats`,
409
- inputBEEF: beef.toBinary(),
410
- inputs: [{
411
- outpoint,
412
- inputDescription: "Listed ordinal",
413
- unlockingScriptLength: 500, // Estimate for purchase unlock (preimage + outputs)
414
- }],
415
- outputs,
416
- options: { signAndProcess: false },
417
- });
418
- if ("error" in createResult && createResult.error) {
419
- return { error: String(createResult.error) };
420
- }
421
- if (!createResult.signableTransaction) {
422
- return { error: "no-signable-transaction" };
423
- }
424
- // Parse the transaction to build purchase unlock
425
- const tx = Transaction.fromBEEF(createResult.signableTransaction.tx);
426
- // Build purchase unlocking script
427
- const unlockingScript = await buildPurchaseUnlockingScript(tx, 0, listingOutput.satoshis ?? 1, listingOutput.lockingScript);
428
- // Sign and broadcast
429
- const signResult = await cwi.signAction({
430
- reference: createResult.signableTransaction.reference,
431
- spends: {
432
- 0: { unlockingScript: unlockingScript.toHex() },
433
- },
434
- });
435
- if ("error" in signResult) {
436
- return { error: String(signResult.error) };
550
+ catch (error) {
551
+ return { error: error instanceof Error ? error.message : "unknown-error" };
437
552
  }
438
- return {
439
- txid: signResult.txid,
440
- rawtx: signResult.tx ? Utils.toHex(signResult.tx) : undefined,
441
- };
442
- }
443
- catch (error) {
444
- return { error: error instanceof Error ? error.message : "unknown-error" };
445
- }
446
- }
553
+ },
554
+ };
555
+ // ============================================================================
556
+ // Module exports
557
+ // ============================================================================
558
+ /** All ordinals skills for registry */
559
+ export const ordinalsSkills = [
560
+ listOrdinals,
561
+ deriveCancelAddress,
562
+ transferOrdinal,
563
+ listOrdinal,
564
+ cancelListing,
565
+ purchaseOrdinal,
566
+ ];