@1sat/wallet-toolbox 0.0.9 → 0.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/balance/index.d.ts +20 -8
- package/dist/api/balance/index.js +104 -51
- package/dist/api/broadcast/index.d.ts +5 -3
- package/dist/api/broadcast/index.js +65 -37
- package/dist/api/index.d.ts +15 -15
- package/dist/api/index.js +38 -17
- package/dist/api/inscriptions/index.d.ts +9 -9
- package/dist/api/inscriptions/index.js +79 -31
- package/dist/api/locks/index.d.ts +15 -12
- package/dist/api/locks/index.js +252 -194
- package/dist/api/ordinals/index.d.ts +50 -35
- package/dist/api/ordinals/index.js +469 -349
- package/dist/api/payments/index.d.ts +15 -4
- package/dist/api/payments/index.js +147 -92
- package/dist/api/signing/index.d.ts +8 -5
- package/dist/api/signing/index.js +70 -33
- 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 +37 -38
- package/dist/api/tokens/index.js +398 -341
- package/dist/index.d.ts +1 -0
- package/dist/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 +10 -1
- package/dist/OneSatWallet.d.ts +0 -316
- package/dist/OneSatWallet.js +0 -956
- package/dist/api/OneSatApi.d.ts +0 -100
- package/dist/api/OneSatApi.js +0 -156
- package/dist/indexers/TransactionParser.d.ts +0 -53
package/dist/api/tokens/index.js
CHANGED
|
@@ -1,229 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Tokens Module
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Skills for managing BSV21 tokens.
|
|
5
5
|
*/
|
|
6
6
|
import { BigNumber, LockingScript, OP, P2PKH, PublicKey, Transaction, TransactionSignature, UnlockingScript, Utils, } from "@bsv/sdk";
|
|
7
7
|
import { BSV21, OrdLock } from "@bopen-io/templates";
|
|
8
8
|
import { BSV21_BASKET } from "../constants";
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
*/
|
|
9
|
+
// ============================================================================
|
|
10
|
+
// Constants
|
|
11
|
+
// ============================================================================
|
|
12
|
+
const BSV21_PROTOCOL = [1, "bsv21"];
|
|
13
|
+
// ============================================================================
|
|
14
|
+
// Internal helpers
|
|
15
|
+
// ============================================================================
|
|
227
16
|
function buildSerializedOutput(satoshis, script) {
|
|
228
17
|
const writer = new Utils.Writer();
|
|
229
18
|
writer.writeUInt64LEBn(new BigNumber(satoshis));
|
|
@@ -231,16 +20,11 @@ function buildSerializedOutput(satoshis, script) {
|
|
|
231
20
|
writer.write(script);
|
|
232
21
|
return writer.toArray();
|
|
233
22
|
}
|
|
234
|
-
/**
|
|
235
|
-
* Build OrdLock purchase unlocking script.
|
|
236
|
-
* The purchase path requires no signature - just preimage and output data.
|
|
237
|
-
*/
|
|
238
23
|
async function buildPurchaseUnlockingScript(tx, inputIndex, sourceSatoshis, lockingScript) {
|
|
239
24
|
if (tx.outputs.length < 2) {
|
|
240
25
|
throw new Error("Malformed transaction: requires at least 2 outputs");
|
|
241
26
|
}
|
|
242
|
-
const script = new UnlockingScript()
|
|
243
|
-
.writeBin(buildSerializedOutput(tx.outputs[0].satoshis ?? 0, tx.outputs[0].lockingScript.toBinary()));
|
|
27
|
+
const script = new UnlockingScript().writeBin(buildSerializedOutput(tx.outputs[0].satoshis ?? 0, tx.outputs[0].lockingScript.toBinary()));
|
|
244
28
|
if (tx.outputs.length > 2) {
|
|
245
29
|
const writer = new Utils.Writer();
|
|
246
30
|
for (const output of tx.outputs.slice(2)) {
|
|
@@ -269,132 +53,405 @@ async function buildPurchaseUnlockingScript(tx, inputIndex, sourceSatoshis, lock
|
|
|
269
53
|
lockTime: tx.lockTime,
|
|
270
54
|
scope: TransactionSignature.SIGHASH_ALL |
|
|
271
55
|
TransactionSignature.SIGHASH_ANYONECANPAY |
|
|
272
|
-
TransactionSignature.SIGHASH_FORKID
|
|
56
|
+
TransactionSignature.SIGHASH_FORKID,
|
|
273
57
|
});
|
|
274
58
|
return script.writeBin(preimage).writeOpCode(OP.OP_0);
|
|
275
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
|
+
}
|
|
276
68
|
/**
|
|
277
|
-
*
|
|
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)
|
|
69
|
+
* List BSV21 token outputs from the wallet.
|
|
285
70
|
*/
|
|
286
|
-
export
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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
|
+
}
|
|
314
138
|
}
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
const
|
|
351
|
-
|
|
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);
|
|
352
271
|
outputs.push({
|
|
353
|
-
lockingScript:
|
|
354
|
-
satoshis:
|
|
355
|
-
outputDescription: "
|
|
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
|
+
}),
|
|
356
281
|
});
|
|
357
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
|
+
};
|
|
358
311
|
}
|
|
359
|
-
|
|
360
|
-
|
|
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" };
|
|
312
|
+
catch (error) {
|
|
313
|
+
return { error: error instanceof Error ? error.message : "unknown-error" };
|
|
377
314
|
}
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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)" },
|
|
387
334
|
},
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
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
|
+
};
|
|
391
447
|
}
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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];
|