@1sat/wallet-toolbox 0.0.9 → 0.0.10

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.
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * Locks Module
3
3
  *
4
- * Functions for time-locking BSV.
4
+ * Skills for time-locking BSV.
5
5
  */
6
- import { type WalletInterface, type WalletOutput } from "@bsv/sdk";
6
+ import type { Skill } from "../skills/types";
7
7
  export interface LockBsvRequest {
8
8
  /** Amount in satoshis to lock */
9
9
  satoshis: number;
@@ -23,22 +23,25 @@ export interface LockOperationResponse {
23
23
  rawtx?: string;
24
24
  error?: string;
25
25
  }
26
- /**
27
- * List locked outputs from the lock basket.
28
- * Returns WalletOutput[] directly - use tags for metadata (lock:until:).
29
- */
30
- export declare function listLocks(cwi: WalletInterface, limit?: number): Promise<WalletOutput[]>;
26
+ /** Input for getLockData skill (no required params) */
27
+ export type GetLockDataInput = Record<string, never>;
31
28
  /**
32
29
  * Get lock data summary.
33
30
  */
34
- export declare function getLockData(cwi: WalletInterface, chain?: "main" | "test", wocApiKey?: string): Promise<LockData>;
31
+ export declare const getLockData: Skill<GetLockDataInput, LockData>;
32
+ /** Input for lockBsv skill */
33
+ export interface LockBsvInput {
34
+ requests: LockBsvRequest[];
35
+ }
35
36
  /**
36
37
  * Lock BSV until a block height.
37
- * Derives lock address using hardcoded keyID.
38
38
  */
39
- export declare function lockBsv(cwi: WalletInterface, requests: LockBsvRequest[]): Promise<LockOperationResponse>;
39
+ export declare const lockBsv: Skill<LockBsvInput, LockOperationResponse>;
40
+ /** Input for unlockBsv skill (no required params) */
41
+ export type UnlockBsvInput = Record<string, never>;
40
42
  /**
41
43
  * Unlock matured BSV locks.
42
- * Uses createSignature with stored keyID to sign unlock transactions.
43
44
  */
44
- export declare function unlockBsv(cwi: WalletInterface, chain?: "main" | "test", wocApiKey?: string): Promise<LockOperationResponse>;
45
+ export declare const unlockBsv: Skill<UnlockBsvInput, LockOperationResponse>;
46
+ /** All lock skills for registry */
47
+ export declare const locksSkills: (Skill<GetLockDataInput, LockData> | Skill<LockBsvInput, LockOperationResponse> | Skill<UnlockBsvInput, LockOperationResponse>)[];
@@ -1,16 +1,18 @@
1
1
  /**
2
2
  * Locks Module
3
3
  *
4
- * Functions for time-locking BSV.
4
+ * Skills for time-locking BSV.
5
5
  */
6
6
  import { Hash, PublicKey, Script, Transaction, TransactionSignature, Utils, } from "@bsv/sdk";
7
- import { LOCK_BASKET, LOCK_PREFIX, LOCK_SUFFIX, MIN_UNLOCK_SATS } from "../constants";
8
- import { getChainInfo } from "../balance";
9
- // Hardcoded keyID for all locks - deterministic derivation
7
+ import { LOCK_BASKET, LOCK_PREFIX, LOCK_SUFFIX, MIN_UNLOCK_SATS, WOC_MAINNET_URL, WOC_TESTNET_URL } from "../constants";
8
+ // ============================================================================
9
+ // Constants
10
+ // ============================================================================
11
+ const LOCK_PROTOCOL = [1, "lock"];
10
12
  const LOCK_KEY_ID = "lock";
11
- /**
12
- * Build lock script for time-locked BSV.
13
- */
13
+ // ============================================================================
14
+ // Internal helpers
15
+ // ============================================================================
14
16
  function buildLockScript(address, until) {
15
17
  const pkh = Utils.fromBase58Check(address).data;
16
18
  return new Script()
@@ -19,215 +21,271 @@ function buildLockScript(address, until) {
19
21
  .writeNumber(until)
20
22
  .writeScript(Script.fromHex(LOCK_SUFFIX));
21
23
  }
22
- /**
23
- * List locked outputs from the lock basket.
24
- * Returns WalletOutput[] directly - use tags for metadata (lock:until:).
25
- */
26
- export async function listLocks(cwi, limit = 10000) {
27
- const result = await cwi.listOutputs({
28
- basket: LOCK_BASKET,
29
- includeTags: true,
30
- limit,
31
- });
32
- return result.outputs;
33
- }
34
- /**
35
- * Get lock data summary.
36
- */
37
- export async function getLockData(cwi, chain = "main", wocApiKey) {
38
- const lockData = { totalLocked: 0, unlockable: 0, nextUnlock: 0 };
39
- const chainInfo = await getChainInfo(chain, wocApiKey);
40
- const currentHeight = chainInfo?.blocks || 0;
41
- const outputs = await listLocks(cwi);
42
- for (const o of outputs) {
43
- const lockTag = o.tags?.find((t) => t.startsWith("lock:until:"));
44
- if (!lockTag)
45
- continue;
46
- const until = parseInt(lockTag.slice(11), 10);
47
- lockData.totalLocked += o.satoshis;
48
- if (until <= currentHeight) {
49
- lockData.unlockable += o.satoshis;
50
- }
51
- else if (!lockData.nextUnlock || until < lockData.nextUnlock) {
52
- lockData.nextUnlock = until;
53
- }
54
- }
55
- if (lockData.unlockable < MIN_UNLOCK_SATS * outputs.length) {
56
- lockData.unlockable = 0;
57
- }
58
- return lockData;
59
- }
60
- /**
61
- * Lock BSV until a block height.
62
- * Derives lock address using hardcoded keyID.
63
- */
64
- export async function lockBsv(cwi, requests) {
24
+ async function getChainInfoInternal(chain, wocApiKey) {
25
+ const baseUrl = chain === "main" ? WOC_MAINNET_URL : WOC_TESTNET_URL;
26
+ const headers = {};
27
+ if (wocApiKey)
28
+ headers["woc-api-key"] = wocApiKey;
65
29
  try {
66
- if (!requests || requests.length === 0) {
67
- return { error: "no-lock-requests" };
68
- }
69
- // Derive lock address once (same for all locks)
70
- const { publicKey } = await cwi.getPublicKey({
71
- protocolID: [1, "lock"],
72
- keyID: LOCK_KEY_ID,
73
- counterparty: "self",
74
- forSelf: true,
75
- });
76
- const lockAddress = PublicKey.fromString(publicKey).toAddress();
77
- const outputs = [];
78
- for (const req of requests) {
79
- if (req.satoshis <= 0)
80
- return { error: "invalid-satoshis" };
81
- if (req.until <= 0)
82
- return { error: "invalid-block-height" };
83
- const lockingScript = buildLockScript(lockAddress, req.until);
84
- outputs.push({
85
- lockingScript: lockingScript.toHex(),
86
- satoshis: req.satoshis,
87
- outputDescription: `Lock ${req.satoshis} sats until block ${req.until}`,
88
- basket: LOCK_BASKET,
89
- tags: [`lock:until:${req.until}`],
90
- });
91
- }
92
- const result = await cwi.createAction({
93
- description: `Lock BSV in ${requests.length} output(s)`,
94
- outputs,
95
- });
96
- if (!result.txid) {
97
- return { error: "no-txid-returned" };
98
- }
99
- return { txid: result.txid, rawtx: result.tx ? Utils.toHex(result.tx) : undefined };
30
+ const response = await fetch(`${baseUrl}/chain/info`, { headers });
31
+ if (!response.ok)
32
+ return null;
33
+ return await response.json();
100
34
  }
101
- catch (error) {
102
- return { error: error instanceof Error ? error.message : "unknown-error" };
35
+ catch {
36
+ return null;
103
37
  }
104
38
  }
105
39
  /**
106
- * Unlock matured BSV locks.
107
- * Uses createSignature with stored keyID to sign unlock transactions.
40
+ * Get lock data summary.
108
41
  */
109
- export async function unlockBsv(cwi, chain = "main", wocApiKey) {
110
- try {
111
- // Get current block height
112
- const chainInfo = await getChainInfo(chain, wocApiKey);
42
+ export const getLockData = {
43
+ meta: {
44
+ name: "getLockData",
45
+ description: "Get summary of time-locked BSV (total, unlockable, next unlock height)",
46
+ category: "locks",
47
+ inputSchema: {
48
+ type: "object",
49
+ properties: {},
50
+ },
51
+ },
52
+ async execute(ctx) {
53
+ const lockData = { totalLocked: 0, unlockable: 0, nextUnlock: 0 };
54
+ const chainInfo = await getChainInfoInternal(ctx.chain, ctx.wocApiKey);
113
55
  const currentHeight = chainInfo?.blocks || 0;
114
- if (currentHeight === 0) {
115
- return { error: "could-not-get-block-height" };
116
- }
117
- // Get lock outputs from basket
118
- const result = await cwi.listOutputs({
56
+ const result = await ctx.wallet.listOutputs({
119
57
  basket: LOCK_BASKET,
120
58
  includeTags: true,
121
- include: "locking scripts",
122
59
  limit: 10000,
123
60
  });
124
- // Filter for matured locks
125
- const maturedLocks = [];
126
- for (const o of result.outputs) {
127
- const untilTag = o.tags?.find((t) => t.startsWith("lock:until:"));
61
+ const outputs = result.outputs;
62
+ for (const o of outputs) {
63
+ const untilTag = o.tags?.find((t) => t.startsWith("until:"));
128
64
  if (!untilTag)
129
65
  continue;
130
- const until = parseInt(untilTag.slice(11), 10);
66
+ const until = Number.parseInt(untilTag.slice(6), 10);
67
+ lockData.totalLocked += o.satoshis;
131
68
  if (until <= currentHeight) {
132
- maturedLocks.push({ output: o, until });
69
+ lockData.unlockable += o.satoshis;
70
+ }
71
+ else if (!lockData.nextUnlock || until < lockData.nextUnlock) {
72
+ lockData.nextUnlock = until;
133
73
  }
134
74
  }
135
- if (maturedLocks.length === 0) {
136
- return { error: "no-matured-locks" };
137
- }
138
- // Check minimum unlock amount
139
- const totalSats = maturedLocks.reduce((sum, l) => sum + l.output.satoshis, 0);
140
- if (totalSats < MIN_UNLOCK_SATS * maturedLocks.length) {
141
- return { error: "insufficient-unlock-amount" };
75
+ if (lockData.unlockable < MIN_UNLOCK_SATS * outputs.length) {
76
+ lockData.unlockable = 0;
142
77
  }
143
- // Find max until value for lockTime
144
- const maxUntil = Math.max(...maturedLocks.map((l) => l.until));
145
- // Build createAction args
146
- const createResult = await cwi.createAction({
147
- description: `Unlock ${maturedLocks.length} lock(s)`,
148
- inputs: maturedLocks.map((l) => ({
149
- outpoint: l.output.outpoint,
150
- inputDescription: "Locked BSV",
151
- unlockingScriptLength: 180, // sig + pubkey + preimage estimate
152
- sequenceNumber: 0, // Must be < 0xffffffff for nLockTime
153
- })),
154
- outputs: [
155
- {
156
- lockingScript: "", // Will be filled by wallet as change
157
- satoshis: 0, // Will be calculated by wallet
158
- outputDescription: "Unlocked BSV",
78
+ return lockData;
79
+ },
80
+ };
81
+ /**
82
+ * Lock BSV until a block height.
83
+ */
84
+ export const lockBsv = {
85
+ meta: {
86
+ name: "lockBsv",
87
+ description: "Lock BSV until a specific block height",
88
+ category: "locks",
89
+ inputSchema: {
90
+ type: "object",
91
+ properties: {
92
+ requests: {
93
+ type: "array",
94
+ description: "Array of lock requests",
95
+ items: {
96
+ type: "object",
97
+ properties: {
98
+ satoshis: { type: "integer", description: "Amount in satoshis to lock" },
99
+ until: { type: "integer", description: "Block height until which to lock" },
100
+ },
101
+ required: ["satoshis", "until"],
102
+ },
159
103
  },
160
- ],
161
- lockTime: maxUntil,
162
- options: { signAndProcess: false },
163
- });
164
- if ("error" in createResult && createResult.error) {
165
- return { error: String(createResult.error) };
104
+ },
105
+ required: ["requests"],
106
+ },
107
+ },
108
+ async execute(ctx, input) {
109
+ try {
110
+ const { requests } = input;
111
+ if (!requests || requests.length === 0) {
112
+ return { error: "no-lock-requests" };
113
+ }
114
+ const { publicKey } = await ctx.wallet.getPublicKey({
115
+ protocolID: LOCK_PROTOCOL,
116
+ keyID: LOCK_KEY_ID,
117
+ counterparty: "self",
118
+ forSelf: true,
119
+ });
120
+ const lockAddress = PublicKey.fromString(publicKey).toAddress();
121
+ const outputs = [];
122
+ for (const req of requests) {
123
+ if (req.satoshis <= 0)
124
+ return { error: "invalid-satoshis" };
125
+ if (req.until <= 0)
126
+ return { error: "invalid-block-height" };
127
+ const lockingScript = buildLockScript(lockAddress, req.until);
128
+ outputs.push({
129
+ lockingScript: lockingScript.toHex(),
130
+ satoshis: req.satoshis,
131
+ outputDescription: `Lock ${req.satoshis} sats until block ${req.until}`,
132
+ basket: LOCK_BASKET,
133
+ tags: [`until:${req.until}`],
134
+ customInstructions: JSON.stringify({
135
+ protocolID: LOCK_PROTOCOL,
136
+ keyID: LOCK_KEY_ID,
137
+ }),
138
+ });
139
+ }
140
+ const result = await ctx.wallet.createAction({
141
+ description: `Lock BSV in ${requests.length} output(s)`,
142
+ outputs,
143
+ });
144
+ if (!result.txid) {
145
+ return { error: "no-txid-returned" };
146
+ }
147
+ return { txid: result.txid, rawtx: result.tx ? Utils.toHex(result.tx) : undefined };
166
148
  }
167
- if (!createResult.signableTransaction) {
168
- return { error: "no-signable-transaction" };
149
+ catch (error) {
150
+ return { error: error instanceof Error ? error.message : "unknown-error" };
169
151
  }
170
- // Parse transaction from BEEF
171
- const tx = Transaction.fromBEEF(createResult.signableTransaction.tx);
172
- // Build unlocking scripts for each input
173
- const spends = {};
174
- for (let i = 0; i < maturedLocks.length; i++) {
175
- const lock = maturedLocks[i];
176
- const input = tx.inputs[i];
177
- const lockingScript = Script.fromHex(lock.output.lockingScript);
178
- // Build preimage for signature
179
- const preimage = TransactionSignature.format({
180
- sourceTXID: input.sourceTXID,
181
- sourceOutputIndex: input.sourceOutputIndex,
182
- sourceSatoshis: lock.output.satoshis,
183
- transactionVersion: tx.version,
184
- otherInputs: tx.inputs.filter((_, idx) => idx !== i),
185
- outputs: tx.outputs,
186
- inputIndex: i,
187
- subscript: lockingScript,
188
- inputSequence: 0,
189
- lockTime: tx.lockTime,
190
- scope: TransactionSignature.SIGHASH_ALL |
191
- TransactionSignature.SIGHASH_ANYONECANPAY |
192
- TransactionSignature.SIGHASH_FORKID,
152
+ },
153
+ };
154
+ /**
155
+ * Unlock matured BSV locks.
156
+ */
157
+ export const unlockBsv = {
158
+ meta: {
159
+ name: "unlockBsv",
160
+ description: "Unlock all matured time-locked BSV",
161
+ category: "locks",
162
+ inputSchema: {
163
+ type: "object",
164
+ properties: {},
165
+ },
166
+ },
167
+ async execute(ctx) {
168
+ try {
169
+ const chainInfo = await getChainInfoInternal(ctx.chain, ctx.wocApiKey);
170
+ const currentHeight = chainInfo?.blocks || 0;
171
+ if (currentHeight === 0) {
172
+ return { error: "could-not-get-block-height" };
173
+ }
174
+ const result = await ctx.wallet.listOutputs({
175
+ basket: LOCK_BASKET,
176
+ includeTags: true,
177
+ includeCustomInstructions: true,
178
+ include: "locking scripts",
179
+ limit: 10000,
193
180
  });
194
- // Hash preimage for signing
195
- const sighash = Hash.sha256(Hash.sha256(preimage));
196
- // Get signature via createSignature using hardcoded keyID
197
- const { signature } = await cwi.createSignature({
198
- protocolID: [1, "lock"],
199
- keyID: LOCK_KEY_ID,
200
- counterparty: "self",
201
- hashToDirectlySign: Array.from(sighash),
181
+ const maturedLocks = [];
182
+ for (const o of result.outputs) {
183
+ const untilTag = o.tags?.find((t) => t.startsWith("until:"));
184
+ if (!untilTag)
185
+ continue;
186
+ const until = Number.parseInt(untilTag.slice(6), 10);
187
+ let protocolID = LOCK_PROTOCOL;
188
+ let keyID = LOCK_KEY_ID;
189
+ if (o.customInstructions) {
190
+ const instructions = JSON.parse(o.customInstructions);
191
+ protocolID = instructions.protocolID ?? LOCK_PROTOCOL;
192
+ keyID = instructions.keyID ?? LOCK_KEY_ID;
193
+ }
194
+ if (until <= currentHeight) {
195
+ maturedLocks.push({ output: o, until, protocolID, keyID });
196
+ }
197
+ }
198
+ if (maturedLocks.length === 0) {
199
+ return { error: "no-matured-locks" };
200
+ }
201
+ const totalSats = maturedLocks.reduce((sum, l) => sum + l.output.satoshis, 0);
202
+ if (totalSats < MIN_UNLOCK_SATS * maturedLocks.length) {
203
+ return { error: "insufficient-unlock-amount" };
204
+ }
205
+ const maxUntil = Math.max(...maturedLocks.map((l) => l.until));
206
+ const createResult = await ctx.wallet.createAction({
207
+ description: `Unlock ${maturedLocks.length} lock(s)`,
208
+ inputs: maturedLocks.map((l) => ({
209
+ outpoint: l.output.outpoint,
210
+ inputDescription: "Locked BSV",
211
+ unlockingScriptLength: 180,
212
+ sequenceNumber: 0,
213
+ })),
214
+ outputs: [
215
+ {
216
+ lockingScript: "",
217
+ satoshis: 0,
218
+ outputDescription: "Unlocked BSV",
219
+ },
220
+ ],
221
+ lockTime: maxUntil,
222
+ options: { signAndProcess: false },
202
223
  });
203
- // Get public key
204
- const { publicKey } = await cwi.getPublicKey({
205
- protocolID: [1, "lock"],
206
- keyID: LOCK_KEY_ID,
207
- counterparty: "self",
208
- forSelf: true,
224
+ if ("error" in createResult && createResult.error) {
225
+ return { error: String(createResult.error) };
226
+ }
227
+ if (!createResult.signableTransaction) {
228
+ return { error: "no-signable-transaction" };
229
+ }
230
+ const tx = Transaction.fromBEEF(createResult.signableTransaction.tx);
231
+ const spends = {};
232
+ for (let i = 0; i < maturedLocks.length; i++) {
233
+ const lock = maturedLocks[i];
234
+ const input = tx.inputs[i];
235
+ const lockingScript = Script.fromHex(lock.output.lockingScript);
236
+ const preimage = TransactionSignature.format({
237
+ sourceTXID: input.sourceTXID,
238
+ sourceOutputIndex: input.sourceOutputIndex,
239
+ sourceSatoshis: lock.output.satoshis,
240
+ transactionVersion: tx.version,
241
+ otherInputs: tx.inputs.filter((_, idx) => idx !== i),
242
+ outputs: tx.outputs,
243
+ inputIndex: i,
244
+ subscript: lockingScript,
245
+ inputSequence: 0,
246
+ lockTime: tx.lockTime,
247
+ scope: TransactionSignature.SIGHASH_ALL |
248
+ TransactionSignature.SIGHASH_ANYONECANPAY |
249
+ TransactionSignature.SIGHASH_FORKID,
250
+ });
251
+ const sighash = Hash.sha256(Hash.sha256(preimage));
252
+ const { signature } = await ctx.wallet.createSignature({
253
+ protocolID: lock.protocolID,
254
+ keyID: lock.keyID,
255
+ counterparty: "self",
256
+ hashToDirectlySign: Array.from(sighash),
257
+ });
258
+ const { publicKey } = await ctx.wallet.getPublicKey({
259
+ protocolID: lock.protocolID,
260
+ keyID: lock.keyID,
261
+ counterparty: "self",
262
+ forSelf: true,
263
+ });
264
+ const unlockingScript = new Script()
265
+ .writeBin(signature)
266
+ .writeBin(Utils.toArray(publicKey, "hex"))
267
+ .writeBin(Array.from(preimage));
268
+ spends[i] = { unlockingScript: unlockingScript.toHex() };
269
+ }
270
+ const signResult = await ctx.wallet.signAction({
271
+ reference: createResult.signableTransaction.reference,
272
+ spends,
209
273
  });
210
- // Build unlocking script: <sig> <pubkey> <preimage>
211
- const unlockingScript = new Script()
212
- .writeBin(signature)
213
- .writeBin(Utils.toArray(publicKey, "hex"))
214
- .writeBin(Array.from(preimage));
215
- spends[i] = { unlockingScript: unlockingScript.toHex() };
274
+ if ("error" in signResult) {
275
+ return { error: String(signResult.error) };
276
+ }
277
+ return {
278
+ txid: signResult.txid,
279
+ rawtx: signResult.tx ? Utils.toHex(signResult.tx) : undefined,
280
+ };
216
281
  }
217
- // Sign and broadcast
218
- const signResult = await cwi.signAction({
219
- reference: createResult.signableTransaction.reference,
220
- spends,
221
- });
222
- if ("error" in signResult) {
223
- return { error: String(signResult.error) };
282
+ catch (error) {
283
+ return { error: error instanceof Error ? error.message : "unknown-error" };
224
284
  }
225
- return {
226
- txid: signResult.txid,
227
- rawtx: signResult.tx ? Utils.toHex(signResult.tx) : undefined,
228
- };
229
- }
230
- catch (error) {
231
- return { error: error instanceof Error ? error.message : "unknown-error" };
232
- }
233
- }
285
+ },
286
+ };
287
+ // ============================================================================
288
+ // Module exports
289
+ // ============================================================================
290
+ /** All lock skills for registry */
291
+ export const locksSkills = [getLockData, lockBsv, unlockBsv];