@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,229 +1,18 @@
1
1
  /**
2
2
  * Tokens Module
3
3
  *
4
- * Functions for managing BSV21 tokens.
4
+ * Skills for managing BSV21 tokens.
5
5
  */
6
6
  import { BigNumber, LockingScript, OP, P2PKH, PublicKey, Transaction, TransactionSignature, UnlockingScript, Utils, } from "@bsv/sdk";
7
7
  import { BSV21, OrdLock } from "@bopen-io/templates";
8
8
  import { BSV21_BASKET } from "../constants";
9
- /**
10
- * List BSV21 token outputs from the bsv21 basket.
11
- * Returns WalletOutput[] directly - use tags for metadata.
12
- */
13
- export async function listTokens(cwi, limit = 10000) {
14
- const result = await cwi.listOutputs({
15
- basket: BSV21_BASKET,
16
- includeTags: true,
17
- limit,
18
- });
19
- return result.outputs;
20
- }
21
- /**
22
- * Get aggregated BSV21 token balances.
23
- * Groups outputs by token ID and sums amounts.
24
- */
25
- export async function getBsv21Balances(cwi) {
26
- const outputs = await listTokens(cwi);
27
- const balanceMap = new Map();
28
- for (const o of outputs) {
29
- const idTag = o.tags?.find((t) => t.startsWith("id:"));
30
- const amtTag = o.tags?.find((t) => t.startsWith("amt:"))?.slice(4);
31
- if (!idTag || !amtTag)
32
- continue;
33
- const idContent = idTag.slice(3);
34
- const lastColonIdx = idContent.lastIndexOf(":");
35
- if (lastColonIdx === -1)
36
- continue;
37
- const tokenId = idContent.slice(0, lastColonIdx);
38
- const status = idContent.slice(lastColonIdx + 1);
39
- if (status === "invalid")
40
- continue;
41
- const isConfirmed = status === "valid";
42
- const amt = BigInt(amtTag);
43
- const dec = parseInt(o.tags?.find((t) => t.startsWith("dec:"))?.slice(4) || "0", 10);
44
- const symTag = o.tags?.find((t) => t.startsWith("sym:"))?.slice(4);
45
- const iconTag = o.tags?.find((t) => t.startsWith("icon:"))?.slice(5);
46
- const existing = balanceMap.get(tokenId);
47
- if (existing) {
48
- if (isConfirmed)
49
- existing.confirmed += amt;
50
- else
51
- existing.pending += amt;
52
- }
53
- else {
54
- balanceMap.set(tokenId, {
55
- id: tokenId,
56
- confirmed: isConfirmed ? amt : 0n,
57
- pending: isConfirmed ? 0n : amt,
58
- sym: symTag,
59
- icon: iconTag,
60
- dec,
61
- });
62
- }
63
- }
64
- return Array.from(balanceMap.values()).map((b) => ({
65
- p: "bsv-20",
66
- op: "transfer",
67
- dec: b.dec,
68
- amt: (b.confirmed + b.pending).toString(),
69
- id: b.id,
70
- sym: b.sym,
71
- icon: b.icon,
72
- all: { confirmed: b.confirmed, pending: b.pending },
73
- listed: { confirmed: 0n, pending: 0n },
74
- }));
75
- }
76
- /**
77
- * Send BSV21 tokens to an address.
78
- *
79
- * Flow:
80
- * 1. Get token UTXOs from basket
81
- * 2. Validate each with BSV21 service
82
- * 3. Select UTXOs until we have enough tokens
83
- * 4. Build transfer inscription outputs
84
- * 5. Create and sign transaction
85
- */
86
- export async function sendBsv21(cwi, request, services) {
87
- try {
88
- const { tokenId, address, amount: rawAmount } = request;
89
- const amount = typeof rawAmount === "string" ? BigInt(rawAmount) : rawAmount;
90
- // Validate amount
91
- if (amount <= 0n) {
92
- return { error: "amount-must-be-positive" };
93
- }
94
- // Validate token ID format (txid_vout)
95
- const parts = tokenId.split("_");
96
- if (parts.length !== 2 || parts[0].length !== 64 || !/^\d+$/.test(parts[1])) {
97
- return { error: "invalid-token-id-format" };
98
- }
99
- // Get token UTXOs from basket
100
- const result = await cwi.listOutputs({
101
- basket: BSV21_BASKET,
102
- includeTags: true,
103
- include: "locking scripts",
104
- limit: 10000,
105
- });
106
- // Filter UTXOs matching tokenId (exclude invalid)
107
- const tokenUtxos = result.outputs.filter((o) => {
108
- const idTag = o.tags?.find((t) => t.startsWith("id:"));
109
- if (!idTag)
110
- return false;
111
- const idContent = idTag.slice(3); // Remove "id:" prefix
112
- const lastColonIdx = idContent.lastIndexOf(":");
113
- if (lastColonIdx === -1)
114
- return false;
115
- const id = idContent.slice(0, lastColonIdx);
116
- const status = idContent.slice(lastColonIdx + 1);
117
- return id === tokenId && status !== "invalid";
118
- });
119
- if (tokenUtxos.length === 0) {
120
- return { error: "no-token-utxos-found" };
121
- }
122
- // Validate UTXOs with BSV21 service and select valid ones
123
- const selected = [];
124
- let totalIn = 0n;
125
- for (const utxo of tokenUtxos) {
126
- if (totalIn >= amount)
127
- break;
128
- // Get amount from tag
129
- const amtTag = utxo.tags?.find((t) => t.startsWith("amt:"));
130
- if (!amtTag)
131
- continue;
132
- const utxoAmount = BigInt(amtTag.slice(4));
133
- // Validate with BSV21 service if available
134
- if (services?.bsv21) {
135
- try {
136
- const [txid] = utxo.outpoint.split("_");
137
- const validation = await services.bsv21.getTokenByTxid(tokenId, txid);
138
- // If the output is returned and exists in outputs, it's valid
139
- const outputData = validation.outputs.find((o) => `${validation.txid}_${o.vout}` === utxo.outpoint);
140
- if (!outputData) {
141
- continue; // Skip if not found in response
142
- }
143
- }
144
- catch {
145
- // If 404 or any error, the overlay doesn't know about it - skip
146
- continue;
147
- }
148
- }
149
- selected.push(utxo);
150
- totalIn += utxoAmount;
151
- }
152
- if (totalIn < amount) {
153
- return { error: "insufficient-validated-tokens" };
154
- }
155
- // Build outputs
156
- const outputs = [];
157
- // Output 1: Token transfer to destination
158
- const p2pkh = new P2PKH();
159
- const destinationLockingScript = p2pkh.lock(address);
160
- const transferScript = BSV21.transfer(tokenId, amount).lock(destinationLockingScript);
161
- outputs.push({
162
- lockingScript: transferScript.toHex(),
163
- satoshis: 1,
164
- outputDescription: `Send ${amount} tokens`,
165
- });
166
- // Output 2: Token change (if any)
167
- const change = totalIn - amount;
168
- if (change > 0n) {
169
- // Derive change address
170
- const keyID = `${tokenId}-change-${Date.now()}`;
171
- const { publicKey } = await cwi.getPublicKey({
172
- protocolID: [1, "bsv21"],
173
- keyID,
174
- counterparty: "self",
175
- forSelf: true,
176
- });
177
- const changeAddress = PublicKey.fromString(publicKey).toAddress();
178
- const changeLockingScript = p2pkh.lock(changeAddress);
179
- const changeScript = BSV21.transfer(tokenId, change).lock(changeLockingScript);
180
- outputs.push({
181
- lockingScript: changeScript.toHex(),
182
- satoshis: 1,
183
- outputDescription: "Token change",
184
- basket: BSV21_BASKET,
185
- tags: [`id:${tokenId}:pending`, `amt:${change}`],
186
- });
187
- }
188
- // Get token symbol for description
189
- const symTag = tokenUtxos[0]?.tags?.find((t) => t.startsWith("sym:"));
190
- const symbol = symTag ? symTag.slice(4) : tokenId.slice(0, 8);
191
- // Create the transaction
192
- const createResult = await cwi.createAction({
193
- description: `Send ${amount} ${symbol}`,
194
- inputs: selected.map((o) => ({
195
- outpoint: o.outpoint,
196
- inputDescription: "Token input",
197
- })),
198
- outputs,
199
- options: { signAndProcess: false },
200
- });
201
- if ("error" in createResult && createResult.error) {
202
- return { error: String(createResult.error) };
203
- }
204
- if (!createResult.signableTransaction) {
205
- return { error: "no-signable-transaction" };
206
- }
207
- // Sign and broadcast - all inputs are P2PKH, wallet handles signing
208
- const signResult = await cwi.signAction({
209
- reference: createResult.signableTransaction.reference,
210
- spends: {},
211
- });
212
- if ("error" in signResult) {
213
- return { error: String(signResult.error) };
214
- }
215
- return {
216
- txid: signResult.txid,
217
- rawtx: signResult.tx ? Buffer.from(signResult.tx).toString("hex") : undefined,
218
- };
219
- }
220
- catch (error) {
221
- return { error: error instanceof Error ? error.message : "unknown-error" };
222
- }
223
- }
224
- /**
225
- * Build serialized transaction output (satoshis + script) for OrdLock unlock.
226
- */
9
+ // ============================================================================
10
+ // Constants
11
+ // ============================================================================
12
+ const BSV21_PROTOCOL = [1, "bsv21"];
13
+ // ============================================================================
14
+ // Internal helpers
15
+ // ============================================================================
227
16
  function buildSerializedOutput(satoshis, script) {
228
17
  const writer = new Utils.Writer();
229
18
  writer.writeUInt64LEBn(new BigNumber(satoshis));
@@ -231,16 +20,11 @@ function buildSerializedOutput(satoshis, script) {
231
20
  writer.write(script);
232
21
  return writer.toArray();
233
22
  }
234
- /**
235
- * Build OrdLock purchase unlocking script.
236
- * The purchase path requires no signature - just preimage and output data.
237
- */
238
23
  async function buildPurchaseUnlockingScript(tx, inputIndex, sourceSatoshis, lockingScript) {
239
24
  if (tx.outputs.length < 2) {
240
25
  throw new Error("Malformed transaction: requires at least 2 outputs");
241
26
  }
242
- const script = new UnlockingScript()
243
- .writeBin(buildSerializedOutput(tx.outputs[0].satoshis ?? 0, tx.outputs[0].lockingScript.toBinary()));
27
+ const script = new UnlockingScript().writeBin(buildSerializedOutput(tx.outputs[0].satoshis ?? 0, tx.outputs[0].lockingScript.toBinary()));
244
28
  if (tx.outputs.length > 2) {
245
29
  const writer = new Utils.Writer();
246
30
  for (const output of tx.outputs.slice(2)) {
@@ -269,132 +53,405 @@ async function buildPurchaseUnlockingScript(tx, inputIndex, sourceSatoshis, lock
269
53
  lockTime: tx.lockTime,
270
54
  scope: TransactionSignature.SIGHASH_ALL |
271
55
  TransactionSignature.SIGHASH_ANYONECANPAY |
272
- TransactionSignature.SIGHASH_FORKID
56
+ TransactionSignature.SIGHASH_FORKID,
273
57
  });
274
58
  return script.writeBin(preimage).writeOpCode(OP.OP_0);
275
59
  }
60
+ async function listTokensInternal(ctx, limit = 10000) {
61
+ const result = await ctx.wallet.listOutputs({
62
+ basket: BSV21_BASKET,
63
+ includeTags: true,
64
+ limit,
65
+ });
66
+ return result.outputs;
67
+ }
276
68
  /**
277
- * Purchase BSV21 tokens from marketplace.
278
- *
279
- * Flow:
280
- * 1. Fetch listing BEEF to get the locking script
281
- * 2. Decode OrdLock to get price and payout
282
- * 3. Build BSV21 transfer output for buyer
283
- * 4. Build payment output for seller
284
- * 5. Build custom OrdLock purchase unlock (preimage only, no signature)
69
+ * List BSV21 token outputs from the wallet.
285
70
  */
286
- export async function purchaseBsv21(cwi, request, services) {
287
- try {
288
- const { tokenId, outpoint, amount: rawAmount, marketplaceAddress, marketplaceRate } = request;
289
- const tokenAmount = typeof rawAmount === "string" ? BigInt(rawAmount) : rawAmount;
290
- if (!services) {
291
- return { error: "services-required-for-purchase" };
292
- }
293
- // Parse outpoint
294
- const parts = outpoint.split("_");
295
- if (parts.length !== 2) {
296
- return { error: "invalid-outpoint-format" };
297
- }
298
- const [txid, voutStr] = parts;
299
- const vout = parseInt(voutStr, 10);
300
- // Fetch listing BEEF to get the locking script
301
- const beef = await services.getBeefForTxid(txid);
302
- const listingBeefTx = beef.findTxid(txid);
303
- if (!listingBeefTx?.tx) {
304
- return { error: "listing-transaction-not-found" };
305
- }
306
- const listingOutput = listingBeefTx.tx.outputs[vout];
307
- if (!listingOutput) {
308
- return { error: "listing-output-not-found" };
309
- }
310
- // Decode OrdLock from listing script
311
- const ordLockData = OrdLock.decode(listingOutput.lockingScript);
312
- if (!ordLockData) {
313
- return { error: "not-an-ordlock-listing" };
71
+ export const listTokens = {
72
+ meta: {
73
+ name: "listTokens",
74
+ description: "List BSV21 token outputs from the wallet",
75
+ category: "tokens",
76
+ inputSchema: {
77
+ type: "object",
78
+ properties: {
79
+ limit: { type: "integer", description: "Max number of tokens to return (default: 10000)" },
80
+ },
81
+ },
82
+ },
83
+ async execute(ctx, input) {
84
+ return listTokensInternal(ctx, input.limit);
85
+ },
86
+ };
87
+ /**
88
+ * Get aggregated BSV21 token balances.
89
+ */
90
+ export const getBsv21Balances = {
91
+ meta: {
92
+ name: "getBsv21Balances",
93
+ description: "Get aggregated BSV21 token balances grouped by token ID",
94
+ category: "tokens",
95
+ inputSchema: {
96
+ type: "object",
97
+ properties: {},
98
+ },
99
+ },
100
+ async execute(ctx) {
101
+ const outputs = await listTokensInternal(ctx);
102
+ const balanceMap = new Map();
103
+ for (const o of outputs) {
104
+ const idTag = o.tags?.find((t) => t.startsWith("id:"));
105
+ const amtTag = o.tags?.find((t) => t.startsWith("amt:"))?.slice(4);
106
+ if (!idTag || !amtTag)
107
+ continue;
108
+ const idContent = idTag.slice(3);
109
+ const lastColonIdx = idContent.lastIndexOf(":");
110
+ if (lastColonIdx === -1)
111
+ continue;
112
+ const tokenId = idContent.slice(0, lastColonIdx);
113
+ const status = idContent.slice(lastColonIdx + 1);
114
+ if (status === "invalid")
115
+ continue;
116
+ const isConfirmed = status === "valid";
117
+ const amt = BigInt(amtTag);
118
+ const dec = Number.parseInt(o.tags?.find((t) => t.startsWith("dec:"))?.slice(4) || "0", 10);
119
+ const symTag = o.tags?.find((t) => t.startsWith("sym:"))?.slice(4);
120
+ const iconTag = o.tags?.find((t) => t.startsWith("icon:"))?.slice(5);
121
+ const existing = balanceMap.get(tokenId);
122
+ if (existing) {
123
+ if (isConfirmed)
124
+ existing.confirmed += amt;
125
+ else
126
+ existing.pending += amt;
127
+ }
128
+ else {
129
+ balanceMap.set(tokenId, {
130
+ id: tokenId,
131
+ confirmed: isConfirmed ? amt : 0n,
132
+ pending: isConfirmed ? 0n : amt,
133
+ sym: symTag,
134
+ icon: iconTag,
135
+ dec,
136
+ });
137
+ }
314
138
  }
315
- // Derive our token receive address
316
- const { publicKey } = await cwi.getPublicKey({
317
- protocolID: [1, "bsv21"],
318
- keyID: `${tokenId}-${outpoint}`,
319
- counterparty: "self",
320
- forSelf: true,
321
- });
322
- const ourTokenAddress = PublicKey.fromString(publicKey).toAddress();
323
- // Build outputs
324
- const outputs = [];
325
- // Output 0: Token transfer to buyer (BSV21 inscription + P2PKH)
326
- const p2pkh = new P2PKH();
327
- const buyerLockingScript = p2pkh.lock(ourTokenAddress);
328
- const transferScript = BSV21.transfer(tokenId, tokenAmount).lock(buyerLockingScript);
329
- outputs.push({
330
- lockingScript: transferScript.toHex(),
331
- satoshis: 1,
332
- outputDescription: "Purchased tokens",
333
- basket: BSV21_BASKET,
334
- tags: [`id:${tokenId}:pending`, `amt:${tokenAmount}`],
335
- });
336
- // Output 1: Payment to seller (from payout in OrdLock)
337
- // Parse payout: 8-byte LE satoshis + varint script length + script
338
- const payoutReader = new Utils.Reader(ordLockData.payout);
339
- const payoutSatoshis = payoutReader.readUInt64LEBn().toNumber();
340
- const payoutScriptLen = payoutReader.readVarIntNum();
341
- const payoutScriptBin = payoutReader.read(payoutScriptLen);
342
- const payoutLockingScript = LockingScript.fromBinary(payoutScriptBin);
343
- outputs.push({
344
- lockingScript: payoutLockingScript.toHex(),
345
- satoshis: payoutSatoshis,
346
- outputDescription: "Payment to seller",
347
- });
348
- // Output 2+ (optional): Marketplace fee
349
- if (marketplaceAddress && marketplaceRate && marketplaceRate > 0) {
350
- const marketFee = Math.ceil(payoutSatoshis * marketplaceRate);
351
- if (marketFee > 0) {
139
+ return Array.from(balanceMap.values()).map((b) => ({
140
+ p: "bsv-20",
141
+ op: "transfer",
142
+ dec: b.dec,
143
+ amt: (b.confirmed + b.pending).toString(),
144
+ id: b.id,
145
+ sym: b.sym,
146
+ icon: b.icon,
147
+ all: { confirmed: b.confirmed, pending: b.pending },
148
+ listed: { confirmed: 0n, pending: 0n },
149
+ }));
150
+ },
151
+ };
152
+ /**
153
+ * Send BSV21 tokens to an address.
154
+ */
155
+ export const sendBsv21 = {
156
+ meta: {
157
+ name: "sendBsv21",
158
+ description: "Send BSV21 tokens to a counterparty, address, or paymail",
159
+ category: "tokens",
160
+ inputSchema: {
161
+ type: "object",
162
+ properties: {
163
+ tokenId: { type: "string", description: "Token ID (txid_vout format)" },
164
+ amount: { type: "string", description: "Amount to send (as string for bigint)" },
165
+ counterparty: { type: "string", description: "Recipient identity public key (hex)" },
166
+ address: { type: "string", description: "Recipient P2PKH address" },
167
+ paymail: { type: "string", description: "Recipient paymail address" },
168
+ },
169
+ required: ["tokenId", "amount"],
170
+ },
171
+ },
172
+ async execute(ctx, input) {
173
+ try {
174
+ const { tokenId, counterparty, address, paymail, amount: rawAmount } = input;
175
+ const amount = typeof rawAmount === "string" ? BigInt(rawAmount) : rawAmount;
176
+ if (!counterparty && !address && !paymail) {
177
+ return { error: "must-provide-counterparty-address-or-paymail" };
178
+ }
179
+ if (amount <= 0n) {
180
+ return { error: "amount-must-be-positive" };
181
+ }
182
+ const parts = tokenId.split("_");
183
+ if (parts.length !== 2 || parts[0].length !== 64 || !/^\d+$/.test(parts[1])) {
184
+ return { error: "invalid-token-id-format" };
185
+ }
186
+ const result = await ctx.wallet.listOutputs({
187
+ basket: BSV21_BASKET,
188
+ includeTags: true,
189
+ include: "locking scripts",
190
+ limit: 10000,
191
+ });
192
+ const tokenUtxos = result.outputs.filter((o) => {
193
+ const idTag = o.tags?.find((t) => t.startsWith("id:"));
194
+ if (!idTag)
195
+ return false;
196
+ const idContent = idTag.slice(3);
197
+ const lastColonIdx = idContent.lastIndexOf(":");
198
+ if (lastColonIdx === -1)
199
+ return false;
200
+ const id = idContent.slice(0, lastColonIdx);
201
+ const status = idContent.slice(lastColonIdx + 1);
202
+ return id === tokenId && status !== "invalid";
203
+ });
204
+ if (tokenUtxos.length === 0) {
205
+ return { error: "no-token-utxos-found" };
206
+ }
207
+ const selected = [];
208
+ let totalIn = 0n;
209
+ for (const utxo of tokenUtxos) {
210
+ if (totalIn >= amount)
211
+ break;
212
+ const amtTag = utxo.tags?.find((t) => t.startsWith("amt:"));
213
+ if (!amtTag)
214
+ continue;
215
+ const utxoAmount = BigInt(amtTag.slice(4));
216
+ if (ctx.services?.bsv21) {
217
+ try {
218
+ const [txid] = utxo.outpoint.split("_");
219
+ const validation = await ctx.services.bsv21.getTokenByTxid(tokenId, txid);
220
+ const outputData = validation.outputs.find((o) => `${validation.txid}_${o.vout}` === utxo.outpoint);
221
+ if (!outputData)
222
+ continue;
223
+ }
224
+ catch {
225
+ continue;
226
+ }
227
+ }
228
+ selected.push(utxo);
229
+ totalIn += utxoAmount;
230
+ }
231
+ if (totalIn < amount) {
232
+ return { error: "insufficient-validated-tokens" };
233
+ }
234
+ let recipientAddress;
235
+ if (counterparty) {
236
+ const { publicKey } = await ctx.wallet.getPublicKey({
237
+ protocolID: BSV21_PROTOCOL,
238
+ keyID: `${tokenId}-${Date.now()}`,
239
+ counterparty,
240
+ forSelf: false,
241
+ });
242
+ recipientAddress = PublicKey.fromString(publicKey).toAddress();
243
+ }
244
+ else if (paymail) {
245
+ return { error: "paymail-not-yet-implemented" };
246
+ }
247
+ else {
248
+ recipientAddress = address;
249
+ }
250
+ const outputs = [];
251
+ const p2pkh = new P2PKH();
252
+ const destinationLockingScript = p2pkh.lock(recipientAddress);
253
+ const transferScript = BSV21.transfer(tokenId, amount).lock(destinationLockingScript);
254
+ outputs.push({
255
+ lockingScript: transferScript.toHex(),
256
+ satoshis: 1,
257
+ outputDescription: `Send ${amount} tokens`,
258
+ });
259
+ const change = totalIn - amount;
260
+ if (change > 0n) {
261
+ const changeKeyID = `${tokenId}-${Date.now()}`;
262
+ const { publicKey } = await ctx.wallet.getPublicKey({
263
+ protocolID: BSV21_PROTOCOL,
264
+ keyID: changeKeyID,
265
+ counterparty: "self",
266
+ forSelf: true,
267
+ });
268
+ const changeAddress = PublicKey.fromString(publicKey).toAddress();
269
+ const changeLockingScript = p2pkh.lock(changeAddress);
270
+ const changeScript = BSV21.transfer(tokenId, change).lock(changeLockingScript);
352
271
  outputs.push({
353
- lockingScript: p2pkh.lock(marketplaceAddress).toHex(),
354
- satoshis: marketFee,
355
- outputDescription: "Marketplace fee",
272
+ lockingScript: changeScript.toHex(),
273
+ satoshis: 1,
274
+ outputDescription: "Token change",
275
+ basket: BSV21_BASKET,
276
+ tags: [`id:${tokenId}`, `amt:${change}`],
277
+ customInstructions: JSON.stringify({
278
+ protocolID: BSV21_PROTOCOL,
279
+ keyID: changeKeyID,
280
+ }),
356
281
  });
357
282
  }
283
+ const symTag = tokenUtxos[0]?.tags?.find((t) => t.startsWith("sym:"));
284
+ const symbol = symTag ? symTag.slice(4) : tokenId.slice(0, 8);
285
+ const createResult = await ctx.wallet.createAction({
286
+ description: `Send ${amount} ${symbol}`,
287
+ inputs: selected.map((o) => ({
288
+ outpoint: o.outpoint,
289
+ inputDescription: "Token input",
290
+ })),
291
+ outputs,
292
+ options: { signAndProcess: false },
293
+ });
294
+ if ("error" in createResult && createResult.error) {
295
+ return { error: String(createResult.error) };
296
+ }
297
+ if (!createResult.signableTransaction) {
298
+ return { error: "no-signable-transaction" };
299
+ }
300
+ const signResult = await ctx.wallet.signAction({
301
+ reference: createResult.signableTransaction.reference,
302
+ spends: {},
303
+ });
304
+ if ("error" in signResult) {
305
+ return { error: String(signResult.error) };
306
+ }
307
+ return {
308
+ txid: signResult.txid,
309
+ rawtx: signResult.tx ? Utils.toHex(signResult.tx) : undefined,
310
+ };
358
311
  }
359
- // Create the transaction with signAndProcess: false
360
- // The listing input needs custom unlocking script
361
- const createResult = await cwi.createAction({
362
- description: `Purchase ${tokenAmount} tokens for ${payoutSatoshis} sats`,
363
- inputBEEF: beef.toBinary(),
364
- inputs: [{
365
- outpoint,
366
- inputDescription: "Listed token",
367
- unlockingScriptLength: 500, // Estimate for purchase unlock (preimage + outputs)
368
- }],
369
- outputs,
370
- options: { signAndProcess: false },
371
- });
372
- if ("error" in createResult && createResult.error) {
373
- return { error: String(createResult.error) };
374
- }
375
- if (!createResult.signableTransaction) {
376
- return { error: "no-signable-transaction" };
312
+ catch (error) {
313
+ return { error: error instanceof Error ? error.message : "unknown-error" };
377
314
  }
378
- // Parse the transaction to build purchase unlock
379
- const tx = Transaction.fromBEEF(createResult.signableTransaction.tx);
380
- // Build purchase unlocking script
381
- const unlockingScript = await buildPurchaseUnlockingScript(tx, 0, listingOutput.satoshis ?? 1, listingOutput.lockingScript);
382
- // Sign and broadcast
383
- const signResult = await cwi.signAction({
384
- reference: createResult.signableTransaction.reference,
385
- spends: {
386
- 0: { unlockingScript: unlockingScript.toHex() },
315
+ },
316
+ };
317
+ /**
318
+ * Purchase BSV21 tokens from marketplace.
319
+ */
320
+ export const purchaseBsv21 = {
321
+ meta: {
322
+ name: "purchaseBsv21",
323
+ description: "Purchase BSV21 tokens from the marketplace",
324
+ category: "tokens",
325
+ requiresServices: true,
326
+ inputSchema: {
327
+ type: "object",
328
+ properties: {
329
+ tokenId: { type: "string", description: "Token ID (txid_vout format)" },
330
+ outpoint: { type: "string", description: "Outpoint of the listed token UTXO" },
331
+ amount: { type: "string", description: "Amount of tokens in the listing (as string)" },
332
+ marketplaceAddress: { type: "string", description: "Marketplace fee address" },
333
+ marketplaceRate: { type: "number", description: "Marketplace fee rate (0-1)" },
387
334
  },
388
- });
389
- if ("error" in signResult) {
390
- return { error: String(signResult.error) };
335
+ required: ["tokenId", "outpoint", "amount"],
336
+ },
337
+ },
338
+ async execute(ctx, input) {
339
+ try {
340
+ const { tokenId, outpoint, amount: rawAmount, marketplaceAddress, marketplaceRate } = input;
341
+ const tokenAmount = typeof rawAmount === "string" ? BigInt(rawAmount) : rawAmount;
342
+ if (!ctx.services) {
343
+ return { error: "services-required-for-purchase" };
344
+ }
345
+ const parts = outpoint.split("_");
346
+ if (parts.length !== 2) {
347
+ return { error: "invalid-outpoint-format" };
348
+ }
349
+ const [txid, voutStr] = parts;
350
+ const vout = Number.parseInt(voutStr, 10);
351
+ try {
352
+ await ctx.services.bsv21.getTokenByTxid(tokenId, txid);
353
+ }
354
+ catch {
355
+ return { error: "listing-not-found-in-overlay" };
356
+ }
357
+ const beef = await ctx.services.getBeefForTxid(txid);
358
+ const listingBeefTx = beef.findTxid(txid);
359
+ if (!listingBeefTx?.tx) {
360
+ return { error: "listing-transaction-not-found" };
361
+ }
362
+ const listingOutput = listingBeefTx.tx.outputs[vout];
363
+ if (!listingOutput) {
364
+ return { error: "listing-output-not-found" };
365
+ }
366
+ const ordLockData = OrdLock.decode(listingOutput.lockingScript);
367
+ if (!ordLockData) {
368
+ return { error: "not-an-ordlock-listing" };
369
+ }
370
+ const bsv21KeyID = `${tokenId}-${outpoint}`;
371
+ const { publicKey } = await ctx.wallet.getPublicKey({
372
+ protocolID: BSV21_PROTOCOL,
373
+ keyID: bsv21KeyID,
374
+ counterparty: "self",
375
+ forSelf: true,
376
+ });
377
+ const ourTokenAddress = PublicKey.fromString(publicKey).toAddress();
378
+ const outputs = [];
379
+ const p2pkh = new P2PKH();
380
+ const buyerLockingScript = p2pkh.lock(ourTokenAddress);
381
+ const transferScript = BSV21.transfer(tokenId, tokenAmount).lock(buyerLockingScript);
382
+ outputs.push({
383
+ lockingScript: transferScript.toHex(),
384
+ satoshis: 1,
385
+ outputDescription: "Purchased tokens",
386
+ basket: BSV21_BASKET,
387
+ tags: [`id:${tokenId}`, `amt:${tokenAmount}`],
388
+ customInstructions: JSON.stringify({
389
+ protocolID: BSV21_PROTOCOL,
390
+ keyID: bsv21KeyID,
391
+ }),
392
+ });
393
+ const payoutReader = new Utils.Reader(ordLockData.payout);
394
+ const payoutSatoshis = payoutReader.readUInt64LEBn().toNumber();
395
+ const payoutScriptLen = payoutReader.readVarIntNum();
396
+ const payoutScriptBin = payoutReader.read(payoutScriptLen);
397
+ const payoutLockingScript = LockingScript.fromBinary(payoutScriptBin);
398
+ outputs.push({
399
+ lockingScript: payoutLockingScript.toHex(),
400
+ satoshis: payoutSatoshis,
401
+ outputDescription: "Payment to seller",
402
+ });
403
+ if (marketplaceAddress && marketplaceRate && marketplaceRate > 0) {
404
+ const marketFee = Math.ceil(payoutSatoshis * marketplaceRate);
405
+ if (marketFee > 0) {
406
+ outputs.push({
407
+ lockingScript: p2pkh.lock(marketplaceAddress).toHex(),
408
+ satoshis: marketFee,
409
+ outputDescription: "Marketplace fee",
410
+ });
411
+ }
412
+ }
413
+ const createResult = await ctx.wallet.createAction({
414
+ description: `Purchase ${tokenAmount} tokens for ${payoutSatoshis} sats`,
415
+ inputBEEF: beef.toBinary(),
416
+ inputs: [
417
+ {
418
+ outpoint,
419
+ inputDescription: "Listed token",
420
+ unlockingScriptLength: 500,
421
+ },
422
+ ],
423
+ outputs,
424
+ options: { signAndProcess: false },
425
+ });
426
+ if ("error" in createResult && createResult.error) {
427
+ return { error: String(createResult.error) };
428
+ }
429
+ if (!createResult.signableTransaction) {
430
+ return { error: "no-signable-transaction" };
431
+ }
432
+ const tx = Transaction.fromBEEF(createResult.signableTransaction.tx);
433
+ const unlockingScript = await buildPurchaseUnlockingScript(tx, 0, listingOutput.satoshis ?? 1, listingOutput.lockingScript);
434
+ const signResult = await ctx.wallet.signAction({
435
+ reference: createResult.signableTransaction.reference,
436
+ spends: {
437
+ 0: { unlockingScript: unlockingScript.toHex() },
438
+ },
439
+ });
440
+ if ("error" in signResult) {
441
+ return { error: String(signResult.error) };
442
+ }
443
+ return {
444
+ txid: signResult.txid,
445
+ rawtx: signResult.tx ? Utils.toHex(signResult.tx) : undefined,
446
+ };
391
447
  }
392
- return {
393
- txid: signResult.txid,
394
- rawtx: signResult.tx ? Buffer.from(signResult.tx).toString("hex") : undefined,
395
- };
396
- }
397
- catch (error) {
398
- return { error: error instanceof Error ? error.message : "unknown-error" };
399
- }
400
- }
448
+ catch (error) {
449
+ return { error: error instanceof Error ? error.message : "unknown-error" };
450
+ }
451
+ },
452
+ };
453
+ // ============================================================================
454
+ // Module exports
455
+ // ============================================================================
456
+ /** All token skills for registry */
457
+ export const tokensSkills = [listTokens, getBsv21Balances, sendBsv21, purchaseBsv21];