@1sat/wallet-toolbox 0.0.7 → 0.0.9

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 (57) hide show
  1. package/dist/api/OneSatApi.d.ts +100 -0
  2. package/dist/api/OneSatApi.js +156 -0
  3. package/dist/api/balance/index.d.ts +38 -0
  4. package/dist/api/balance/index.js +82 -0
  5. package/dist/api/broadcast/index.d.ts +22 -0
  6. package/dist/api/broadcast/index.js +45 -0
  7. package/dist/api/constants.d.ts +21 -0
  8. package/dist/api/constants.js +29 -0
  9. package/dist/api/index.d.ts +36 -0
  10. package/dist/api/index.js +38 -0
  11. package/dist/api/inscriptions/index.d.ts +25 -0
  12. package/dist/api/inscriptions/index.js +50 -0
  13. package/dist/api/locks/index.d.ts +44 -0
  14. package/dist/api/locks/index.js +233 -0
  15. package/dist/api/ordinals/index.d.ts +87 -0
  16. package/dist/api/ordinals/index.js +446 -0
  17. package/dist/api/payments/index.d.ts +37 -0
  18. package/dist/api/payments/index.js +130 -0
  19. package/dist/api/signing/index.d.ts +32 -0
  20. package/dist/api/signing/index.js +41 -0
  21. package/dist/api/tokens/index.d.ts +88 -0
  22. package/dist/api/tokens/index.js +400 -0
  23. package/dist/cwi/chrome.d.ts +11 -0
  24. package/dist/cwi/chrome.js +39 -0
  25. package/dist/cwi/event.d.ts +11 -0
  26. package/dist/cwi/event.js +38 -0
  27. package/dist/cwi/factory.d.ts +14 -0
  28. package/dist/cwi/factory.js +44 -0
  29. package/dist/cwi/index.d.ts +11 -0
  30. package/dist/cwi/index.js +11 -0
  31. package/dist/cwi/types.d.ts +39 -0
  32. package/dist/cwi/types.js +39 -0
  33. package/dist/index.d.ts +2 -1
  34. package/dist/index.js +5 -1
  35. package/dist/indexers/Bsv21Indexer.js +1 -1
  36. package/dist/indexers/CosignIndexer.js +2 -1
  37. package/dist/indexers/InscriptionIndexer.js +4 -5
  38. package/dist/indexers/LockIndexer.js +2 -1
  39. package/dist/indexers/MapIndexer.js +1 -1
  40. package/dist/indexers/OrdLockIndexer.js +2 -1
  41. package/dist/indexers/OriginIndexer.js +1 -1
  42. package/dist/indexers/index.d.ts +1 -1
  43. package/dist/indexers/types.d.ts +18 -0
  44. package/dist/services/OneSatServices.d.ts +19 -10
  45. package/dist/services/OneSatServices.js +201 -39
  46. package/dist/services/client/ChaintracksClient.d.ts +55 -13
  47. package/dist/services/client/ChaintracksClient.js +123 -28
  48. package/dist/services/client/OrdfsClient.d.ts +2 -2
  49. package/dist/services/client/OrdfsClient.js +4 -3
  50. package/dist/services/client/TxoClient.js +9 -0
  51. package/dist/sync/AddressManager.d.ts +85 -0
  52. package/dist/sync/AddressManager.js +107 -0
  53. package/dist/sync/SyncManager.d.ts +207 -0
  54. package/dist/sync/SyncManager.js +507 -0
  55. package/dist/sync/index.d.ts +4 -0
  56. package/dist/sync/index.js +2 -0
  57. package/package.json +5 -4
@@ -0,0 +1,446 @@
1
+ /**
2
+ * Ordinals Module
3
+ *
4
+ * Functions for managing ordinals/inscriptions.
5
+ * Returns WalletOutput[] directly from the SDK - no custom mapping needed.
6
+ */
7
+ import { BigNumber, Hash, LockingScript, OP, P2PKH, PublicKey, Script, Transaction, TransactionSignature, UnlockingScript, Utils, } from "@bsv/sdk";
8
+ import { OrdLock } from "@bopen-io/templates";
9
+ import { ORDINALS_BASKET, ORDLOCK_PREFIX, ORDLOCK_SUFFIX } from "../constants";
10
+ // Protocol for ordinal listing key derivation (security level 1 = low, self-only)
11
+ 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({
24
+ protocolID: ORDINAL_LISTING_PROTOCOL,
25
+ keyID: outpoint,
26
+ forSelf: true,
27
+ });
28
+ const publicKey = PublicKey.fromString(result.publicKey);
29
+ return publicKey.toAddress();
30
+ }
31
+ /**
32
+ * Build OrdLock script for listing an ordinal.
33
+ */
34
+ function buildOrdLockScript(ordAddress, payAddress, price) {
35
+ const cancelPkh = Utils.fromBase58Check(ordAddress).data;
36
+ const payPkh = Utils.fromBase58Check(payAddress).data;
37
+ const payoutScript = new P2PKH().lock(payPkh).toBinary();
38
+ const writer = new Utils.Writer();
39
+ writer.writeUInt64LEBn(new BigNumber(price));
40
+ writer.writeVarIntNum(payoutScript.length);
41
+ writer.write(payoutScript);
42
+ const payoutOutput = writer.toArray();
43
+ return new Script()
44
+ .writeScript(Script.fromHex(ORDLOCK_PREFIX))
45
+ .writeBin(cancelPkh)
46
+ .writeBin(payoutOutput)
47
+ .writeScript(Script.fromHex(ORDLOCK_SUFFIX));
48
+ }
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,
61
+ });
62
+ return result.outputs;
63
+ }
64
+ /**
65
+ * Build CreateActionArgs for transferring an ordinal.
66
+ * Does NOT execute - returns params for createAction.
67
+ */
68
+ export async function buildTransferOrdinal(cwi, request) {
69
+ const { outpoint, destination } = request;
70
+ if (isPaymail(destination)) {
71
+ return { error: "paymail-not-yet-implemented" };
72
+ }
73
+ const result = await cwi.listOutputs({
74
+ basket: ORDINALS_BASKET,
75
+ include: "locking scripts",
76
+ limit: 10000,
77
+ });
78
+ if (!result.outputs.find((o) => o.outpoint === outpoint)) {
79
+ return { error: "ordinal-not-found" };
80
+ }
81
+ return {
82
+ description: "Transfer ordinal",
83
+ inputs: [{ outpoint, inputDescription: "Ordinal to transfer" }],
84
+ outputs: [{
85
+ lockingScript: new P2PKH().lock(destination).toHex(),
86
+ satoshis: 1,
87
+ outputDescription: "Ordinal transfer",
88
+ }],
89
+ };
90
+ }
91
+ /**
92
+ * Build CreateActionArgs for listing an ordinal for sale.
93
+ * Does NOT execute - returns params for createAction.
94
+ * If cancelAddress is not provided, it will be derived from the CWI.
95
+ */
96
+ export async function buildListOrdinal(cwi, request) {
97
+ const { outpoint, price, payAddress } = request;
98
+ if (!payAddress)
99
+ return { error: "missing-pay-address" };
100
+ if (price <= 0)
101
+ return { error: "invalid-price" };
102
+ const result = await cwi.listOutputs({
103
+ basket: ORDINALS_BASKET,
104
+ include: "locking scripts",
105
+ limit: 10000,
106
+ });
107
+ if (!result.outputs.find((o) => o.outpoint === outpoint)) {
108
+ return { error: "ordinal-not-found" };
109
+ }
110
+ // Derive cancel address if not provided
111
+ const cancelAddress = request.cancelAddress ?? await deriveCancelAddress(cwi, outpoint);
112
+ const lockingScript = buildOrdLockScript(cancelAddress, payAddress, price);
113
+ return {
114
+ description: `List ordinal for ${price} sats`,
115
+ inputs: [{ outpoint, inputDescription: "Ordinal to list" }],
116
+ outputs: [{
117
+ lockingScript: lockingScript.toHex(),
118
+ satoshis: 1,
119
+ outputDescription: `List ordinal for ${price} sats`,
120
+ basket: ORDINALS_BASKET,
121
+ tags: [`origin:${outpoint}`, `price:${price}`],
122
+ }],
123
+ };
124
+ }
125
+ /**
126
+ * Transfer an ordinal to a new address.
127
+ */
128
+ export async function transferOrdinal(cwi, request) {
129
+ try {
130
+ const params = await buildTransferOrdinal(cwi, request);
131
+ if ("error" in params) {
132
+ return params;
133
+ }
134
+ const result = await cwi.createAction(params);
135
+ if (!result.txid) {
136
+ return { error: "no-txid-returned" };
137
+ }
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
+ }
144
+ /**
145
+ * List an ordinal for sale on the global orderbook.
146
+ */
147
+ export async function listOrdinal(cwi, request) {
148
+ try {
149
+ const params = await buildListOrdinal(cwi, request);
150
+ if ("error" in params) {
151
+ return params;
152
+ }
153
+ const result = await cwi.createAction(params);
154
+ if (!result.txid) {
155
+ return { error: "no-txid-returned" };
156
+ }
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
+ }
163
+ /**
164
+ * Cancel an ordinal listing.
165
+ * Uses the origin tag to recover the keyID for signing.
166
+ * Cancel unlock script: <sig> <pubkey> OP_1
167
+ */
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() },
260
+ },
261
+ });
262
+ if ("error" in signResult) {
263
+ return { error: String(signResult.error) };
264
+ }
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()));
298
+ }
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
+ }
326
+ /**
327
+ * 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
+ */
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
+ });
403
+ }
404
+ }
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) };
437
+ }
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
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Payments Module
3
+ *
4
+ * Functions for sending BSV payments.
5
+ */
6
+ import { type WalletInterface } from "@bsv/sdk";
7
+ export interface SendBsvRequest {
8
+ /** Destination address (P2PKH) */
9
+ address?: string;
10
+ /** Destination paymail */
11
+ paymail?: string;
12
+ /** Amount in satoshis */
13
+ satoshis: number;
14
+ /** Custom locking script (hex) */
15
+ script?: string;
16
+ /** OP_RETURN data */
17
+ data?: string[];
18
+ /** Inscription data */
19
+ inscription?: {
20
+ base64Data: string;
21
+ mimeType: string;
22
+ map?: Record<string, string>;
23
+ };
24
+ }
25
+ export interface SendBsvResponse {
26
+ txid?: string;
27
+ rawtx?: string;
28
+ error?: string;
29
+ }
30
+ /**
31
+ * Send BSV to one or more destinations.
32
+ */
33
+ export declare function sendBsv(cwi: WalletInterface, requests: SendBsvRequest[]): Promise<SendBsvResponse>;
34
+ /**
35
+ * Send all BSV to a destination address.
36
+ */
37
+ export declare function sendAllBsv(cwi: WalletInterface, destination: string): Promise<SendBsvResponse>;
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Payments Module
3
+ *
4
+ * Functions for sending BSV payments.
5
+ */
6
+ import { P2PKH, Script, Utils, } from "@bsv/sdk";
7
+ import { Inscription } from "@bopen-io/templates";
8
+ import { FUNDING_BASKET } from "../constants";
9
+ /**
10
+ * Check if address is a paymail.
11
+ */
12
+ function isPaymail(address) {
13
+ return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(address);
14
+ }
15
+ /**
16
+ * Build an inscription locking script.
17
+ */
18
+ function buildInscriptionScript(address, base64Data, mimeType) {
19
+ const content = Utils.toArray(base64Data, "base64");
20
+ const inscription = Inscription.create(new Uint8Array(content), mimeType);
21
+ const inscriptionScript = inscription.lock();
22
+ const p2pkhScript = new P2PKH().lock(address);
23
+ const combined = new Script();
24
+ for (const chunk of inscriptionScript.chunks)
25
+ combined.chunks.push(chunk);
26
+ for (const chunk of p2pkhScript.chunks)
27
+ combined.chunks.push(chunk);
28
+ return combined;
29
+ }
30
+ /**
31
+ * Send BSV to one or more destinations.
32
+ */
33
+ export async function sendBsv(cwi, requests) {
34
+ try {
35
+ if (!requests || requests.length === 0) {
36
+ return { error: "no-requests" };
37
+ }
38
+ const outputs = [];
39
+ for (const req of requests) {
40
+ let lockingScript;
41
+ if (req.script) {
42
+ lockingScript = Script.fromHex(req.script);
43
+ }
44
+ else if (req.address) {
45
+ if (req.inscription) {
46
+ lockingScript = buildInscriptionScript(req.address, req.inscription.base64Data, req.inscription.mimeType);
47
+ }
48
+ else {
49
+ lockingScript = new P2PKH().lock(req.address);
50
+ }
51
+ }
52
+ else if (req.data && req.data.length > 0) {
53
+ try {
54
+ lockingScript = Script.fromASM(`OP_0 OP_RETURN ${req.data.join(" ")}`);
55
+ }
56
+ catch {
57
+ return { error: "invalid-data" };
58
+ }
59
+ }
60
+ else if (req.paymail) {
61
+ return { error: "paymail-not-yet-implemented" };
62
+ }
63
+ else {
64
+ return { error: "invalid-request" };
65
+ }
66
+ outputs.push({
67
+ lockingScript: lockingScript.toHex(),
68
+ satoshis: req.satoshis,
69
+ outputDescription: `Payment of ${req.satoshis} sats`,
70
+ });
71
+ }
72
+ const result = await cwi.createAction({
73
+ description: `Send ${requests.length} payment(s)`,
74
+ outputs,
75
+ options: { signAndProcess: true },
76
+ });
77
+ if (!result.txid) {
78
+ return { error: "no-txid-returned" };
79
+ }
80
+ return { txid: result.txid, rawtx: result.tx ? Utils.toHex(result.tx) : undefined };
81
+ }
82
+ catch (error) {
83
+ return { error: error instanceof Error ? error.message : "unknown-error" };
84
+ }
85
+ }
86
+ /**
87
+ * Send all BSV to a destination address.
88
+ */
89
+ export async function sendAllBsv(cwi, destination) {
90
+ try {
91
+ if (isPaymail(destination)) {
92
+ return { error: "paymail-not-yet-implemented" };
93
+ }
94
+ const listResult = await cwi.listOutputs({
95
+ basket: FUNDING_BASKET,
96
+ include: "locking scripts",
97
+ limit: 10000,
98
+ });
99
+ if (!listResult.outputs || listResult.outputs.length === 0) {
100
+ return { error: "no-funds" };
101
+ }
102
+ const totalSats = listResult.outputs.reduce((sum, o) => sum + o.satoshis, 0);
103
+ const estimatedFee = Math.ceil((listResult.outputs.length * 150 + 44) * 1);
104
+ const sendAmount = totalSats - estimatedFee;
105
+ if (sendAmount <= 0) {
106
+ return { error: "insufficient-funds-for-fee" };
107
+ }
108
+ const inputs = listResult.outputs.map((o) => ({
109
+ outpoint: o.outpoint,
110
+ inputDescription: "Sweep funds",
111
+ }));
112
+ const result = await cwi.createAction({
113
+ description: "Send all BSV",
114
+ inputs,
115
+ outputs: [{
116
+ lockingScript: new P2PKH().lock(destination).toHex(),
117
+ satoshis: sendAmount,
118
+ outputDescription: "Sweep all funds",
119
+ }],
120
+ options: { signAndProcess: true },
121
+ });
122
+ if (!result.txid) {
123
+ return { error: "no-txid-returned" };
124
+ }
125
+ return { txid: result.txid, rawtx: result.tx ? Utils.toHex(result.tx) : undefined };
126
+ }
127
+ catch (error) {
128
+ return { error: error instanceof Error ? error.message : "unknown-error" };
129
+ }
130
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Signing Module
3
+ *
4
+ * Functions for message signing.
5
+ */
6
+ import { type WalletInterface } from "@bsv/sdk";
7
+ export interface SignMessageRequest {
8
+ /** Message to sign */
9
+ message: string;
10
+ /** Message encoding */
11
+ encoding?: "utf8" | "hex" | "base64";
12
+ /** Derivation tag for key selection */
13
+ tag?: {
14
+ label: string;
15
+ id: string;
16
+ domain: string;
17
+ meta: Record<string, string>;
18
+ };
19
+ }
20
+ export interface SignedMessage {
21
+ address: string;
22
+ pubKey: string;
23
+ message: string;
24
+ sig: string;
25
+ derivationTag?: SignMessageRequest["tag"];
26
+ }
27
+ /**
28
+ * Sign a message using BSM (Bitcoin Signed Message) format.
29
+ */
30
+ export declare function signMessage(cwi: WalletInterface, request: SignMessageRequest): Promise<SignedMessage | {
31
+ error: string;
32
+ }>;
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Signing Module
3
+ *
4
+ * Functions for message signing.
5
+ */
6
+ import { BigNumber, BSM, PublicKey, Signature, Utils, } from "@bsv/sdk";
7
+ import { MESSAGE_SIGNING_PROTOCOL } from "../constants";
8
+ /**
9
+ * Sign a message using BSM (Bitcoin Signed Message) format.
10
+ */
11
+ export async function signMessage(cwi, request) {
12
+ try {
13
+ const { message, encoding = "utf8", tag } = request;
14
+ const messageBytes = Utils.toArray(message, encoding);
15
+ const msgHash = BSM.magicHash(messageBytes);
16
+ const keyID = tag ? `${tag.label}:${tag.id}:${tag.domain}` : "identity";
17
+ const result = await cwi.createSignature({
18
+ protocolID: MESSAGE_SIGNING_PROTOCOL,
19
+ keyID,
20
+ hashToDirectlySign: Array.from(msgHash),
21
+ });
22
+ const pubKeyResult = await cwi.getPublicKey({
23
+ protocolID: MESSAGE_SIGNING_PROTOCOL,
24
+ keyID,
25
+ forSelf: true,
26
+ });
27
+ const publicKey = PublicKey.fromString(pubKeyResult.publicKey);
28
+ const signature = Signature.fromDER(result.signature);
29
+ const recovery = signature.CalculateRecoveryFactor(publicKey, new BigNumber(msgHash));
30
+ return {
31
+ address: publicKey.toAddress(),
32
+ pubKey: publicKey.toString(),
33
+ message,
34
+ sig: signature.toCompact(recovery, true, "base64"),
35
+ derivationTag: tag,
36
+ };
37
+ }
38
+ catch (error) {
39
+ return { error: error instanceof Error ? error.message : "unknown-error" };
40
+ }
41
+ }