@arkade-os/sdk 0.1.4 → 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.
Files changed (114) hide show
  1. package/README.md +156 -174
  2. package/dist/cjs/arknote/index.js +61 -58
  3. package/dist/cjs/bip322/errors.js +13 -0
  4. package/dist/cjs/bip322/index.js +178 -0
  5. package/dist/cjs/forfeit.js +14 -25
  6. package/dist/cjs/identity/singleKey.js +68 -0
  7. package/dist/cjs/index.js +41 -17
  8. package/dist/cjs/providers/ark.js +253 -317
  9. package/dist/cjs/providers/indexer.js +525 -0
  10. package/dist/cjs/providers/onchain.js +193 -15
  11. package/dist/cjs/script/address.js +48 -17
  12. package/dist/cjs/script/base.js +120 -3
  13. package/dist/cjs/script/default.js +18 -4
  14. package/dist/cjs/script/tapscript.js +46 -14
  15. package/dist/cjs/script/vhtlc.js +27 -7
  16. package/dist/cjs/tree/signingSession.js +63 -106
  17. package/dist/cjs/tree/txTree.js +193 -0
  18. package/dist/cjs/tree/validation.js +79 -155
  19. package/dist/cjs/utils/anchor.js +35 -0
  20. package/dist/cjs/utils/arkTransaction.js +108 -0
  21. package/dist/cjs/utils/transactionHistory.js +84 -72
  22. package/dist/cjs/utils/txSizeEstimator.js +12 -0
  23. package/dist/cjs/utils/unknownFields.js +211 -0
  24. package/dist/cjs/wallet/index.js +12 -0
  25. package/dist/cjs/wallet/onchain.js +201 -0
  26. package/dist/cjs/wallet/ramps.js +95 -0
  27. package/dist/cjs/wallet/serviceWorker/db/vtxo/idb.js +32 -0
  28. package/dist/cjs/wallet/serviceWorker/request.js +15 -12
  29. package/dist/cjs/wallet/serviceWorker/response.js +22 -27
  30. package/dist/cjs/wallet/serviceWorker/utils.js +8 -0
  31. package/dist/cjs/wallet/serviceWorker/wallet.js +58 -34
  32. package/dist/cjs/wallet/serviceWorker/worker.js +117 -108
  33. package/dist/cjs/wallet/unroll.js +270 -0
  34. package/dist/cjs/wallet/wallet.js +701 -454
  35. package/dist/esm/arknote/index.js +61 -57
  36. package/dist/esm/bip322/errors.js +9 -0
  37. package/dist/esm/bip322/index.js +174 -0
  38. package/dist/esm/forfeit.js +15 -26
  39. package/dist/esm/identity/singleKey.js +64 -0
  40. package/dist/esm/index.js +30 -12
  41. package/dist/esm/providers/ark.js +252 -317
  42. package/dist/esm/providers/indexer.js +521 -0
  43. package/dist/esm/providers/onchain.js +193 -15
  44. package/dist/esm/script/address.js +48 -17
  45. package/dist/esm/script/base.js +120 -3
  46. package/dist/esm/script/default.js +18 -4
  47. package/dist/esm/script/tapscript.js +46 -14
  48. package/dist/esm/script/vhtlc.js +27 -7
  49. package/dist/esm/tree/signingSession.js +65 -108
  50. package/dist/esm/tree/txTree.js +189 -0
  51. package/dist/esm/tree/validation.js +75 -152
  52. package/dist/esm/utils/anchor.js +31 -0
  53. package/dist/esm/utils/arkTransaction.js +105 -0
  54. package/dist/esm/utils/transactionHistory.js +84 -72
  55. package/dist/esm/utils/txSizeEstimator.js +12 -0
  56. package/dist/esm/utils/unknownFields.js +173 -0
  57. package/dist/esm/wallet/index.js +9 -0
  58. package/dist/esm/wallet/onchain.js +196 -0
  59. package/dist/esm/wallet/ramps.js +91 -0
  60. package/dist/esm/wallet/serviceWorker/db/vtxo/idb.js +32 -0
  61. package/dist/esm/wallet/serviceWorker/request.js +15 -12
  62. package/dist/esm/wallet/serviceWorker/response.js +22 -27
  63. package/dist/esm/wallet/serviceWorker/utils.js +8 -0
  64. package/dist/esm/wallet/serviceWorker/wallet.js +59 -35
  65. package/dist/esm/wallet/serviceWorker/worker.js +117 -108
  66. package/dist/esm/wallet/unroll.js +267 -0
  67. package/dist/esm/wallet/wallet.js +674 -461
  68. package/dist/types/arknote/index.d.ts +40 -13
  69. package/dist/types/bip322/errors.d.ts +6 -0
  70. package/dist/types/bip322/index.d.ts +57 -0
  71. package/dist/types/forfeit.d.ts +2 -14
  72. package/dist/types/identity/singleKey.d.ts +27 -0
  73. package/dist/types/index.d.ts +23 -12
  74. package/dist/types/providers/ark.d.ts +114 -95
  75. package/dist/types/providers/indexer.d.ts +186 -0
  76. package/dist/types/providers/onchain.d.ts +41 -11
  77. package/dist/types/script/address.d.ts +26 -2
  78. package/dist/types/script/base.d.ts +13 -3
  79. package/dist/types/script/default.d.ts +22 -0
  80. package/dist/types/script/tapscript.d.ts +61 -5
  81. package/dist/types/script/vhtlc.d.ts +27 -0
  82. package/dist/types/tree/signingSession.d.ts +5 -5
  83. package/dist/types/tree/txTree.d.ts +28 -0
  84. package/dist/types/tree/validation.d.ts +15 -22
  85. package/dist/types/utils/anchor.d.ts +19 -0
  86. package/dist/types/utils/arkTransaction.d.ts +27 -0
  87. package/dist/types/utils/transactionHistory.d.ts +7 -1
  88. package/dist/types/utils/txSizeEstimator.d.ts +3 -0
  89. package/dist/types/utils/unknownFields.d.ts +83 -0
  90. package/dist/types/wallet/index.d.ts +51 -50
  91. package/dist/types/wallet/onchain.d.ts +49 -0
  92. package/dist/types/wallet/ramps.d.ts +32 -0
  93. package/dist/types/wallet/serviceWorker/db/vtxo/idb.d.ts +2 -0
  94. package/dist/types/wallet/serviceWorker/db/vtxo/index.d.ts +2 -0
  95. package/dist/types/wallet/serviceWorker/request.d.ts +14 -16
  96. package/dist/types/wallet/serviceWorker/response.d.ts +17 -19
  97. package/dist/types/wallet/serviceWorker/utils.d.ts +8 -0
  98. package/dist/types/wallet/serviceWorker/wallet.d.ts +36 -8
  99. package/dist/types/wallet/serviceWorker/worker.d.ts +7 -3
  100. package/dist/types/wallet/unroll.d.ts +102 -0
  101. package/dist/types/wallet/wallet.d.ts +71 -25
  102. package/package.json +14 -15
  103. package/dist/cjs/identity/inMemoryKey.js +0 -40
  104. package/dist/cjs/tree/vtxoTree.js +0 -231
  105. package/dist/cjs/utils/coinselect.js +0 -73
  106. package/dist/cjs/utils/psbt.js +0 -137
  107. package/dist/esm/identity/inMemoryKey.js +0 -36
  108. package/dist/esm/tree/vtxoTree.js +0 -191
  109. package/dist/esm/utils/coinselect.js +0 -69
  110. package/dist/esm/utils/psbt.js +0 -131
  111. package/dist/types/identity/inMemoryKey.d.ts +0 -12
  112. package/dist/types/tree/vtxoTree.d.ts +0 -33
  113. package/dist/types/utils/coinselect.d.ts +0 -21
  114. 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}/v1/fees/recommended`);
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.halfHourFee; // Return the "medium" priority fee rate
36
+ const fees = (await response.json());
37
+ return fees["1"] ?? undefined;
26
38
  }
27
- async broadcastTransaction(txHex) {
28
- const response = await fetch(`${this.baseUrl}/tx`, {
29
- method: "POST",
30
- headers: {
31
- "Content-Type": "text/plain",
32
- },
33
- body: txHex,
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
- // ArkAddress is a bech32m encoded address with a custom HRP (ark/tark)
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, tweakedPubKey, hrp) {
26
+ constructor(serverPubKey, vtxoTaprootKey, hrp, version = 0) {
6
27
  this.serverPubKey = serverPubKey;
7
- this.tweakedPubKey = tweakedPubKey;
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 (tweakedPubKey.length !== 32) {
13
- throw new Error("Invalid tweaked public key length");
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 are server pubkey, next 32 bytes are tweaked pubkey
23
- if (data.length !== 64) {
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 serverPubKey = data.slice(0, 32);
27
- const tweakedPubKey = data.slice(32, 64);
28
- return new ArkAddress(serverPubKey, tweakedPubKey, decoded.prefix);
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 tweaked pubkey
32
- const data = new Uint8Array(64);
33
- data.set(this.serverPubKey, 0);
34
- data.set(this.tweakedPubKey, 32);
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.tweakedPubKey]);
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
  }
@@ -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(scripts) {
11
- return new VtxoScript(scripts.map(hex.decode));
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
- return this.scripts.map(hex.encode);
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
- // DefaultVtxo is the default implementation of a VtxoScript.
5
- // it contains 1 forfeit path and 1 exit path.
6
- // forfeit = (Alice + Server)
7
- // exit = (Alice) after csvTimelock
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 script that requires a threshold of signatures
33
- * from the specified pubkeys.
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;