@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.
- 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 -454
- 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 -461
- 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 -25
- 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
package/dist/esm/script/vhtlc.js
CHANGED
|
@@ -2,13 +2,33 @@ import { Script } from "@scure/btc-signer";
|
|
|
2
2
|
import { CLTVMultisigTapscript, ConditionCSVMultisigTapscript, ConditionMultisigTapscript, CSVMultisigTapscript, MultisigTapscript, } from './tapscript.js';
|
|
3
3
|
import { hex } from "@scure/base";
|
|
4
4
|
import { VtxoScript } from './base.js';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Virtual Hash Time Lock Contract (VHTLC) implementation.
|
|
7
|
+
*
|
|
8
|
+
* VHTLC is a contract that enables atomic swaps and conditional payments
|
|
9
|
+
* in the Ark protocol. It provides multiple spending paths:
|
|
10
|
+
*
|
|
11
|
+
* - **claim**: Receiver can claim funds by revealing the preimage
|
|
12
|
+
* - **refund**: Sender and receiver can collaboratively refund
|
|
13
|
+
* - **refundWithoutReceiver**: Sender can refund after locktime expires
|
|
14
|
+
* - **unilateralClaim**: Receiver can claim unilaterally after delay
|
|
15
|
+
* - **unilateralRefund**: Sender and receiver can refund unilaterally after delay
|
|
16
|
+
* - **unilateralRefundWithoutReceiver**: Sender can refund unilaterally after delay
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```typescript
|
|
20
|
+
* const vhtlc = new VHTLC.Script({
|
|
21
|
+
* sender: alicePubKey,
|
|
22
|
+
* receiver: bobPubKey,
|
|
23
|
+
* server: serverPubKey,
|
|
24
|
+
* preimageHash: hash160(secret),
|
|
25
|
+
* refundLocktime: BigInt(chainTip + 10),
|
|
26
|
+
* unilateralClaimDelay: { type: 'blocks', value: 100n },
|
|
27
|
+
* unilateralRefundDelay: { type: 'blocks', value: 102n },
|
|
28
|
+
* unilateralRefundWithoutReceiverDelay: { type: 'blocks', value: 103n }
|
|
29
|
+
* });
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
12
32
|
export var VHTLC;
|
|
13
33
|
(function (VHTLC) {
|
|
14
34
|
class Script extends VtxoScript {
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import * as musig2 from '../musig2/index.js';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { base64, hex } from "@scure/base";
|
|
2
|
+
import { Script, SigHash } from "@scure/btc-signer";
|
|
3
|
+
import { hex } from "@scure/base";
|
|
5
4
|
import { schnorr, secp256k1 } from "@noble/curves/secp256k1";
|
|
6
|
-
import { randomPrivateKeyBytes } from "@scure/btc-signer/utils";
|
|
7
|
-
|
|
5
|
+
import { randomPrivateKeyBytes, sha256x2 } from "@scure/btc-signer/utils";
|
|
6
|
+
import { CosignerPublicKey, getArkPsbtFields } from '../utils/unknownFields.js';
|
|
7
|
+
export const ErrMissingVtxoGraph = new Error("missing vtxo graph");
|
|
8
8
|
export const ErrMissingAggregateKey = new Error("missing aggregate key");
|
|
9
9
|
export class TreeSignerSession {
|
|
10
10
|
constructor(secretKey) {
|
|
11
11
|
this.secretKey = secretKey;
|
|
12
12
|
this.myNonces = null;
|
|
13
13
|
this.aggregateNonces = null;
|
|
14
|
-
this.
|
|
14
|
+
this.graph = null;
|
|
15
15
|
this.scriptRoot = null;
|
|
16
16
|
this.rootSharedOutputAmount = null;
|
|
17
17
|
}
|
|
@@ -20,7 +20,7 @@ export class TreeSignerSession {
|
|
|
20
20
|
return new TreeSignerSession(secretKey);
|
|
21
21
|
}
|
|
22
22
|
init(tree, scriptRoot, rootInputAmount) {
|
|
23
|
-
this.
|
|
23
|
+
this.graph = tree;
|
|
24
24
|
this.scriptRoot = scriptRoot;
|
|
25
25
|
this.rootSharedOutputAmount = rootInputAmount;
|
|
26
26
|
}
|
|
@@ -28,24 +28,16 @@ export class TreeSignerSession {
|
|
|
28
28
|
return secp256k1.getPublicKey(this.secretKey);
|
|
29
29
|
}
|
|
30
30
|
getNonces() {
|
|
31
|
-
if (!this.
|
|
32
|
-
throw
|
|
31
|
+
if (!this.graph)
|
|
32
|
+
throw ErrMissingVtxoGraph;
|
|
33
33
|
if (!this.myNonces) {
|
|
34
34
|
this.myNonces = this.generateNonces();
|
|
35
35
|
}
|
|
36
|
-
const
|
|
37
|
-
for (const
|
|
38
|
-
|
|
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);
|
|
36
|
+
const publicNonces = new Map();
|
|
37
|
+
for (const [txid, nonces] of this.myNonces) {
|
|
38
|
+
publicNonces.set(txid, { pubNonce: nonces.pubNonce });
|
|
47
39
|
}
|
|
48
|
-
return
|
|
40
|
+
return publicNonces;
|
|
49
41
|
}
|
|
50
42
|
setAggregatedNonces(nonces) {
|
|
51
43
|
if (this.aggregateNonces)
|
|
@@ -53,71 +45,55 @@ export class TreeSignerSession {
|
|
|
53
45
|
this.aggregateNonces = nonces;
|
|
54
46
|
}
|
|
55
47
|
sign() {
|
|
56
|
-
if (!this.
|
|
57
|
-
throw
|
|
48
|
+
if (!this.graph)
|
|
49
|
+
throw ErrMissingVtxoGraph;
|
|
58
50
|
if (!this.aggregateNonces)
|
|
59
51
|
throw new Error("nonces not set");
|
|
60
52
|
if (!this.myNonces)
|
|
61
53
|
throw new Error("nonces not generated");
|
|
62
|
-
const sigs =
|
|
63
|
-
for (
|
|
64
|
-
const
|
|
65
|
-
|
|
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);
|
|
54
|
+
const sigs = new Map();
|
|
55
|
+
for (const g of this.graph) {
|
|
56
|
+
const sig = this.signPartial(g);
|
|
57
|
+
sigs.set(g.txid, sig);
|
|
78
58
|
}
|
|
79
59
|
return sigs;
|
|
80
60
|
}
|
|
81
61
|
generateNonces() {
|
|
82
|
-
if (!this.
|
|
83
|
-
throw
|
|
84
|
-
const myNonces =
|
|
62
|
+
if (!this.graph)
|
|
63
|
+
throw ErrMissingVtxoGraph;
|
|
64
|
+
const myNonces = new Map();
|
|
85
65
|
const publicKey = secp256k1.getPublicKey(this.secretKey);
|
|
86
|
-
for (const
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
const nonces = musig2.generateNonces(publicKey);
|
|
90
|
-
levelNonces.push(nonces);
|
|
91
|
-
}
|
|
92
|
-
myNonces.push(levelNonces);
|
|
66
|
+
for (const g of this.graph) {
|
|
67
|
+
const nonces = musig2.generateNonces(publicKey);
|
|
68
|
+
myNonces.set(g.txid, nonces);
|
|
93
69
|
}
|
|
94
70
|
return myNonces;
|
|
95
71
|
}
|
|
96
|
-
signPartial(
|
|
97
|
-
if (!this.
|
|
72
|
+
signPartial(g) {
|
|
73
|
+
if (!this.graph || !this.scriptRoot || !this.rootSharedOutputAmount) {
|
|
98
74
|
throw TreeSignerSession.NOT_INITIALIZED;
|
|
99
75
|
}
|
|
100
76
|
if (!this.myNonces || !this.aggregateNonces) {
|
|
101
77
|
throw new Error("session not properly initialized");
|
|
102
78
|
}
|
|
103
|
-
const myNonce = this.myNonces
|
|
79
|
+
const myNonce = this.myNonces.get(g.txid);
|
|
104
80
|
if (!myNonce)
|
|
105
|
-
|
|
106
|
-
const aggNonce = this.aggregateNonces
|
|
81
|
+
throw new Error("missing private nonce");
|
|
82
|
+
const aggNonce = this.aggregateNonces.get(g.txid);
|
|
107
83
|
if (!aggNonce)
|
|
108
84
|
throw new Error("missing aggregate nonce");
|
|
109
85
|
const prevoutAmounts = [];
|
|
110
86
|
const prevoutScripts = [];
|
|
111
|
-
const cosigners =
|
|
87
|
+
const cosigners = getArkPsbtFields(g.root, 0, CosignerPublicKey).map((c) => c.key);
|
|
112
88
|
const { finalKey } = musig2.aggregateKeys(cosigners, true, {
|
|
113
89
|
taprootTweak: this.scriptRoot,
|
|
114
90
|
});
|
|
115
|
-
for (let inputIndex = 0; inputIndex <
|
|
116
|
-
const prevout = getPrevOutput(finalKey, this.
|
|
91
|
+
for (let inputIndex = 0; inputIndex < g.root.inputsLength; inputIndex++) {
|
|
92
|
+
const prevout = getPrevOutput(finalKey, this.graph, this.rootSharedOutputAmount, g.root);
|
|
117
93
|
prevoutAmounts.push(prevout.amount);
|
|
118
94
|
prevoutScripts.push(prevout.script);
|
|
119
95
|
}
|
|
120
|
-
const message =
|
|
96
|
+
const message = g.root.preimageWitnessV1(0, // always first input
|
|
121
97
|
prevoutScripts, SigHash.DEFAULT, prevoutAmounts);
|
|
122
98
|
return musig2.sign(myNonce.secNonce, this.secretKey, aggNonce.pubNonce, cosigners, message, {
|
|
123
99
|
taprootTweak: this.scriptRoot,
|
|
@@ -129,66 +105,47 @@ TreeSignerSession.NOT_INITIALIZED = new Error("session not initialized, call ini
|
|
|
129
105
|
// Helper function to validate tree signatures
|
|
130
106
|
export async function validateTreeSigs(finalAggregatedKey, sharedOutputAmount, vtxoTree) {
|
|
131
107
|
// Iterate through each level of the tree
|
|
132
|
-
for (const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
if (!isValid) {
|
|
149
|
-
throw new Error("invalid signature");
|
|
150
|
-
}
|
|
108
|
+
for (const g of vtxoTree) {
|
|
109
|
+
// Parse the transaction
|
|
110
|
+
const input = g.root.getInput(0);
|
|
111
|
+
// Check if input has signature
|
|
112
|
+
if (!input.tapKeySig) {
|
|
113
|
+
throw new Error("unsigned tree input");
|
|
114
|
+
}
|
|
115
|
+
// Get the previous output information
|
|
116
|
+
const prevout = getPrevOutput(finalAggregatedKey, vtxoTree, sharedOutputAmount, g.root);
|
|
117
|
+
// Calculate the message that was signed
|
|
118
|
+
const message = g.root.preimageWitnessV1(0, // always first input
|
|
119
|
+
[prevout.script], SigHash.DEFAULT, [prevout.amount]);
|
|
120
|
+
// Verify the signature
|
|
121
|
+
const isValid = schnorr.verify(input.tapKeySig, message, finalAggregatedKey);
|
|
122
|
+
if (!isValid) {
|
|
123
|
+
throw new Error("invalid signature");
|
|
151
124
|
}
|
|
152
125
|
}
|
|
153
126
|
}
|
|
154
|
-
function getPrevOutput(finalKey,
|
|
155
|
-
//
|
|
127
|
+
function getPrevOutput(finalKey, graph, sharedOutputAmount, tx) {
|
|
128
|
+
// generate P2TR script from musig2 final key
|
|
156
129
|
const pkScript = Script.encode(["OP_1", finalKey.slice(1)]);
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
if (
|
|
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) {
|
|
130
|
+
const txid = hex.encode(sha256x2(tx.toBytes(true)).reverse());
|
|
131
|
+
// if the input is the root input, return the shared output amount
|
|
132
|
+
if (txid === graph.txid) {
|
|
167
133
|
return {
|
|
168
134
|
amount: sharedOutputAmount,
|
|
169
135
|
script: pkScript,
|
|
170
136
|
};
|
|
171
137
|
}
|
|
172
|
-
//
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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)
|
|
138
|
+
// find the parent transaction
|
|
139
|
+
const parentInput = tx.getInput(0);
|
|
140
|
+
if (!parentInput.txid)
|
|
141
|
+
throw new Error("missing parent input txid");
|
|
142
|
+
const parentTxid = hex.encode(new Uint8Array(parentInput.txid));
|
|
143
|
+
const parent = graph.find(parentTxid);
|
|
144
|
+
if (!parent)
|
|
145
|
+
throw new Error("parent tx not found");
|
|
146
|
+
if (parentInput.index === undefined)
|
|
190
147
|
throw new Error("missing input index");
|
|
191
|
-
const parentOutput =
|
|
148
|
+
const parentOutput = parent.root.getOutput(parentInput.index);
|
|
192
149
|
if (!parentOutput)
|
|
193
150
|
throw new Error("parent output not found");
|
|
194
151
|
if (!parentOutput.amount)
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { Transaction } from "@scure/btc-signer";
|
|
2
|
+
import { base64 } from "@scure/base";
|
|
3
|
+
import { hex } from "@scure/base";
|
|
4
|
+
import { sha256x2 } from "@scure/btc-signer/utils";
|
|
5
|
+
/**
|
|
6
|
+
* TxTree is a graph of bitcoin transactions.
|
|
7
|
+
* It is used to represent batch tree created during settlement session
|
|
8
|
+
*/
|
|
9
|
+
export class TxTree {
|
|
10
|
+
constructor(root, children = new Map()) {
|
|
11
|
+
this.root = root;
|
|
12
|
+
this.children = children;
|
|
13
|
+
}
|
|
14
|
+
static create(chunks) {
|
|
15
|
+
if (chunks.length === 0) {
|
|
16
|
+
throw new Error("empty chunks");
|
|
17
|
+
}
|
|
18
|
+
// Create a map to store all chunks by their txid for easy lookup
|
|
19
|
+
const chunksByTxid = new Map();
|
|
20
|
+
for (const chunk of chunks) {
|
|
21
|
+
const decodedChunk = decodeNode(chunk);
|
|
22
|
+
const txid = hex.encode(sha256x2(decodedChunk.tx.toBytes(true)).reverse());
|
|
23
|
+
chunksByTxid.set(txid, decodedChunk);
|
|
24
|
+
}
|
|
25
|
+
// Find the root chunks (the ones that aren't referenced as a child)
|
|
26
|
+
const rootTxids = [];
|
|
27
|
+
for (const [txid] of chunksByTxid) {
|
|
28
|
+
let isChild = false;
|
|
29
|
+
for (const [otherTxid, otherChunk] of chunksByTxid) {
|
|
30
|
+
if (otherTxid === txid) {
|
|
31
|
+
// skip self
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
// check if the current chunk is a child of the other chunk
|
|
35
|
+
isChild = hasChild(otherChunk, txid);
|
|
36
|
+
if (isChild) {
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// if the chunk is not a child of any other chunk, it is a root
|
|
41
|
+
if (!isChild) {
|
|
42
|
+
rootTxids.push(txid);
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (rootTxids.length === 0) {
|
|
47
|
+
throw new Error("no root chunk found");
|
|
48
|
+
}
|
|
49
|
+
if (rootTxids.length > 1) {
|
|
50
|
+
throw new Error(`multiple root chunks found: ${rootTxids.join(", ")}`);
|
|
51
|
+
}
|
|
52
|
+
const graph = buildGraph(rootTxids[0], chunksByTxid);
|
|
53
|
+
if (!graph) {
|
|
54
|
+
throw new Error(`chunk not found for root txid: ${rootTxids[0]}`);
|
|
55
|
+
}
|
|
56
|
+
// verify that the number of chunks is equal to the number node in the graph
|
|
57
|
+
if (graph.nbOfNodes() !== chunks.length) {
|
|
58
|
+
throw new Error(`number of chunks (${chunks.length}) is not equal to the number of nodes in the graph (${graph.nbOfNodes()})`);
|
|
59
|
+
}
|
|
60
|
+
return graph;
|
|
61
|
+
}
|
|
62
|
+
nbOfNodes() {
|
|
63
|
+
let count = 1; // count this node
|
|
64
|
+
for (const child of this.children.values()) {
|
|
65
|
+
count += child.nbOfNodes();
|
|
66
|
+
}
|
|
67
|
+
return count;
|
|
68
|
+
}
|
|
69
|
+
validate() {
|
|
70
|
+
if (!this.root) {
|
|
71
|
+
throw new Error("unexpected nil root");
|
|
72
|
+
}
|
|
73
|
+
const nbOfOutputs = this.root.outputsLength;
|
|
74
|
+
const nbOfInputs = this.root.inputsLength;
|
|
75
|
+
if (nbOfInputs !== 1) {
|
|
76
|
+
throw new Error(`unexpected number of inputs: ${nbOfInputs}, expected 1`);
|
|
77
|
+
}
|
|
78
|
+
// the children map can't be bigger than the number of outputs (excluding the P2A)
|
|
79
|
+
// a graph can be "partial" and specify only some of the outputs as children,
|
|
80
|
+
// that's why we allow len(g.Children) to be less than nbOfOutputs-1
|
|
81
|
+
if (this.children.size > nbOfOutputs - 1) {
|
|
82
|
+
throw new Error(`unexpected number of children: ${this.children.size}, expected maximum ${nbOfOutputs - 1}`);
|
|
83
|
+
}
|
|
84
|
+
// validate each child
|
|
85
|
+
for (const [outputIndex, child] of this.children) {
|
|
86
|
+
if (outputIndex >= nbOfOutputs) {
|
|
87
|
+
throw new Error(`output index ${outputIndex} is out of bounds (nb of outputs: ${nbOfOutputs})`);
|
|
88
|
+
}
|
|
89
|
+
child.validate();
|
|
90
|
+
const childInput = child.root.getInput(0);
|
|
91
|
+
const parentTxid = hex.encode(sha256x2(this.root.toBytes(true)).reverse());
|
|
92
|
+
// verify the input of the child is the output of the parent
|
|
93
|
+
if (!childInput.txid ||
|
|
94
|
+
hex.encode(childInput.txid) !== parentTxid ||
|
|
95
|
+
childInput.index !== outputIndex) {
|
|
96
|
+
throw new Error(`input of child ${outputIndex} is not the output of the parent`);
|
|
97
|
+
}
|
|
98
|
+
// verify the sum of the child's outputs is equal to the output of the parent
|
|
99
|
+
let childOutputsSum = 0n;
|
|
100
|
+
for (let i = 0; i < child.root.outputsLength; i++) {
|
|
101
|
+
const output = child.root.getOutput(i);
|
|
102
|
+
if (output?.amount) {
|
|
103
|
+
childOutputsSum += output.amount;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const parentOutput = this.root.getOutput(outputIndex);
|
|
107
|
+
if (!parentOutput?.amount) {
|
|
108
|
+
throw new Error(`parent output ${outputIndex} has no amount`);
|
|
109
|
+
}
|
|
110
|
+
if (childOutputsSum !== parentOutput.amount) {
|
|
111
|
+
throw new Error(`sum of child's outputs is not equal to the output of the parent: ${childOutputsSum} != ${parentOutput.amount}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
leaves() {
|
|
116
|
+
if (this.children.size === 0) {
|
|
117
|
+
return [this.root];
|
|
118
|
+
}
|
|
119
|
+
const leaves = [];
|
|
120
|
+
for (const child of this.children.values()) {
|
|
121
|
+
leaves.push(...child.leaves());
|
|
122
|
+
}
|
|
123
|
+
return leaves;
|
|
124
|
+
}
|
|
125
|
+
get txid() {
|
|
126
|
+
return hex.encode(sha256x2(this.root.toBytes(true)).reverse());
|
|
127
|
+
}
|
|
128
|
+
find(txid) {
|
|
129
|
+
if (txid === this.txid) {
|
|
130
|
+
return this;
|
|
131
|
+
}
|
|
132
|
+
for (const child of this.children.values()) {
|
|
133
|
+
const found = child.find(txid);
|
|
134
|
+
if (found) {
|
|
135
|
+
return found;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
update(txid, fn) {
|
|
141
|
+
if (txid === this.txid) {
|
|
142
|
+
fn(this.root);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
for (const child of this.children.values()) {
|
|
146
|
+
try {
|
|
147
|
+
child.update(txid, fn);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
// Continue searching in other children if not found
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
throw new Error(`tx not found: ${txid}`);
|
|
156
|
+
}
|
|
157
|
+
*[Symbol.iterator]() {
|
|
158
|
+
yield this;
|
|
159
|
+
for (const child of this.children.values()) {
|
|
160
|
+
yield* child;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
// Helper function to check if a chunk has a specific child
|
|
165
|
+
function hasChild(chunk, childTxid) {
|
|
166
|
+
return Object.values(chunk.children).includes(childTxid);
|
|
167
|
+
}
|
|
168
|
+
// buildGraph recursively builds the TxGraph starting from the given txid
|
|
169
|
+
function buildGraph(rootTxid, chunksByTxid) {
|
|
170
|
+
const chunk = chunksByTxid.get(rootTxid);
|
|
171
|
+
if (!chunk) {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
const rootTx = chunk.tx;
|
|
175
|
+
const children = new Map();
|
|
176
|
+
// Recursively build children graphs
|
|
177
|
+
for (const [outputIndexStr, childTxid] of Object.entries(chunk.children)) {
|
|
178
|
+
const outputIndex = parseInt(outputIndexStr);
|
|
179
|
+
const childGraph = buildGraph(childTxid, chunksByTxid);
|
|
180
|
+
if (childGraph) {
|
|
181
|
+
children.set(outputIndex, childGraph);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return new TxTree(rootTx, children);
|
|
185
|
+
}
|
|
186
|
+
function decodeNode(chunk) {
|
|
187
|
+
const tx = Transaction.fromPSBT(base64.decode(chunk.tx));
|
|
188
|
+
return { tx, children: chunk.children };
|
|
189
|
+
}
|