@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.
- package/README.md +312 -0
- package/dist/cjs/arknote/index.js +86 -0
- package/dist/cjs/forfeit.js +38 -0
- package/dist/cjs/identity/inMemoryKey.js +40 -0
- package/dist/cjs/identity/index.js +2 -0
- package/dist/cjs/index.js +48 -0
- package/dist/cjs/musig2/index.js +10 -0
- package/dist/cjs/musig2/keys.js +57 -0
- package/dist/cjs/musig2/nonces.js +44 -0
- package/dist/cjs/musig2/sign.js +102 -0
- package/dist/cjs/networks.js +26 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/providers/ark.js +530 -0
- package/dist/cjs/providers/onchain.js +61 -0
- package/dist/cjs/script/address.js +45 -0
- package/dist/cjs/script/base.js +51 -0
- package/dist/cjs/script/default.js +40 -0
- package/dist/cjs/script/tapscript.js +528 -0
- package/dist/cjs/script/vhtlc.js +84 -0
- package/dist/cjs/tree/signingSession.js +238 -0
- package/dist/cjs/tree/validation.js +184 -0
- package/dist/cjs/tree/vtxoTree.js +197 -0
- package/dist/cjs/utils/bip21.js +114 -0
- package/dist/cjs/utils/coinselect.js +73 -0
- package/dist/cjs/utils/psbt.js +124 -0
- package/dist/cjs/utils/transactionHistory.js +148 -0
- package/dist/cjs/utils/txSizeEstimator.js +95 -0
- package/dist/cjs/wallet/index.js +8 -0
- package/dist/cjs/wallet/serviceWorker/db/vtxo/idb.js +153 -0
- package/dist/cjs/wallet/serviceWorker/db/vtxo/index.js +2 -0
- package/dist/cjs/wallet/serviceWorker/request.js +75 -0
- package/dist/cjs/wallet/serviceWorker/response.js +187 -0
- package/dist/cjs/wallet/serviceWorker/wallet.js +332 -0
- package/dist/cjs/wallet/serviceWorker/worker.js +452 -0
- package/dist/cjs/wallet/wallet.js +720 -0
- package/dist/esm/arknote/index.js +81 -0
- package/dist/esm/forfeit.js +35 -0
- package/dist/esm/identity/inMemoryKey.js +36 -0
- package/dist/esm/identity/index.js +1 -0
- package/dist/esm/index.js +39 -0
- package/dist/esm/musig2/index.js +3 -0
- package/dist/esm/musig2/keys.js +21 -0
- package/dist/esm/musig2/nonces.js +8 -0
- package/dist/esm/musig2/sign.js +63 -0
- package/dist/esm/networks.js +22 -0
- package/dist/esm/package.json +3 -0
- package/dist/esm/providers/ark.js +526 -0
- package/dist/esm/providers/onchain.js +57 -0
- package/dist/esm/script/address.js +41 -0
- package/dist/esm/script/base.js +46 -0
- package/dist/esm/script/default.js +37 -0
- package/dist/esm/script/tapscript.js +491 -0
- package/dist/esm/script/vhtlc.js +81 -0
- package/dist/esm/tree/signingSession.js +200 -0
- package/dist/esm/tree/validation.js +179 -0
- package/dist/esm/tree/vtxoTree.js +157 -0
- package/dist/esm/utils/bip21.js +110 -0
- package/dist/esm/utils/coinselect.js +69 -0
- package/dist/esm/utils/psbt.js +118 -0
- package/dist/esm/utils/transactionHistory.js +145 -0
- package/dist/esm/utils/txSizeEstimator.js +91 -0
- package/dist/esm/wallet/index.js +5 -0
- package/dist/esm/wallet/serviceWorker/db/vtxo/idb.js +149 -0
- package/dist/esm/wallet/serviceWorker/db/vtxo/index.js +1 -0
- package/dist/esm/wallet/serviceWorker/request.js +72 -0
- package/dist/esm/wallet/serviceWorker/response.js +184 -0
- package/dist/esm/wallet/serviceWorker/wallet.js +328 -0
- package/dist/esm/wallet/serviceWorker/worker.js +448 -0
- package/dist/esm/wallet/wallet.js +716 -0
- package/dist/types/arknote/index.d.ts +17 -0
- package/dist/types/forfeit.d.ts +15 -0
- package/dist/types/identity/inMemoryKey.d.ts +12 -0
- package/dist/types/identity/index.d.ts +7 -0
- package/dist/types/index.d.ts +22 -0
- package/dist/types/musig2/index.d.ts +4 -0
- package/dist/types/musig2/keys.d.ts +9 -0
- package/dist/types/musig2/nonces.d.ts +13 -0
- package/dist/types/musig2/sign.d.ts +27 -0
- package/dist/types/networks.d.ts +16 -0
- package/dist/types/providers/ark.d.ts +126 -0
- package/dist/types/providers/onchain.d.ts +36 -0
- package/dist/types/script/address.d.ts +10 -0
- package/dist/types/script/base.d.ts +26 -0
- package/dist/types/script/default.d.ts +19 -0
- package/dist/types/script/tapscript.d.ts +94 -0
- package/dist/types/script/vhtlc.d.ts +31 -0
- package/dist/types/tree/signingSession.d.ts +32 -0
- package/dist/types/tree/validation.d.ts +22 -0
- package/dist/types/tree/vtxoTree.d.ts +32 -0
- package/dist/types/utils/bip21.d.ts +21 -0
- package/dist/types/utils/coinselect.d.ts +21 -0
- package/dist/types/utils/psbt.d.ts +11 -0
- package/dist/types/utils/transactionHistory.d.ts +2 -0
- package/dist/types/utils/txSizeEstimator.d.ts +27 -0
- package/dist/types/wallet/index.d.ts +122 -0
- package/dist/types/wallet/serviceWorker/db/vtxo/idb.d.ts +18 -0
- package/dist/types/wallet/serviceWorker/db/vtxo/index.d.ts +12 -0
- package/dist/types/wallet/serviceWorker/request.d.ts +68 -0
- package/dist/types/wallet/serviceWorker/response.d.ts +107 -0
- package/dist/types/wallet/serviceWorker/wallet.d.ts +23 -0
- package/dist/types/wallet/serviceWorker/worker.d.ts +26 -0
- package/dist/types/wallet/wallet.d.ts +42 -0
- 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
|
+
}
|