@arkade-os/sdk 0.3.0-alpha.8 → 0.3.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 +48 -14
- package/dist/cjs/arknote/index.js +3 -3
- package/dist/cjs/forfeit.js +2 -2
- package/dist/cjs/identity/singleKey.js +8 -8
- package/dist/cjs/index.js +13 -5
- package/dist/cjs/{bip322 → intent}/index.js +38 -61
- package/dist/cjs/musig2/index.js +2 -1
- package/dist/cjs/musig2/nonces.js +4 -0
- package/dist/cjs/providers/ark.js +76 -45
- package/dist/cjs/providers/errors.js +59 -0
- package/dist/cjs/providers/expoArk.js +15 -170
- package/dist/cjs/providers/expoIndexer.js +22 -111
- package/dist/cjs/providers/expoUtils.js +124 -0
- package/dist/cjs/providers/onchain.js +19 -20
- package/dist/cjs/repositories/walletRepository.js +64 -28
- package/dist/cjs/script/base.js +15 -7
- package/dist/cjs/script/tapscript.js +20 -21
- package/dist/cjs/script/vhtlc.js +2 -2
- package/dist/cjs/tree/signingSession.js +44 -11
- package/dist/cjs/tree/txTree.js +3 -4
- package/dist/cjs/tree/validation.js +2 -3
- package/dist/cjs/utils/arkTransaction.js +105 -15
- package/dist/cjs/utils/transaction.js +28 -0
- package/dist/cjs/utils/unknownFields.js +7 -7
- package/dist/cjs/wallet/onchain.js +6 -7
- package/dist/cjs/wallet/serviceWorker/response.js +32 -0
- package/dist/cjs/wallet/serviceWorker/utils.js +2 -0
- package/dist/cjs/wallet/serviceWorker/wallet.js +7 -8
- package/dist/cjs/wallet/serviceWorker/worker.js +46 -27
- package/dist/cjs/wallet/unroll.js +7 -9
- package/dist/cjs/wallet/utils.js +9 -0
- package/dist/cjs/wallet/vtxo-manager.js +323 -0
- package/dist/cjs/wallet/wallet.js +98 -125
- package/dist/esm/arknote/index.js +2 -2
- package/dist/esm/forfeit.js +1 -1
- package/dist/esm/identity/singleKey.js +9 -9
- package/dist/esm/index.js +14 -10
- package/dist/esm/{bip322 → intent}/index.js +32 -54
- package/dist/esm/musig2/index.js +1 -1
- package/dist/esm/musig2/nonces.js +3 -0
- package/dist/esm/providers/ark.js +76 -45
- package/dist/esm/providers/errors.js +54 -0
- package/dist/esm/providers/expoArk.js +15 -137
- package/dist/esm/providers/expoIndexer.js +22 -78
- package/dist/esm/providers/expoUtils.js +87 -0
- package/dist/esm/providers/onchain.js +19 -20
- package/dist/esm/repositories/walletRepository.js +64 -28
- package/dist/esm/script/base.js +12 -4
- package/dist/esm/script/tapscript.js +1 -2
- package/dist/esm/script/vhtlc.js +1 -1
- package/dist/esm/tree/signingSession.js +45 -12
- package/dist/esm/tree/txTree.js +3 -4
- package/dist/esm/tree/validation.js +2 -3
- package/dist/esm/utils/arkTransaction.js +97 -8
- package/dist/esm/utils/transaction.js +24 -0
- package/dist/esm/utils/unknownFields.js +3 -3
- package/dist/esm/wallet/onchain.js +3 -4
- package/dist/esm/wallet/serviceWorker/response.js +32 -0
- package/dist/esm/wallet/serviceWorker/utils.js +1 -0
- package/dist/esm/wallet/serviceWorker/wallet.js +8 -9
- package/dist/esm/wallet/serviceWorker/worker.js +48 -29
- package/dist/esm/wallet/unroll.js +5 -7
- package/dist/esm/wallet/utils.js +8 -0
- package/dist/esm/wallet/vtxo-manager.js +317 -0
- package/dist/esm/wallet/wallet.js +92 -119
- package/dist/types/arknote/index.d.ts +1 -1
- package/dist/types/forfeit.d.ts +2 -2
- package/dist/types/identity/index.d.ts +2 -2
- package/dist/types/identity/singleKey.d.ts +2 -2
- package/dist/types/index.d.ts +9 -7
- package/dist/types/intent/index.d.ts +41 -0
- package/dist/types/musig2/index.d.ts +1 -1
- package/dist/types/musig2/nonces.d.ts +1 -0
- package/dist/types/providers/ark.d.ts +62 -26
- package/dist/types/providers/errors.d.ts +13 -0
- package/dist/types/providers/expoIndexer.d.ts +2 -10
- package/dist/types/providers/expoUtils.d.ts +18 -0
- package/dist/types/providers/indexer.d.ts +1 -9
- package/dist/types/providers/onchain.d.ts +6 -2
- package/dist/types/repositories/walletRepository.d.ts +9 -5
- package/dist/types/script/base.d.ts +5 -2
- package/dist/types/tree/signingSession.d.ts +16 -11
- package/dist/types/utils/anchor.d.ts +2 -2
- package/dist/types/utils/arkTransaction.d.ts +12 -4
- package/dist/types/utils/transaction.d.ts +13 -0
- package/dist/types/utils/unknownFields.d.ts +4 -4
- package/dist/types/wallet/index.d.ts +6 -4
- package/dist/types/wallet/onchain.d.ts +1 -1
- package/dist/types/wallet/serviceWorker/response.d.ts +16 -2
- package/dist/types/wallet/serviceWorker/utils.d.ts +1 -0
- package/dist/types/wallet/serviceWorker/wallet.d.ts +2 -2
- package/dist/types/wallet/serviceWorker/worker.d.ts +7 -1
- package/dist/types/wallet/unroll.d.ts +1 -1
- package/dist/types/wallet/utils.d.ts +2 -1
- package/dist/types/wallet/vtxo-manager.d.ts +179 -0
- package/dist/types/wallet/wallet.d.ts +8 -4
- package/package.json +1 -2
- package/dist/cjs/bip322/errors.js +0 -13
- package/dist/esm/bip322/errors.js +0 -9
- package/dist/types/bip322/errors.d.ts +0 -6
- package/dist/types/bip322/index.d.ts +0 -57
|
@@ -3,7 +3,7 @@ import { Script } from "@scure/btc-signer/script.js";
|
|
|
3
3
|
import { SigHash } from "@scure/btc-signer/transaction.js";
|
|
4
4
|
import { hex } from "@scure/base";
|
|
5
5
|
import { schnorr, secp256k1 } from "@noble/curves/secp256k1.js";
|
|
6
|
-
import { randomPrivateKeyBytes
|
|
6
|
+
import { randomPrivateKeyBytes } from "@scure/btc-signer/utils.js";
|
|
7
7
|
import { CosignerPublicKey, getArkPsbtFields } from '../utils/unknownFields.js';
|
|
8
8
|
export const ErrMissingVtxoGraph = new Error("missing vtxo graph");
|
|
9
9
|
export const ErrMissingAggregateKey = new Error("missing aggregate key");
|
|
@@ -20,15 +20,15 @@ export class TreeSignerSession {
|
|
|
20
20
|
const secretKey = randomPrivateKeyBytes();
|
|
21
21
|
return new TreeSignerSession(secretKey);
|
|
22
22
|
}
|
|
23
|
-
init(tree, scriptRoot, rootInputAmount) {
|
|
23
|
+
async init(tree, scriptRoot, rootInputAmount) {
|
|
24
24
|
this.graph = tree;
|
|
25
25
|
this.scriptRoot = scriptRoot;
|
|
26
26
|
this.rootSharedOutputAmount = rootInputAmount;
|
|
27
27
|
}
|
|
28
|
-
getPublicKey() {
|
|
28
|
+
async getPublicKey() {
|
|
29
29
|
return secp256k1.getPublicKey(this.secretKey);
|
|
30
30
|
}
|
|
31
|
-
getNonces() {
|
|
31
|
+
async getNonces() {
|
|
32
32
|
if (!this.graph)
|
|
33
33
|
throw ErrMissingVtxoGraph;
|
|
34
34
|
if (!this.myNonces) {
|
|
@@ -40,12 +40,46 @@ export class TreeSignerSession {
|
|
|
40
40
|
}
|
|
41
41
|
return publicNonces;
|
|
42
42
|
}
|
|
43
|
-
|
|
44
|
-
if (this.
|
|
45
|
-
throw
|
|
46
|
-
this.aggregateNonces
|
|
43
|
+
async aggregatedNonces(txid, noncesByPubkey) {
|
|
44
|
+
if (!this.graph)
|
|
45
|
+
throw ErrMissingVtxoGraph;
|
|
46
|
+
if (!this.aggregateNonces) {
|
|
47
|
+
this.aggregateNonces = new Map();
|
|
48
|
+
}
|
|
49
|
+
if (!this.myNonces) {
|
|
50
|
+
await this.getNonces(); // generate nonces if not generated yet
|
|
51
|
+
}
|
|
52
|
+
if (this.aggregateNonces.has(txid)) {
|
|
53
|
+
return {
|
|
54
|
+
hasAllNonces: this.aggregateNonces.size === this.myNonces?.size,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
const myNonce = this.myNonces.get(txid);
|
|
58
|
+
if (!myNonce)
|
|
59
|
+
throw new Error(`missing nonce for txid ${txid}`);
|
|
60
|
+
const myPublicKey = await this.getPublicKey();
|
|
61
|
+
// set my nonce to not rely on server
|
|
62
|
+
noncesByPubkey.set(hex.encode(myPublicKey.subarray(1)), myNonce);
|
|
63
|
+
const tx = this.graph.find(txid);
|
|
64
|
+
if (!tx)
|
|
65
|
+
throw new Error(`missing tx for txid ${txid}`);
|
|
66
|
+
const cosigners = getArkPsbtFields(tx.root, 0, CosignerPublicKey).map((c) => hex.encode(c.key.subarray(1)) // xonly pubkey
|
|
67
|
+
);
|
|
68
|
+
const pubNonces = [];
|
|
69
|
+
for (const cosigner of cosigners) {
|
|
70
|
+
const nonce = noncesByPubkey.get(cosigner);
|
|
71
|
+
if (!nonce) {
|
|
72
|
+
throw new Error(`missing nonce for cosigner ${cosigner}`);
|
|
73
|
+
}
|
|
74
|
+
pubNonces.push(nonce.pubNonce);
|
|
75
|
+
}
|
|
76
|
+
const aggregateNonce = musig2.aggregateNonces(pubNonces);
|
|
77
|
+
this.aggregateNonces.set(txid, { pubNonce: aggregateNonce });
|
|
78
|
+
return {
|
|
79
|
+
hasAllNonces: this.aggregateNonces.size === this.myNonces?.size,
|
|
80
|
+
};
|
|
47
81
|
}
|
|
48
|
-
sign() {
|
|
82
|
+
async sign() {
|
|
49
83
|
if (!this.graph)
|
|
50
84
|
throw ErrMissingVtxoGraph;
|
|
51
85
|
if (!this.aggregateNonces)
|
|
@@ -128,9 +162,8 @@ export async function validateTreeSigs(finalAggregatedKey, sharedOutputAmount, v
|
|
|
128
162
|
function getPrevOutput(finalKey, graph, sharedOutputAmount, tx) {
|
|
129
163
|
// generate P2TR script from musig2 final key
|
|
130
164
|
const pkScript = Script.encode(["OP_1", finalKey.slice(1)]);
|
|
131
|
-
const txid = hex.encode(sha256x2(tx.toBytes(true)).reverse());
|
|
132
165
|
// if the input is the root input, return the shared output amount
|
|
133
|
-
if (
|
|
166
|
+
if (tx.id === graph.txid) {
|
|
134
167
|
return {
|
|
135
168
|
amount: sharedOutputAmount,
|
|
136
169
|
script: pkScript,
|
|
@@ -140,7 +173,7 @@ function getPrevOutput(finalKey, graph, sharedOutputAmount, tx) {
|
|
|
140
173
|
const parentInput = tx.getInput(0);
|
|
141
174
|
if (!parentInput.txid)
|
|
142
175
|
throw new Error("missing parent input txid");
|
|
143
|
-
const parentTxid = hex.encode(
|
|
176
|
+
const parentTxid = hex.encode(parentInput.txid);
|
|
144
177
|
const parent = graph.find(parentTxid);
|
|
145
178
|
if (!parent)
|
|
146
179
|
throw new Error("parent tx not found");
|
package/dist/esm/tree/txTree.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { Transaction } from "@scure/btc-signer/transaction.js";
|
|
2
2
|
import { base64 } from "@scure/base";
|
|
3
3
|
import { hex } from "@scure/base";
|
|
4
|
-
import { sha256x2 } from "@scure/btc-signer/utils.js";
|
|
5
4
|
/**
|
|
6
5
|
* TxTree is a graph of bitcoin transactions.
|
|
7
6
|
* It is used to represent batch tree created during settlement session
|
|
@@ -19,7 +18,7 @@ export class TxTree {
|
|
|
19
18
|
const chunksByTxid = new Map();
|
|
20
19
|
for (const chunk of chunks) {
|
|
21
20
|
const decodedChunk = decodeNode(chunk);
|
|
22
|
-
const txid =
|
|
21
|
+
const txid = decodedChunk.tx.id;
|
|
23
22
|
chunksByTxid.set(txid, decodedChunk);
|
|
24
23
|
}
|
|
25
24
|
// Find the root chunks (the ones that aren't referenced as a child)
|
|
@@ -88,7 +87,7 @@ export class TxTree {
|
|
|
88
87
|
}
|
|
89
88
|
child.validate();
|
|
90
89
|
const childInput = child.root.getInput(0);
|
|
91
|
-
const parentTxid =
|
|
90
|
+
const parentTxid = this.root.id;
|
|
92
91
|
// verify the input of the child is the output of the parent
|
|
93
92
|
if (!childInput.txid ||
|
|
94
93
|
hex.encode(childInput.txid) !== parentTxid ||
|
|
@@ -123,7 +122,7 @@ export class TxTree {
|
|
|
123
122
|
return leaves;
|
|
124
123
|
}
|
|
125
124
|
get txid() {
|
|
126
|
-
return
|
|
125
|
+
return this.root.id;
|
|
127
126
|
}
|
|
128
127
|
find(txid) {
|
|
129
128
|
if (txid === this.txid) {
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { hex } from "@scure/base";
|
|
2
2
|
import { Transaction } from "@scure/btc-signer/transaction.js";
|
|
3
3
|
import { base64 } from "@scure/base";
|
|
4
|
-
import { sha256x2 } from "@scure/btc-signer/utils.js";
|
|
5
4
|
import { aggregateKeys } from '../musig2/index.js';
|
|
6
5
|
import { CosignerPublicKey, getArkPsbtFields } from '../utils/unknownFields.js';
|
|
7
6
|
export const ErrInvalidSettlementTx = (tx) => new Error(`invalid settlement transaction: ${tx}`);
|
|
@@ -25,7 +24,7 @@ export function validateConnectorsTxGraph(settlementTxB64, connectorsGraph) {
|
|
|
25
24
|
const settlementTx = Transaction.fromPSBT(base64.decode(settlementTxB64));
|
|
26
25
|
if (settlementTx.outputsLength <= BATCH_OUTPUT_CONNECTORS_INDEX)
|
|
27
26
|
throw ErrInvalidSettlementTxOutputs;
|
|
28
|
-
const expectedRootTxid =
|
|
27
|
+
const expectedRootTxid = settlementTx.id;
|
|
29
28
|
if (!rootInput.txid)
|
|
30
29
|
throw ErrWrongSettlementTxid;
|
|
31
30
|
if (hex.encode(rootInput.txid) !== expectedRootTxid)
|
|
@@ -52,7 +51,7 @@ export function validateVtxoTxGraph(graph, roundTransaction, sweepTapTreeRoot) {
|
|
|
52
51
|
throw ErrEmptyTree;
|
|
53
52
|
}
|
|
54
53
|
const rootInput = graph.root.getInput(0);
|
|
55
|
-
const commitmentTxid =
|
|
54
|
+
const commitmentTxid = roundTransaction.id;
|
|
56
55
|
if (!rootInput.txid ||
|
|
57
56
|
hex.encode(rootInput.txid) !== commitmentTxid ||
|
|
58
57
|
rootInput.index !== BATCH_OUTPUT_VTXO_INDEX) {
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { schnorr } from "@noble/curves/secp256k1.js";
|
|
2
|
+
import { hex } from "@scure/base";
|
|
3
|
+
import { DEFAULT_SEQUENCE, SigHash } from "@scure/btc-signer";
|
|
4
|
+
import { tapLeafHash } from "@scure/btc-signer/payment.js";
|
|
2
5
|
import { CLTVMultisigTapscript, decodeTapscript, } from '../script/tapscript.js';
|
|
3
6
|
import { scriptFromTapLeafScript, VtxoScript, } from '../script/base.js';
|
|
4
7
|
import { P2A } from './anchor.js';
|
|
5
|
-
import { hex } from "@scure/base";
|
|
6
|
-
import { sha256x2 } from "@scure/btc-signer/utils.js";
|
|
7
8
|
import { setArkPsbtField, VtxoTaprootTree } from './unknownFields.js';
|
|
9
|
+
import { Transaction } from './transaction.js';
|
|
8
10
|
/**
|
|
9
11
|
* Builds an offchain transaction with checkpoint transactions.
|
|
10
12
|
*
|
|
@@ -44,8 +46,6 @@ function buildVirtualTx(inputs, outputs) {
|
|
|
44
46
|
}
|
|
45
47
|
const tx = new Transaction({
|
|
46
48
|
version: 3,
|
|
47
|
-
allowUnknown: true,
|
|
48
|
-
allowUnknownOutputs: true,
|
|
49
49
|
lockTime: Number(lockTime),
|
|
50
50
|
});
|
|
51
51
|
for (const [i, input] of inputs.entries()) {
|
|
@@ -70,8 +70,7 @@ function buildVirtualTx(inputs, outputs) {
|
|
|
70
70
|
}
|
|
71
71
|
function buildCheckpointTx(vtxo, serverUnrollScript) {
|
|
72
72
|
// create the checkpoint vtxo script from collaborative closure
|
|
73
|
-
const collaborativeClosure = decodeTapscript(vtxo.
|
|
74
|
-
scriptFromTapLeafScript(vtxo.tapLeafScript));
|
|
73
|
+
const collaborativeClosure = decodeTapscript(scriptFromTapLeafScript(vtxo.tapLeafScript));
|
|
75
74
|
// create the checkpoint vtxo script combining collaborative closure and server unroll script
|
|
76
75
|
const checkpointVtxoScript = new VtxoScript([
|
|
77
76
|
serverUnrollScript.script,
|
|
@@ -88,7 +87,7 @@ function buildCheckpointTx(vtxo, serverUnrollScript) {
|
|
|
88
87
|
const collaborativeLeafProof = checkpointVtxoScript.findLeaf(hex.encode(collaborativeClosure.script));
|
|
89
88
|
// create the checkpoint input that will be used as input of the virtual tx
|
|
90
89
|
const checkpointInput = {
|
|
91
|
-
txid:
|
|
90
|
+
txid: checkpointTx.id,
|
|
92
91
|
vout: 0,
|
|
93
92
|
value: vtxo.value,
|
|
94
93
|
tapLeafScript: collaborativeLeafProof,
|
|
@@ -115,3 +114,93 @@ export function hasBoardingTxExpired(coin, boardingTimelock) {
|
|
|
115
114
|
const blockTime = BigInt(Math.floor(coin.status.block_time));
|
|
116
115
|
return blockTime + boardingTimelock.value <= now;
|
|
117
116
|
}
|
|
117
|
+
/**
|
|
118
|
+
* Formats a sighash type as a hex string (e.g., 0x01)
|
|
119
|
+
*/
|
|
120
|
+
function formatSighash(type) {
|
|
121
|
+
return `0x${type.toString(16).padStart(2, "0")}`;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Verify tapscript signatures on a transaction input
|
|
125
|
+
* @param tx Transaction to verify
|
|
126
|
+
* @param inputIndex Index of the input to verify
|
|
127
|
+
* @param requiredSigners List of required signer pubkeys (hex encoded)
|
|
128
|
+
* @param excludePubkeys List of pubkeys to exclude from verification (hex encoded, e.g., server key not yet signed)
|
|
129
|
+
* @param allowedSighashTypes List of allowed sighash types (defaults to [SigHash.DEFAULT])
|
|
130
|
+
* @throws Error if verification fails
|
|
131
|
+
*/
|
|
132
|
+
export function verifyTapscriptSignatures(tx, inputIndex, requiredSigners, excludePubkeys = [], allowedSighashTypes = [SigHash.DEFAULT]) {
|
|
133
|
+
const input = tx.getInput(inputIndex);
|
|
134
|
+
// Collect prevout scripts and amounts for ALL inputs (required for preimageWitnessV1)
|
|
135
|
+
const prevoutScripts = [];
|
|
136
|
+
const prevoutAmounts = [];
|
|
137
|
+
for (let i = 0; i < tx.inputsLength; i++) {
|
|
138
|
+
const inp = tx.getInput(i);
|
|
139
|
+
if (!inp.witnessUtxo) {
|
|
140
|
+
throw new Error(`Input ${i} is missing witnessUtxo`);
|
|
141
|
+
}
|
|
142
|
+
prevoutScripts.push(inp.witnessUtxo.script);
|
|
143
|
+
prevoutAmounts.push(inp.witnessUtxo.amount);
|
|
144
|
+
}
|
|
145
|
+
// Verify tapScriptSig signatures
|
|
146
|
+
if (!input.tapScriptSig || input.tapScriptSig.length === 0) {
|
|
147
|
+
throw new Error(`Input ${inputIndex} is missing tapScriptSig`);
|
|
148
|
+
}
|
|
149
|
+
// Verify each signature in tapScriptSig
|
|
150
|
+
for (const [tapScriptSigData, signature] of input.tapScriptSig) {
|
|
151
|
+
const pubKey = tapScriptSigData.pubKey;
|
|
152
|
+
const pubKeyHex = hex.encode(pubKey);
|
|
153
|
+
// Skip verification for excluded pubkeys
|
|
154
|
+
if (excludePubkeys.includes(pubKeyHex)) {
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
// Extract sighash type from signature
|
|
158
|
+
// Schnorr signatures are 64 bytes, with optional 1-byte sighash appended
|
|
159
|
+
const sighashType = signature.length === 65 ? signature[64] : SigHash.DEFAULT;
|
|
160
|
+
const sig = signature.subarray(0, 64);
|
|
161
|
+
// Verify sighash type is allowed
|
|
162
|
+
if (!allowedSighashTypes.includes(sighashType)) {
|
|
163
|
+
const sighashName = formatSighash(sighashType);
|
|
164
|
+
throw new Error(`Unallowed sighash type ${sighashName} for input ${inputIndex}, pubkey ${pubKeyHex}.`);
|
|
165
|
+
}
|
|
166
|
+
// Find the tapLeafScript that matches this signature's leafHash
|
|
167
|
+
if (!input.tapLeafScript || input.tapLeafScript.length === 0) {
|
|
168
|
+
throw new Error();
|
|
169
|
+
}
|
|
170
|
+
// Search for the leaf that matches the leafHash in tapScriptSigData
|
|
171
|
+
const leafHash = tapScriptSigData.leafHash;
|
|
172
|
+
const leafHashHex = hex.encode(leafHash);
|
|
173
|
+
let matchingScript;
|
|
174
|
+
let matchingVersion;
|
|
175
|
+
for (const [_, scriptWithVersion] of input.tapLeafScript) {
|
|
176
|
+
const script = scriptWithVersion.subarray(0, -1);
|
|
177
|
+
const version = scriptWithVersion[scriptWithVersion.length - 1];
|
|
178
|
+
// Compute the leaf hash for this script and compare as hex strings
|
|
179
|
+
const computedLeafHash = tapLeafHash(script, version);
|
|
180
|
+
const computedHex = hex.encode(computedLeafHash);
|
|
181
|
+
if (computedHex === leafHashHex) {
|
|
182
|
+
matchingScript = script;
|
|
183
|
+
matchingVersion = version;
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (!matchingScript || matchingVersion === undefined) {
|
|
188
|
+
throw new Error(`Input ${inputIndex}: No tapLeafScript found matching leafHash ${hex.encode(leafHash)}`);
|
|
189
|
+
}
|
|
190
|
+
// Reconstruct the message that was signed
|
|
191
|
+
// Note: preimageWitnessV1 requires ALL input prevout scripts and amounts
|
|
192
|
+
const message = tx.preimageWitnessV1(inputIndex, prevoutScripts, sighashType, prevoutAmounts, undefined, matchingScript, matchingVersion);
|
|
193
|
+
// Verify the schnorr signature
|
|
194
|
+
const isValid = schnorr.verify(sig, message, pubKey);
|
|
195
|
+
if (!isValid) {
|
|
196
|
+
throw new Error(`Invalid signature for input ${inputIndex}, pubkey ${pubKeyHex}`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
// Verify we have signatures from all required signers (excluding those we're skipping)
|
|
200
|
+
const signedPubkeys = input.tapScriptSig.map(([data]) => hex.encode(data.pubKey));
|
|
201
|
+
const requiredNotExcluded = requiredSigners.filter((pk) => !excludePubkeys.includes(pk));
|
|
202
|
+
const missingSigners = requiredNotExcluded.filter((pk) => !signedPubkeys.includes(pk));
|
|
203
|
+
if (missingSigners.length > 0) {
|
|
204
|
+
throw new Error(`Missing signatures from: ${missingSigners.map((pk) => pk.slice(0, 16)).join(", ")}...`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Transaction as BtcSignerTransaction } from "@scure/btc-signer";
|
|
2
|
+
/**
|
|
3
|
+
* Transaction is a wrapper around the @scure/btc-signer Transaction class.
|
|
4
|
+
* It adds the Ark protocol specific options to the transaction.
|
|
5
|
+
*/
|
|
6
|
+
export class Transaction extends BtcSignerTransaction {
|
|
7
|
+
constructor(opts) {
|
|
8
|
+
super(withArkOpts(opts));
|
|
9
|
+
}
|
|
10
|
+
static fromPSBT(psbt_, opts) {
|
|
11
|
+
return BtcSignerTransaction.fromPSBT(psbt_, withArkOpts(opts));
|
|
12
|
+
}
|
|
13
|
+
static fromRaw(raw, opts) {
|
|
14
|
+
return BtcSignerTransaction.fromRaw(raw, withArkOpts(opts));
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
Transaction.ARK_TX_OPTS = {
|
|
18
|
+
allowUnknown: true,
|
|
19
|
+
allowUnknownOutputs: true,
|
|
20
|
+
allowUnknownInputs: true,
|
|
21
|
+
};
|
|
22
|
+
function withArkOpts(opts) {
|
|
23
|
+
return { ...Transaction.ARK_TX_OPTS, ...opts };
|
|
24
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as bip68 from "bip68";
|
|
2
|
-
import { RawWitness, ScriptNum } from "@scure/btc-signer
|
|
2
|
+
import { RawWitness, ScriptNum } from "@scure/btc-signer";
|
|
3
3
|
import { hex } from "@scure/base";
|
|
4
4
|
/**
|
|
5
5
|
* ArkPsbtFieldKey is the key values for ark psbt fields.
|
|
@@ -13,9 +13,9 @@ export var ArkPsbtFieldKey;
|
|
|
13
13
|
})(ArkPsbtFieldKey || (ArkPsbtFieldKey = {}));
|
|
14
14
|
/**
|
|
15
15
|
* ArkPsbtFieldKeyType is the type of the ark psbt field key.
|
|
16
|
-
* Every ark psbt field has key type
|
|
16
|
+
* Every ark psbt field has key type 222.
|
|
17
17
|
*/
|
|
18
|
-
export const ArkPsbtFieldKeyType =
|
|
18
|
+
export const ArkPsbtFieldKeyType = 222;
|
|
19
19
|
/**
|
|
20
20
|
* setArkPsbtField appends a new unknown field to the input at inputIndex
|
|
21
21
|
*
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { p2tr } from "@scure/btc-signer
|
|
1
|
+
import { p2tr } from "@scure/btc-signer";
|
|
2
2
|
import { getNetwork } from '../networks.js';
|
|
3
3
|
import { ESPLORA_URL, EsploraProvider, } from '../providers/onchain.js';
|
|
4
|
-
import { Transaction } from "@scure/btc-signer/transaction.js";
|
|
5
4
|
import { findP2AOutput, P2A } from '../utils/anchor.js';
|
|
6
5
|
import { TxWeightEstimator } from '../utils/txSizeEstimator.js';
|
|
6
|
+
import { Transaction } from '../utils/transaction.js';
|
|
7
7
|
/**
|
|
8
8
|
* Onchain Bitcoin wallet implementation for traditional Bitcoin transactions.
|
|
9
9
|
*
|
|
@@ -105,9 +105,8 @@ export class OnchainWallet {
|
|
|
105
105
|
async bumpP2A(parent) {
|
|
106
106
|
const parentVsize = parent.vsize;
|
|
107
107
|
let child = new Transaction({
|
|
108
|
-
allowUnknownInputs: true,
|
|
109
|
-
allowLegacyWitnessUtxo: true,
|
|
110
108
|
version: 3,
|
|
109
|
+
allowLegacyWitnessUtxo: true,
|
|
111
110
|
});
|
|
112
111
|
child.addInput(findP2AOutput(parent)); // throws if not found
|
|
113
112
|
const childVsize = TxWeightEstimator.create()
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import { hex } from "@scure/base";
|
|
2
|
+
function getRandomId() {
|
|
3
|
+
const randomValue = crypto.getRandomValues(new Uint8Array(16));
|
|
4
|
+
return hex.encode(randomValue);
|
|
5
|
+
}
|
|
1
6
|
/**
|
|
2
7
|
* Response is the namespace that contains the response types for the service worker.
|
|
3
8
|
*/
|
|
@@ -184,4 +189,31 @@ export var Response;
|
|
|
184
189
|
};
|
|
185
190
|
}
|
|
186
191
|
Response.walletReloaded = walletReloaded;
|
|
192
|
+
function isVtxoUpdate(response) {
|
|
193
|
+
return response.type === "VTXO_UPDATE";
|
|
194
|
+
}
|
|
195
|
+
Response.isVtxoUpdate = isVtxoUpdate;
|
|
196
|
+
function vtxoUpdate(newVtxos, spentVtxos) {
|
|
197
|
+
return {
|
|
198
|
+
type: "VTXO_UPDATE",
|
|
199
|
+
id: getRandomId(), // spontaneous update, not tied to a request
|
|
200
|
+
success: true,
|
|
201
|
+
spentVtxos,
|
|
202
|
+
newVtxos,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
Response.vtxoUpdate = vtxoUpdate;
|
|
206
|
+
function isUtxoUpdate(response) {
|
|
207
|
+
return response.type === "UTXO_UPDATE";
|
|
208
|
+
}
|
|
209
|
+
Response.isUtxoUpdate = isUtxoUpdate;
|
|
210
|
+
function utxoUpdate(coins) {
|
|
211
|
+
return {
|
|
212
|
+
type: "UTXO_UPDATE",
|
|
213
|
+
id: getRandomId(), // spontaneous update, not tied to a request
|
|
214
|
+
success: true,
|
|
215
|
+
coins,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
Response.utxoUpdate = utxoUpdate;
|
|
187
219
|
})(Response || (Response = {}));
|
|
@@ -3,7 +3,7 @@ import { hex } from "@scure/base";
|
|
|
3
3
|
import { IndexedDBStorageAdapter } from '../../storage/indexedDB.js';
|
|
4
4
|
import { WalletRepositoryImpl } from '../../repositories/walletRepository.js';
|
|
5
5
|
import { ContractRepositoryImpl } from '../../repositories/contractRepository.js';
|
|
6
|
-
import { setupServiceWorker } from './utils.js';
|
|
6
|
+
import { DEFAULT_DB_NAME, setupServiceWorker } from './utils.js';
|
|
7
7
|
const isPrivateKeyIdentity = (identity) => {
|
|
8
8
|
return typeof identity.toHex === "function";
|
|
9
9
|
};
|
|
@@ -22,7 +22,7 @@ export class ServiceWorkerWallet {
|
|
|
22
22
|
}
|
|
23
23
|
static async create(options) {
|
|
24
24
|
// Default to IndexedDB for service worker context
|
|
25
|
-
const storage = options.
|
|
25
|
+
const storage = new IndexedDBStorageAdapter(options.dbName || DEFAULT_DB_NAME, options.dbVersion);
|
|
26
26
|
// Create repositories
|
|
27
27
|
const walletRepo = new WalletRepositoryImpl(storage);
|
|
28
28
|
const contractRepo = new ContractRepositoryImpl(storage);
|
|
@@ -31,7 +31,7 @@ export class ServiceWorkerWallet {
|
|
|
31
31
|
? options.identity
|
|
32
32
|
: null;
|
|
33
33
|
if (!identity) {
|
|
34
|
-
throw new Error("ServiceWorkerWallet.create() requires a Identity that can expose
|
|
34
|
+
throw new Error("ServiceWorkerWallet.create() requires a Identity that can expose a single private key");
|
|
35
35
|
}
|
|
36
36
|
// Extract private key for service worker initialization
|
|
37
37
|
const privateKey = identity.toHex();
|
|
@@ -74,13 +74,9 @@ export class ServiceWorkerWallet {
|
|
|
74
74
|
// Register and setup the service worker
|
|
75
75
|
const serviceWorker = await setupServiceWorker(options.serviceWorkerPath);
|
|
76
76
|
// Use the existing create method
|
|
77
|
-
return
|
|
78
|
-
|
|
79
|
-
arkServerUrl: options.arkServerUrl,
|
|
80
|
-
esploraUrl: options.esploraUrl,
|
|
81
|
-
identity: options.identity,
|
|
77
|
+
return ServiceWorkerWallet.create({
|
|
78
|
+
...options,
|
|
82
79
|
serviceWorker,
|
|
83
|
-
storage: options.storage,
|
|
84
80
|
});
|
|
85
81
|
}
|
|
86
82
|
// send a message and wait for a response
|
|
@@ -257,6 +253,9 @@ export class ServiceWorkerWallet {
|
|
|
257
253
|
return new Promise((resolve, reject) => {
|
|
258
254
|
const messageHandler = (event) => {
|
|
259
255
|
const response = event.data;
|
|
256
|
+
if (response.id !== message.id) {
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
260
259
|
if (!response.success) {
|
|
261
260
|
navigator.serviceWorker.removeEventListener("message", messageHandler);
|
|
262
261
|
reject(new Error(response.message));
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/// <reference lib="webworker" />
|
|
2
2
|
import { SingleKey } from '../../identity/singleKey.js';
|
|
3
|
-
import { isRecoverable, isSpendable, isSubdust } from '../index.js';
|
|
3
|
+
import { isRecoverable, isSpendable, isSubdust, } from '../index.js';
|
|
4
4
|
import { Wallet } from '../wallet.js';
|
|
5
5
|
import { Request } from './request.js';
|
|
6
6
|
import { Response } from './response.js';
|
|
@@ -10,15 +10,18 @@ import { RestIndexerProvider } from '../../providers/indexer.js';
|
|
|
10
10
|
import { hex } from "@scure/base";
|
|
11
11
|
import { IndexedDBStorageAdapter } from '../../storage/indexedDB.js';
|
|
12
12
|
import { WalletRepositoryImpl, } from '../../repositories/walletRepository.js';
|
|
13
|
-
import { extendVirtualCoin } from '../utils.js';
|
|
13
|
+
import { extendCoin, extendVirtualCoin } from '../utils.js';
|
|
14
|
+
import { DEFAULT_DB_NAME } from './utils.js';
|
|
14
15
|
/**
|
|
15
16
|
* Worker is a class letting to interact with ServiceWorkerWallet from the client
|
|
16
17
|
* it aims to be run in a service worker context
|
|
17
18
|
*/
|
|
18
19
|
export class Worker {
|
|
19
|
-
constructor(messageCallback = () => { }) {
|
|
20
|
+
constructor(dbName = DEFAULT_DB_NAME, dbVersion = 1, messageCallback = () => { }) {
|
|
21
|
+
this.dbName = dbName;
|
|
22
|
+
this.dbVersion = dbVersion;
|
|
20
23
|
this.messageCallback = messageCallback;
|
|
21
|
-
this.storage = new IndexedDBStorageAdapter(
|
|
24
|
+
this.storage = new IndexedDBStorageAdapter(dbName, dbVersion);
|
|
22
25
|
this.walletRepository = new WalletRepositoryImpl(this.storage);
|
|
23
26
|
}
|
|
24
27
|
/**
|
|
@@ -54,6 +57,15 @@ export class Worker {
|
|
|
54
57
|
spent: allVtxos.filter((vtxo) => !isSpendable(vtxo)),
|
|
55
58
|
};
|
|
56
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* Get all boarding utxos from wallet repository
|
|
62
|
+
*/
|
|
63
|
+
async getAllBoardingUtxos() {
|
|
64
|
+
if (!this.wallet)
|
|
65
|
+
return [];
|
|
66
|
+
const address = await this.wallet.getBoardingAddress();
|
|
67
|
+
return await this.walletRepository.getUtxos(address);
|
|
68
|
+
}
|
|
57
69
|
async start(withServiceWorkerUpdate = true) {
|
|
58
70
|
self.addEventListener("message", async (event) => {
|
|
59
71
|
await this.handleMessage(event);
|
|
@@ -104,6 +116,9 @@ export class Worker {
|
|
|
104
116
|
const txs = await this.wallet.getTransactionHistory();
|
|
105
117
|
if (txs)
|
|
106
118
|
await this.walletRepository.saveTransactions(address, txs);
|
|
119
|
+
// unsubscribe previous subscription if any
|
|
120
|
+
if (this.incomingFundsSubscription)
|
|
121
|
+
this.incomingFundsSubscription();
|
|
107
122
|
// subscribe for incoming funds and notify all clients when new funds arrive
|
|
108
123
|
this.incomingFundsSubscription = await this.wallet.notifyIncomingFunds(async (funds) => {
|
|
109
124
|
if (funds.type === "vtxo") {
|
|
@@ -121,11 +136,19 @@ export class Worker {
|
|
|
121
136
|
...spentVtxos,
|
|
122
137
|
]);
|
|
123
138
|
// notify all clients about the vtxo update
|
|
124
|
-
this.sendMessageToAllClients(
|
|
139
|
+
this.sendMessageToAllClients(Response.vtxoUpdate(newVtxos, spentVtxos));
|
|
125
140
|
}
|
|
126
141
|
if (funds.type === "utxo") {
|
|
142
|
+
const newUtxos = funds.coins.map((utxo) => extendCoin(this.wallet, utxo));
|
|
143
|
+
if (newUtxos.length === 0) {
|
|
144
|
+
this.sendMessageToAllClients(Response.utxoUpdate([]));
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const boardingAddress = await this.wallet?.getBoardingAddress();
|
|
148
|
+
// save utxos using unified repository
|
|
149
|
+
await this.walletRepository.saveUtxos(boardingAddress, newUtxos);
|
|
127
150
|
// notify all clients about the utxo update
|
|
128
|
-
this.sendMessageToAllClients(
|
|
151
|
+
this.sendMessageToAllClients(Response.utxoUpdate(funds.coins));
|
|
129
152
|
}
|
|
130
153
|
});
|
|
131
154
|
}
|
|
@@ -282,7 +305,7 @@ export class Worker {
|
|
|
282
305
|
}
|
|
283
306
|
try {
|
|
284
307
|
const [boardingUtxos, spendableVtxos, sweptVtxos] = await Promise.all([
|
|
285
|
-
this.
|
|
308
|
+
this.getAllBoardingUtxos(),
|
|
286
309
|
this.getSpendableVtxos(),
|
|
287
310
|
this.getSweptVtxos(),
|
|
288
311
|
]);
|
|
@@ -350,22 +373,21 @@ export class Worker {
|
|
|
350
373
|
return;
|
|
351
374
|
}
|
|
352
375
|
try {
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
event.source?.postMessage(Response.vtxos(message.id, vtxos));
|
|
376
|
+
const vtxos = await this.getSpendableVtxos();
|
|
377
|
+
const dustAmount = this.wallet.dustAmount;
|
|
378
|
+
const includeRecoverable = message.filter?.withRecoverable ?? false;
|
|
379
|
+
const filteredVtxos = includeRecoverable
|
|
380
|
+
? vtxos
|
|
381
|
+
: vtxos.filter((v) => {
|
|
382
|
+
if (dustAmount != null && isSubdust(v, dustAmount)) {
|
|
383
|
+
return false;
|
|
384
|
+
}
|
|
385
|
+
if (isRecoverable(v)) {
|
|
386
|
+
return false;
|
|
387
|
+
}
|
|
388
|
+
return true;
|
|
389
|
+
});
|
|
390
|
+
event.source?.postMessage(Response.vtxos(message.id, filteredVtxos));
|
|
369
391
|
}
|
|
370
392
|
catch (error) {
|
|
371
393
|
console.error("Error getting vtxos:", error);
|
|
@@ -388,7 +410,7 @@ export class Worker {
|
|
|
388
410
|
return;
|
|
389
411
|
}
|
|
390
412
|
try {
|
|
391
|
-
const boardingUtxos = await this.
|
|
413
|
+
const boardingUtxos = await this.getAllBoardingUtxos();
|
|
392
414
|
event.source?.postMessage(Response.boardingUtxos(message.id, boardingUtxos));
|
|
393
415
|
}
|
|
394
416
|
catch (error) {
|
|
@@ -510,15 +532,12 @@ export class Worker {
|
|
|
510
532
|
event.source?.postMessage(Response.error(message.id, "Unknown message type"));
|
|
511
533
|
}
|
|
512
534
|
}
|
|
513
|
-
async sendMessageToAllClients(
|
|
535
|
+
async sendMessageToAllClients(message) {
|
|
514
536
|
self.clients
|
|
515
537
|
.matchAll({ includeUncontrolled: true, type: "window" })
|
|
516
538
|
.then((clients) => {
|
|
517
539
|
clients.forEach((client) => {
|
|
518
|
-
client.postMessage(
|
|
519
|
-
type,
|
|
520
|
-
message,
|
|
521
|
-
});
|
|
540
|
+
client.postMessage(message);
|
|
522
541
|
});
|
|
523
542
|
});
|
|
524
543
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { SigHash, Transaction } from "@scure/btc-signer/transaction.js";
|
|
2
|
-
import { ChainTxType } from '../providers/indexer.js';
|
|
3
1
|
import { base64, hex } from "@scure/base";
|
|
2
|
+
import { SigHash, TaprootControlBlock } from "@scure/btc-signer";
|
|
3
|
+
import { ChainTxType } from '../providers/indexer.js';
|
|
4
4
|
import { VtxoScript } from '../script/base.js';
|
|
5
|
-
import { TaprootControlBlock, } from "@scure/btc-signer/psbt.js";
|
|
6
5
|
import { TxWeightEstimator } from '../utils/txSizeEstimator.js';
|
|
7
6
|
import { Wallet } from './wallet.js';
|
|
7
|
+
import { Transaction } from '../utils/transaction.js';
|
|
8
8
|
export var Unroll;
|
|
9
9
|
(function (Unroll) {
|
|
10
10
|
let StepType;
|
|
@@ -103,9 +103,7 @@ export var Unroll;
|
|
|
103
103
|
if (virtualTxs.txs.length === 0) {
|
|
104
104
|
throw new Error(`Tx ${nextTxToBroadcast.txid} not found`);
|
|
105
105
|
}
|
|
106
|
-
const tx = Transaction.fromPSBT(base64.decode(virtualTxs.txs[0])
|
|
107
|
-
allowUnknownInputs: true,
|
|
108
|
-
});
|
|
106
|
+
const tx = Transaction.fromPSBT(base64.decode(virtualTxs.txs[0]));
|
|
109
107
|
// finalize the tree transaction
|
|
110
108
|
if (nextTxToBroadcast.type === ChainTxType.TREE) {
|
|
111
109
|
const input = tx.getInput(0);
|
|
@@ -198,7 +196,7 @@ export var Unroll;
|
|
|
198
196
|
});
|
|
199
197
|
txWeightEstimator.addTapscriptInput(64, spendingLeaf[1].length, TaprootControlBlock.encode(spendingLeaf[0]).length);
|
|
200
198
|
}
|
|
201
|
-
const tx = new Transaction({
|
|
199
|
+
const tx = new Transaction({ version: 2 });
|
|
202
200
|
for (const input of inputs) {
|
|
203
201
|
tx.addInput(input);
|
|
204
202
|
}
|
package/dist/esm/wallet/utils.js
CHANGED
|
@@ -6,3 +6,11 @@ export function extendVirtualCoin(wallet, vtxo) {
|
|
|
6
6
|
tapTree: wallet.offchainTapscript.encode(),
|
|
7
7
|
};
|
|
8
8
|
}
|
|
9
|
+
export function extendCoin(wallet, utxo) {
|
|
10
|
+
return {
|
|
11
|
+
...utxo,
|
|
12
|
+
forfeitTapLeafScript: wallet.boardingTapscript.forfeit(),
|
|
13
|
+
intentTapLeafScript: wallet.boardingTapscript.exit(),
|
|
14
|
+
tapTree: wallet.boardingTapscript.encode(),
|
|
15
|
+
};
|
|
16
|
+
}
|