@1sat/wallet-toolbox 0.0.8 → 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.
- package/dist/api/balance/index.d.ts +50 -0
- package/dist/api/balance/index.js +135 -0
- package/dist/api/broadcast/index.d.ts +24 -0
- package/dist/api/broadcast/index.js +73 -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 +59 -0
- package/dist/api/inscriptions/index.d.ts +25 -0
- package/dist/api/inscriptions/index.js +98 -0
- package/dist/api/locks/index.d.ts +47 -0
- package/dist/api/locks/index.js +291 -0
- package/dist/api/ordinals/index.d.ts +102 -0
- package/dist/api/ordinals/index.js +566 -0
- package/dist/api/payments/index.d.ts +48 -0
- package/dist/api/payments/index.js +185 -0
- package/dist/api/signing/index.d.ts +35 -0
- package/dist/api/signing/index.js +78 -0
- package/dist/api/skills/registry.d.ts +61 -0
- package/dist/api/skills/registry.js +74 -0
- package/dist/api/skills/types.d.ts +71 -0
- package/dist/api/skills/types.js +14 -0
- package/dist/api/tokens/index.d.ts +87 -0
- package/dist/api/tokens/index.js +457 -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 +3 -1
- package/dist/index.js +7 -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/dist/wallet/factory.d.ts +64 -0
- package/dist/wallet/factory.js +129 -0
- package/dist/wallet/index.d.ts +1 -0
- package/dist/wallet/index.js +1 -0
- package/package.json +14 -4
- package/dist/OneSatWallet.d.ts +0 -316
- package/dist/OneSatWallet.js +0 -956
- package/dist/indexers/TransactionParser.d.ts +0 -53
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tokens Module
|
|
3
|
+
*
|
|
4
|
+
* Skills for managing BSV21 tokens.
|
|
5
|
+
*/
|
|
6
|
+
import { BigNumber, LockingScript, OP, P2PKH, PublicKey, Transaction, TransactionSignature, UnlockingScript, Utils, } from "@bsv/sdk";
|
|
7
|
+
import { BSV21, OrdLock } from "@bopen-io/templates";
|
|
8
|
+
import { BSV21_BASKET } from "../constants";
|
|
9
|
+
// ============================================================================
|
|
10
|
+
// Constants
|
|
11
|
+
// ============================================================================
|
|
12
|
+
const BSV21_PROTOCOL = [1, "bsv21"];
|
|
13
|
+
// ============================================================================
|
|
14
|
+
// Internal helpers
|
|
15
|
+
// ============================================================================
|
|
16
|
+
function buildSerializedOutput(satoshis, script) {
|
|
17
|
+
const writer = new Utils.Writer();
|
|
18
|
+
writer.writeUInt64LEBn(new BigNumber(satoshis));
|
|
19
|
+
writer.writeVarIntNum(script.length);
|
|
20
|
+
writer.write(script);
|
|
21
|
+
return writer.toArray();
|
|
22
|
+
}
|
|
23
|
+
async function buildPurchaseUnlockingScript(tx, inputIndex, sourceSatoshis, lockingScript) {
|
|
24
|
+
if (tx.outputs.length < 2) {
|
|
25
|
+
throw new Error("Malformed transaction: requires at least 2 outputs");
|
|
26
|
+
}
|
|
27
|
+
const script = new UnlockingScript().writeBin(buildSerializedOutput(tx.outputs[0].satoshis ?? 0, tx.outputs[0].lockingScript.toBinary()));
|
|
28
|
+
if (tx.outputs.length > 2) {
|
|
29
|
+
const writer = new Utils.Writer();
|
|
30
|
+
for (const output of tx.outputs.slice(2)) {
|
|
31
|
+
writer.write(buildSerializedOutput(output.satoshis ?? 0, output.lockingScript.toBinary()));
|
|
32
|
+
}
|
|
33
|
+
script.writeBin(writer.toArray());
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
script.writeOpCode(OP.OP_0);
|
|
37
|
+
}
|
|
38
|
+
const input = tx.inputs[inputIndex];
|
|
39
|
+
const sourceTXID = input.sourceTXID ?? input.sourceTransaction?.id("hex");
|
|
40
|
+
if (!sourceTXID) {
|
|
41
|
+
throw new Error("sourceTXID is required");
|
|
42
|
+
}
|
|
43
|
+
const preimage = TransactionSignature.format({
|
|
44
|
+
sourceTXID,
|
|
45
|
+
sourceOutputIndex: input.sourceOutputIndex,
|
|
46
|
+
sourceSatoshis,
|
|
47
|
+
transactionVersion: tx.version,
|
|
48
|
+
otherInputs: [],
|
|
49
|
+
inputIndex,
|
|
50
|
+
outputs: tx.outputs,
|
|
51
|
+
inputSequence: input.sequence ?? 0xffffffff,
|
|
52
|
+
subscript: lockingScript,
|
|
53
|
+
lockTime: tx.lockTime,
|
|
54
|
+
scope: TransactionSignature.SIGHASH_ALL |
|
|
55
|
+
TransactionSignature.SIGHASH_ANYONECANPAY |
|
|
56
|
+
TransactionSignature.SIGHASH_FORKID,
|
|
57
|
+
});
|
|
58
|
+
return script.writeBin(preimage).writeOpCode(OP.OP_0);
|
|
59
|
+
}
|
|
60
|
+
async function listTokensInternal(ctx, limit = 10000) {
|
|
61
|
+
const result = await ctx.wallet.listOutputs({
|
|
62
|
+
basket: BSV21_BASKET,
|
|
63
|
+
includeTags: true,
|
|
64
|
+
limit,
|
|
65
|
+
});
|
|
66
|
+
return result.outputs;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* List BSV21 token outputs from the wallet.
|
|
70
|
+
*/
|
|
71
|
+
export const listTokens = {
|
|
72
|
+
meta: {
|
|
73
|
+
name: "listTokens",
|
|
74
|
+
description: "List BSV21 token outputs from the wallet",
|
|
75
|
+
category: "tokens",
|
|
76
|
+
inputSchema: {
|
|
77
|
+
type: "object",
|
|
78
|
+
properties: {
|
|
79
|
+
limit: { type: "integer", description: "Max number of tokens to return (default: 10000)" },
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
async execute(ctx, input) {
|
|
84
|
+
return listTokensInternal(ctx, input.limit);
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* Get aggregated BSV21 token balances.
|
|
89
|
+
*/
|
|
90
|
+
export const getBsv21Balances = {
|
|
91
|
+
meta: {
|
|
92
|
+
name: "getBsv21Balances",
|
|
93
|
+
description: "Get aggregated BSV21 token balances grouped by token ID",
|
|
94
|
+
category: "tokens",
|
|
95
|
+
inputSchema: {
|
|
96
|
+
type: "object",
|
|
97
|
+
properties: {},
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
async execute(ctx) {
|
|
101
|
+
const outputs = await listTokensInternal(ctx);
|
|
102
|
+
const balanceMap = new Map();
|
|
103
|
+
for (const o of outputs) {
|
|
104
|
+
const idTag = o.tags?.find((t) => t.startsWith("id:"));
|
|
105
|
+
const amtTag = o.tags?.find((t) => t.startsWith("amt:"))?.slice(4);
|
|
106
|
+
if (!idTag || !amtTag)
|
|
107
|
+
continue;
|
|
108
|
+
const idContent = idTag.slice(3);
|
|
109
|
+
const lastColonIdx = idContent.lastIndexOf(":");
|
|
110
|
+
if (lastColonIdx === -1)
|
|
111
|
+
continue;
|
|
112
|
+
const tokenId = idContent.slice(0, lastColonIdx);
|
|
113
|
+
const status = idContent.slice(lastColonIdx + 1);
|
|
114
|
+
if (status === "invalid")
|
|
115
|
+
continue;
|
|
116
|
+
const isConfirmed = status === "valid";
|
|
117
|
+
const amt = BigInt(amtTag);
|
|
118
|
+
const dec = Number.parseInt(o.tags?.find((t) => t.startsWith("dec:"))?.slice(4) || "0", 10);
|
|
119
|
+
const symTag = o.tags?.find((t) => t.startsWith("sym:"))?.slice(4);
|
|
120
|
+
const iconTag = o.tags?.find((t) => t.startsWith("icon:"))?.slice(5);
|
|
121
|
+
const existing = balanceMap.get(tokenId);
|
|
122
|
+
if (existing) {
|
|
123
|
+
if (isConfirmed)
|
|
124
|
+
existing.confirmed += amt;
|
|
125
|
+
else
|
|
126
|
+
existing.pending += amt;
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
balanceMap.set(tokenId, {
|
|
130
|
+
id: tokenId,
|
|
131
|
+
confirmed: isConfirmed ? amt : 0n,
|
|
132
|
+
pending: isConfirmed ? 0n : amt,
|
|
133
|
+
sym: symTag,
|
|
134
|
+
icon: iconTag,
|
|
135
|
+
dec,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return Array.from(balanceMap.values()).map((b) => ({
|
|
140
|
+
p: "bsv-20",
|
|
141
|
+
op: "transfer",
|
|
142
|
+
dec: b.dec,
|
|
143
|
+
amt: (b.confirmed + b.pending).toString(),
|
|
144
|
+
id: b.id,
|
|
145
|
+
sym: b.sym,
|
|
146
|
+
icon: b.icon,
|
|
147
|
+
all: { confirmed: b.confirmed, pending: b.pending },
|
|
148
|
+
listed: { confirmed: 0n, pending: 0n },
|
|
149
|
+
}));
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
/**
|
|
153
|
+
* Send BSV21 tokens to an address.
|
|
154
|
+
*/
|
|
155
|
+
export const sendBsv21 = {
|
|
156
|
+
meta: {
|
|
157
|
+
name: "sendBsv21",
|
|
158
|
+
description: "Send BSV21 tokens to a counterparty, address, or paymail",
|
|
159
|
+
category: "tokens",
|
|
160
|
+
inputSchema: {
|
|
161
|
+
type: "object",
|
|
162
|
+
properties: {
|
|
163
|
+
tokenId: { type: "string", description: "Token ID (txid_vout format)" },
|
|
164
|
+
amount: { type: "string", description: "Amount to send (as string for bigint)" },
|
|
165
|
+
counterparty: { type: "string", description: "Recipient identity public key (hex)" },
|
|
166
|
+
address: { type: "string", description: "Recipient P2PKH address" },
|
|
167
|
+
paymail: { type: "string", description: "Recipient paymail address" },
|
|
168
|
+
},
|
|
169
|
+
required: ["tokenId", "amount"],
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
async execute(ctx, input) {
|
|
173
|
+
try {
|
|
174
|
+
const { tokenId, counterparty, address, paymail, amount: rawAmount } = input;
|
|
175
|
+
const amount = typeof rawAmount === "string" ? BigInt(rawAmount) : rawAmount;
|
|
176
|
+
if (!counterparty && !address && !paymail) {
|
|
177
|
+
return { error: "must-provide-counterparty-address-or-paymail" };
|
|
178
|
+
}
|
|
179
|
+
if (amount <= 0n) {
|
|
180
|
+
return { error: "amount-must-be-positive" };
|
|
181
|
+
}
|
|
182
|
+
const parts = tokenId.split("_");
|
|
183
|
+
if (parts.length !== 2 || parts[0].length !== 64 || !/^\d+$/.test(parts[1])) {
|
|
184
|
+
return { error: "invalid-token-id-format" };
|
|
185
|
+
}
|
|
186
|
+
const result = await ctx.wallet.listOutputs({
|
|
187
|
+
basket: BSV21_BASKET,
|
|
188
|
+
includeTags: true,
|
|
189
|
+
include: "locking scripts",
|
|
190
|
+
limit: 10000,
|
|
191
|
+
});
|
|
192
|
+
const tokenUtxos = result.outputs.filter((o) => {
|
|
193
|
+
const idTag = o.tags?.find((t) => t.startsWith("id:"));
|
|
194
|
+
if (!idTag)
|
|
195
|
+
return false;
|
|
196
|
+
const idContent = idTag.slice(3);
|
|
197
|
+
const lastColonIdx = idContent.lastIndexOf(":");
|
|
198
|
+
if (lastColonIdx === -1)
|
|
199
|
+
return false;
|
|
200
|
+
const id = idContent.slice(0, lastColonIdx);
|
|
201
|
+
const status = idContent.slice(lastColonIdx + 1);
|
|
202
|
+
return id === tokenId && status !== "invalid";
|
|
203
|
+
});
|
|
204
|
+
if (tokenUtxos.length === 0) {
|
|
205
|
+
return { error: "no-token-utxos-found" };
|
|
206
|
+
}
|
|
207
|
+
const selected = [];
|
|
208
|
+
let totalIn = 0n;
|
|
209
|
+
for (const utxo of tokenUtxos) {
|
|
210
|
+
if (totalIn >= amount)
|
|
211
|
+
break;
|
|
212
|
+
const amtTag = utxo.tags?.find((t) => t.startsWith("amt:"));
|
|
213
|
+
if (!amtTag)
|
|
214
|
+
continue;
|
|
215
|
+
const utxoAmount = BigInt(amtTag.slice(4));
|
|
216
|
+
if (ctx.services?.bsv21) {
|
|
217
|
+
try {
|
|
218
|
+
const [txid] = utxo.outpoint.split("_");
|
|
219
|
+
const validation = await ctx.services.bsv21.getTokenByTxid(tokenId, txid);
|
|
220
|
+
const outputData = validation.outputs.find((o) => `${validation.txid}_${o.vout}` === utxo.outpoint);
|
|
221
|
+
if (!outputData)
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
selected.push(utxo);
|
|
229
|
+
totalIn += utxoAmount;
|
|
230
|
+
}
|
|
231
|
+
if (totalIn < amount) {
|
|
232
|
+
return { error: "insufficient-validated-tokens" };
|
|
233
|
+
}
|
|
234
|
+
let recipientAddress;
|
|
235
|
+
if (counterparty) {
|
|
236
|
+
const { publicKey } = await ctx.wallet.getPublicKey({
|
|
237
|
+
protocolID: BSV21_PROTOCOL,
|
|
238
|
+
keyID: `${tokenId}-${Date.now()}`,
|
|
239
|
+
counterparty,
|
|
240
|
+
forSelf: false,
|
|
241
|
+
});
|
|
242
|
+
recipientAddress = PublicKey.fromString(publicKey).toAddress();
|
|
243
|
+
}
|
|
244
|
+
else if (paymail) {
|
|
245
|
+
return { error: "paymail-not-yet-implemented" };
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
recipientAddress = address;
|
|
249
|
+
}
|
|
250
|
+
const outputs = [];
|
|
251
|
+
const p2pkh = new P2PKH();
|
|
252
|
+
const destinationLockingScript = p2pkh.lock(recipientAddress);
|
|
253
|
+
const transferScript = BSV21.transfer(tokenId, amount).lock(destinationLockingScript);
|
|
254
|
+
outputs.push({
|
|
255
|
+
lockingScript: transferScript.toHex(),
|
|
256
|
+
satoshis: 1,
|
|
257
|
+
outputDescription: `Send ${amount} tokens`,
|
|
258
|
+
});
|
|
259
|
+
const change = totalIn - amount;
|
|
260
|
+
if (change > 0n) {
|
|
261
|
+
const changeKeyID = `${tokenId}-${Date.now()}`;
|
|
262
|
+
const { publicKey } = await ctx.wallet.getPublicKey({
|
|
263
|
+
protocolID: BSV21_PROTOCOL,
|
|
264
|
+
keyID: changeKeyID,
|
|
265
|
+
counterparty: "self",
|
|
266
|
+
forSelf: true,
|
|
267
|
+
});
|
|
268
|
+
const changeAddress = PublicKey.fromString(publicKey).toAddress();
|
|
269
|
+
const changeLockingScript = p2pkh.lock(changeAddress);
|
|
270
|
+
const changeScript = BSV21.transfer(tokenId, change).lock(changeLockingScript);
|
|
271
|
+
outputs.push({
|
|
272
|
+
lockingScript: changeScript.toHex(),
|
|
273
|
+
satoshis: 1,
|
|
274
|
+
outputDescription: "Token change",
|
|
275
|
+
basket: BSV21_BASKET,
|
|
276
|
+
tags: [`id:${tokenId}`, `amt:${change}`],
|
|
277
|
+
customInstructions: JSON.stringify({
|
|
278
|
+
protocolID: BSV21_PROTOCOL,
|
|
279
|
+
keyID: changeKeyID,
|
|
280
|
+
}),
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
const symTag = tokenUtxos[0]?.tags?.find((t) => t.startsWith("sym:"));
|
|
284
|
+
const symbol = symTag ? symTag.slice(4) : tokenId.slice(0, 8);
|
|
285
|
+
const createResult = await ctx.wallet.createAction({
|
|
286
|
+
description: `Send ${amount} ${symbol}`,
|
|
287
|
+
inputs: selected.map((o) => ({
|
|
288
|
+
outpoint: o.outpoint,
|
|
289
|
+
inputDescription: "Token input",
|
|
290
|
+
})),
|
|
291
|
+
outputs,
|
|
292
|
+
options: { signAndProcess: false },
|
|
293
|
+
});
|
|
294
|
+
if ("error" in createResult && createResult.error) {
|
|
295
|
+
return { error: String(createResult.error) };
|
|
296
|
+
}
|
|
297
|
+
if (!createResult.signableTransaction) {
|
|
298
|
+
return { error: "no-signable-transaction" };
|
|
299
|
+
}
|
|
300
|
+
const signResult = await ctx.wallet.signAction({
|
|
301
|
+
reference: createResult.signableTransaction.reference,
|
|
302
|
+
spends: {},
|
|
303
|
+
});
|
|
304
|
+
if ("error" in signResult) {
|
|
305
|
+
return { error: String(signResult.error) };
|
|
306
|
+
}
|
|
307
|
+
return {
|
|
308
|
+
txid: signResult.txid,
|
|
309
|
+
rawtx: signResult.tx ? Utils.toHex(signResult.tx) : undefined,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
catch (error) {
|
|
313
|
+
return { error: error instanceof Error ? error.message : "unknown-error" };
|
|
314
|
+
}
|
|
315
|
+
},
|
|
316
|
+
};
|
|
317
|
+
/**
|
|
318
|
+
* Purchase BSV21 tokens from marketplace.
|
|
319
|
+
*/
|
|
320
|
+
export const purchaseBsv21 = {
|
|
321
|
+
meta: {
|
|
322
|
+
name: "purchaseBsv21",
|
|
323
|
+
description: "Purchase BSV21 tokens from the marketplace",
|
|
324
|
+
category: "tokens",
|
|
325
|
+
requiresServices: true,
|
|
326
|
+
inputSchema: {
|
|
327
|
+
type: "object",
|
|
328
|
+
properties: {
|
|
329
|
+
tokenId: { type: "string", description: "Token ID (txid_vout format)" },
|
|
330
|
+
outpoint: { type: "string", description: "Outpoint of the listed token UTXO" },
|
|
331
|
+
amount: { type: "string", description: "Amount of tokens in the listing (as string)" },
|
|
332
|
+
marketplaceAddress: { type: "string", description: "Marketplace fee address" },
|
|
333
|
+
marketplaceRate: { type: "number", description: "Marketplace fee rate (0-1)" },
|
|
334
|
+
},
|
|
335
|
+
required: ["tokenId", "outpoint", "amount"],
|
|
336
|
+
},
|
|
337
|
+
},
|
|
338
|
+
async execute(ctx, input) {
|
|
339
|
+
try {
|
|
340
|
+
const { tokenId, outpoint, amount: rawAmount, marketplaceAddress, marketplaceRate } = input;
|
|
341
|
+
const tokenAmount = typeof rawAmount === "string" ? BigInt(rawAmount) : rawAmount;
|
|
342
|
+
if (!ctx.services) {
|
|
343
|
+
return { error: "services-required-for-purchase" };
|
|
344
|
+
}
|
|
345
|
+
const parts = outpoint.split("_");
|
|
346
|
+
if (parts.length !== 2) {
|
|
347
|
+
return { error: "invalid-outpoint-format" };
|
|
348
|
+
}
|
|
349
|
+
const [txid, voutStr] = parts;
|
|
350
|
+
const vout = Number.parseInt(voutStr, 10);
|
|
351
|
+
try {
|
|
352
|
+
await ctx.services.bsv21.getTokenByTxid(tokenId, txid);
|
|
353
|
+
}
|
|
354
|
+
catch {
|
|
355
|
+
return { error: "listing-not-found-in-overlay" };
|
|
356
|
+
}
|
|
357
|
+
const beef = await ctx.services.getBeefForTxid(txid);
|
|
358
|
+
const listingBeefTx = beef.findTxid(txid);
|
|
359
|
+
if (!listingBeefTx?.tx) {
|
|
360
|
+
return { error: "listing-transaction-not-found" };
|
|
361
|
+
}
|
|
362
|
+
const listingOutput = listingBeefTx.tx.outputs[vout];
|
|
363
|
+
if (!listingOutput) {
|
|
364
|
+
return { error: "listing-output-not-found" };
|
|
365
|
+
}
|
|
366
|
+
const ordLockData = OrdLock.decode(listingOutput.lockingScript);
|
|
367
|
+
if (!ordLockData) {
|
|
368
|
+
return { error: "not-an-ordlock-listing" };
|
|
369
|
+
}
|
|
370
|
+
const bsv21KeyID = `${tokenId}-${outpoint}`;
|
|
371
|
+
const { publicKey } = await ctx.wallet.getPublicKey({
|
|
372
|
+
protocolID: BSV21_PROTOCOL,
|
|
373
|
+
keyID: bsv21KeyID,
|
|
374
|
+
counterparty: "self",
|
|
375
|
+
forSelf: true,
|
|
376
|
+
});
|
|
377
|
+
const ourTokenAddress = PublicKey.fromString(publicKey).toAddress();
|
|
378
|
+
const outputs = [];
|
|
379
|
+
const p2pkh = new P2PKH();
|
|
380
|
+
const buyerLockingScript = p2pkh.lock(ourTokenAddress);
|
|
381
|
+
const transferScript = BSV21.transfer(tokenId, tokenAmount).lock(buyerLockingScript);
|
|
382
|
+
outputs.push({
|
|
383
|
+
lockingScript: transferScript.toHex(),
|
|
384
|
+
satoshis: 1,
|
|
385
|
+
outputDescription: "Purchased tokens",
|
|
386
|
+
basket: BSV21_BASKET,
|
|
387
|
+
tags: [`id:${tokenId}`, `amt:${tokenAmount}`],
|
|
388
|
+
customInstructions: JSON.stringify({
|
|
389
|
+
protocolID: BSV21_PROTOCOL,
|
|
390
|
+
keyID: bsv21KeyID,
|
|
391
|
+
}),
|
|
392
|
+
});
|
|
393
|
+
const payoutReader = new Utils.Reader(ordLockData.payout);
|
|
394
|
+
const payoutSatoshis = payoutReader.readUInt64LEBn().toNumber();
|
|
395
|
+
const payoutScriptLen = payoutReader.readVarIntNum();
|
|
396
|
+
const payoutScriptBin = payoutReader.read(payoutScriptLen);
|
|
397
|
+
const payoutLockingScript = LockingScript.fromBinary(payoutScriptBin);
|
|
398
|
+
outputs.push({
|
|
399
|
+
lockingScript: payoutLockingScript.toHex(),
|
|
400
|
+
satoshis: payoutSatoshis,
|
|
401
|
+
outputDescription: "Payment to seller",
|
|
402
|
+
});
|
|
403
|
+
if (marketplaceAddress && marketplaceRate && marketplaceRate > 0) {
|
|
404
|
+
const marketFee = Math.ceil(payoutSatoshis * marketplaceRate);
|
|
405
|
+
if (marketFee > 0) {
|
|
406
|
+
outputs.push({
|
|
407
|
+
lockingScript: p2pkh.lock(marketplaceAddress).toHex(),
|
|
408
|
+
satoshis: marketFee,
|
|
409
|
+
outputDescription: "Marketplace fee",
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
const createResult = await ctx.wallet.createAction({
|
|
414
|
+
description: `Purchase ${tokenAmount} tokens for ${payoutSatoshis} sats`,
|
|
415
|
+
inputBEEF: beef.toBinary(),
|
|
416
|
+
inputs: [
|
|
417
|
+
{
|
|
418
|
+
outpoint,
|
|
419
|
+
inputDescription: "Listed token",
|
|
420
|
+
unlockingScriptLength: 500,
|
|
421
|
+
},
|
|
422
|
+
],
|
|
423
|
+
outputs,
|
|
424
|
+
options: { signAndProcess: false },
|
|
425
|
+
});
|
|
426
|
+
if ("error" in createResult && createResult.error) {
|
|
427
|
+
return { error: String(createResult.error) };
|
|
428
|
+
}
|
|
429
|
+
if (!createResult.signableTransaction) {
|
|
430
|
+
return { error: "no-signable-transaction" };
|
|
431
|
+
}
|
|
432
|
+
const tx = Transaction.fromBEEF(createResult.signableTransaction.tx);
|
|
433
|
+
const unlockingScript = await buildPurchaseUnlockingScript(tx, 0, listingOutput.satoshis ?? 1, listingOutput.lockingScript);
|
|
434
|
+
const signResult = await ctx.wallet.signAction({
|
|
435
|
+
reference: createResult.signableTransaction.reference,
|
|
436
|
+
spends: {
|
|
437
|
+
0: { unlockingScript: unlockingScript.toHex() },
|
|
438
|
+
},
|
|
439
|
+
});
|
|
440
|
+
if ("error" in signResult) {
|
|
441
|
+
return { error: String(signResult.error) };
|
|
442
|
+
}
|
|
443
|
+
return {
|
|
444
|
+
txid: signResult.txid,
|
|
445
|
+
rawtx: signResult.tx ? Utils.toHex(signResult.tx) : undefined,
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
catch (error) {
|
|
449
|
+
return { error: error instanceof Error ? error.message : "unknown-error" };
|
|
450
|
+
}
|
|
451
|
+
},
|
|
452
|
+
};
|
|
453
|
+
// ============================================================================
|
|
454
|
+
// Module exports
|
|
455
|
+
// ============================================================================
|
|
456
|
+
/** All token skills for registry */
|
|
457
|
+
export const tokensSkills = [listTokens, getBsv21Balances, sendBsv21, purchaseBsv21];
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CWI Chrome Transport - For extension popup/options pages
|
|
3
|
+
* Uses chrome.runtime.sendMessage directly to service worker
|
|
4
|
+
*/
|
|
5
|
+
import type { WalletInterface } from '@bsv/sdk';
|
|
6
|
+
/**
|
|
7
|
+
* Create a CWI for extension context (popup, options page).
|
|
8
|
+
* Uses chrome.runtime.sendMessage directly to communicate with service worker.
|
|
9
|
+
*/
|
|
10
|
+
export declare const createChromeCWI: () => WalletInterface;
|
|
11
|
+
export declare const ChromeCWI: WalletInterface;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CWI Chrome Transport - For extension popup/options pages
|
|
3
|
+
* Uses chrome.runtime.sendMessage directly to service worker
|
|
4
|
+
*/
|
|
5
|
+
import { createCWI } from './factory.js';
|
|
6
|
+
/**
|
|
7
|
+
* chrome.runtime.sendMessage-based transport for extension context.
|
|
8
|
+
* Communicates directly with service worker without content script intermediary.
|
|
9
|
+
*/
|
|
10
|
+
const chromeTransport = (action, params) => {
|
|
11
|
+
return new Promise((resolve, reject) => {
|
|
12
|
+
// Use originator at message level (BRC-100 standard)
|
|
13
|
+
// Format as chrome-extension://<id> to match the admin originator in initWallet.ts
|
|
14
|
+
const originator = `chrome-extension://${chrome.runtime?.id}`;
|
|
15
|
+
console.log('[ChromeCWI] Sending message:', { action, originator, paramsType: typeof params });
|
|
16
|
+
chrome.runtime.sendMessage({ action, params, originator }, (response) => {
|
|
17
|
+
if (chrome.runtime.lastError) {
|
|
18
|
+
console.error('[ChromeCWI] Runtime error:', chrome.runtime.lastError.message);
|
|
19
|
+
reject(new Error(chrome.runtime.lastError.message));
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (response.success) {
|
|
23
|
+
console.log('[ChromeCWI] Success:', action);
|
|
24
|
+
resolve(response.data);
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
console.error('[ChromeCWI] Failed:', action, response.error);
|
|
28
|
+
reject(new Error(response.error || 'Unknown error'));
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Create a CWI for extension context (popup, options page).
|
|
35
|
+
* Uses chrome.runtime.sendMessage directly to communicate with service worker.
|
|
36
|
+
*/
|
|
37
|
+
export const createChromeCWI = () => createCWI(chromeTransport);
|
|
38
|
+
// Default instance for convenience
|
|
39
|
+
export const ChromeCWI = createChromeCWI();
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CWI Event Transport - For browser pages
|
|
3
|
+
* Uses CustomEvent pattern, forwarded by content script to service worker
|
|
4
|
+
*/
|
|
5
|
+
import type { WalletInterface } from '@bsv/sdk';
|
|
6
|
+
/**
|
|
7
|
+
* Create a CWI for browser page context.
|
|
8
|
+
* Uses CustomEvent pattern - requires content script to forward to service worker.
|
|
9
|
+
*/
|
|
10
|
+
export declare const createEventCWI: () => WalletInterface;
|
|
11
|
+
export declare const CWI: WalletInterface;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CWI Event Transport - For browser pages
|
|
3
|
+
* Uses CustomEvent pattern, forwarded by content script to service worker
|
|
4
|
+
*/
|
|
5
|
+
import { createCWI } from './factory.js';
|
|
6
|
+
// Event name for requests (listened by content script)
|
|
7
|
+
const YOURS_REQUEST = 'YoursRequest';
|
|
8
|
+
/**
|
|
9
|
+
* CustomEvent-based transport for browser page context.
|
|
10
|
+
* Content script listens for these events and forwards to service worker.
|
|
11
|
+
*/
|
|
12
|
+
const eventTransport = (action, params) => {
|
|
13
|
+
return new Promise((resolve, reject) => {
|
|
14
|
+
const messageId = `${action}-${Date.now()}-${Math.random()}`;
|
|
15
|
+
const requestEvent = new CustomEvent(YOURS_REQUEST, {
|
|
16
|
+
detail: { messageId, type: action, params },
|
|
17
|
+
});
|
|
18
|
+
function onResponse(e) {
|
|
19
|
+
const responseEvent = e;
|
|
20
|
+
const { detail } = responseEvent;
|
|
21
|
+
if (detail.success) {
|
|
22
|
+
resolve(detail.data);
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
reject(new Error(detail.error || 'Unknown error'));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
self.addEventListener(messageId, onResponse, { once: true });
|
|
29
|
+
self.dispatchEvent(requestEvent);
|
|
30
|
+
});
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Create a CWI for browser page context.
|
|
34
|
+
* Uses CustomEvent pattern - requires content script to forward to service worker.
|
|
35
|
+
*/
|
|
36
|
+
export const createEventCWI = () => createCWI(eventTransport);
|
|
37
|
+
// Default instance for convenience
|
|
38
|
+
export const CWI = createEventCWI();
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CWI Factory - Creates WalletInterface implementations with pluggable transport
|
|
3
|
+
*/
|
|
4
|
+
import type { WalletInterface } from '@bsv/sdk';
|
|
5
|
+
import { CWIEventName } from './types.js';
|
|
6
|
+
/**
|
|
7
|
+
* Transport function signature - sends a message and returns response
|
|
8
|
+
*/
|
|
9
|
+
export type CWITransport = <TResult>(action: CWIEventName, params: unknown) => Promise<TResult>;
|
|
10
|
+
/**
|
|
11
|
+
* Create a WalletInterface implementation using the provided transport.
|
|
12
|
+
* The transport handles the actual message passing (CustomEvent, chrome.runtime, etc.)
|
|
13
|
+
*/
|
|
14
|
+
export declare const createCWI: (transport: CWITransport) => WalletInterface;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CWI Factory - Creates WalletInterface implementations with pluggable transport
|
|
3
|
+
*/
|
|
4
|
+
import { CWIEventName } from './types.js';
|
|
5
|
+
/**
|
|
6
|
+
* Create a WalletInterface implementation using the provided transport.
|
|
7
|
+
* The transport handles the actual message passing (CustomEvent, chrome.runtime, etc.)
|
|
8
|
+
*/
|
|
9
|
+
export const createCWI = (transport) => ({
|
|
10
|
+
// Output Management
|
|
11
|
+
listOutputs: (args) => transport(CWIEventName.LIST_OUTPUTS, args),
|
|
12
|
+
relinquishOutput: (args) => transport(CWIEventName.RELINQUISH_OUTPUT, args),
|
|
13
|
+
// Action Management
|
|
14
|
+
createAction: (args) => transport(CWIEventName.CREATE_ACTION, args),
|
|
15
|
+
signAction: (args) => transport(CWIEventName.SIGN_ACTION, args),
|
|
16
|
+
abortAction: (args) => transport(CWIEventName.ABORT_ACTION, args),
|
|
17
|
+
listActions: (args) => transport(CWIEventName.LIST_ACTIONS, args),
|
|
18
|
+
internalizeAction: (args) => transport(CWIEventName.INTERNALIZE_ACTION, args),
|
|
19
|
+
// Key Operations
|
|
20
|
+
getPublicKey: (args) => transport(CWIEventName.GET_PUBLIC_KEY, args),
|
|
21
|
+
revealCounterpartyKeyLinkage: (args) => transport(CWIEventName.REVEAL_COUNTERPARTY_KEY_LINKAGE, args),
|
|
22
|
+
revealSpecificKeyLinkage: (args) => transport(CWIEventName.REVEAL_SPECIFIC_KEY_LINKAGE, args),
|
|
23
|
+
// Cryptographic Operations
|
|
24
|
+
encrypt: (args) => transport(CWIEventName.ENCRYPT, args),
|
|
25
|
+
decrypt: (args) => transport(CWIEventName.DECRYPT, args),
|
|
26
|
+
createHmac: (args) => transport(CWIEventName.CREATE_HMAC, args),
|
|
27
|
+
verifyHmac: (args) => transport(CWIEventName.VERIFY_HMAC, args),
|
|
28
|
+
createSignature: (args) => transport(CWIEventName.CREATE_SIGNATURE, args),
|
|
29
|
+
verifySignature: (args) => transport(CWIEventName.VERIFY_SIGNATURE, args),
|
|
30
|
+
// Certificate Operations
|
|
31
|
+
acquireCertificate: (args) => transport(CWIEventName.ACQUIRE_CERTIFICATE, args),
|
|
32
|
+
listCertificates: (args) => transport(CWIEventName.LIST_CERTIFICATES, args),
|
|
33
|
+
proveCertificate: (args) => transport(CWIEventName.PROVE_CERTIFICATE, args),
|
|
34
|
+
relinquishCertificate: (args) => transport(CWIEventName.RELINQUISH_CERTIFICATE, args),
|
|
35
|
+
discoverByIdentityKey: (args) => transport(CWIEventName.DISCOVER_BY_IDENTITY_KEY, args),
|
|
36
|
+
discoverByAttributes: (args) => transport(CWIEventName.DISCOVER_BY_ATTRIBUTES, args),
|
|
37
|
+
// Status & Info
|
|
38
|
+
isAuthenticated: (args) => transport(CWIEventName.IS_AUTHENTICATED, args),
|
|
39
|
+
waitForAuthentication: (args) => transport(CWIEventName.WAIT_FOR_AUTHENTICATION, args),
|
|
40
|
+
getHeight: (args) => transport(CWIEventName.GET_HEIGHT, args),
|
|
41
|
+
getHeaderForHeight: (args) => transport(CWIEventName.GET_HEADER_FOR_HEIGHT, args),
|
|
42
|
+
getNetwork: (args) => transport(CWIEventName.GET_NETWORK, args),
|
|
43
|
+
getVersion: (args) => transport(CWIEventName.GET_VERSION, args),
|
|
44
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CWI (Chrome Wallet Interface) - BRC-100 WalletInterface implementations
|
|
3
|
+
*
|
|
4
|
+
* Two implementations for different contexts:
|
|
5
|
+
* - event.ts: For browser pages (uses CustomEvent, forwarded by content script)
|
|
6
|
+
* - chrome.ts: For extension popup/options (uses chrome.runtime.sendMessage directly)
|
|
7
|
+
*/
|
|
8
|
+
export { CWIEventName, type CWIResponseDetail } from './types.js';
|
|
9
|
+
export { createCWI, type CWITransport } from './factory.js';
|
|
10
|
+
export { createEventCWI, CWI as EventCWI } from './event.js';
|
|
11
|
+
export { createChromeCWI, ChromeCWI } from './chrome.js';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CWI (Chrome Wallet Interface) - BRC-100 WalletInterface implementations
|
|
3
|
+
*
|
|
4
|
+
* Two implementations for different contexts:
|
|
5
|
+
* - event.ts: For browser pages (uses CustomEvent, forwarded by content script)
|
|
6
|
+
* - chrome.ts: For extension popup/options (uses chrome.runtime.sendMessage directly)
|
|
7
|
+
*/
|
|
8
|
+
export { CWIEventName } from './types.js';
|
|
9
|
+
export { createCWI } from './factory.js';
|
|
10
|
+
export { createEventCWI, CWI as EventCWI } from './event.js';
|
|
11
|
+
export { createChromeCWI, ChromeCWI } from './chrome.js';
|