@arkade-os/sdk 0.1.3 → 0.2.0
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/README.md +156 -174
- package/dist/cjs/arknote/index.js +61 -58
- package/dist/cjs/bip322/errors.js +13 -0
- package/dist/cjs/bip322/index.js +178 -0
- package/dist/cjs/forfeit.js +14 -25
- package/dist/cjs/identity/singleKey.js +68 -0
- package/dist/cjs/index.js +41 -17
- package/dist/cjs/providers/ark.js +253 -317
- package/dist/cjs/providers/indexer.js +525 -0
- package/dist/cjs/providers/onchain.js +193 -15
- package/dist/cjs/script/address.js +48 -17
- package/dist/cjs/script/base.js +120 -3
- package/dist/cjs/script/default.js +18 -4
- package/dist/cjs/script/tapscript.js +46 -14
- package/dist/cjs/script/vhtlc.js +27 -7
- package/dist/cjs/tree/signingSession.js +63 -106
- package/dist/cjs/tree/txTree.js +193 -0
- package/dist/cjs/tree/validation.js +79 -155
- package/dist/cjs/utils/anchor.js +35 -0
- package/dist/cjs/utils/arkTransaction.js +108 -0
- package/dist/cjs/utils/transactionHistory.js +84 -72
- package/dist/cjs/utils/txSizeEstimator.js +12 -0
- package/dist/cjs/utils/unknownFields.js +211 -0
- package/dist/cjs/wallet/index.js +12 -0
- package/dist/cjs/wallet/onchain.js +201 -0
- package/dist/cjs/wallet/ramps.js +95 -0
- package/dist/cjs/wallet/serviceWorker/db/vtxo/idb.js +32 -0
- package/dist/cjs/wallet/serviceWorker/request.js +15 -12
- package/dist/cjs/wallet/serviceWorker/response.js +22 -27
- package/dist/cjs/wallet/serviceWorker/utils.js +8 -0
- package/dist/cjs/wallet/serviceWorker/wallet.js +58 -34
- package/dist/cjs/wallet/serviceWorker/worker.js +117 -108
- package/dist/cjs/wallet/unroll.js +270 -0
- package/dist/cjs/wallet/wallet.js +701 -459
- package/dist/esm/arknote/index.js +61 -57
- package/dist/esm/bip322/errors.js +9 -0
- package/dist/esm/bip322/index.js +174 -0
- package/dist/esm/forfeit.js +15 -26
- package/dist/esm/identity/singleKey.js +64 -0
- package/dist/esm/index.js +30 -12
- package/dist/esm/providers/ark.js +252 -317
- package/dist/esm/providers/indexer.js +521 -0
- package/dist/esm/providers/onchain.js +193 -15
- package/dist/esm/script/address.js +48 -17
- package/dist/esm/script/base.js +120 -3
- package/dist/esm/script/default.js +18 -4
- package/dist/esm/script/tapscript.js +46 -14
- package/dist/esm/script/vhtlc.js +27 -7
- package/dist/esm/tree/signingSession.js +65 -108
- package/dist/esm/tree/txTree.js +189 -0
- package/dist/esm/tree/validation.js +75 -152
- package/dist/esm/utils/anchor.js +31 -0
- package/dist/esm/utils/arkTransaction.js +105 -0
- package/dist/esm/utils/transactionHistory.js +84 -72
- package/dist/esm/utils/txSizeEstimator.js +12 -0
- package/dist/esm/utils/unknownFields.js +173 -0
- package/dist/esm/wallet/index.js +9 -0
- package/dist/esm/wallet/onchain.js +196 -0
- package/dist/esm/wallet/ramps.js +91 -0
- package/dist/esm/wallet/serviceWorker/db/vtxo/idb.js +32 -0
- package/dist/esm/wallet/serviceWorker/request.js +15 -12
- package/dist/esm/wallet/serviceWorker/response.js +22 -27
- package/dist/esm/wallet/serviceWorker/utils.js +8 -0
- package/dist/esm/wallet/serviceWorker/wallet.js +59 -35
- package/dist/esm/wallet/serviceWorker/worker.js +117 -108
- package/dist/esm/wallet/unroll.js +267 -0
- package/dist/esm/wallet/wallet.js +674 -466
- package/dist/types/arknote/index.d.ts +40 -13
- package/dist/types/bip322/errors.d.ts +6 -0
- package/dist/types/bip322/index.d.ts +57 -0
- package/dist/types/forfeit.d.ts +2 -14
- package/dist/types/identity/singleKey.d.ts +27 -0
- package/dist/types/index.d.ts +23 -12
- package/dist/types/providers/ark.d.ts +114 -95
- package/dist/types/providers/indexer.d.ts +186 -0
- package/dist/types/providers/onchain.d.ts +41 -11
- package/dist/types/script/address.d.ts +26 -2
- package/dist/types/script/base.d.ts +13 -3
- package/dist/types/script/default.d.ts +22 -0
- package/dist/types/script/tapscript.d.ts +61 -5
- package/dist/types/script/vhtlc.d.ts +27 -0
- package/dist/types/tree/signingSession.d.ts +5 -5
- package/dist/types/tree/txTree.d.ts +28 -0
- package/dist/types/tree/validation.d.ts +15 -22
- package/dist/types/utils/anchor.d.ts +19 -0
- package/dist/types/utils/arkTransaction.d.ts +27 -0
- package/dist/types/utils/transactionHistory.d.ts +7 -1
- package/dist/types/utils/txSizeEstimator.d.ts +3 -0
- package/dist/types/utils/unknownFields.d.ts +83 -0
- package/dist/types/wallet/index.d.ts +51 -50
- package/dist/types/wallet/onchain.d.ts +49 -0
- package/dist/types/wallet/ramps.d.ts +32 -0
- package/dist/types/wallet/serviceWorker/db/vtxo/idb.d.ts +2 -0
- package/dist/types/wallet/serviceWorker/db/vtxo/index.d.ts +2 -0
- package/dist/types/wallet/serviceWorker/request.d.ts +14 -16
- package/dist/types/wallet/serviceWorker/response.d.ts +17 -19
- package/dist/types/wallet/serviceWorker/utils.d.ts +8 -0
- package/dist/types/wallet/serviceWorker/wallet.d.ts +36 -8
- package/dist/types/wallet/serviceWorker/worker.d.ts +7 -3
- package/dist/types/wallet/unroll.d.ts +102 -0
- package/dist/types/wallet/wallet.d.ts +71 -26
- package/package.json +14 -15
- package/dist/cjs/identity/inMemoryKey.js +0 -40
- package/dist/cjs/tree/vtxoTree.js +0 -231
- package/dist/cjs/utils/coinselect.js +0 -73
- package/dist/cjs/utils/psbt.js +0 -137
- package/dist/esm/identity/inMemoryKey.js +0 -36
- package/dist/esm/tree/vtxoTree.js +0 -191
- package/dist/esm/utils/coinselect.js +0 -69
- package/dist/esm/utils/psbt.js +0 -131
- package/dist/types/identity/inMemoryKey.d.ts +0 -12
- package/dist/types/tree/vtxoTree.d.ts +0 -33
- package/dist/types/utils/coinselect.d.ts +0 -21
- package/dist/types/utils/psbt.d.ts +0 -11
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The default base URLs for esplora API providers.
|
|
3
|
+
*/
|
|
1
4
|
export const ESPLORA_URL = {
|
|
2
5
|
bitcoin: "https://mempool.space/api",
|
|
3
6
|
testnet: "https://mempool.space/testnet/api",
|
|
@@ -5,6 +8,15 @@ export const ESPLORA_URL = {
|
|
|
5
8
|
mutinynet: "https://mutinynet.com/api",
|
|
6
9
|
regtest: "http://localhost:3000",
|
|
7
10
|
};
|
|
11
|
+
/**
|
|
12
|
+
* Implementation of the onchain provider interface for esplora REST API.
|
|
13
|
+
* @see https://mempool.space/docs/api/rest
|
|
14
|
+
* @example
|
|
15
|
+
* ```typescript
|
|
16
|
+
* const provider = new EsploraProvider("https://mempool.space/api");
|
|
17
|
+
* const utxos = await provider.getCoins("bcrt1q679zsd45msawvr7782r0twvmukns3drlstjt77");
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
8
20
|
export class EsploraProvider {
|
|
9
21
|
constructor(baseUrl) {
|
|
10
22
|
this.baseUrl = baseUrl;
|
|
@@ -17,26 +29,22 @@ export class EsploraProvider {
|
|
|
17
29
|
return response.json();
|
|
18
30
|
}
|
|
19
31
|
async getFeeRate() {
|
|
20
|
-
const response = await fetch(`${this.baseUrl}/
|
|
32
|
+
const response = await fetch(`${this.baseUrl}/fee-estimates`);
|
|
21
33
|
if (!response.ok) {
|
|
22
34
|
throw new Error(`Failed to fetch fee rate: ${response.statusText}`);
|
|
23
35
|
}
|
|
24
|
-
const fees = await response.json();
|
|
25
|
-
return fees
|
|
36
|
+
const fees = (await response.json());
|
|
37
|
+
return fees["1"] ?? undefined;
|
|
26
38
|
}
|
|
27
|
-
async broadcastTransaction(
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
if (!response.ok) {
|
|
36
|
-
const error = await response.text();
|
|
37
|
-
throw new Error(`Failed to broadcast transaction: ${error}`);
|
|
39
|
+
async broadcastTransaction(...txs) {
|
|
40
|
+
switch (txs.length) {
|
|
41
|
+
case 1:
|
|
42
|
+
return this.broadcastTx(txs[0]);
|
|
43
|
+
case 2:
|
|
44
|
+
return this.broadcastPackage(txs[0], txs[1]);
|
|
45
|
+
default:
|
|
46
|
+
throw new Error("Only 1 or 1C1P package can be broadcast");
|
|
38
47
|
}
|
|
39
|
-
return response.text(); // Returns the txid
|
|
40
48
|
}
|
|
41
49
|
async getTxOutspends(txid) {
|
|
42
50
|
const response = await fetch(`${this.baseUrl}/tx/${txid}/outspends`);
|
|
@@ -55,15 +63,185 @@ export class EsploraProvider {
|
|
|
55
63
|
return response.json();
|
|
56
64
|
}
|
|
57
65
|
async getTxStatus(txid) {
|
|
66
|
+
// make sure tx exists in mempool or in block
|
|
67
|
+
const txresponse = await fetch(`${this.baseUrl}/tx/${txid}`);
|
|
68
|
+
if (!txresponse.ok) {
|
|
69
|
+
throw new Error(txresponse.statusText);
|
|
70
|
+
}
|
|
71
|
+
const tx = await txresponse.json();
|
|
72
|
+
if (!tx.status.confirmed) {
|
|
73
|
+
return { confirmed: false };
|
|
74
|
+
}
|
|
58
75
|
const response = await fetch(`${this.baseUrl}/tx/${txid}/status`);
|
|
59
76
|
if (!response.ok) {
|
|
60
77
|
throw new Error(`Failed to get transaction status: ${response.statusText}`);
|
|
61
78
|
}
|
|
62
79
|
const data = await response.json();
|
|
80
|
+
if (!data.confirmed) {
|
|
81
|
+
return { confirmed: false };
|
|
82
|
+
}
|
|
63
83
|
return {
|
|
64
84
|
confirmed: data.confirmed,
|
|
65
85
|
blockTime: data.block_time,
|
|
66
86
|
blockHeight: data.block_height,
|
|
67
87
|
};
|
|
68
88
|
}
|
|
89
|
+
async watchAddresses(addresses, callback) {
|
|
90
|
+
let intervalId = null;
|
|
91
|
+
const wsUrl = this.baseUrl.replace(/^http(s)?:/, "ws$1:") + "/v1/ws";
|
|
92
|
+
const poll = async () => {
|
|
93
|
+
// websocket is not reliable, so we will fallback to polling
|
|
94
|
+
const pollingInterval = 5000; // 5 seconds
|
|
95
|
+
const getAllTxs = () => {
|
|
96
|
+
return Promise.all(addresses.map((address) => this.getTransactions(address))).then((txArrays) => txArrays.flat());
|
|
97
|
+
};
|
|
98
|
+
// initial fetch to get existing transactions
|
|
99
|
+
const initialTxs = await getAllTxs();
|
|
100
|
+
// we use block_time in key to also notify when a transaction is confirmed
|
|
101
|
+
const txKey = (tx) => `${tx.txid}_${tx.status.block_time}`;
|
|
102
|
+
// polling for new transactions
|
|
103
|
+
intervalId = setInterval(async () => {
|
|
104
|
+
try {
|
|
105
|
+
// get current transactions
|
|
106
|
+
// we will compare with initialTxs to find new ones
|
|
107
|
+
const currentTxs = await getAllTxs();
|
|
108
|
+
// create a set of existing transactions to avoid duplicates
|
|
109
|
+
const existingTxs = new Set(initialTxs.map(txKey));
|
|
110
|
+
// filter out transactions that are already in initialTxs
|
|
111
|
+
const newTxs = currentTxs.filter((tx) => !existingTxs.has(txKey(tx)));
|
|
112
|
+
if (newTxs.length > 0) {
|
|
113
|
+
// Update the tracking set instead of growing the array
|
|
114
|
+
initialTxs.push(...newTxs);
|
|
115
|
+
callback(newTxs);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
console.error("Error in polling mechanism:", error);
|
|
120
|
+
}
|
|
121
|
+
}, pollingInterval);
|
|
122
|
+
};
|
|
123
|
+
let ws = null;
|
|
124
|
+
try {
|
|
125
|
+
ws = new WebSocket(wsUrl);
|
|
126
|
+
ws.addEventListener("open", () => {
|
|
127
|
+
// subscribe to address updates
|
|
128
|
+
const subscribeMsg = {
|
|
129
|
+
"track-addresses": addresses,
|
|
130
|
+
};
|
|
131
|
+
ws.send(JSON.stringify(subscribeMsg));
|
|
132
|
+
});
|
|
133
|
+
ws.addEventListener("message", (event) => {
|
|
134
|
+
try {
|
|
135
|
+
const newTxs = [];
|
|
136
|
+
const message = JSON.parse(event.data.toString());
|
|
137
|
+
if (!message["multi-address-transactions"])
|
|
138
|
+
return;
|
|
139
|
+
const aux = message["multi-address-transactions"];
|
|
140
|
+
for (const address in aux) {
|
|
141
|
+
for (const type of [
|
|
142
|
+
"mempool",
|
|
143
|
+
"confirmed",
|
|
144
|
+
"removed",
|
|
145
|
+
]) {
|
|
146
|
+
if (!aux[address][type])
|
|
147
|
+
continue;
|
|
148
|
+
newTxs.push(...aux[address][type].filter(isExplorerTransaction));
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
// callback with new transactions
|
|
152
|
+
if (newTxs.length > 0)
|
|
153
|
+
callback(newTxs);
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
console.error("Failed to process WebSocket message:", error);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
ws.addEventListener("error", async () => {
|
|
160
|
+
// if websocket is not available, fallback to polling
|
|
161
|
+
await poll();
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
if (intervalId)
|
|
166
|
+
clearInterval(intervalId);
|
|
167
|
+
// if websocket is not available, fallback to polling
|
|
168
|
+
await poll();
|
|
169
|
+
}
|
|
170
|
+
const stopFunc = () => {
|
|
171
|
+
if (ws && ws.readyState === WebSocket.OPEN)
|
|
172
|
+
ws.close();
|
|
173
|
+
if (intervalId)
|
|
174
|
+
clearInterval(intervalId);
|
|
175
|
+
};
|
|
176
|
+
return stopFunc;
|
|
177
|
+
}
|
|
178
|
+
async getChainTip() {
|
|
179
|
+
const tipBlocks = await fetch(`${this.baseUrl}/blocks/tip`);
|
|
180
|
+
if (!tipBlocks.ok) {
|
|
181
|
+
throw new Error(`Failed to get chain tip: ${tipBlocks.statusText}`);
|
|
182
|
+
}
|
|
183
|
+
const tip = await tipBlocks.json();
|
|
184
|
+
if (!isValidBlocksTip(tip)) {
|
|
185
|
+
throw new Error(`Invalid chain tip: ${JSON.stringify(tip)}`);
|
|
186
|
+
}
|
|
187
|
+
if (tip.length === 0) {
|
|
188
|
+
throw new Error("No chain tip found");
|
|
189
|
+
}
|
|
190
|
+
const hash = tip[0].id;
|
|
191
|
+
return {
|
|
192
|
+
height: tip[0].height,
|
|
193
|
+
time: tip[0].mediantime,
|
|
194
|
+
hash,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
async broadcastPackage(parent, child) {
|
|
198
|
+
const response = await fetch(`${this.baseUrl}/txs/package`, {
|
|
199
|
+
method: "POST",
|
|
200
|
+
headers: {
|
|
201
|
+
"Content-Type": "application/json",
|
|
202
|
+
},
|
|
203
|
+
body: JSON.stringify([parent, child]),
|
|
204
|
+
});
|
|
205
|
+
if (!response.ok) {
|
|
206
|
+
const error = await response.text();
|
|
207
|
+
throw new Error(`Failed to broadcast package: ${error}`);
|
|
208
|
+
}
|
|
209
|
+
return response.json();
|
|
210
|
+
}
|
|
211
|
+
async broadcastTx(tx) {
|
|
212
|
+
const response = await fetch(`${this.baseUrl}/tx`, {
|
|
213
|
+
method: "POST",
|
|
214
|
+
headers: {
|
|
215
|
+
"Content-Type": "text/plain",
|
|
216
|
+
},
|
|
217
|
+
body: tx,
|
|
218
|
+
});
|
|
219
|
+
if (!response.ok) {
|
|
220
|
+
const error = await response.text();
|
|
221
|
+
throw new Error(`Failed to broadcast transaction: ${error}`);
|
|
222
|
+
}
|
|
223
|
+
return response.text();
|
|
224
|
+
}
|
|
69
225
|
}
|
|
226
|
+
function isValidBlocksTip(tip) {
|
|
227
|
+
return (Array.isArray(tip) &&
|
|
228
|
+
tip.every((t) => {
|
|
229
|
+
t &&
|
|
230
|
+
typeof t === "object" &&
|
|
231
|
+
typeof t.id === "string" &&
|
|
232
|
+
t.id.length > 0 &&
|
|
233
|
+
typeof t.height === "number" &&
|
|
234
|
+
t.height >= 0 &&
|
|
235
|
+
typeof t.mediantime === "number" &&
|
|
236
|
+
t.mediantime > 0;
|
|
237
|
+
}));
|
|
238
|
+
}
|
|
239
|
+
const isExplorerTransaction = (tx) => {
|
|
240
|
+
return (typeof tx.txid === "string" &&
|
|
241
|
+
Array.isArray(tx.vout) &&
|
|
242
|
+
tx.vout.every((vout) => typeof vout.scriptpubkey_address === "string" &&
|
|
243
|
+
typeof vout.value === "string") &&
|
|
244
|
+
typeof tx.status === "object" &&
|
|
245
|
+
typeof tx.status.confirmed === "boolean" &&
|
|
246
|
+
typeof tx.status.block_time === "number");
|
|
247
|
+
};
|
|
@@ -1,16 +1,40 @@
|
|
|
1
1
|
import { bech32m } from "@scure/base";
|
|
2
2
|
import { Script } from "@scure/btc-signer";
|
|
3
|
-
|
|
3
|
+
/**
|
|
4
|
+
* ArkAddress allows to create and decode bech32m encoded ark address.
|
|
5
|
+
* An ark address is composed of:
|
|
6
|
+
* - a human readable prefix (hrp)
|
|
7
|
+
* - a version byte (1 byte)
|
|
8
|
+
* - a server public key (32 bytes)
|
|
9
|
+
* - a vtxo taproot public key (32 bytes)
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```typescript
|
|
13
|
+
* const address = new ArkAddress(
|
|
14
|
+
* new Uint8Array(32), // server public key
|
|
15
|
+
* new Uint8Array(32), // vtxo taproot public key
|
|
16
|
+
* "ark"
|
|
17
|
+
* );
|
|
18
|
+
*
|
|
19
|
+
* const encoded = address.encode();
|
|
20
|
+
* console.log("address: ", encoded);
|
|
21
|
+
*
|
|
22
|
+
* const decoded = ArkAddress.decode(encoded);
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
4
25
|
export class ArkAddress {
|
|
5
|
-
constructor(serverPubKey,
|
|
26
|
+
constructor(serverPubKey, vtxoTaprootKey, hrp, version = 0) {
|
|
6
27
|
this.serverPubKey = serverPubKey;
|
|
7
|
-
this.
|
|
28
|
+
this.vtxoTaprootKey = vtxoTaprootKey;
|
|
8
29
|
this.hrp = hrp;
|
|
30
|
+
this.version = version;
|
|
9
31
|
if (serverPubKey.length !== 32) {
|
|
10
|
-
throw new Error("Invalid server public key length"
|
|
32
|
+
throw new Error("Invalid server public key length, expected 32 bytes, got " +
|
|
33
|
+
serverPubKey.length);
|
|
11
34
|
}
|
|
12
|
-
if (
|
|
13
|
-
throw new Error("Invalid
|
|
35
|
+
if (vtxoTaprootKey.length !== 32) {
|
|
36
|
+
throw new Error("Invalid vtxo taproot public key length, expected 32 bytes, got " +
|
|
37
|
+
vtxoTaprootKey.length);
|
|
14
38
|
}
|
|
15
39
|
}
|
|
16
40
|
static decode(address) {
|
|
@@ -19,23 +43,30 @@ export class ArkAddress {
|
|
|
19
43
|
throw new Error("Invalid address");
|
|
20
44
|
}
|
|
21
45
|
const data = new Uint8Array(bech32m.fromWords(decoded.words));
|
|
22
|
-
// First 32 bytes
|
|
23
|
-
if (data.length !==
|
|
24
|
-
throw new Error("Invalid data length");
|
|
46
|
+
// First the version byte, then 32 bytes server pubkey, then 32 bytes vtxo taproot pubkey
|
|
47
|
+
if (data.length !== 1 + 32 + 32) {
|
|
48
|
+
throw new Error("Invalid data length, expected 65 bytes, got " + data.length);
|
|
25
49
|
}
|
|
26
|
-
const
|
|
27
|
-
const
|
|
28
|
-
|
|
50
|
+
const version = data[0];
|
|
51
|
+
const serverPubKey = data.slice(1, 33);
|
|
52
|
+
const vtxoTaprootPubKey = data.slice(33, 65);
|
|
53
|
+
return new ArkAddress(serverPubKey, vtxoTaprootPubKey, decoded.prefix, version);
|
|
29
54
|
}
|
|
30
55
|
encode() {
|
|
31
|
-
// Combine server pubkey and
|
|
32
|
-
const data = new Uint8Array(
|
|
33
|
-
data
|
|
34
|
-
data.set(this.
|
|
56
|
+
// Combine version byte, server pubkey, and vtxo taproot pubkey
|
|
57
|
+
const data = new Uint8Array(1 + 32 + 32);
|
|
58
|
+
data[0] = this.version;
|
|
59
|
+
data.set(this.serverPubKey, 1);
|
|
60
|
+
data.set(this.vtxoTaprootKey, 33);
|
|
35
61
|
const words = bech32m.toWords(data);
|
|
36
62
|
return bech32m.encode(this.hrp, words, 1023);
|
|
37
63
|
}
|
|
64
|
+
// pkScript is the script that should be used to send non-dust funds to the address
|
|
38
65
|
get pkScript() {
|
|
39
|
-
return Script.encode(["OP_1", this.
|
|
66
|
+
return Script.encode(["OP_1", this.vtxoTaprootKey]);
|
|
67
|
+
}
|
|
68
|
+
// subdustPkScript is the script that should be used to send sub-dust funds to the address
|
|
69
|
+
get subdustPkScript() {
|
|
70
|
+
return Script.encode(["RETURN", this.vtxoTaprootKey]);
|
|
40
71
|
}
|
|
41
72
|
}
|
package/dist/esm/script/base.js
CHANGED
|
@@ -3,12 +3,22 @@ import { TAPROOT_UNSPENDABLE_KEY, } from "@scure/btc-signer/utils";
|
|
|
3
3
|
import { ArkAddress } from './address.js';
|
|
4
4
|
import { Script } from "@scure/btc-signer";
|
|
5
5
|
import { hex } from "@scure/base";
|
|
6
|
+
import { ConditionCSVMultisigTapscript, CSVMultisigTapscript, } from './tapscript.js';
|
|
6
7
|
export function scriptFromTapLeafScript(leaf) {
|
|
7
8
|
return leaf[1].subarray(0, leaf[1].length - 1); // remove the version byte
|
|
8
9
|
}
|
|
10
|
+
/**
|
|
11
|
+
* VtxoScript is a script that contains a list of tapleaf scripts.
|
|
12
|
+
* It is used to create vtxo scripts.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```typescript
|
|
16
|
+
* const vtxoScript = new VtxoScript([new Uint8Array(32), new Uint8Array(32)]);
|
|
17
|
+
*/
|
|
9
18
|
export class VtxoScript {
|
|
10
|
-
static decode(
|
|
11
|
-
|
|
19
|
+
static decode(tapTree) {
|
|
20
|
+
const leaves = decodeTaprootTree(tapTree);
|
|
21
|
+
return new VtxoScript(leaves);
|
|
12
22
|
}
|
|
13
23
|
constructor(scripts) {
|
|
14
24
|
this.scripts = scripts;
|
|
@@ -22,7 +32,8 @@ export class VtxoScript {
|
|
|
22
32
|
this.tweakedPublicKey = payment.tweakedPubkey;
|
|
23
33
|
}
|
|
24
34
|
encode() {
|
|
25
|
-
|
|
35
|
+
const tapTree = encodeTaprootTree(this.scripts);
|
|
36
|
+
return tapTree;
|
|
26
37
|
}
|
|
27
38
|
address(prefix, serverPubKey) {
|
|
28
39
|
return new ArkAddress(serverPubKey, this.tweakedPublicKey, prefix);
|
|
@@ -43,4 +54,110 @@ export class VtxoScript {
|
|
|
43
54
|
}
|
|
44
55
|
return leaf;
|
|
45
56
|
}
|
|
57
|
+
exitPaths() {
|
|
58
|
+
const paths = [];
|
|
59
|
+
for (const leaf of this.leaves) {
|
|
60
|
+
try {
|
|
61
|
+
const tapscript = CSVMultisigTapscript.decode(scriptFromTapLeafScript(leaf));
|
|
62
|
+
paths.push(tapscript);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
catch (e) {
|
|
66
|
+
try {
|
|
67
|
+
const tapscript = ConditionCSVMultisigTapscript.decode(scriptFromTapLeafScript(leaf));
|
|
68
|
+
paths.push(tapscript);
|
|
69
|
+
}
|
|
70
|
+
catch (e) {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return paths;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function decodeTaprootTree(tapTree) {
|
|
79
|
+
let offset = 0;
|
|
80
|
+
const scripts = [];
|
|
81
|
+
// Read number of leaves
|
|
82
|
+
const [numLeaves, numLeavesSize] = decodeCompactSizeUint(tapTree, offset);
|
|
83
|
+
offset += numLeavesSize;
|
|
84
|
+
// Read each leaf
|
|
85
|
+
for (let i = 0; i < numLeaves; i++) {
|
|
86
|
+
// Skip depth (1 byte)
|
|
87
|
+
offset += 1;
|
|
88
|
+
// Skip leaf version (1 byte)
|
|
89
|
+
offset += 1;
|
|
90
|
+
// Read script length
|
|
91
|
+
const [scriptLength, scriptLengthSize] = decodeCompactSizeUint(tapTree, offset);
|
|
92
|
+
offset += scriptLengthSize;
|
|
93
|
+
// Read script content
|
|
94
|
+
const script = tapTree.slice(offset, offset + scriptLength);
|
|
95
|
+
scripts.push(script);
|
|
96
|
+
offset += scriptLength;
|
|
97
|
+
}
|
|
98
|
+
return scripts;
|
|
99
|
+
}
|
|
100
|
+
function decodeCompactSizeUint(data, offset) {
|
|
101
|
+
const firstByte = data[offset];
|
|
102
|
+
if (firstByte < 0xfd) {
|
|
103
|
+
return [firstByte, 1];
|
|
104
|
+
}
|
|
105
|
+
else if (firstByte === 0xfd) {
|
|
106
|
+
const value = new DataView(data.buffer).getUint16(offset + 1, true);
|
|
107
|
+
return [value, 3];
|
|
108
|
+
}
|
|
109
|
+
else if (firstByte === 0xfe) {
|
|
110
|
+
const value = new DataView(data.buffer).getUint32(offset + 1, true);
|
|
111
|
+
return [value, 5];
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
const value = Number(new DataView(data.buffer).getBigUint64(offset + 1, true));
|
|
115
|
+
return [value, 9];
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function encodeTaprootTree(leaves) {
|
|
119
|
+
const chunks = [];
|
|
120
|
+
// Write number of leaves as compact size uint
|
|
121
|
+
chunks.push(encodeCompactSizeUint(leaves.length));
|
|
122
|
+
for (const tapscript of leaves) {
|
|
123
|
+
// Write depth (always 1 for now)
|
|
124
|
+
chunks.push(new Uint8Array([1]));
|
|
125
|
+
// Write leaf version (0xc0 for tapscript)
|
|
126
|
+
chunks.push(new Uint8Array([0xc0]));
|
|
127
|
+
// Write script length and script
|
|
128
|
+
chunks.push(encodeCompactSizeUint(tapscript.length));
|
|
129
|
+
chunks.push(tapscript);
|
|
130
|
+
}
|
|
131
|
+
// Concatenate all chunks
|
|
132
|
+
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
|
133
|
+
const result = new Uint8Array(totalLength);
|
|
134
|
+
let offset = 0;
|
|
135
|
+
for (const chunk of chunks) {
|
|
136
|
+
result.set(chunk, offset);
|
|
137
|
+
offset += chunk.length;
|
|
138
|
+
}
|
|
139
|
+
return result;
|
|
140
|
+
}
|
|
141
|
+
function encodeCompactSizeUint(value) {
|
|
142
|
+
if (value < 0xfd) {
|
|
143
|
+
return new Uint8Array([value]);
|
|
144
|
+
}
|
|
145
|
+
else if (value <= 0xffff) {
|
|
146
|
+
const buffer = new Uint8Array(3);
|
|
147
|
+
buffer[0] = 0xfd;
|
|
148
|
+
new DataView(buffer.buffer).setUint16(1, value, true);
|
|
149
|
+
return buffer;
|
|
150
|
+
}
|
|
151
|
+
else if (value <= 0xffffffff) {
|
|
152
|
+
const buffer = new Uint8Array(5);
|
|
153
|
+
buffer[0] = 0xfe;
|
|
154
|
+
new DataView(buffer.buffer).setUint32(1, value, true);
|
|
155
|
+
return buffer;
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
const buffer = new Uint8Array(9);
|
|
159
|
+
buffer[0] = 0xff;
|
|
160
|
+
new DataView(buffer.buffer).setBigUint64(1, BigInt(value), true);
|
|
161
|
+
return buffer;
|
|
162
|
+
}
|
|
46
163
|
}
|
|
@@ -1,12 +1,26 @@
|
|
|
1
1
|
import { VtxoScript } from './base.js';
|
|
2
2
|
import { CSVMultisigTapscript, MultisigTapscript, } from './tapscript.js';
|
|
3
3
|
import { hex } from "@scure/base";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
/**
|
|
5
|
+
* DefaultVtxo is the default implementation of a VtxoScript.
|
|
6
|
+
* It contains 1 forfeit path and 1 exit path.
|
|
7
|
+
* - forfeit = (Alice + Server)
|
|
8
|
+
* - exit = (Alice) after csvTimelock
|
|
9
|
+
*/
|
|
8
10
|
export var DefaultVtxo;
|
|
9
11
|
(function (DefaultVtxo) {
|
|
12
|
+
/**
|
|
13
|
+
* DefaultVtxo.Script is the class letting to create the vtxo script.
|
|
14
|
+
* @example
|
|
15
|
+
* ```typescript
|
|
16
|
+
* const vtxoScript = new DefaultVtxo.Script({
|
|
17
|
+
* pubKey: new Uint8Array(32),
|
|
18
|
+
* serverPubKey: new Uint8Array(32),
|
|
19
|
+
* });
|
|
20
|
+
*
|
|
21
|
+
* console.log("script pub key:", vtxoScript.pkScript)
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
10
24
|
class Script extends VtxoScript {
|
|
11
25
|
constructor(options) {
|
|
12
26
|
const { pubKey, serverPubKey, csvTimelock = Script.DEFAULT_TIMELOCK, } = options;
|
|
@@ -10,6 +10,16 @@ export var TapscriptType;
|
|
|
10
10
|
TapscriptType["ConditionMultisig"] = "condition-multisig";
|
|
11
11
|
TapscriptType["CLTVMultisig"] = "cltv-multisig";
|
|
12
12
|
})(TapscriptType || (TapscriptType = {}));
|
|
13
|
+
/**
|
|
14
|
+
* decodeTapscript is a function that decodes an ark tapsript from a raw script.
|
|
15
|
+
*
|
|
16
|
+
* @throws {Error} if the script is not a valid ark tapscript
|
|
17
|
+
* @example
|
|
18
|
+
* ```typescript
|
|
19
|
+
* const arkTapscript = decodeTapscript(new Uint8Array(32));
|
|
20
|
+
* console.log("type:", arkTapscript.type);
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
13
23
|
export function decodeTapscript(script) {
|
|
14
24
|
const types = [
|
|
15
25
|
MultisigTapscript,
|
|
@@ -29,8 +39,14 @@ export function decodeTapscript(script) {
|
|
|
29
39
|
throw new Error(`Failed to decode: script ${hex.encode(script)} is not a valid tapscript`);
|
|
30
40
|
}
|
|
31
41
|
/**
|
|
32
|
-
* Implements a multi-signature
|
|
33
|
-
*
|
|
42
|
+
* Implements a multi-signature tapscript.
|
|
43
|
+
*
|
|
44
|
+
* <pubkey> CHECKSIGVERIFY <pubkey> CHECKSIG
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* ```typescript
|
|
48
|
+
* const multisigTapscript = MultisigTapscript.encode({ pubkeys: [new Uint8Array(32), new Uint8Array(32)] });
|
|
49
|
+
* ```
|
|
34
50
|
*/
|
|
35
51
|
export var MultisigTapscript;
|
|
36
52
|
(function (MultisigTapscript) {
|
|
@@ -56,7 +72,6 @@ export var MultisigTapscript;
|
|
|
56
72
|
type: TapscriptType.Multisig,
|
|
57
73
|
params,
|
|
58
74
|
script: p2tr_ms(params.pubkeys.length, params.pubkeys).script,
|
|
59
|
-
witnessSize: () => params.pubkeys.length * 64,
|
|
60
75
|
};
|
|
61
76
|
}
|
|
62
77
|
const asm = [];
|
|
@@ -74,7 +89,6 @@ export var MultisigTapscript;
|
|
|
74
89
|
type: TapscriptType.Multisig,
|
|
75
90
|
params,
|
|
76
91
|
script: Script.encode(asm),
|
|
77
|
-
witnessSize: () => params.pubkeys.length * 64,
|
|
78
92
|
};
|
|
79
93
|
}
|
|
80
94
|
MultisigTapscript.encode = encode;
|
|
@@ -145,7 +159,6 @@ export var MultisigTapscript;
|
|
|
145
159
|
type: TapscriptType.Multisig,
|
|
146
160
|
params: { pubkeys, type: MultisigType.CHECKSIGADD },
|
|
147
161
|
script,
|
|
148
|
-
witnessSize: () => pubkeys.length * 64,
|
|
149
162
|
};
|
|
150
163
|
}
|
|
151
164
|
// <pubkey> CHECKSIGVERIFY <pubkey> CHECKSIG
|
|
@@ -189,7 +202,6 @@ export var MultisigTapscript;
|
|
|
189
202
|
type: TapscriptType.Multisig,
|
|
190
203
|
params: { pubkeys, type: MultisigType.CHECKSIG },
|
|
191
204
|
script,
|
|
192
|
-
witnessSize: () => pubkeys.length * 64,
|
|
193
205
|
};
|
|
194
206
|
}
|
|
195
207
|
function is(tapscript) {
|
|
@@ -202,6 +214,13 @@ export var MultisigTapscript;
|
|
|
202
214
|
* after the relative timelock has expired. The timelock can be specified in blocks or seconds.
|
|
203
215
|
*
|
|
204
216
|
* This is the standard exit closure and it is also used for the sweep closure in vtxo trees.
|
|
217
|
+
*
|
|
218
|
+
* <sequence> CHECKSEQUENCEVERIFY DROP <pubkey> CHECKSIG
|
|
219
|
+
*
|
|
220
|
+
* @example
|
|
221
|
+
* ```typescript
|
|
222
|
+
* const csvMultisigTapscript = CSVMultisigTapscript.encode({ timelock: { type: "blocks", value: 144 }, pubkeys: [new Uint8Array(32), new Uint8Array(32)] });
|
|
223
|
+
* ```
|
|
205
224
|
*/
|
|
206
225
|
export var CSVMultisigTapscript;
|
|
207
226
|
(function (CSVMultisigTapscript) {
|
|
@@ -224,7 +243,6 @@ export var CSVMultisigTapscript;
|
|
|
224
243
|
type: TapscriptType.CSVMultisig,
|
|
225
244
|
params,
|
|
226
245
|
script,
|
|
227
|
-
witnessSize: () => params.pubkeys.length * 64,
|
|
228
246
|
};
|
|
229
247
|
}
|
|
230
248
|
CSVMultisigTapscript.encode = encode;
|
|
@@ -270,7 +288,6 @@ export var CSVMultisigTapscript;
|
|
|
270
288
|
...multisig.params,
|
|
271
289
|
},
|
|
272
290
|
script,
|
|
273
|
-
witnessSize: () => multisig.params.pubkeys.length * 64,
|
|
274
291
|
};
|
|
275
292
|
}
|
|
276
293
|
CSVMultisigTapscript.decode = decode;
|
|
@@ -283,6 +300,13 @@ export var CSVMultisigTapscript;
|
|
|
283
300
|
* Combines a condition script with an exit closure. The resulting script requires
|
|
284
301
|
* the condition to be met, followed by the standard exit closure requirements
|
|
285
302
|
* (timelock and signatures).
|
|
303
|
+
*
|
|
304
|
+
* <conditionScript> VERIFY <sequence> CHECKSEQUENCEVERIFY DROP <pubkey> CHECKSIGVERIFY <pubkey> CHECKSIG
|
|
305
|
+
*
|
|
306
|
+
* @example
|
|
307
|
+
* ```typescript
|
|
308
|
+
* const conditionCSVMultisigTapscript = ConditionCSVMultisigTapscript.encode({ conditionScript: new Uint8Array(32), pubkeys: [new Uint8Array(32), new Uint8Array(32)] });
|
|
309
|
+
* ```
|
|
286
310
|
*/
|
|
287
311
|
export var ConditionCSVMultisigTapscript;
|
|
288
312
|
(function (ConditionCSVMultisigTapscript) {
|
|
@@ -296,7 +320,6 @@ export var ConditionCSVMultisigTapscript;
|
|
|
296
320
|
type: TapscriptType.ConditionCSVMultisig,
|
|
297
321
|
params,
|
|
298
322
|
script,
|
|
299
|
-
witnessSize: (conditionSize) => conditionSize + params.pubkeys.length * 64,
|
|
300
323
|
};
|
|
301
324
|
}
|
|
302
325
|
ConditionCSVMultisigTapscript.encode = encode;
|
|
@@ -340,7 +363,6 @@ export var ConditionCSVMultisigTapscript;
|
|
|
340
363
|
...csvMultisig.params,
|
|
341
364
|
},
|
|
342
365
|
script,
|
|
343
|
-
witnessSize: (conditionSize) => conditionSize + csvMultisig.params.pubkeys.length * 64,
|
|
344
366
|
};
|
|
345
367
|
}
|
|
346
368
|
ConditionCSVMultisigTapscript.decode = decode;
|
|
@@ -353,6 +375,13 @@ export var ConditionCSVMultisigTapscript;
|
|
|
353
375
|
* Combines a condition script with a forfeit closure. The resulting script requires
|
|
354
376
|
* the condition to be met, followed by the standard forfeit closure requirements
|
|
355
377
|
* (multi-signature).
|
|
378
|
+
*
|
|
379
|
+
* <conditionScript> VERIFY <pubkey> CHECKSIGVERIFY <pubkey> CHECKSIG
|
|
380
|
+
*
|
|
381
|
+
* @example
|
|
382
|
+
* ```typescript
|
|
383
|
+
* const conditionMultisigTapscript = ConditionMultisigTapscript.encode({ conditionScript: new Uint8Array(32), pubkeys: [new Uint8Array(32), new Uint8Array(32)] });
|
|
384
|
+
* ```
|
|
356
385
|
*/
|
|
357
386
|
export var ConditionMultisigTapscript;
|
|
358
387
|
(function (ConditionMultisigTapscript) {
|
|
@@ -366,7 +395,6 @@ export var ConditionMultisigTapscript;
|
|
|
366
395
|
type: TapscriptType.ConditionMultisig,
|
|
367
396
|
params,
|
|
368
397
|
script,
|
|
369
|
-
witnessSize: (conditionSize) => conditionSize + params.pubkeys.length * 64,
|
|
370
398
|
};
|
|
371
399
|
}
|
|
372
400
|
ConditionMultisigTapscript.encode = encode;
|
|
@@ -410,7 +438,6 @@ export var ConditionMultisigTapscript;
|
|
|
410
438
|
...multisig.params,
|
|
411
439
|
},
|
|
412
440
|
script,
|
|
413
|
-
witnessSize: (conditionSize) => conditionSize + multisig.params.pubkeys.length * 64,
|
|
414
441
|
};
|
|
415
442
|
}
|
|
416
443
|
ConditionMultisigTapscript.decode = decode;
|
|
@@ -423,6 +450,13 @@ export var ConditionMultisigTapscript;
|
|
|
423
450
|
* Implements an absolute timelock (CLTV) script combined with a forfeit closure.
|
|
424
451
|
* The script requires waiting until a specific block height/timestamp before the
|
|
425
452
|
* forfeit closure conditions can be met.
|
|
453
|
+
*
|
|
454
|
+
* <locktime> CHECKLOCKTIMEVERIFY DROP <pubkey> CHECKSIGVERIFY <pubkey> CHECKSIG
|
|
455
|
+
*
|
|
456
|
+
* @example
|
|
457
|
+
* ```typescript
|
|
458
|
+
* const cltvMultisigTapscript = CLTVMultisigTapscript.encode({ absoluteTimelock: 144, pubkeys: [new Uint8Array(32), new Uint8Array(32)] });
|
|
459
|
+
* ```
|
|
426
460
|
*/
|
|
427
461
|
export var CLTVMultisigTapscript;
|
|
428
462
|
(function (CLTVMultisigTapscript) {
|
|
@@ -438,7 +472,6 @@ export var CLTVMultisigTapscript;
|
|
|
438
472
|
type: TapscriptType.CLTVMultisig,
|
|
439
473
|
params,
|
|
440
474
|
script,
|
|
441
|
-
witnessSize: () => params.pubkeys.length * 64,
|
|
442
475
|
};
|
|
443
476
|
}
|
|
444
477
|
CLTVMultisigTapscript.encode = encode;
|
|
@@ -480,7 +513,6 @@ export var CLTVMultisigTapscript;
|
|
|
480
513
|
...multisig.params,
|
|
481
514
|
},
|
|
482
515
|
script,
|
|
483
|
-
witnessSize: () => multisig.params.pubkeys.length * 64,
|
|
484
516
|
};
|
|
485
517
|
}
|
|
486
518
|
CLTVMultisigTapscript.decode = decode;
|