@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.
- 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/Bsv21Indexer.js +1 -1
- package/dist/indexers/CosignIndexer.js +2 -1
- package/dist/indexers/InscriptionIndexer.js +4 -5
- package/dist/indexers/LockIndexer.js +2 -1
- package/dist/indexers/MapIndexer.js +1 -1
- package/dist/indexers/OrdLockIndexer.js +2 -1
- 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,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tokens Module
|
|
3
|
+
*
|
|
4
|
+
* Functions for managing BSV21 tokens.
|
|
5
|
+
*/
|
|
6
|
+
import { type WalletInterface, type WalletOutput } from "@bsv/sdk";
|
|
7
|
+
import type { OneSatServices } from "../../services/OneSatServices";
|
|
8
|
+
export interface Bsv21Balance {
|
|
9
|
+
/** Token protocol (bsv-20) */
|
|
10
|
+
p: string;
|
|
11
|
+
/** Token ID (outpoint for BSV21, tick for BSV20) */
|
|
12
|
+
id: string;
|
|
13
|
+
/** Token symbol */
|
|
14
|
+
sym?: string;
|
|
15
|
+
/** Token icon URL */
|
|
16
|
+
icon?: string;
|
|
17
|
+
/** Decimal places */
|
|
18
|
+
dec: number;
|
|
19
|
+
/** Total amount (confirmed + pending) */
|
|
20
|
+
amt: string;
|
|
21
|
+
/** Breakdown of confirmed vs pending */
|
|
22
|
+
all: {
|
|
23
|
+
confirmed: bigint;
|
|
24
|
+
pending: bigint;
|
|
25
|
+
};
|
|
26
|
+
/** Listed amounts (if applicable) */
|
|
27
|
+
listed: {
|
|
28
|
+
confirmed: bigint;
|
|
29
|
+
pending: bigint;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export interface SendBsv21Request {
|
|
33
|
+
/** Token ID (txid_vout format) */
|
|
34
|
+
tokenId: string;
|
|
35
|
+
/** Destination address */
|
|
36
|
+
address: string;
|
|
37
|
+
/** Amount to send (as bigint or string) */
|
|
38
|
+
amount: bigint | string;
|
|
39
|
+
}
|
|
40
|
+
export interface TokenOperationResponse {
|
|
41
|
+
txid?: string;
|
|
42
|
+
rawtx?: string;
|
|
43
|
+
error?: string;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* List BSV21 token outputs from the bsv21 basket.
|
|
47
|
+
* Returns WalletOutput[] directly - use tags for metadata.
|
|
48
|
+
*/
|
|
49
|
+
export declare function listTokens(cwi: WalletInterface, limit?: number): Promise<WalletOutput[]>;
|
|
50
|
+
/**
|
|
51
|
+
* Get aggregated BSV21 token balances.
|
|
52
|
+
* Groups outputs by token ID and sums amounts.
|
|
53
|
+
*/
|
|
54
|
+
export declare function getBsv21Balances(cwi: WalletInterface): Promise<Bsv21Balance[]>;
|
|
55
|
+
/**
|
|
56
|
+
* Send BSV21 tokens to an address.
|
|
57
|
+
*
|
|
58
|
+
* Flow:
|
|
59
|
+
* 1. Get token UTXOs from basket
|
|
60
|
+
* 2. Validate each with BSV21 service
|
|
61
|
+
* 3. Select UTXOs until we have enough tokens
|
|
62
|
+
* 4. Build transfer inscription outputs
|
|
63
|
+
* 5. Create and sign transaction
|
|
64
|
+
*/
|
|
65
|
+
export declare function sendBsv21(cwi: WalletInterface, request: SendBsv21Request, services?: OneSatServices): Promise<TokenOperationResponse>;
|
|
66
|
+
export interface PurchaseBsv21Request {
|
|
67
|
+
/** Token ID (txid_vout format of the deploy transaction) */
|
|
68
|
+
tokenId: string;
|
|
69
|
+
/** Outpoint of listed token UTXO (OrdLock containing BSV21) */
|
|
70
|
+
outpoint: string;
|
|
71
|
+
/** Amount of tokens in the listing */
|
|
72
|
+
amount: bigint | string;
|
|
73
|
+
/** Optional marketplace fee address */
|
|
74
|
+
marketplaceAddress?: string;
|
|
75
|
+
/** Optional marketplace fee rate (0-1) */
|
|
76
|
+
marketplaceRate?: number;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Purchase BSV21 tokens from marketplace.
|
|
80
|
+
*
|
|
81
|
+
* Flow:
|
|
82
|
+
* 1. Fetch listing BEEF to get the locking script
|
|
83
|
+
* 2. Decode OrdLock to get price and payout
|
|
84
|
+
* 3. Build BSV21 transfer output for buyer
|
|
85
|
+
* 4. Build payment output for seller
|
|
86
|
+
* 5. Build custom OrdLock purchase unlock (preimage only, no signature)
|
|
87
|
+
*/
|
|
88
|
+
export declare function purchaseBsv21(cwi: WalletInterface, request: PurchaseBsv21Request, services?: OneSatServices): Promise<TokenOperationResponse>;
|
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tokens Module
|
|
3
|
+
*
|
|
4
|
+
* Functions 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
|
+
* List BSV21 token outputs from the bsv21 basket.
|
|
11
|
+
* Returns WalletOutput[] directly - use tags for metadata.
|
|
12
|
+
*/
|
|
13
|
+
export async function listTokens(cwi, limit = 10000) {
|
|
14
|
+
const result = await cwi.listOutputs({
|
|
15
|
+
basket: BSV21_BASKET,
|
|
16
|
+
includeTags: true,
|
|
17
|
+
limit,
|
|
18
|
+
});
|
|
19
|
+
return result.outputs;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Get aggregated BSV21 token balances.
|
|
23
|
+
* Groups outputs by token ID and sums amounts.
|
|
24
|
+
*/
|
|
25
|
+
export async function getBsv21Balances(cwi) {
|
|
26
|
+
const outputs = await listTokens(cwi);
|
|
27
|
+
const balanceMap = new Map();
|
|
28
|
+
for (const o of outputs) {
|
|
29
|
+
const idTag = o.tags?.find((t) => t.startsWith("id:"));
|
|
30
|
+
const amtTag = o.tags?.find((t) => t.startsWith("amt:"))?.slice(4);
|
|
31
|
+
if (!idTag || !amtTag)
|
|
32
|
+
continue;
|
|
33
|
+
const idContent = idTag.slice(3);
|
|
34
|
+
const lastColonIdx = idContent.lastIndexOf(":");
|
|
35
|
+
if (lastColonIdx === -1)
|
|
36
|
+
continue;
|
|
37
|
+
const tokenId = idContent.slice(0, lastColonIdx);
|
|
38
|
+
const status = idContent.slice(lastColonIdx + 1);
|
|
39
|
+
if (status === "invalid")
|
|
40
|
+
continue;
|
|
41
|
+
const isConfirmed = status === "valid";
|
|
42
|
+
const amt = BigInt(amtTag);
|
|
43
|
+
const dec = parseInt(o.tags?.find((t) => t.startsWith("dec:"))?.slice(4) || "0", 10);
|
|
44
|
+
const symTag = o.tags?.find((t) => t.startsWith("sym:"))?.slice(4);
|
|
45
|
+
const iconTag = o.tags?.find((t) => t.startsWith("icon:"))?.slice(5);
|
|
46
|
+
const existing = balanceMap.get(tokenId);
|
|
47
|
+
if (existing) {
|
|
48
|
+
if (isConfirmed)
|
|
49
|
+
existing.confirmed += amt;
|
|
50
|
+
else
|
|
51
|
+
existing.pending += amt;
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
balanceMap.set(tokenId, {
|
|
55
|
+
id: tokenId,
|
|
56
|
+
confirmed: isConfirmed ? amt : 0n,
|
|
57
|
+
pending: isConfirmed ? 0n : amt,
|
|
58
|
+
sym: symTag,
|
|
59
|
+
icon: iconTag,
|
|
60
|
+
dec,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return Array.from(balanceMap.values()).map((b) => ({
|
|
65
|
+
p: "bsv-20",
|
|
66
|
+
op: "transfer",
|
|
67
|
+
dec: b.dec,
|
|
68
|
+
amt: (b.confirmed + b.pending).toString(),
|
|
69
|
+
id: b.id,
|
|
70
|
+
sym: b.sym,
|
|
71
|
+
icon: b.icon,
|
|
72
|
+
all: { confirmed: b.confirmed, pending: b.pending },
|
|
73
|
+
listed: { confirmed: 0n, pending: 0n },
|
|
74
|
+
}));
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Send BSV21 tokens to an address.
|
|
78
|
+
*
|
|
79
|
+
* Flow:
|
|
80
|
+
* 1. Get token UTXOs from basket
|
|
81
|
+
* 2. Validate each with BSV21 service
|
|
82
|
+
* 3. Select UTXOs until we have enough tokens
|
|
83
|
+
* 4. Build transfer inscription outputs
|
|
84
|
+
* 5. Create and sign transaction
|
|
85
|
+
*/
|
|
86
|
+
export async function sendBsv21(cwi, request, services) {
|
|
87
|
+
try {
|
|
88
|
+
const { tokenId, address, amount: rawAmount } = request;
|
|
89
|
+
const amount = typeof rawAmount === "string" ? BigInt(rawAmount) : rawAmount;
|
|
90
|
+
// Validate amount
|
|
91
|
+
if (amount <= 0n) {
|
|
92
|
+
return { error: "amount-must-be-positive" };
|
|
93
|
+
}
|
|
94
|
+
// Validate token ID format (txid_vout)
|
|
95
|
+
const parts = tokenId.split("_");
|
|
96
|
+
if (parts.length !== 2 || parts[0].length !== 64 || !/^\d+$/.test(parts[1])) {
|
|
97
|
+
return { error: "invalid-token-id-format" };
|
|
98
|
+
}
|
|
99
|
+
// Get token UTXOs from basket
|
|
100
|
+
const result = await cwi.listOutputs({
|
|
101
|
+
basket: BSV21_BASKET,
|
|
102
|
+
includeTags: true,
|
|
103
|
+
include: "locking scripts",
|
|
104
|
+
limit: 10000,
|
|
105
|
+
});
|
|
106
|
+
// Filter UTXOs matching tokenId (exclude invalid)
|
|
107
|
+
const tokenUtxos = result.outputs.filter((o) => {
|
|
108
|
+
const idTag = o.tags?.find((t) => t.startsWith("id:"));
|
|
109
|
+
if (!idTag)
|
|
110
|
+
return false;
|
|
111
|
+
const idContent = idTag.slice(3); // Remove "id:" prefix
|
|
112
|
+
const lastColonIdx = idContent.lastIndexOf(":");
|
|
113
|
+
if (lastColonIdx === -1)
|
|
114
|
+
return false;
|
|
115
|
+
const id = idContent.slice(0, lastColonIdx);
|
|
116
|
+
const status = idContent.slice(lastColonIdx + 1);
|
|
117
|
+
return id === tokenId && status !== "invalid";
|
|
118
|
+
});
|
|
119
|
+
if (tokenUtxos.length === 0) {
|
|
120
|
+
return { error: "no-token-utxos-found" };
|
|
121
|
+
}
|
|
122
|
+
// Validate UTXOs with BSV21 service and select valid ones
|
|
123
|
+
const selected = [];
|
|
124
|
+
let totalIn = 0n;
|
|
125
|
+
for (const utxo of tokenUtxos) {
|
|
126
|
+
if (totalIn >= amount)
|
|
127
|
+
break;
|
|
128
|
+
// Get amount from tag
|
|
129
|
+
const amtTag = utxo.tags?.find((t) => t.startsWith("amt:"));
|
|
130
|
+
if (!amtTag)
|
|
131
|
+
continue;
|
|
132
|
+
const utxoAmount = BigInt(amtTag.slice(4));
|
|
133
|
+
// Validate with BSV21 service if available
|
|
134
|
+
if (services?.bsv21) {
|
|
135
|
+
try {
|
|
136
|
+
const [txid] = utxo.outpoint.split("_");
|
|
137
|
+
const validation = await services.bsv21.getTokenByTxid(tokenId, txid);
|
|
138
|
+
// If the output is returned and exists in outputs, it's valid
|
|
139
|
+
const outputData = validation.outputs.find((o) => `${validation.txid}_${o.vout}` === utxo.outpoint);
|
|
140
|
+
if (!outputData) {
|
|
141
|
+
continue; // Skip if not found in response
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
// If 404 or any error, the overlay doesn't know about it - skip
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
selected.push(utxo);
|
|
150
|
+
totalIn += utxoAmount;
|
|
151
|
+
}
|
|
152
|
+
if (totalIn < amount) {
|
|
153
|
+
return { error: "insufficient-validated-tokens" };
|
|
154
|
+
}
|
|
155
|
+
// Build outputs
|
|
156
|
+
const outputs = [];
|
|
157
|
+
// Output 1: Token transfer to destination
|
|
158
|
+
const p2pkh = new P2PKH();
|
|
159
|
+
const destinationLockingScript = p2pkh.lock(address);
|
|
160
|
+
const transferScript = BSV21.transfer(tokenId, amount).lock(destinationLockingScript);
|
|
161
|
+
outputs.push({
|
|
162
|
+
lockingScript: transferScript.toHex(),
|
|
163
|
+
satoshis: 1,
|
|
164
|
+
outputDescription: `Send ${amount} tokens`,
|
|
165
|
+
});
|
|
166
|
+
// Output 2: Token change (if any)
|
|
167
|
+
const change = totalIn - amount;
|
|
168
|
+
if (change > 0n) {
|
|
169
|
+
// Derive change address
|
|
170
|
+
const keyID = `${tokenId}-change-${Date.now()}`;
|
|
171
|
+
const { publicKey } = await cwi.getPublicKey({
|
|
172
|
+
protocolID: [1, "bsv21"],
|
|
173
|
+
keyID,
|
|
174
|
+
counterparty: "self",
|
|
175
|
+
forSelf: true,
|
|
176
|
+
});
|
|
177
|
+
const changeAddress = PublicKey.fromString(publicKey).toAddress();
|
|
178
|
+
const changeLockingScript = p2pkh.lock(changeAddress);
|
|
179
|
+
const changeScript = BSV21.transfer(tokenId, change).lock(changeLockingScript);
|
|
180
|
+
outputs.push({
|
|
181
|
+
lockingScript: changeScript.toHex(),
|
|
182
|
+
satoshis: 1,
|
|
183
|
+
outputDescription: "Token change",
|
|
184
|
+
basket: BSV21_BASKET,
|
|
185
|
+
tags: [`id:${tokenId}:pending`, `amt:${change}`],
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
// Get token symbol for description
|
|
189
|
+
const symTag = tokenUtxos[0]?.tags?.find((t) => t.startsWith("sym:"));
|
|
190
|
+
const symbol = symTag ? symTag.slice(4) : tokenId.slice(0, 8);
|
|
191
|
+
// Create the transaction
|
|
192
|
+
const createResult = await cwi.createAction({
|
|
193
|
+
description: `Send ${amount} ${symbol}`,
|
|
194
|
+
inputs: selected.map((o) => ({
|
|
195
|
+
outpoint: o.outpoint,
|
|
196
|
+
inputDescription: "Token input",
|
|
197
|
+
})),
|
|
198
|
+
outputs,
|
|
199
|
+
options: { signAndProcess: false },
|
|
200
|
+
});
|
|
201
|
+
if ("error" in createResult && createResult.error) {
|
|
202
|
+
return { error: String(createResult.error) };
|
|
203
|
+
}
|
|
204
|
+
if (!createResult.signableTransaction) {
|
|
205
|
+
return { error: "no-signable-transaction" };
|
|
206
|
+
}
|
|
207
|
+
// Sign and broadcast - all inputs are P2PKH, wallet handles signing
|
|
208
|
+
const signResult = await cwi.signAction({
|
|
209
|
+
reference: createResult.signableTransaction.reference,
|
|
210
|
+
spends: {},
|
|
211
|
+
});
|
|
212
|
+
if ("error" in signResult) {
|
|
213
|
+
return { error: String(signResult.error) };
|
|
214
|
+
}
|
|
215
|
+
return {
|
|
216
|
+
txid: signResult.txid,
|
|
217
|
+
rawtx: signResult.tx ? Buffer.from(signResult.tx).toString("hex") : undefined,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
catch (error) {
|
|
221
|
+
return { error: error instanceof Error ? error.message : "unknown-error" };
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Build serialized transaction output (satoshis + script) for OrdLock unlock.
|
|
226
|
+
*/
|
|
227
|
+
function buildSerializedOutput(satoshis, script) {
|
|
228
|
+
const writer = new Utils.Writer();
|
|
229
|
+
writer.writeUInt64LEBn(new BigNumber(satoshis));
|
|
230
|
+
writer.writeVarIntNum(script.length);
|
|
231
|
+
writer.write(script);
|
|
232
|
+
return writer.toArray();
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Build OrdLock purchase unlocking script.
|
|
236
|
+
* The purchase path requires no signature - just preimage and output data.
|
|
237
|
+
*/
|
|
238
|
+
async function buildPurchaseUnlockingScript(tx, inputIndex, sourceSatoshis, lockingScript) {
|
|
239
|
+
if (tx.outputs.length < 2) {
|
|
240
|
+
throw new Error("Malformed transaction: requires at least 2 outputs");
|
|
241
|
+
}
|
|
242
|
+
const script = new UnlockingScript()
|
|
243
|
+
.writeBin(buildSerializedOutput(tx.outputs[0].satoshis ?? 0, tx.outputs[0].lockingScript.toBinary()));
|
|
244
|
+
if (tx.outputs.length > 2) {
|
|
245
|
+
const writer = new Utils.Writer();
|
|
246
|
+
for (const output of tx.outputs.slice(2)) {
|
|
247
|
+
writer.write(buildSerializedOutput(output.satoshis ?? 0, output.lockingScript.toBinary()));
|
|
248
|
+
}
|
|
249
|
+
script.writeBin(writer.toArray());
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
script.writeOpCode(OP.OP_0);
|
|
253
|
+
}
|
|
254
|
+
const input = tx.inputs[inputIndex];
|
|
255
|
+
const sourceTXID = input.sourceTXID ?? input.sourceTransaction?.id("hex");
|
|
256
|
+
if (!sourceTXID) {
|
|
257
|
+
throw new Error("sourceTXID is required");
|
|
258
|
+
}
|
|
259
|
+
const preimage = TransactionSignature.format({
|
|
260
|
+
sourceTXID,
|
|
261
|
+
sourceOutputIndex: input.sourceOutputIndex,
|
|
262
|
+
sourceSatoshis,
|
|
263
|
+
transactionVersion: tx.version,
|
|
264
|
+
otherInputs: [],
|
|
265
|
+
inputIndex,
|
|
266
|
+
outputs: tx.outputs,
|
|
267
|
+
inputSequence: input.sequence ?? 0xffffffff,
|
|
268
|
+
subscript: lockingScript,
|
|
269
|
+
lockTime: tx.lockTime,
|
|
270
|
+
scope: TransactionSignature.SIGHASH_ALL |
|
|
271
|
+
TransactionSignature.SIGHASH_ANYONECANPAY |
|
|
272
|
+
TransactionSignature.SIGHASH_FORKID
|
|
273
|
+
});
|
|
274
|
+
return script.writeBin(preimage).writeOpCode(OP.OP_0);
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Purchase BSV21 tokens from marketplace.
|
|
278
|
+
*
|
|
279
|
+
* Flow:
|
|
280
|
+
* 1. Fetch listing BEEF to get the locking script
|
|
281
|
+
* 2. Decode OrdLock to get price and payout
|
|
282
|
+
* 3. Build BSV21 transfer output for buyer
|
|
283
|
+
* 4. Build payment output for seller
|
|
284
|
+
* 5. Build custom OrdLock purchase unlock (preimage only, no signature)
|
|
285
|
+
*/
|
|
286
|
+
export async function purchaseBsv21(cwi, request, services) {
|
|
287
|
+
try {
|
|
288
|
+
const { tokenId, outpoint, amount: rawAmount, marketplaceAddress, marketplaceRate } = request;
|
|
289
|
+
const tokenAmount = typeof rawAmount === "string" ? BigInt(rawAmount) : rawAmount;
|
|
290
|
+
if (!services) {
|
|
291
|
+
return { error: "services-required-for-purchase" };
|
|
292
|
+
}
|
|
293
|
+
// Parse outpoint
|
|
294
|
+
const parts = outpoint.split("_");
|
|
295
|
+
if (parts.length !== 2) {
|
|
296
|
+
return { error: "invalid-outpoint-format" };
|
|
297
|
+
}
|
|
298
|
+
const [txid, voutStr] = parts;
|
|
299
|
+
const vout = parseInt(voutStr, 10);
|
|
300
|
+
// Fetch listing BEEF to get the locking script
|
|
301
|
+
const beef = await services.getBeefForTxid(txid);
|
|
302
|
+
const listingBeefTx = beef.findTxid(txid);
|
|
303
|
+
if (!listingBeefTx?.tx) {
|
|
304
|
+
return { error: "listing-transaction-not-found" };
|
|
305
|
+
}
|
|
306
|
+
const listingOutput = listingBeefTx.tx.outputs[vout];
|
|
307
|
+
if (!listingOutput) {
|
|
308
|
+
return { error: "listing-output-not-found" };
|
|
309
|
+
}
|
|
310
|
+
// Decode OrdLock from listing script
|
|
311
|
+
const ordLockData = OrdLock.decode(listingOutput.lockingScript);
|
|
312
|
+
if (!ordLockData) {
|
|
313
|
+
return { error: "not-an-ordlock-listing" };
|
|
314
|
+
}
|
|
315
|
+
// Derive our token receive address
|
|
316
|
+
const { publicKey } = await cwi.getPublicKey({
|
|
317
|
+
protocolID: [1, "bsv21"],
|
|
318
|
+
keyID: `${tokenId}-${outpoint}`,
|
|
319
|
+
counterparty: "self",
|
|
320
|
+
forSelf: true,
|
|
321
|
+
});
|
|
322
|
+
const ourTokenAddress = PublicKey.fromString(publicKey).toAddress();
|
|
323
|
+
// Build outputs
|
|
324
|
+
const outputs = [];
|
|
325
|
+
// Output 0: Token transfer to buyer (BSV21 inscription + P2PKH)
|
|
326
|
+
const p2pkh = new P2PKH();
|
|
327
|
+
const buyerLockingScript = p2pkh.lock(ourTokenAddress);
|
|
328
|
+
const transferScript = BSV21.transfer(tokenId, tokenAmount).lock(buyerLockingScript);
|
|
329
|
+
outputs.push({
|
|
330
|
+
lockingScript: transferScript.toHex(),
|
|
331
|
+
satoshis: 1,
|
|
332
|
+
outputDescription: "Purchased tokens",
|
|
333
|
+
basket: BSV21_BASKET,
|
|
334
|
+
tags: [`id:${tokenId}:pending`, `amt:${tokenAmount}`],
|
|
335
|
+
});
|
|
336
|
+
// Output 1: Payment to seller (from payout in OrdLock)
|
|
337
|
+
// Parse payout: 8-byte LE satoshis + varint script length + script
|
|
338
|
+
const payoutReader = new Utils.Reader(ordLockData.payout);
|
|
339
|
+
const payoutSatoshis = payoutReader.readUInt64LEBn().toNumber();
|
|
340
|
+
const payoutScriptLen = payoutReader.readVarIntNum();
|
|
341
|
+
const payoutScriptBin = payoutReader.read(payoutScriptLen);
|
|
342
|
+
const payoutLockingScript = LockingScript.fromBinary(payoutScriptBin);
|
|
343
|
+
outputs.push({
|
|
344
|
+
lockingScript: payoutLockingScript.toHex(),
|
|
345
|
+
satoshis: payoutSatoshis,
|
|
346
|
+
outputDescription: "Payment to seller",
|
|
347
|
+
});
|
|
348
|
+
// Output 2+ (optional): Marketplace fee
|
|
349
|
+
if (marketplaceAddress && marketplaceRate && marketplaceRate > 0) {
|
|
350
|
+
const marketFee = Math.ceil(payoutSatoshis * marketplaceRate);
|
|
351
|
+
if (marketFee > 0) {
|
|
352
|
+
outputs.push({
|
|
353
|
+
lockingScript: p2pkh.lock(marketplaceAddress).toHex(),
|
|
354
|
+
satoshis: marketFee,
|
|
355
|
+
outputDescription: "Marketplace fee",
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
// Create the transaction with signAndProcess: false
|
|
360
|
+
// The listing input needs custom unlocking script
|
|
361
|
+
const createResult = await cwi.createAction({
|
|
362
|
+
description: `Purchase ${tokenAmount} tokens for ${payoutSatoshis} sats`,
|
|
363
|
+
inputBEEF: beef.toBinary(),
|
|
364
|
+
inputs: [{
|
|
365
|
+
outpoint,
|
|
366
|
+
inputDescription: "Listed token",
|
|
367
|
+
unlockingScriptLength: 500, // Estimate for purchase unlock (preimage + outputs)
|
|
368
|
+
}],
|
|
369
|
+
outputs,
|
|
370
|
+
options: { signAndProcess: false },
|
|
371
|
+
});
|
|
372
|
+
if ("error" in createResult && createResult.error) {
|
|
373
|
+
return { error: String(createResult.error) };
|
|
374
|
+
}
|
|
375
|
+
if (!createResult.signableTransaction) {
|
|
376
|
+
return { error: "no-signable-transaction" };
|
|
377
|
+
}
|
|
378
|
+
// Parse the transaction to build purchase unlock
|
|
379
|
+
const tx = Transaction.fromBEEF(createResult.signableTransaction.tx);
|
|
380
|
+
// Build purchase unlocking script
|
|
381
|
+
const unlockingScript = await buildPurchaseUnlockingScript(tx, 0, listingOutput.satoshis ?? 1, listingOutput.lockingScript);
|
|
382
|
+
// Sign and broadcast
|
|
383
|
+
const signResult = await cwi.signAction({
|
|
384
|
+
reference: createResult.signableTransaction.reference,
|
|
385
|
+
spends: {
|
|
386
|
+
0: { unlockingScript: unlockingScript.toHex() },
|
|
387
|
+
},
|
|
388
|
+
});
|
|
389
|
+
if ("error" in signResult) {
|
|
390
|
+
return { error: String(signResult.error) };
|
|
391
|
+
}
|
|
392
|
+
return {
|
|
393
|
+
txid: signResult.txid,
|
|
394
|
+
rawtx: signResult.tx ? Buffer.from(signResult.tx).toString("hex") : undefined,
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
catch (error) {
|
|
398
|
+
return { error: error instanceof Error ? error.message : "unknown-error" };
|
|
399
|
+
}
|
|
400
|
+
}
|
|
@@ -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';
|