@1sat/wallet-toolbox 0.0.27 → 0.0.29

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.
@@ -17,6 +17,8 @@ export declare const LOCK_PREFIX = "20d37f4de0d1c735b4d51a5572df0f3d9104d1d9e99d
17
17
  export declare const LOCK_SUFFIX = "88ac7e7601207f75a9011488";
18
18
  export declare const ONESAT_PROTOCOL: [0 | 1 | 2, string];
19
19
  export declare const MESSAGE_SIGNING_PROTOCOL: [0 | 1 | 2, string];
20
+ export declare const BSV21_PROTOCOL: [0 | 1 | 2, string];
21
+ export declare const BSV21_FEE_SATS = 1000;
20
22
  export declare const MAX_INSCRIPTION_BYTES = 100000;
21
23
  export declare const MIN_UNLOCK_SATS = 1500;
22
24
  export declare const EXCHANGE_RATE_CACHE_TTL: number;
@@ -24,6 +24,9 @@ export const LOCK_SUFFIX = "88ac7e7601207f75a9011488";
24
24
  // Protocol IDs
25
25
  export const ONESAT_PROTOCOL = [1, "onesat"];
26
26
  export const MESSAGE_SIGNING_PROTOCOL = [1, "message signing"];
27
+ export const BSV21_PROTOCOL = [1, "bsv21"];
28
+ // Fee constants
29
+ export const BSV21_FEE_SATS = 1000;
27
30
  // Constants
28
31
  export const MAX_INSCRIPTION_BYTES = 100_000;
29
32
  export const MIN_UNLOCK_SATS = 1500;
@@ -6,17 +6,17 @@
6
6
  *
7
7
  * Usage:
8
8
  * ```typescript
9
- * import { createContext, transferOrdinal, skillRegistry } from '@1sat/wallet-toolbox/api';
9
+ * import { createContext, transferOrdinals, skillRegistry } from '@1sat/wallet-toolbox/api';
10
10
  *
11
11
  * // Create context with a BRC-100 compatible wallet
12
12
  * const ctx = createContext(wallet, { services, chain: 'main' });
13
13
  *
14
14
  * // Execute skills directly
15
- * const result = await transferOrdinal.execute(ctx, { outpoint: '...', address: '...' });
15
+ * const result = await transferOrdinals.execute(ctx, { transfers: [...], inputBEEF: [...] });
16
16
  *
17
17
  * // Or via registry (useful for AI agents)
18
- * const skill = skillRegistry.get('transferOrdinal');
19
- * const result = await skill.execute(ctx, { outpoint: '...', address: '...' });
18
+ * const skill = skillRegistry.get('transferOrdinals');
19
+ * const result = await skill.execute(ctx, { transfers: [...], inputBEEF: [...] });
20
20
  *
21
21
  * // Get MCP-compatible tool list
22
22
  * const tools = skillRegistry.toMcpTools();
package/dist/api/index.js CHANGED
@@ -6,17 +6,17 @@
6
6
  *
7
7
  * Usage:
8
8
  * ```typescript
9
- * import { createContext, transferOrdinal, skillRegistry } from '@1sat/wallet-toolbox/api';
9
+ * import { createContext, transferOrdinals, skillRegistry } from '@1sat/wallet-toolbox/api';
10
10
  *
11
11
  * // Create context with a BRC-100 compatible wallet
12
12
  * const ctx = createContext(wallet, { services, chain: 'main' });
13
13
  *
14
14
  * // Execute skills directly
15
- * const result = await transferOrdinal.execute(ctx, { outpoint: '...', address: '...' });
15
+ * const result = await transferOrdinals.execute(ctx, { transfers: [...], inputBEEF: [...] });
16
16
  *
17
17
  * // Or via registry (useful for AI agents)
18
- * const skill = skillRegistry.get('transferOrdinal');
19
- * const result = await skill.execute(ctx, { outpoint: '...', address: '...' });
18
+ * const skill = skillRegistry.get('transferOrdinals');
19
+ * const result = await skill.execute(ctx, { transfers: [...], inputBEEF: [...] });
20
20
  *
21
21
  * // Get MCP-compatible tool list
22
22
  * const tools = skillRegistry.toMcpTools();
@@ -7,19 +7,25 @@
7
7
  import { type WalletOutput, type CreateActionArgs } from "@bsv/sdk";
8
8
  import type { Skill, OneSatContext } from "../skills/types";
9
9
  type PubKeyHex = string;
10
- export interface TransferOrdinalRequest {
11
- /** Outpoint of the ordinal to transfer (txid_vout format) */
12
- outpoint: string;
10
+ export interface TransferItem {
11
+ /** The ordinal output to transfer (from listOutputs) */
12
+ ordinal: WalletOutput;
13
13
  /** Recipient's identity public key (preferred) */
14
14
  counterparty?: PubKeyHex;
15
- /** Legacy: raw P2PKH address */
15
+ /** Raw P2PKH address */
16
16
  address?: string;
17
- /** Paymail address */
18
- paymail?: string;
17
+ }
18
+ export interface TransferOrdinalsRequest {
19
+ /** Ordinals to transfer with their destinations */
20
+ transfers: TransferItem[];
21
+ /** BEEF data from listOutputs (include: 'entire transactions') */
22
+ inputBEEF: number[];
19
23
  }
20
24
  export interface ListOrdinalRequest {
21
- /** Outpoint of ordinal to list */
22
- outpoint: string;
25
+ /** The ordinal output to list (from listOutputs) */
26
+ ordinal: WalletOutput;
27
+ /** BEEF data from listOutputs (include: 'entire transactions') */
28
+ inputBEEF: number[];
23
29
  /** Price in satoshis */
24
30
  price: number;
25
31
  /** Address that receives payment on purchase (BRC-29 receive address) */
@@ -45,10 +51,10 @@ export interface OrdinalOperationResponse {
45
51
  error?: string;
46
52
  }
47
53
  /**
48
- * Build CreateActionArgs for transferring an ordinal.
54
+ * Build CreateActionArgs for transferring one or more ordinals.
49
55
  * Does NOT execute - returns params for createAction.
50
56
  */
51
- export declare function buildTransferOrdinal(ctx: OneSatContext, request: TransferOrdinalRequest): Promise<CreateActionArgs | {
57
+ export declare function buildTransferOrdinals(ctx: OneSatContext, request: TransferOrdinalsRequest): Promise<CreateActionArgs | {
52
58
  error: string;
53
59
  }>;
54
60
  /**
@@ -81,15 +87,17 @@ export declare const deriveCancelAddress: Skill<DeriveCancelAddressInput, string
81
87
  /**
82
88
  * Transfer an ordinal to a new owner.
83
89
  */
84
- export declare const transferOrdinal: Skill<TransferOrdinalRequest, OrdinalOperationResponse>;
90
+ export declare const transferOrdinals: Skill<TransferOrdinalsRequest, OrdinalOperationResponse>;
85
91
  /**
86
92
  * List an ordinal for sale on the global orderbook.
87
93
  */
88
94
  export declare const listOrdinal: Skill<ListOrdinalRequest, OrdinalOperationResponse>;
89
95
  /** Input for cancelListing skill */
90
96
  export interface CancelListingInput {
91
- /** Outpoint of the listing to cancel */
92
- outpoint: string;
97
+ /** The listing output to cancel (from listOutputs, must include lockingScript) */
98
+ listing: WalletOutput;
99
+ /** BEEF data from listOutputs (include: 'entire transactions') */
100
+ inputBEEF: number[];
93
101
  }
94
102
  /**
95
103
  * Cancel an ordinal listing.
@@ -100,5 +108,5 @@ export declare const cancelListing: Skill<CancelListingInput, OrdinalOperationRe
100
108
  */
101
109
  export declare const purchaseOrdinal: Skill<PurchaseOrdinalRequest, OrdinalOperationResponse>;
102
110
  /** All ordinals skills for registry */
103
- export declare const ordinalsSkills: (Skill<ListOrdinalsInput, WalletOutput[]> | Skill<DeriveCancelAddressInput, string> | Skill<TransferOrdinalRequest, OrdinalOperationResponse> | Skill<ListOrdinalRequest, OrdinalOperationResponse> | Skill<PurchaseOrdinalRequest, OrdinalOperationResponse>)[];
111
+ export declare const ordinalsSkills: (Skill<ListOrdinalsInput, WalletOutput[]> | Skill<DeriveCancelAddressInput, string> | Skill<TransferOrdinalsRequest, OrdinalOperationResponse> | Skill<ListOrdinalRequest, OrdinalOperationResponse> | Skill<CancelListingInput, OrdinalOperationResponse> | Skill<PurchaseOrdinalRequest, OrdinalOperationResponse>)[];
104
112
  export {};
@@ -21,6 +21,55 @@ function extractName(customInstructions) {
21
21
  return undefined;
22
22
  }
23
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.filter((_, idx) => idx !== inputIndex).map((inp) => ({
44
+ sourceTXID: inp.sourceTXID ?? inp.sourceTransaction?.id("hex") ?? "",
45
+ sourceOutputIndex: inp.sourceOutputIndex,
46
+ sequence: inp.sequence ?? 0xffffffff,
47
+ })),
48
+ inputIndex,
49
+ outputs: tx.outputs,
50
+ inputSequence: txInput.sequence ?? 0xffffffff,
51
+ subscript: sourceLockingScript,
52
+ lockTime: tx.lockTime,
53
+ scope: TransactionSignature.SIGHASH_ALL | TransactionSignature.SIGHASH_FORKID,
54
+ });
55
+ const sighash = Hash.sha256(Hash.sha256(preimage));
56
+ const { signature } = await ctx.wallet.createSignature({
57
+ protocolID,
58
+ keyID,
59
+ counterparty: "self",
60
+ hashToDirectlySign: Array.from(sighash),
61
+ });
62
+ const { publicKey } = await ctx.wallet.getPublicKey({
63
+ protocolID,
64
+ keyID,
65
+ forSelf: true,
66
+ });
67
+ const sigWithHashtype = [...signature, TransactionSignature.SIGHASH_ALL | TransactionSignature.SIGHASH_FORKID];
68
+ return new UnlockingScript()
69
+ .writeBin(sigWithHashtype)
70
+ .writeBin(Utils.toArray(publicKey, "hex"))
71
+ .toHex();
72
+ }
24
73
  // ============================================================================
25
74
  // Internal helpers
26
75
  // ============================================================================
@@ -95,54 +144,47 @@ async function buildPurchaseUnlockingScript(tx, inputIndex, sourceSatoshis, lock
95
144
  // Builder functions (utilities for advanced use)
96
145
  // ============================================================================
97
146
  /**
98
- * Build CreateActionArgs for transferring an ordinal.
147
+ * Build CreateActionArgs for transferring one or more ordinals.
99
148
  * Does NOT execute - returns params for createAction.
100
149
  */
101
- export async function buildTransferOrdinal(ctx, request) {
102
- const { outpoint, counterparty, address, paymail } = request;
103
- if (!counterparty && !address && !paymail) {
104
- return { error: "must-provide-counterparty-address-or-paymail" };
105
- }
106
- let recipientAddress;
107
- if (counterparty) {
108
- const { publicKey } = await ctx.wallet.getPublicKey({
109
- protocolID: ONESAT_PROTOCOL,
110
- keyID: outpoint,
111
- counterparty,
112
- forSelf: false,
113
- });
114
- recipientAddress = PublicKey.fromString(publicKey).toAddress();
115
- }
116
- else if (paymail) {
117
- return { error: "paymail-not-yet-implemented" };
118
- }
119
- else {
120
- recipientAddress = address;
121
- }
122
- const result = await ctx.wallet.listOutputs({
123
- basket: ORDINALS_BASKET,
124
- includeTags: true,
125
- includeCustomInstructions: true,
126
- include: "locking scripts",
127
- limit: 10000,
128
- });
129
- const sourceOutput = result.outputs.find((o) => o.outpoint === outpoint);
130
- if (!sourceOutput) {
131
- return { error: "ordinal-not-found" };
150
+ export async function buildTransferOrdinals(ctx, request) {
151
+ const { transfers, inputBEEF } = request;
152
+ if (!transfers.length) {
153
+ return { error: "no-transfers" };
132
154
  }
133
- // Preserve important tags from source output
134
- const tags = [];
135
- for (const tag of sourceOutput.tags ?? []) {
136
- if (tag.startsWith("type:") || tag.startsWith("origin:") || tag.startsWith("name:")) {
137
- tags.push(tag);
155
+ const inputs = [];
156
+ const outputs = [];
157
+ for (const { ordinal, counterparty, address } of transfers) {
158
+ if (!counterparty && !address) {
159
+ return { error: "must-provide-counterparty-or-address" };
138
160
  }
139
- }
140
- const sourceName = extractName(sourceOutput.customInstructions);
141
- return {
142
- description: "Transfer ordinal",
143
- inputs: [{ outpoint, inputDescription: "Ordinal to transfer" }],
144
- outputs: [
145
- {
161
+ const outpoint = ordinal.outpoint;
162
+ let recipientAddress;
163
+ if (counterparty) {
164
+ const { publicKey } = await ctx.wallet.getPublicKey({
165
+ protocolID: ONESAT_PROTOCOL,
166
+ keyID: outpoint,
167
+ counterparty,
168
+ forSelf: false,
169
+ });
170
+ recipientAddress = PublicKey.fromString(publicKey).toAddress();
171
+ }
172
+ else {
173
+ recipientAddress = address;
174
+ }
175
+ // Preserve important tags from source output
176
+ const tags = [];
177
+ for (const tag of ordinal.tags ?? []) {
178
+ if (tag.startsWith("type:") || tag.startsWith("origin:") || tag.startsWith("name:")) {
179
+ tags.push(tag);
180
+ }
181
+ }
182
+ const sourceName = extractName(ordinal.customInstructions);
183
+ inputs.push({ outpoint, inputDescription: "Ordinal to transfer", unlockingScriptLength: 108 });
184
+ // Only track output in wallet when transferring to a counterparty (wallet can derive keys to spend it)
185
+ // External address transfers are NOT tracked since the wallet cannot spend them
186
+ if (counterparty) {
187
+ outputs.push({
146
188
  lockingScript: new P2PKH().lock(recipientAddress).toHex(),
147
189
  satoshis: 1,
148
190
  outputDescription: "Ordinal transfer",
@@ -153,8 +195,22 @@ export async function buildTransferOrdinal(ctx, request) {
153
195
  keyID: outpoint,
154
196
  ...(sourceName && { name: sourceName }),
155
197
  }),
156
- },
157
- ],
198
+ });
199
+ }
200
+ else {
201
+ // External address - output is not tracked in wallet
202
+ outputs.push({
203
+ lockingScript: new P2PKH().lock(recipientAddress).toHex(),
204
+ satoshis: 1,
205
+ outputDescription: "Ordinal transfer to external address",
206
+ });
207
+ }
208
+ }
209
+ return {
210
+ description: transfers.length === 1 ? "Transfer ordinal" : `Transfer ${transfers.length} ordinals`,
211
+ inputBEEF,
212
+ inputs,
213
+ outputs,
158
214
  };
159
215
  }
160
216
  /**
@@ -162,27 +218,17 @@ export async function buildTransferOrdinal(ctx, request) {
162
218
  * Does NOT execute - returns params for createAction.
163
219
  */
164
220
  export async function buildListOrdinal(ctx, request) {
165
- const { outpoint, price, payAddress } = request;
221
+ const { ordinal, inputBEEF, price, payAddress } = request;
166
222
  if (!payAddress)
167
223
  return { error: "missing-pay-address" };
168
224
  if (price <= 0)
169
225
  return { error: "invalid-price" };
170
- const result = await ctx.wallet.listOutputs({
171
- basket: ORDINALS_BASKET,
172
- includeTags: true,
173
- includeCustomInstructions: true,
174
- include: "locking scripts",
175
- limit: 10000,
176
- });
177
- const sourceOutput = result.outputs.find((o) => o.outpoint === outpoint);
178
- if (!sourceOutput) {
179
- return { error: "ordinal-not-found" };
180
- }
181
- const typeTag = sourceOutput.tags?.find((t) => t.startsWith("type:"));
182
- const originTag = sourceOutput.tags?.find((t) => t.startsWith("origin:"));
183
- const nameTag = sourceOutput.tags?.find((t) => t.startsWith("name:"));
226
+ const outpoint = ordinal.outpoint;
227
+ const typeTag = ordinal.tags?.find((t) => t.startsWith("type:"));
228
+ const originTag = ordinal.tags?.find((t) => t.startsWith("origin:"));
229
+ const nameTag = ordinal.tags?.find((t) => t.startsWith("name:"));
184
230
  const originOutpoint = originTag ? originTag.slice(7) : outpoint;
185
- const sourceName = extractName(sourceOutput.customInstructions);
231
+ const sourceName = extractName(ordinal.customInstructions);
186
232
  const cancelAddress = await deriveCancelAddressInternal(ctx, outpoint);
187
233
  const lockingScript = buildOrdLockScript(cancelAddress, payAddress, price);
188
234
  const tags = ["ordlock", `origin:${originOutpoint}`, `price:${price}`];
@@ -192,7 +238,8 @@ export async function buildListOrdinal(ctx, request) {
192
238
  tags.push(nameTag);
193
239
  return {
194
240
  description: `List ordinal for ${price} sats`,
195
- inputs: [{ outpoint, inputDescription: "Ordinal to list" }],
241
+ inputBEEF,
242
+ inputs: [{ outpoint, inputDescription: "Ordinal to list", unlockingScriptLength: 108 }],
196
243
  outputs: [
197
244
  {
198
245
  lockingScript: lockingScript.toHex(),
@@ -259,36 +306,71 @@ export const deriveCancelAddress = {
259
306
  /**
260
307
  * Transfer an ordinal to a new owner.
261
308
  */
262
- export const transferOrdinal = {
309
+ export const transferOrdinals = {
263
310
  meta: {
264
- name: "transferOrdinal",
265
- description: "Transfer an ordinal to a new owner via counterparty pubkey, address, or paymail",
311
+ name: "transferOrdinals",
312
+ description: "Transfer one or more ordinals to new owners",
266
313
  category: "ordinals",
267
314
  inputSchema: {
268
315
  type: "object",
269
316
  properties: {
270
- outpoint: { type: "string", description: "Outpoint of the ordinal (txid_vout format)" },
271
- counterparty: { type: "string", description: "Recipient identity public key (hex)" },
272
- address: { type: "string", description: "Recipient P2PKH address" },
273
- paymail: { type: "string", description: "Recipient paymail address" },
317
+ transfers: {
318
+ type: "array",
319
+ description: "Ordinals to transfer with destinations",
320
+ items: {
321
+ type: "object",
322
+ properties: {
323
+ ordinal: { type: "object", description: "WalletOutput from listOutputs" },
324
+ counterparty: { type: "string", description: "Recipient identity public key (hex)" },
325
+ address: { type: "string", description: "Recipient P2PKH address" },
326
+ },
327
+ required: ["ordinal"],
328
+ },
329
+ },
330
+ inputBEEF: { type: "array", description: "BEEF from listOutputs with include: 'entire transactions'" },
274
331
  },
275
- required: ["outpoint"],
332
+ required: ["transfers", "inputBEEF"],
276
333
  },
277
334
  },
278
335
  async execute(ctx, input) {
279
336
  try {
280
- const params = await buildTransferOrdinal(ctx, input);
337
+ const params = await buildTransferOrdinals(ctx, input);
281
338
  if ("error" in params) {
282
339
  return params;
283
340
  }
284
- const result = await ctx.wallet.createAction({
341
+ const createResult = await ctx.wallet.createAction({
285
342
  ...params,
286
- options: { randomizeOutputs: false },
343
+ options: { signAndProcess: false, randomizeOutputs: false },
287
344
  });
288
- if (!result.txid) {
289
- return { error: "no-txid-returned" };
345
+ if (!createResult.signableTransaction) {
346
+ return { error: "no-signable-transaction" };
290
347
  }
291
- return { txid: result.txid, rawtx: result.tx ? Utils.toHex(result.tx) : undefined };
348
+ const tx = Transaction.fromBEEF(createResult.signableTransaction.tx);
349
+ const spends = {};
350
+ for (let i = 0; i < input.transfers.length; i++) {
351
+ const { ordinal } = input.transfers[i];
352
+ console.log(`[transferOrdinals] Input ${i}: outpoint=${ordinal.outpoint}, customInstructions=${ordinal.customInstructions}`);
353
+ if (!ordinal.customInstructions) {
354
+ return { error: `missing-custom-instructions-for-${ordinal.outpoint}` };
355
+ }
356
+ const { protocolID, keyID } = JSON.parse(ordinal.customInstructions);
357
+ console.log(`[transferOrdinals] Input ${i}: protocolID=${JSON.stringify(protocolID)}, keyID=${keyID}`);
358
+ const unlocking = await signP2PKHInput(ctx, tx, i, protocolID, keyID);
359
+ if (typeof unlocking !== "string")
360
+ return unlocking;
361
+ spends[i] = { unlockingScript: unlocking };
362
+ }
363
+ const signResult = await ctx.wallet.signAction({
364
+ reference: createResult.signableTransaction.reference,
365
+ spends,
366
+ });
367
+ if ("error" in signResult) {
368
+ return { error: String(signResult.error) };
369
+ }
370
+ return {
371
+ txid: signResult.txid,
372
+ rawtx: signResult.tx ? Utils.toHex(signResult.tx) : undefined,
373
+ };
292
374
  }
293
375
  catch (error) {
294
376
  return { error: error instanceof Error ? error.message : "unknown-error" };
@@ -306,11 +388,12 @@ export const listOrdinal = {
306
388
  inputSchema: {
307
389
  type: "object",
308
390
  properties: {
309
- outpoint: { type: "string", description: "Outpoint of the ordinal to list" },
391
+ ordinal: { type: "object", description: "WalletOutput from listOutputs" },
392
+ inputBEEF: { type: "array", description: "BEEF from listOutputs with include: 'entire transactions'" },
310
393
  price: { type: "integer", description: "Price in satoshis" },
311
394
  payAddress: { type: "string", description: "Address to receive payment on purchase" },
312
395
  },
313
- required: ["outpoint", "price", "payAddress"],
396
+ required: ["ordinal", "inputBEEF", "price", "payAddress"],
314
397
  },
315
398
  },
316
399
  async execute(ctx, input) {
@@ -319,14 +402,32 @@ export const listOrdinal = {
319
402
  if ("error" in params) {
320
403
  return params;
321
404
  }
322
- const result = await ctx.wallet.createAction({
405
+ const createResult = await ctx.wallet.createAction({
323
406
  ...params,
324
- options: { randomizeOutputs: false },
407
+ options: { signAndProcess: false, randomizeOutputs: false },
408
+ });
409
+ if (!createResult.signableTransaction) {
410
+ return { error: "no-signable-transaction" };
411
+ }
412
+ if (!input.ordinal.customInstructions) {
413
+ return { error: "missing-custom-instructions" };
414
+ }
415
+ const { protocolID, keyID } = JSON.parse(input.ordinal.customInstructions);
416
+ const tx = Transaction.fromBEEF(createResult.signableTransaction.tx);
417
+ const unlocking = await signP2PKHInput(ctx, tx, 0, protocolID, keyID);
418
+ if (typeof unlocking !== "string")
419
+ return unlocking;
420
+ const signResult = await ctx.wallet.signAction({
421
+ reference: createResult.signableTransaction.reference,
422
+ spends: { 0: { unlockingScript: unlocking } },
325
423
  });
326
- if (!result.txid) {
327
- return { error: "no-txid-returned" };
424
+ if ("error" in signResult) {
425
+ return { error: String(signResult.error) };
328
426
  }
329
- return { txid: result.txid, rawtx: result.tx ? Utils.toHex(result.tx) : undefined };
427
+ return {
428
+ txid: signResult.txid,
429
+ rawtx: signResult.tx ? Utils.toHex(signResult.tx) : undefined,
430
+ };
330
431
  }
331
432
  catch (error) {
332
433
  return { error: error instanceof Error ? error.message : "unknown-error" };
@@ -344,25 +445,16 @@ export const cancelListing = {
344
445
  inputSchema: {
345
446
  type: "object",
346
447
  properties: {
347
- outpoint: { type: "string", description: "Outpoint of the listing to cancel" },
448
+ listing: { type: "object", description: "WalletOutput of the listing (must include lockingScript)" },
449
+ inputBEEF: { type: "array", description: "BEEF from listOutputs with include: 'entire transactions'" },
348
450
  },
349
- required: ["outpoint"],
451
+ required: ["listing", "inputBEEF"],
350
452
  },
351
453
  },
352
454
  async execute(ctx, input) {
353
455
  try {
354
- const { outpoint } = input;
355
- const result = await ctx.wallet.listOutputs({
356
- basket: ORDINALS_BASKET,
357
- includeTags: true,
358
- includeCustomInstructions: true,
359
- include: "locking scripts",
360
- limit: 10000,
361
- });
362
- const listing = result.outputs.find((o) => o.outpoint === outpoint);
363
- if (!listing) {
364
- return { error: "listing-not-found" };
365
- }
456
+ const { listing, inputBEEF } = input;
457
+ const outpoint = listing.outpoint;
366
458
  if (!listing.customInstructions) {
367
459
  return { error: "missing-custom-instructions" };
368
460
  }
@@ -380,6 +472,7 @@ export const cancelListing = {
380
472
  tags.push(nameTag);
381
473
  const createResult = await ctx.wallet.createAction({
382
474
  description: "Cancel ordinal listing",
475
+ inputBEEF,
383
476
  inputs: [
384
477
  {
385
478
  outpoint,
@@ -407,7 +500,10 @@ export const cancelListing = {
407
500
  }
408
501
  const tx = Transaction.fromBEEF(createResult.signableTransaction.tx);
409
502
  const txInput = tx.inputs[0];
410
- const lockingScript = Script.fromHex(listing.lockingScript);
503
+ const lockingScript = txInput.sourceTransaction?.outputs[txInput.sourceOutputIndex]?.lockingScript;
504
+ if (!lockingScript) {
505
+ return { error: "missing-locking-script" };
506
+ }
411
507
  const sourceTXID = txInput.sourceTXID ?? txInput.sourceTransaction?.id("hex");
412
508
  if (!sourceTXID) {
413
509
  return { error: "missing-source-txid" };
@@ -612,7 +708,7 @@ export const purchaseOrdinal = {
612
708
  export const ordinalsSkills = [
613
709
  listOrdinals,
614
710
  deriveCancelAddress,
615
- transferOrdinal,
711
+ transferOrdinals,
616
712
  listOrdinal,
617
713
  cancelListing,
618
714
  purchaseOrdinal,
@@ -5,7 +5,7 @@
5
5
  */
6
6
  import type { OneSatContext, Skill } from "../skills/types";
7
7
  import type { IndexedOutput } from "../../services/types";
8
- import type { SweepBsvRequest, SweepBsvResponse, SweepInput, SweepOrdinalsRequest, SweepOrdinalsResponse } from "./types";
8
+ import type { SweepBsvRequest, SweepBsvResponse, SweepInput, SweepOrdinalsRequest, SweepOrdinalsResponse, SweepBsv21Request, SweepBsv21Response } from "./types";
9
9
  export * from "./types";
10
10
  /**
11
11
  * Prepare sweep inputs from IndexedOutput objects by fetching locking scripts.
@@ -27,4 +27,11 @@ export declare const sweepBsv: Skill<SweepBsvRequest, SweepBsvResponse>;
27
27
  * transferred to a derived address using the wallet's key derivation.
28
28
  */
29
29
  export declare const sweepOrdinals: Skill<SweepOrdinalsRequest, SweepOrdinalsResponse>;
30
- export declare const sweepSkills: (Skill<SweepBsvRequest, SweepBsvResponse> | Skill<SweepOrdinalsRequest, SweepOrdinalsResponse>)[];
30
+ /**
31
+ * Sweep BSV-21 tokens from external inputs into the destination wallet.
32
+ *
33
+ * Consolidates all token inputs into a single output. All inputs must be
34
+ * for the same tokenId. Creates a fee output to the overlay fund address.
35
+ */
36
+ export declare const sweepBsv21: Skill<SweepBsv21Request, SweepBsv21Response>;
37
+ export declare const sweepSkills: (Skill<SweepBsvRequest, SweepBsvResponse> | Skill<SweepOrdinalsRequest, SweepOrdinalsResponse> | Skill<SweepBsv21Request, SweepBsv21Response>)[];
@@ -4,7 +4,9 @@
4
4
  * Functions for sweeping assets from external wallets into a BRC-100 wallet.
5
5
  */
6
6
  import { P2PKH, PrivateKey, PublicKey, Transaction, } from "@bsv/sdk";
7
- import { ONESAT_PROTOCOL } from "../constants";
7
+ import { BSV21 } from "@bopen-io/templates";
8
+ import { ONESAT_PROTOCOL, BSV21_PROTOCOL, BSV21_FEE_SATS, BSV21_BASKET } from "../constants";
9
+ import { deriveFundAddress } from "../../indexers";
8
10
  export * from "./types";
9
11
  /**
10
12
  * Prepare sweep inputs from IndexedOutput objects by fetching locking scripts.
@@ -308,17 +310,19 @@ export const sweepOrdinals = {
308
310
  tags.push(`type:${input.contentType}`);
309
311
  if (input.origin)
310
312
  tags.push(`origin:${input.origin}`);
313
+ const customInstructions = JSON.stringify({
314
+ protocolID: ONESAT_PROTOCOL,
315
+ keyID: input.outpoint,
316
+ ...(input.name && { name: input.name.slice(0, 64) }),
317
+ });
318
+ console.log(`[sweepOrdinals] Output for ${input.outpoint}: keyID=${input.outpoint}, customInstructions=${customInstructions}`);
311
319
  outputs.push({
312
320
  lockingScript: lockingScript.toHex(),
313
321
  satoshis: 1,
314
322
  outputDescription: `Ordinal ${input.origin ?? input.outpoint}`,
315
323
  basket: "1sat",
316
324
  tags,
317
- customInstructions: JSON.stringify({
318
- protocolID: ONESAT_PROTOCOL,
319
- keyID: input.outpoint,
320
- ...(input.name && { name: input.name.slice(0, 64) }),
321
- }),
325
+ customInstructions,
322
326
  });
323
327
  }
324
328
  const beefData = firstBeef.toBinary();
@@ -383,5 +387,177 @@ export const sweepOrdinals = {
383
387
  }
384
388
  },
385
389
  };
390
+ /**
391
+ * Sweep BSV-21 tokens from external inputs into the destination wallet.
392
+ *
393
+ * Consolidates all token inputs into a single output. All inputs must be
394
+ * for the same tokenId. Creates a fee output to the overlay fund address.
395
+ */
396
+ export const sweepBsv21 = {
397
+ meta: {
398
+ name: "sweepBsv21",
399
+ description: "Sweep BSV-21 tokens from external wallet (via WIF) into the connected wallet",
400
+ category: "sweep",
401
+ requiresServices: true,
402
+ inputSchema: {
403
+ type: "object",
404
+ properties: {
405
+ inputs: {
406
+ type: "array",
407
+ description: "Token UTXOs to sweep (must all be same tokenId)",
408
+ items: {
409
+ type: "object",
410
+ properties: {
411
+ outpoint: { type: "string", description: "Outpoint (txid_vout)" },
412
+ satoshis: { type: "integer", description: "Satoshis (should be 1)" },
413
+ lockingScript: { type: "string", description: "Locking script hex" },
414
+ tokenId: { type: "string", description: "Token ID (txid_vout format)" },
415
+ amount: { type: "string", description: "Token amount as string" },
416
+ },
417
+ required: ["outpoint", "satoshis", "lockingScript", "tokenId", "amount"],
418
+ },
419
+ },
420
+ wif: {
421
+ type: "string",
422
+ description: "WIF private key controlling the inputs",
423
+ },
424
+ },
425
+ required: ["inputs", "wif"],
426
+ },
427
+ },
428
+ async execute(ctx, request) {
429
+ if (!ctx.services) {
430
+ return { error: "services-required" };
431
+ }
432
+ try {
433
+ const { inputs, wif } = request;
434
+ if (!inputs || inputs.length === 0) {
435
+ return { error: "no-inputs" };
436
+ }
437
+ // Validate all inputs have the same tokenId
438
+ const tokenId = inputs[0].tokenId;
439
+ if (!inputs.every((i) => i.tokenId === tokenId)) {
440
+ return { error: "mixed-token-ids" };
441
+ }
442
+ // Parse WIF
443
+ const privateKey = PrivateKey.fromWif(wif);
444
+ // Sum all input amounts
445
+ const totalAmount = inputs.reduce((sum, i) => sum + BigInt(i.amount), 0n);
446
+ if (totalAmount <= 0n) {
447
+ return { error: "no-token-amount" };
448
+ }
449
+ // Fetch BEEF for all input transactions and merge them
450
+ const txids = [...new Set(inputs.map((i) => i.outpoint.split("_")[0]))];
451
+ console.log(`[sweepBsv21] Fetching BEEF for ${txids.length} transactions`);
452
+ const firstBeef = await ctx.services.getBeefForTxid(txids[0]);
453
+ for (let i = 1; i < txids.length; i++) {
454
+ const additionalBeef = await ctx.services.getBeefForTxid(txids[i]);
455
+ firstBeef.mergeBeef(additionalBeef);
456
+ }
457
+ console.log(`[sweepBsv21] Merged BEEF valid=${firstBeef.isValid()}, txs=${firstBeef.txs.length}`);
458
+ // Build input descriptors
459
+ const inputDescriptors = inputs.map((input) => {
460
+ const [txid, voutStr] = input.outpoint.split("_");
461
+ return {
462
+ outpoint: `${txid}.${voutStr}`,
463
+ inputDescription: `Token input ${input.outpoint}`,
464
+ unlockingScriptLength: 108,
465
+ sequenceNumber: 0xffffffff,
466
+ };
467
+ });
468
+ // Build outputs
469
+ const outputs = [];
470
+ // 1. Token output (1 sat) - derive key for this token
471
+ const keyID = `${tokenId}-${Date.now()}`;
472
+ const pubKeyResult = await ctx.wallet.getPublicKey({
473
+ protocolID: BSV21_PROTOCOL,
474
+ keyID,
475
+ forSelf: true,
476
+ });
477
+ if (!pubKeyResult.publicKey) {
478
+ return { error: "failed-to-derive-key" };
479
+ }
480
+ const derivedAddress = PublicKey.fromString(pubKeyResult.publicKey).toAddress();
481
+ const p2pkh = new P2PKH();
482
+ const destinationLockingScript = p2pkh.lock(derivedAddress);
483
+ const transferScript = BSV21.transfer(tokenId, totalAmount).lock(destinationLockingScript);
484
+ outputs.push({
485
+ lockingScript: transferScript.toHex(),
486
+ satoshis: 1,
487
+ outputDescription: `Sweep ${totalAmount} tokens`,
488
+ basket: BSV21_BASKET,
489
+ tags: [`id:${tokenId}`, `amt:${totalAmount}`],
490
+ customInstructions: JSON.stringify({
491
+ protocolID: BSV21_PROTOCOL,
492
+ keyID,
493
+ }),
494
+ });
495
+ // 2. Fee output (1000 sats) to overlay fund address
496
+ const fundAddress = deriveFundAddress(tokenId);
497
+ outputs.push({
498
+ lockingScript: p2pkh.lock(fundAddress).toHex(),
499
+ satoshis: BSV21_FEE_SATS,
500
+ outputDescription: "Overlay processing fee",
501
+ });
502
+ const beefData = firstBeef.toBinary();
503
+ // Create action to get signable transaction
504
+ const createResult = await ctx.wallet.createAction({
505
+ description: `Sweep ${inputs.length} token UTXO${inputs.length !== 1 ? "s" : ""}`,
506
+ inputBEEF: beefData,
507
+ inputs: inputDescriptors,
508
+ outputs,
509
+ options: { signAndProcess: false, randomizeOutputs: false },
510
+ });
511
+ if ("error" in createResult && createResult.error) {
512
+ return { error: String(createResult.error) };
513
+ }
514
+ if (!createResult.signableTransaction) {
515
+ return { error: "no-signable-transaction" };
516
+ }
517
+ // Sign each input with our external key
518
+ const tx = Transaction.fromBEEF(createResult.signableTransaction.tx);
519
+ // Build a set of outpoints we control
520
+ const ourOutpoints = new Set(inputs.map((input) => {
521
+ const [txid, vout] = input.outpoint.split("_");
522
+ return `${txid}.${vout}`;
523
+ }));
524
+ // Set up P2PKH unlocker on each input we control
525
+ for (let i = 0; i < tx.inputs.length; i++) {
526
+ const txInput = tx.inputs[i];
527
+ const inputOutpoint = `${txInput.sourceTXID}.${txInput.sourceOutputIndex}`;
528
+ if (ourOutpoints.has(inputOutpoint)) {
529
+ txInput.unlockingScriptTemplate = p2pkh.unlock(privateKey, "all", true);
530
+ }
531
+ }
532
+ await tx.sign();
533
+ // Extract unlocking scripts for signAction
534
+ const spends = {};
535
+ for (let i = 0; i < tx.inputs.length; i++) {
536
+ const txInput = tx.inputs[i];
537
+ const inputOutpoint = `${txInput.sourceTXID}.${txInput.sourceOutputIndex}`;
538
+ if (ourOutpoints.has(inputOutpoint)) {
539
+ spends[i] = { unlockingScript: txInput.unlockingScript?.toHex() ?? "" };
540
+ }
541
+ }
542
+ // Complete the action with our signatures
543
+ const signResult = await ctx.wallet.signAction({
544
+ reference: createResult.signableTransaction.reference,
545
+ spends,
546
+ });
547
+ if ("error" in signResult) {
548
+ return { error: String(signResult.error) };
549
+ }
550
+ return {
551
+ txid: signResult.txid,
552
+ beef: signResult.tx ? Array.from(signResult.tx) : undefined,
553
+ };
554
+ }
555
+ catch (error) {
556
+ return {
557
+ error: error instanceof Error ? error.message : "unknown-error",
558
+ };
559
+ }
560
+ },
561
+ };
386
562
  // Export skills array for registry
387
- export const sweepSkills = [sweepBsv, sweepOrdinals];
563
+ export const sweepSkills = [sweepBsv, sweepOrdinals, sweepBsv21];
@@ -53,3 +53,26 @@ export interface SweepOrdinalsResponse {
53
53
  /** Error message if failed */
54
54
  error?: string;
55
55
  }
56
+ /** Input for BSV-21 token sweep operations */
57
+ export interface SweepBsv21Input extends SweepInput {
58
+ /** Token ID (txid_vout format) */
59
+ tokenId: string;
60
+ /** Token amount as string (bigint serialization) */
61
+ amount: string;
62
+ }
63
+ /** Request to sweep BSV-21 tokens */
64
+ export interface SweepBsv21Request {
65
+ /** Token UTXOs to sweep (must all be same tokenId) */
66
+ inputs: SweepBsv21Input[];
67
+ /** WIF private key controlling the inputs */
68
+ wif: string;
69
+ }
70
+ /** Response from BSV-21 token sweep operation */
71
+ export interface SweepBsv21Response {
72
+ /** Transaction ID if successful */
73
+ txid?: string;
74
+ /** BEEF (transaction with validity proof) */
75
+ beef?: number[];
76
+ /** Error message if failed */
77
+ error?: string;
78
+ }
@@ -5,11 +5,8 @@
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
- import { BSV21_BASKET } from "../constants";
9
- // ============================================================================
10
- // Constants
11
- // ============================================================================
12
- const BSV21_PROTOCOL = [1, "bsv21"];
8
+ import { BSV21_BASKET, BSV21_PROTOCOL, BSV21_FEE_SATS } from "../constants";
9
+ import { deriveFundAddress } from "../../indexers";
13
10
  // ============================================================================
14
11
  // Internal helpers
15
12
  // ============================================================================
@@ -256,6 +253,13 @@ export const sendBsv21 = {
256
253
  satoshis: 1,
257
254
  outputDescription: `Send ${amount} tokens`,
258
255
  });
256
+ // Fee output to overlay fund address
257
+ const fundAddress = deriveFundAddress(tokenId);
258
+ outputs.push({
259
+ lockingScript: p2pkh.lock(fundAddress).toHex(),
260
+ satoshis: BSV21_FEE_SATS,
261
+ outputDescription: "Overlay processing fee",
262
+ });
259
263
  const change = totalIn - amount;
260
264
  if (change > 0n) {
261
265
  const changeKeyID = `${tokenId}-${Date.now()}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@1sat/wallet-toolbox",
3
- "version": "0.0.27",
3
+ "version": "0.0.29",
4
4
  "description": "BSV wallet library extending @bsv/wallet-toolbox with 1Sat Ordinals protocol support",
5
5
  "author": "1Sat Team",
6
6
  "license": "MIT",