@1sat/wallet-toolbox 0.0.8 → 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.
- package/dist/api/OneSatApi.d.ts +100 -0
- package/dist/api/OneSatApi.js +156 -0
- package/dist/api/balance/index.d.ts +38 -0
- package/dist/api/balance/index.js +82 -0
- package/dist/api/broadcast/index.d.ts +22 -0
- package/dist/api/broadcast/index.js +45 -0
- package/dist/api/constants.d.ts +21 -0
- package/dist/api/constants.js +29 -0
- package/dist/api/index.d.ts +36 -0
- package/dist/api/index.js +38 -0
- package/dist/api/inscriptions/index.d.ts +25 -0
- package/dist/api/inscriptions/index.js +50 -0
- package/dist/api/locks/index.d.ts +44 -0
- package/dist/api/locks/index.js +233 -0
- package/dist/api/ordinals/index.d.ts +87 -0
- package/dist/api/ordinals/index.js +446 -0
- package/dist/api/payments/index.d.ts +37 -0
- package/dist/api/payments/index.js +130 -0
- package/dist/api/signing/index.d.ts +32 -0
- package/dist/api/signing/index.js +41 -0
- package/dist/api/tokens/index.d.ts +88 -0
- package/dist/api/tokens/index.js +400 -0
- package/dist/cwi/chrome.d.ts +11 -0
- package/dist/cwi/chrome.js +39 -0
- package/dist/cwi/event.d.ts +11 -0
- package/dist/cwi/event.js +38 -0
- package/dist/cwi/factory.d.ts +14 -0
- package/dist/cwi/factory.js +44 -0
- package/dist/cwi/index.d.ts +11 -0
- package/dist/cwi/index.js +11 -0
- package/dist/cwi/types.d.ts +39 -0
- package/dist/cwi/types.js +39 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +5 -1
- package/dist/indexers/CosignIndexer.js +1 -0
- package/dist/indexers/InscriptionIndexer.js +3 -4
- package/dist/indexers/LockIndexer.js +1 -0
- package/dist/indexers/OrdLockIndexer.js +1 -0
- package/dist/indexers/OriginIndexer.js +1 -1
- package/dist/indexers/index.d.ts +1 -1
- package/dist/indexers/types.d.ts +18 -0
- package/dist/services/OneSatServices.d.ts +19 -10
- package/dist/services/OneSatServices.js +201 -39
- package/dist/services/client/ChaintracksClient.d.ts +55 -13
- package/dist/services/client/ChaintracksClient.js +123 -28
- package/dist/services/client/OrdfsClient.d.ts +2 -2
- package/dist/services/client/OrdfsClient.js +4 -3
- package/dist/services/client/TxoClient.js +9 -0
- package/dist/sync/AddressManager.d.ts +85 -0
- package/dist/sync/AddressManager.js +107 -0
- package/dist/sync/SyncManager.d.ts +207 -0
- package/dist/sync/SyncManager.js +507 -0
- package/dist/sync/index.d.ts +4 -0
- package/dist/sync/index.js +2 -0
- 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
|
+
}
|