@arkade-os/sdk 0.0.16

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 (103) hide show
  1. package/README.md +312 -0
  2. package/dist/cjs/arknote/index.js +86 -0
  3. package/dist/cjs/forfeit.js +38 -0
  4. package/dist/cjs/identity/inMemoryKey.js +40 -0
  5. package/dist/cjs/identity/index.js +2 -0
  6. package/dist/cjs/index.js +48 -0
  7. package/dist/cjs/musig2/index.js +10 -0
  8. package/dist/cjs/musig2/keys.js +57 -0
  9. package/dist/cjs/musig2/nonces.js +44 -0
  10. package/dist/cjs/musig2/sign.js +102 -0
  11. package/dist/cjs/networks.js +26 -0
  12. package/dist/cjs/package.json +3 -0
  13. package/dist/cjs/providers/ark.js +530 -0
  14. package/dist/cjs/providers/onchain.js +61 -0
  15. package/dist/cjs/script/address.js +45 -0
  16. package/dist/cjs/script/base.js +51 -0
  17. package/dist/cjs/script/default.js +40 -0
  18. package/dist/cjs/script/tapscript.js +528 -0
  19. package/dist/cjs/script/vhtlc.js +84 -0
  20. package/dist/cjs/tree/signingSession.js +238 -0
  21. package/dist/cjs/tree/validation.js +184 -0
  22. package/dist/cjs/tree/vtxoTree.js +197 -0
  23. package/dist/cjs/utils/bip21.js +114 -0
  24. package/dist/cjs/utils/coinselect.js +73 -0
  25. package/dist/cjs/utils/psbt.js +124 -0
  26. package/dist/cjs/utils/transactionHistory.js +148 -0
  27. package/dist/cjs/utils/txSizeEstimator.js +95 -0
  28. package/dist/cjs/wallet/index.js +8 -0
  29. package/dist/cjs/wallet/serviceWorker/db/vtxo/idb.js +153 -0
  30. package/dist/cjs/wallet/serviceWorker/db/vtxo/index.js +2 -0
  31. package/dist/cjs/wallet/serviceWorker/request.js +75 -0
  32. package/dist/cjs/wallet/serviceWorker/response.js +187 -0
  33. package/dist/cjs/wallet/serviceWorker/wallet.js +332 -0
  34. package/dist/cjs/wallet/serviceWorker/worker.js +452 -0
  35. package/dist/cjs/wallet/wallet.js +720 -0
  36. package/dist/esm/arknote/index.js +81 -0
  37. package/dist/esm/forfeit.js +35 -0
  38. package/dist/esm/identity/inMemoryKey.js +36 -0
  39. package/dist/esm/identity/index.js +1 -0
  40. package/dist/esm/index.js +39 -0
  41. package/dist/esm/musig2/index.js +3 -0
  42. package/dist/esm/musig2/keys.js +21 -0
  43. package/dist/esm/musig2/nonces.js +8 -0
  44. package/dist/esm/musig2/sign.js +63 -0
  45. package/dist/esm/networks.js +22 -0
  46. package/dist/esm/package.json +3 -0
  47. package/dist/esm/providers/ark.js +526 -0
  48. package/dist/esm/providers/onchain.js +57 -0
  49. package/dist/esm/script/address.js +41 -0
  50. package/dist/esm/script/base.js +46 -0
  51. package/dist/esm/script/default.js +37 -0
  52. package/dist/esm/script/tapscript.js +491 -0
  53. package/dist/esm/script/vhtlc.js +81 -0
  54. package/dist/esm/tree/signingSession.js +200 -0
  55. package/dist/esm/tree/validation.js +179 -0
  56. package/dist/esm/tree/vtxoTree.js +157 -0
  57. package/dist/esm/utils/bip21.js +110 -0
  58. package/dist/esm/utils/coinselect.js +69 -0
  59. package/dist/esm/utils/psbt.js +118 -0
  60. package/dist/esm/utils/transactionHistory.js +145 -0
  61. package/dist/esm/utils/txSizeEstimator.js +91 -0
  62. package/dist/esm/wallet/index.js +5 -0
  63. package/dist/esm/wallet/serviceWorker/db/vtxo/idb.js +149 -0
  64. package/dist/esm/wallet/serviceWorker/db/vtxo/index.js +1 -0
  65. package/dist/esm/wallet/serviceWorker/request.js +72 -0
  66. package/dist/esm/wallet/serviceWorker/response.js +184 -0
  67. package/dist/esm/wallet/serviceWorker/wallet.js +328 -0
  68. package/dist/esm/wallet/serviceWorker/worker.js +448 -0
  69. package/dist/esm/wallet/wallet.js +716 -0
  70. package/dist/types/arknote/index.d.ts +17 -0
  71. package/dist/types/forfeit.d.ts +15 -0
  72. package/dist/types/identity/inMemoryKey.d.ts +12 -0
  73. package/dist/types/identity/index.d.ts +7 -0
  74. package/dist/types/index.d.ts +22 -0
  75. package/dist/types/musig2/index.d.ts +4 -0
  76. package/dist/types/musig2/keys.d.ts +9 -0
  77. package/dist/types/musig2/nonces.d.ts +13 -0
  78. package/dist/types/musig2/sign.d.ts +27 -0
  79. package/dist/types/networks.d.ts +16 -0
  80. package/dist/types/providers/ark.d.ts +126 -0
  81. package/dist/types/providers/onchain.d.ts +36 -0
  82. package/dist/types/script/address.d.ts +10 -0
  83. package/dist/types/script/base.d.ts +26 -0
  84. package/dist/types/script/default.d.ts +19 -0
  85. package/dist/types/script/tapscript.d.ts +94 -0
  86. package/dist/types/script/vhtlc.d.ts +31 -0
  87. package/dist/types/tree/signingSession.d.ts +32 -0
  88. package/dist/types/tree/validation.d.ts +22 -0
  89. package/dist/types/tree/vtxoTree.d.ts +32 -0
  90. package/dist/types/utils/bip21.d.ts +21 -0
  91. package/dist/types/utils/coinselect.d.ts +21 -0
  92. package/dist/types/utils/psbt.d.ts +11 -0
  93. package/dist/types/utils/transactionHistory.d.ts +2 -0
  94. package/dist/types/utils/txSizeEstimator.d.ts +27 -0
  95. package/dist/types/wallet/index.d.ts +122 -0
  96. package/dist/types/wallet/serviceWorker/db/vtxo/idb.d.ts +18 -0
  97. package/dist/types/wallet/serviceWorker/db/vtxo/index.d.ts +12 -0
  98. package/dist/types/wallet/serviceWorker/request.d.ts +68 -0
  99. package/dist/types/wallet/serviceWorker/response.d.ts +107 -0
  100. package/dist/types/wallet/serviceWorker/wallet.d.ts +23 -0
  101. package/dist/types/wallet/serviceWorker/worker.d.ts +26 -0
  102. package/dist/types/wallet/wallet.d.ts +42 -0
  103. package/package.json +88 -0
@@ -0,0 +1,200 @@
1
+ import * as musig2 from '../musig2/index.js';
2
+ import { getCosignerKeys } from './vtxoTree.js';
3
+ import { Script, SigHash, Transaction } from "@scure/btc-signer";
4
+ import { base64, hex } from "@scure/base";
5
+ import { schnorr, secp256k1 } from "@noble/curves/secp256k1";
6
+ import { randomPrivateKeyBytes } from "@scure/btc-signer/utils";
7
+ export const ErrMissingVtxoTree = new Error("missing vtxo tree");
8
+ export const ErrMissingAggregateKey = new Error("missing aggregate key");
9
+ export class TreeSignerSession {
10
+ constructor(secretKey) {
11
+ this.secretKey = secretKey;
12
+ this.myNonces = null;
13
+ this.aggregateNonces = null;
14
+ this.tree = null;
15
+ this.scriptRoot = null;
16
+ this.rootSharedOutputAmount = null;
17
+ }
18
+ static random() {
19
+ const secretKey = randomPrivateKeyBytes();
20
+ return new TreeSignerSession(secretKey);
21
+ }
22
+ init(tree, scriptRoot, rootInputAmount) {
23
+ this.tree = tree;
24
+ this.scriptRoot = scriptRoot;
25
+ this.rootSharedOutputAmount = rootInputAmount;
26
+ }
27
+ getPublicKey() {
28
+ return secp256k1.getPublicKey(this.secretKey);
29
+ }
30
+ getNonces() {
31
+ if (!this.tree)
32
+ throw ErrMissingVtxoTree;
33
+ if (!this.myNonces) {
34
+ this.myNonces = this.generateNonces();
35
+ }
36
+ const nonces = [];
37
+ for (const levelNonces of this.myNonces) {
38
+ const levelPubNonces = [];
39
+ for (const nonce of levelNonces) {
40
+ if (!nonce) {
41
+ levelPubNonces.push(null);
42
+ continue;
43
+ }
44
+ levelPubNonces.push({ pubNonce: nonce.pubNonce });
45
+ }
46
+ nonces.push(levelPubNonces);
47
+ }
48
+ return nonces;
49
+ }
50
+ setAggregatedNonces(nonces) {
51
+ if (this.aggregateNonces)
52
+ throw new Error("nonces already set");
53
+ this.aggregateNonces = nonces;
54
+ }
55
+ sign() {
56
+ if (!this.tree)
57
+ throw ErrMissingVtxoTree;
58
+ if (!this.aggregateNonces)
59
+ throw new Error("nonces not set");
60
+ if (!this.myNonces)
61
+ throw new Error("nonces not generated");
62
+ const sigs = [];
63
+ for (let levelIndex = 0; levelIndex < this.tree.levels.length; levelIndex++) {
64
+ const levelSigs = [];
65
+ const level = this.tree.levels[levelIndex];
66
+ for (let nodeIndex = 0; nodeIndex < level.length; nodeIndex++) {
67
+ const node = level[nodeIndex];
68
+ const tx = Transaction.fromPSBT(base64.decode(node.tx));
69
+ const sig = this.signPartial(tx, levelIndex, nodeIndex);
70
+ if (sig) {
71
+ levelSigs.push(sig);
72
+ }
73
+ else {
74
+ levelSigs.push(null);
75
+ }
76
+ }
77
+ sigs.push(levelSigs);
78
+ }
79
+ return sigs;
80
+ }
81
+ generateNonces() {
82
+ if (!this.tree)
83
+ throw ErrMissingVtxoTree;
84
+ const myNonces = [];
85
+ const publicKey = secp256k1.getPublicKey(this.secretKey);
86
+ for (const level of this.tree.levels) {
87
+ const levelNonces = [];
88
+ for (let i = 0; i < level.length; i++) {
89
+ const nonces = musig2.generateNonces(publicKey);
90
+ levelNonces.push(nonces);
91
+ }
92
+ myNonces.push(levelNonces);
93
+ }
94
+ return myNonces;
95
+ }
96
+ signPartial(tx, levelIndex, nodeIndex) {
97
+ if (!this.tree || !this.scriptRoot || !this.rootSharedOutputAmount) {
98
+ throw TreeSignerSession.NOT_INITIALIZED;
99
+ }
100
+ if (!this.myNonces || !this.aggregateNonces) {
101
+ throw new Error("session not properly initialized");
102
+ }
103
+ const myNonce = this.myNonces[levelIndex][nodeIndex];
104
+ if (!myNonce)
105
+ return null;
106
+ const aggNonce = this.aggregateNonces[levelIndex][nodeIndex];
107
+ if (!aggNonce)
108
+ throw new Error("missing aggregate nonce");
109
+ const prevoutAmounts = [];
110
+ const prevoutScripts = [];
111
+ const cosigners = getCosignerKeys(tx);
112
+ const { finalKey } = musig2.aggregateKeys(cosigners, true, {
113
+ taprootTweak: this.scriptRoot,
114
+ });
115
+ for (let inputIndex = 0; inputIndex < tx.inputsLength; inputIndex++) {
116
+ const prevout = getPrevOutput(finalKey, this.tree, this.rootSharedOutputAmount, tx);
117
+ prevoutAmounts.push(prevout.amount);
118
+ prevoutScripts.push(prevout.script);
119
+ }
120
+ const message = tx.preimageWitnessV1(0, // always first input
121
+ prevoutScripts, SigHash.DEFAULT, prevoutAmounts);
122
+ return musig2.sign(myNonce.secNonce, this.secretKey, aggNonce.pubNonce, cosigners, message, {
123
+ taprootTweak: this.scriptRoot,
124
+ sortKeys: true,
125
+ });
126
+ }
127
+ }
128
+ TreeSignerSession.NOT_INITIALIZED = new Error("session not initialized, call init method");
129
+ // Helper function to validate tree signatures
130
+ export async function validateTreeSigs(finalAggregatedKey, sharedOutputAmount, vtxoTree) {
131
+ // Iterate through each level of the tree
132
+ for (const level of vtxoTree.levels) {
133
+ for (const node of level) {
134
+ // Parse the transaction
135
+ const tx = Transaction.fromPSBT(base64.decode(node.tx));
136
+ const input = tx.getInput(0);
137
+ // Check if input has signature
138
+ if (!input.tapKeySig) {
139
+ throw new Error("unsigned tree input");
140
+ }
141
+ // Get the previous output information
142
+ const prevout = getPrevOutput(finalAggregatedKey, vtxoTree, sharedOutputAmount, tx);
143
+ // Calculate the message that was signed
144
+ const message = tx.preimageWitnessV1(0, // always first input
145
+ [prevout.script], SigHash.DEFAULT, [prevout.amount]);
146
+ // Verify the signature
147
+ const isValid = schnorr.verify(input.tapKeySig, message, finalAggregatedKey);
148
+ if (!isValid) {
149
+ throw new Error("invalid signature");
150
+ }
151
+ }
152
+ }
153
+ }
154
+ function getPrevOutput(finalKey, vtxoTree, sharedOutputAmount, partial) {
155
+ // Generate P2TR script
156
+ const pkScript = Script.encode(["OP_1", finalKey.slice(1)]);
157
+ // Get root node
158
+ const rootNode = vtxoTree.levels[0][0];
159
+ if (!rootNode)
160
+ throw new Error("empty vtxo tree");
161
+ const input = partial.getInput(0);
162
+ if (!input.txid)
163
+ throw new Error("missing input txid");
164
+ const parentTxID = hex.encode(input.txid);
165
+ // Check if parent is root
166
+ if (rootNode.parentTxid === parentTxID) {
167
+ return {
168
+ amount: sharedOutputAmount,
169
+ script: pkScript,
170
+ };
171
+ }
172
+ // Search for parent in tree
173
+ let parent = null;
174
+ for (const level of vtxoTree.levels) {
175
+ for (const node of level) {
176
+ if (node.txid === parentTxID) {
177
+ parent = node;
178
+ break;
179
+ }
180
+ }
181
+ if (parent)
182
+ break;
183
+ }
184
+ if (!parent) {
185
+ throw new Error("parent tx not found");
186
+ }
187
+ // Parse parent tx
188
+ const parentTx = Transaction.fromPSBT(base64.decode(parent.tx));
189
+ if (!input.index)
190
+ throw new Error("missing input index");
191
+ const parentOutput = parentTx.getOutput(input.index);
192
+ if (!parentOutput)
193
+ throw new Error("parent output not found");
194
+ if (!parentOutput.amount)
195
+ throw new Error("parent output amount not found");
196
+ return {
197
+ amount: parentOutput.amount,
198
+ script: pkScript,
199
+ };
200
+ }
@@ -0,0 +1,179 @@
1
+ import { hex } from "@scure/base";
2
+ import { Transaction } from "@scure/btc-signer";
3
+ import { base64 } from "@scure/base";
4
+ import { sha256x2 } from "@scure/btc-signer/utils";
5
+ import { aggregateKeys } from '../musig2/index.js';
6
+ import { getCosignerKeys, TxTreeError } from './vtxoTree.js';
7
+ export const ErrInvalidSettlementTx = new TxTreeError("invalid settlement transaction");
8
+ export const ErrInvalidSettlementTxOutputs = new TxTreeError("invalid settlement transaction outputs");
9
+ export const ErrEmptyTree = new TxTreeError("empty tree");
10
+ export const ErrInvalidRootLevel = new TxTreeError("invalid root level");
11
+ export const ErrNumberOfInputs = new TxTreeError("invalid number of inputs");
12
+ export const ErrWrongSettlementTxid = new TxTreeError("wrong settlement txid");
13
+ export const ErrInvalidAmount = new TxTreeError("invalid amount");
14
+ export const ErrNoLeaves = new TxTreeError("no leaves");
15
+ export const ErrNodeTxEmpty = new TxTreeError("node transaction empty");
16
+ export const ErrNodeTxidEmpty = new TxTreeError("node txid empty");
17
+ export const ErrNodeParentTxidEmpty = new TxTreeError("node parent txid empty");
18
+ export const ErrNodeTxidDifferent = new TxTreeError("node txid different");
19
+ export const ErrParentTxidInput = new TxTreeError("parent txid input mismatch");
20
+ export const ErrLeafChildren = new TxTreeError("leaf node has children");
21
+ export const ErrInvalidTaprootScript = new TxTreeError("invalid taproot script");
22
+ export const ErrInternalKey = new TxTreeError("invalid internal key");
23
+ export const ErrInvalidControlBlock = new TxTreeError("invalid control block");
24
+ export const ErrInvalidRootTransaction = new TxTreeError("invalid root transaction");
25
+ export const ErrInvalidNodeTransaction = new TxTreeError("invalid node transaction");
26
+ const SHARED_OUTPUT_INDEX = 0;
27
+ const CONNECTORS_OUTPUT_INDEX = 1;
28
+ export function validateConnectorsTree(settlementTxB64, connectorsTree) {
29
+ connectorsTree.validate();
30
+ const rootNode = connectorsTree.root();
31
+ if (!rootNode)
32
+ throw ErrEmptyTree;
33
+ const rootTx = Transaction.fromPSBT(base64.decode(rootNode.tx));
34
+ if (rootTx.inputsLength !== 1)
35
+ throw ErrNumberOfInputs;
36
+ const rootInput = rootTx.getInput(0);
37
+ const settlementTx = Transaction.fromPSBT(base64.decode(settlementTxB64));
38
+ if (settlementTx.outputsLength <= CONNECTORS_OUTPUT_INDEX)
39
+ throw ErrInvalidSettlementTxOutputs;
40
+ const expectedRootTxid = hex.encode(sha256x2(settlementTx.toBytes(true)).reverse());
41
+ if (!rootInput.txid)
42
+ throw ErrWrongSettlementTxid;
43
+ if (hex.encode(rootInput.txid) !== expectedRootTxid)
44
+ throw ErrWrongSettlementTxid;
45
+ if (rootInput.index !== CONNECTORS_OUTPUT_INDEX)
46
+ throw ErrWrongSettlementTxid;
47
+ }
48
+ export function validateVtxoTree(settlementTx, vtxoTree, sweepTapTreeRoot) {
49
+ vtxoTree.validate();
50
+ // Parse settlement transaction
51
+ let settlementTransaction;
52
+ try {
53
+ settlementTransaction = Transaction.fromPSBT(base64.decode(settlementTx));
54
+ }
55
+ catch {
56
+ throw ErrInvalidSettlementTx;
57
+ }
58
+ if (settlementTransaction.outputsLength <= SHARED_OUTPUT_INDEX) {
59
+ throw ErrInvalidSettlementTxOutputs;
60
+ }
61
+ const sharedOutput = settlementTransaction.getOutput(SHARED_OUTPUT_INDEX);
62
+ if (!sharedOutput?.amount)
63
+ throw ErrInvalidSettlementTxOutputs;
64
+ const sharedOutputAmount = sharedOutput.amount;
65
+ const nbNodes = vtxoTree.numberOfNodes();
66
+ if (nbNodes === 0) {
67
+ throw ErrEmptyTree;
68
+ }
69
+ if (vtxoTree.levels[0].length !== 1) {
70
+ throw ErrInvalidRootLevel;
71
+ }
72
+ // Check root input is connected to settlement tx
73
+ const rootNode = vtxoTree.levels[0][0];
74
+ let rootTx;
75
+ try {
76
+ rootTx = Transaction.fromPSBT(base64.decode(rootNode.tx));
77
+ }
78
+ catch {
79
+ throw ErrInvalidRootTransaction;
80
+ }
81
+ if (rootTx.inputsLength !== 1) {
82
+ throw ErrNumberOfInputs;
83
+ }
84
+ const rootInput = rootTx.getInput(0);
85
+ if (!rootInput.txid || rootInput.index === undefined)
86
+ throw ErrWrongSettlementTxid;
87
+ const settlementTxid = hex.encode(sha256x2(settlementTransaction.toBytes(true)).reverse());
88
+ if (hex.encode(rootInput.txid) !== settlementTxid ||
89
+ rootInput.index !== SHARED_OUTPUT_INDEX) {
90
+ throw ErrWrongSettlementTxid;
91
+ }
92
+ // Check root output amounts
93
+ let sumRootValue = 0n;
94
+ for (let i = 0; i < rootTx.outputsLength; i++) {
95
+ const output = rootTx.getOutput(i);
96
+ if (!output?.amount)
97
+ continue;
98
+ sumRootValue += output.amount;
99
+ }
100
+ if (sumRootValue >= sharedOutputAmount) {
101
+ throw ErrInvalidAmount;
102
+ }
103
+ if (vtxoTree.leaves().length === 0) {
104
+ throw ErrNoLeaves;
105
+ }
106
+ // Validate each node in the tree
107
+ for (const level of vtxoTree.levels) {
108
+ for (const node of level) {
109
+ validateNode(vtxoTree, node, sweepTapTreeRoot);
110
+ }
111
+ }
112
+ }
113
+ function validateNode(vtxoTree, node, tapTreeRoot) {
114
+ if (!node.tx)
115
+ throw ErrNodeTxEmpty;
116
+ if (!node.txid)
117
+ throw ErrNodeTxidEmpty;
118
+ if (!node.parentTxid)
119
+ throw ErrNodeParentTxidEmpty;
120
+ // Parse node transaction
121
+ let tx;
122
+ try {
123
+ tx = Transaction.fromPSBT(base64.decode(node.tx));
124
+ }
125
+ catch {
126
+ throw ErrInvalidNodeTransaction;
127
+ }
128
+ const txid = hex.encode(sha256x2(tx.toBytes(true)).reverse());
129
+ if (txid !== node.txid) {
130
+ throw ErrNodeTxidDifferent;
131
+ }
132
+ if (tx.inputsLength !== 1) {
133
+ throw ErrNumberOfInputs;
134
+ }
135
+ const input = tx.getInput(0);
136
+ if (!input.txid)
137
+ throw ErrParentTxidInput;
138
+ if (hex.encode(input.txid) !== node.parentTxid) {
139
+ throw ErrParentTxidInput;
140
+ }
141
+ const children = vtxoTree.children(node.txid);
142
+ if (node.leaf && children.length >= 1) {
143
+ throw ErrLeafChildren;
144
+ }
145
+ // Validate each child
146
+ for (let childIndex = 0; childIndex < children.length; childIndex++) {
147
+ const child = children[childIndex];
148
+ const childTx = Transaction.fromPSBT(base64.decode(child.tx));
149
+ const parentOutput = tx.getOutput(childIndex);
150
+ if (!parentOutput?.script)
151
+ throw ErrInvalidTaprootScript;
152
+ const previousScriptKey = parentOutput.script.slice(2);
153
+ if (previousScriptKey.length !== 32) {
154
+ throw ErrInvalidTaprootScript;
155
+ }
156
+ // Get cosigner keys from input
157
+ const cosignerKeys = getCosignerKeys(childTx);
158
+ // Aggregate keys
159
+ const { finalKey } = aggregateKeys(cosignerKeys, true, {
160
+ taprootTweak: tapTreeRoot,
161
+ });
162
+ if (hex.encode(finalKey) !== hex.encode(previousScriptKey.slice(2))) {
163
+ throw ErrInternalKey;
164
+ }
165
+ // Check amounts
166
+ let sumChildAmount = 0n;
167
+ for (let i = 0; i < childTx.outputsLength; i++) {
168
+ const output = childTx.getOutput(i);
169
+ if (!output?.amount)
170
+ continue;
171
+ sumChildAmount += output.amount;
172
+ }
173
+ if (!parentOutput.amount)
174
+ throw ErrInvalidAmount;
175
+ if (sumChildAmount >= parentOutput.amount) {
176
+ throw ErrInvalidAmount;
177
+ }
178
+ }
179
+ }
@@ -0,0 +1,157 @@
1
+ import * as bip68 from "bip68";
2
+ import { ScriptNum, Transaction } from "@scure/btc-signer";
3
+ import { sha256x2 } from "@scure/btc-signer/utils";
4
+ import { base64, hex } from "@scure/base";
5
+ export class TxTreeError extends Error {
6
+ constructor(message) {
7
+ super(message);
8
+ this.name = "TxTreeError";
9
+ }
10
+ }
11
+ export const ErrLeafNotFound = new TxTreeError("leaf not found in tx tree");
12
+ export const ErrParentNotFound = new TxTreeError("parent not found");
13
+ // TxTree is represented as a matrix of Node objects
14
+ // the first level of the matrix is the root of the tree
15
+ export class TxTree {
16
+ constructor(tree) {
17
+ this.tree = tree;
18
+ }
19
+ get levels() {
20
+ return this.tree;
21
+ }
22
+ // Returns the root node of the vtxo tree
23
+ root() {
24
+ if (this.tree.length <= 0 || this.tree[0].length <= 0) {
25
+ throw new TxTreeError("empty vtxo tree");
26
+ }
27
+ return this.tree[0][0];
28
+ }
29
+ // Returns the leaves of the vtxo tree
30
+ leaves() {
31
+ const leaves = [...this.tree[this.tree.length - 1]];
32
+ // Check other levels for leaf nodes
33
+ for (let i = 0; i < this.tree.length - 1; i++) {
34
+ for (const node of this.tree[i]) {
35
+ if (node.leaf) {
36
+ leaves.push(node);
37
+ }
38
+ }
39
+ }
40
+ return leaves;
41
+ }
42
+ // Returns all nodes that have the given node as parent
43
+ children(nodeTxid) {
44
+ const children = [];
45
+ for (const level of this.tree) {
46
+ for (const node of level) {
47
+ if (node.parentTxid === nodeTxid) {
48
+ children.push(node);
49
+ }
50
+ }
51
+ }
52
+ return children;
53
+ }
54
+ // Returns the total number of nodes in the vtxo tree
55
+ numberOfNodes() {
56
+ return this.tree.reduce((count, level) => count + level.length, 0);
57
+ }
58
+ // Returns the branch of the given vtxo txid from root to leaf
59
+ branch(vtxoTxid) {
60
+ const branch = [];
61
+ const leaves = this.leaves();
62
+ // Check if the vtxo is a leaf
63
+ const leaf = leaves.find((leaf) => leaf.txid === vtxoTxid);
64
+ if (!leaf) {
65
+ throw ErrLeafNotFound;
66
+ }
67
+ branch.push(leaf);
68
+ const rootTxid = this.root().txid;
69
+ while (branch[0].txid !== rootTxid) {
70
+ const parent = this.findParent(branch[0]);
71
+ branch.unshift(parent);
72
+ }
73
+ return branch;
74
+ }
75
+ // Helper method to find parent of a node
76
+ findParent(node) {
77
+ for (const level of this.tree) {
78
+ for (const potentialParent of level) {
79
+ if (potentialParent.txid === node.parentTxid) {
80
+ return potentialParent;
81
+ }
82
+ }
83
+ }
84
+ throw ErrParentNotFound;
85
+ }
86
+ // Validates that the tree is coherent by checking txids and parent relationships
87
+ validate() {
88
+ // Skip the root level, validate from level 1 onwards
89
+ for (let i = 1; i < this.tree.length; i++) {
90
+ for (const node of this.tree[i]) {
91
+ // Verify that the node's transaction matches its claimed txid
92
+ const tx = Transaction.fromPSBT(base64.decode(node.tx));
93
+ const txid = hex.encode(sha256x2(tx.toBytes(true)).reverse());
94
+ if (txid !== node.txid) {
95
+ throw new TxTreeError(`node ${node.txid} has txid ${node.txid}, but computed txid is ${txid}`);
96
+ }
97
+ // Verify that the node has a valid parent
98
+ try {
99
+ this.findParent(node);
100
+ }
101
+ catch (err) {
102
+ throw new TxTreeError(`node ${node.txid} has no parent: ${err instanceof Error ? err.message : String(err)}`);
103
+ }
104
+ }
105
+ }
106
+ }
107
+ }
108
+ const COSIGNER_KEY_PREFIX = new Uint8Array("cosigner".split("").map((c) => c.charCodeAt(0)));
109
+ const VTXO_TREE_EXPIRY_PSBT_KEY = new Uint8Array("expiry".split("").map((c) => c.charCodeAt(0)));
110
+ export function getVtxoTreeExpiry(input) {
111
+ if (!input.unknown)
112
+ return null;
113
+ for (const u of input.unknown) {
114
+ // Check if key contains the VTXO tree expiry key
115
+ if (u.key.length < VTXO_TREE_EXPIRY_PSBT_KEY.length)
116
+ continue;
117
+ let found = true;
118
+ for (let i = 0; i < VTXO_TREE_EXPIRY_PSBT_KEY.length; i++) {
119
+ if (u.key[i] !== VTXO_TREE_EXPIRY_PSBT_KEY[i]) {
120
+ found = false;
121
+ break;
122
+ }
123
+ }
124
+ if (found) {
125
+ const value = ScriptNum(6, true).decode(u.value);
126
+ const { blocks, seconds } = bip68.decode(Number(value));
127
+ return {
128
+ type: blocks ? "blocks" : "seconds",
129
+ value: BigInt(blocks ?? seconds ?? 0),
130
+ };
131
+ }
132
+ }
133
+ return null;
134
+ }
135
+ function parsePrefixedCosignerKey(key) {
136
+ if (key.length < COSIGNER_KEY_PREFIX.length)
137
+ return false;
138
+ for (let i = 0; i < COSIGNER_KEY_PREFIX.length; i++) {
139
+ if (key[i] !== COSIGNER_KEY_PREFIX[i])
140
+ return false;
141
+ }
142
+ return true;
143
+ }
144
+ export function getCosignerKeys(tx) {
145
+ const keys = [];
146
+ const input = tx.getInput(0);
147
+ if (!input.unknown)
148
+ return keys;
149
+ for (const unknown of input.unknown) {
150
+ const ok = parsePrefixedCosignerKey(new Uint8Array([unknown[0].type, ...unknown[0].key]));
151
+ if (!ok)
152
+ continue;
153
+ // Assuming the value is already a valid public key in compressed format
154
+ keys.push(unknown[1]);
155
+ }
156
+ return keys;
157
+ }
@@ -0,0 +1,110 @@
1
+ export var BIP21Error;
2
+ (function (BIP21Error) {
3
+ BIP21Error["INVALID_URI"] = "Invalid BIP21 URI";
4
+ BIP21Error["INVALID_ADDRESS"] = "Invalid address";
5
+ })(BIP21Error || (BIP21Error = {}));
6
+ export class BIP21 {
7
+ static create(params) {
8
+ const { address, ...options } = params;
9
+ // Build query string
10
+ const queryParams = {};
11
+ for (const [key, value] of Object.entries(options)) {
12
+ if (value === undefined)
13
+ continue;
14
+ if (key === "amount") {
15
+ if (!isFinite(value)) {
16
+ console.warn("Invalid amount");
17
+ continue;
18
+ }
19
+ if (value < 0) {
20
+ continue;
21
+ }
22
+ queryParams[key] = value;
23
+ }
24
+ else if (key === "ark") {
25
+ // Validate ARK address format
26
+ if (typeof value === "string" &&
27
+ (value.startsWith("ark") || value.startsWith("tark"))) {
28
+ queryParams[key] = value;
29
+ }
30
+ else {
31
+ console.warn("Invalid ARK address format");
32
+ }
33
+ }
34
+ else if (key === "sp") {
35
+ // Validate Silent Payment address format (placeholder)
36
+ if (typeof value === "string" && value.startsWith("sp")) {
37
+ queryParams[key] = value;
38
+ }
39
+ else {
40
+ console.warn("Invalid Silent Payment address format");
41
+ }
42
+ }
43
+ else if (typeof value === "string" || typeof value === "number") {
44
+ queryParams[key] = value;
45
+ }
46
+ }
47
+ const query = Object.keys(queryParams).length > 0
48
+ ? "?" +
49
+ new URLSearchParams(Object.fromEntries(Object.entries(queryParams).map(([k, v]) => [
50
+ k,
51
+ String(v),
52
+ ]))).toString()
53
+ : "";
54
+ return `bitcoin:${address ? address.toLowerCase() : ""}${query}`;
55
+ }
56
+ static parse(uri) {
57
+ if (!uri.toLowerCase().startsWith("bitcoin:")) {
58
+ throw new Error(BIP21Error.INVALID_URI);
59
+ }
60
+ // Remove bitcoin: prefix, preserving case of the rest
61
+ const withoutPrefix = uri.slice(uri.toLowerCase().indexOf("bitcoin:") + 8);
62
+ const [address, query] = withoutPrefix.split("?");
63
+ const params = {};
64
+ if (address) {
65
+ params.address = address.toLowerCase();
66
+ }
67
+ if (query) {
68
+ const queryParams = new URLSearchParams(query);
69
+ for (const [key, value] of queryParams.entries()) {
70
+ if (!value)
71
+ continue;
72
+ if (key === "amount") {
73
+ const amount = Number(value);
74
+ if (!isFinite(amount)) {
75
+ continue;
76
+ }
77
+ if (amount < 0) {
78
+ continue;
79
+ }
80
+ params[key] = amount;
81
+ }
82
+ else if (key === "ark") {
83
+ // Validate ARK address format
84
+ if (value.startsWith("ark") || value.startsWith("tark")) {
85
+ params[key] = value;
86
+ }
87
+ else {
88
+ console.warn("Invalid ARK address format");
89
+ }
90
+ }
91
+ else if (key === "sp") {
92
+ // Validate Silent Payment address format (placeholder)
93
+ if (value.startsWith("sp")) {
94
+ params[key] = value;
95
+ }
96
+ else {
97
+ console.warn("Invalid Silent Payment address format");
98
+ }
99
+ }
100
+ else {
101
+ params[key] = value;
102
+ }
103
+ }
104
+ }
105
+ return {
106
+ originalString: uri,
107
+ params,
108
+ };
109
+ }
110
+ }