@arkade-os/sdk 0.1.4 → 0.2.1
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 +157 -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 +43 -17
- package/dist/cjs/providers/ark.js +261 -321
- 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 +61 -20
- package/dist/cjs/script/vhtlc.js +85 -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 +61 -34
- package/dist/cjs/wallet/serviceWorker/worker.js +120 -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 +31 -12
- package/dist/esm/providers/ark.js +259 -320
- 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 +61 -20
- package/dist/esm/script/vhtlc.js +85 -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 +62 -35
- package/dist/esm/wallet/serviceWorker/worker.js +120 -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 +24 -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 +37 -35
- 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
|
@@ -1,233 +1,216 @@
|
|
|
1
1
|
import { base64, hex } from "@scure/base";
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
2
|
+
import * as bip68 from "bip68";
|
|
3
|
+
import { Address, OutScript, tapLeafHash } from "@scure/btc-signer/payment";
|
|
4
|
+
import { SigHash, Transaction } from "@scure/btc-signer";
|
|
5
|
+
import { TaprootControlBlock, } from "@scure/btc-signer/psbt";
|
|
5
6
|
import { vtxosToTxs } from '../utils/transactionHistory.js';
|
|
6
|
-
import { BIP21 } from '../utils/bip21.js';
|
|
7
7
|
import { ArkAddress } from '../script/address.js';
|
|
8
8
|
import { DefaultVtxo } from '../script/default.js';
|
|
9
|
-
import { selectCoins, selectVirtualCoins } from '../utils/coinselect.js';
|
|
10
9
|
import { getNetwork } from '../networks.js';
|
|
11
10
|
import { ESPLORA_URL, EsploraProvider, } from '../providers/onchain.js';
|
|
12
11
|
import { SettlementEventType, RestArkProvider, } from '../providers/ark.js';
|
|
13
12
|
import { buildForfeitTx } from '../forfeit.js';
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import { CSVMultisigTapscript
|
|
19
|
-
import {
|
|
13
|
+
import { validateConnectorsTxGraph, validateVtxoTxGraph, } from '../tree/validation.js';
|
|
14
|
+
import { isRecoverable, isSpendable, isSubdust, TxType, } from './index.js';
|
|
15
|
+
import { sha256, sha256x2 } from "@scure/btc-signer/utils";
|
|
16
|
+
import { VtxoScript } from '../script/base.js';
|
|
17
|
+
import { CSVMultisigTapscript } from '../script/tapscript.js';
|
|
18
|
+
import { buildOffchainTx } from '../utils/arkTransaction.js';
|
|
20
19
|
import { ArkNote } from '../arknote/index.js';
|
|
21
|
-
|
|
20
|
+
import { BIP322 } from '../bip322/index.js';
|
|
21
|
+
import { RestIndexerProvider } from '../providers/indexer.js';
|
|
22
|
+
import { TxTree } from '../tree/txTree.js';
|
|
23
|
+
/**
|
|
24
|
+
* Main wallet implementation for Bitcoin transactions with Ark protocol support.
|
|
25
|
+
* The wallet does not store any data locally and relies on Ark and onchain
|
|
26
|
+
* providers to fetch UTXOs and VTXOs.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```typescript
|
|
30
|
+
* // Create a wallet
|
|
31
|
+
* const wallet = await Wallet.create({
|
|
32
|
+
* identity: SingleKey.fromHex('your_private_key'),
|
|
33
|
+
* arkServerUrl: 'https://ark.example.com',
|
|
34
|
+
* esploraUrl: 'https://mempool.space/api'
|
|
35
|
+
* });
|
|
36
|
+
*
|
|
37
|
+
* // Get addresses
|
|
38
|
+
* const arkAddress = await wallet.getAddress();
|
|
39
|
+
* const boardingAddress = await wallet.getBoardingAddress();
|
|
40
|
+
*
|
|
41
|
+
* // Send bitcoin
|
|
42
|
+
* const txid = await wallet.sendBitcoin({
|
|
43
|
+
* address: 'tb1...',
|
|
44
|
+
* amount: 50000
|
|
45
|
+
* });
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
22
48
|
export class Wallet {
|
|
23
|
-
constructor(identity, network,
|
|
49
|
+
constructor(identity, network, networkName, onchainProvider, arkProvider, indexerProvider, arkServerPublicKey, offchainTapscript, boardingTapscript, serverUnrollScript, forfeitOutputScript, dustAmount) {
|
|
24
50
|
this.identity = identity;
|
|
25
51
|
this.network = network;
|
|
52
|
+
this.networkName = networkName;
|
|
26
53
|
this.onchainProvider = onchainProvider;
|
|
27
|
-
this.onchainP2TR = onchainP2TR;
|
|
28
54
|
this.arkProvider = arkProvider;
|
|
55
|
+
this.indexerProvider = indexerProvider;
|
|
29
56
|
this.arkServerPublicKey = arkServerPublicKey;
|
|
30
57
|
this.offchainTapscript = offchainTapscript;
|
|
31
58
|
this.boardingTapscript = boardingTapscript;
|
|
59
|
+
this.serverUnrollScript = serverUnrollScript;
|
|
60
|
+
this.forfeitOutputScript = forfeitOutputScript;
|
|
61
|
+
this.dustAmount = dustAmount;
|
|
32
62
|
}
|
|
33
63
|
static async create(config) {
|
|
34
|
-
const network = getNetwork(config.network);
|
|
35
|
-
const onchainProvider = new EsploraProvider(config.esploraUrl || ESPLORA_URL[config.network]);
|
|
36
|
-
// Derive onchain address
|
|
37
64
|
const pubkey = config.identity.xOnlyPublicKey();
|
|
38
65
|
if (!pubkey) {
|
|
39
66
|
throw new Error("Invalid configured public key");
|
|
40
67
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
get onchainAddress() {
|
|
79
|
-
return this.onchainP2TR.address || "";
|
|
80
|
-
}
|
|
81
|
-
get boardingAddress() {
|
|
82
|
-
if (!this.boardingTapscript || !this.arkServerPublicKey) {
|
|
83
|
-
throw new Error("Boarding address not configured");
|
|
84
|
-
}
|
|
85
|
-
return this.boardingTapscript.address(this.network.hrp, this.arkServerPublicKey);
|
|
86
|
-
}
|
|
87
|
-
get boardingOnchainAddress() {
|
|
88
|
-
if (!this.boardingTapscript) {
|
|
89
|
-
throw new Error("Boarding address not configured");
|
|
90
|
-
}
|
|
91
|
-
return this.boardingTapscript.onchainAddress(this.network);
|
|
68
|
+
const arkProvider = new RestArkProvider(config.arkServerUrl);
|
|
69
|
+
const indexerProvider = new RestIndexerProvider(config.arkServerUrl);
|
|
70
|
+
const info = await arkProvider.getInfo();
|
|
71
|
+
const network = getNetwork(info.network);
|
|
72
|
+
const onchainProvider = new EsploraProvider(config.esploraUrl || ESPLORA_URL[info.network]);
|
|
73
|
+
const exitTimelock = {
|
|
74
|
+
value: info.unilateralExitDelay,
|
|
75
|
+
type: info.unilateralExitDelay < 512n ? "blocks" : "seconds",
|
|
76
|
+
};
|
|
77
|
+
const boardingTimelock = {
|
|
78
|
+
value: info.boardingExitDelay,
|
|
79
|
+
type: info.boardingExitDelay < 512n ? "blocks" : "seconds",
|
|
80
|
+
};
|
|
81
|
+
// Generate tapscripts for offchain and boarding address
|
|
82
|
+
const serverPubKey = hex.decode(info.signerPubkey).slice(1);
|
|
83
|
+
const bareVtxoTapscript = new DefaultVtxo.Script({
|
|
84
|
+
pubKey: pubkey,
|
|
85
|
+
serverPubKey,
|
|
86
|
+
csvTimelock: exitTimelock,
|
|
87
|
+
});
|
|
88
|
+
const boardingTapscript = new DefaultVtxo.Script({
|
|
89
|
+
pubKey: pubkey,
|
|
90
|
+
serverPubKey,
|
|
91
|
+
csvTimelock: boardingTimelock,
|
|
92
|
+
});
|
|
93
|
+
// Save tapscripts
|
|
94
|
+
const offchainTapscript = bareVtxoTapscript;
|
|
95
|
+
// the serverUnrollScript is the one used to create output scripts of the checkpoint transactions
|
|
96
|
+
const serverUnrollScript = CSVMultisigTapscript.encode({
|
|
97
|
+
timelock: exitTimelock,
|
|
98
|
+
pubkeys: [serverPubKey],
|
|
99
|
+
});
|
|
100
|
+
// parse the server forfeit address
|
|
101
|
+
// server is expecting funds to be sent to this address
|
|
102
|
+
const forfeitAddress = Address(network).decode(info.forfeitAddress);
|
|
103
|
+
const forfeitOutputScript = OutScript.encode(forfeitAddress);
|
|
104
|
+
return new Wallet(config.identity, network, info.network, onchainProvider, arkProvider, indexerProvider, serverPubKey, offchainTapscript, boardingTapscript, serverUnrollScript, forfeitOutputScript, info.dust);
|
|
92
105
|
}
|
|
93
|
-
get
|
|
94
|
-
if (!this.offchainTapscript || !this.arkServerPublicKey) {
|
|
95
|
-
throw new Error("Offchain address not configured");
|
|
96
|
-
}
|
|
106
|
+
get arkAddress() {
|
|
97
107
|
return this.offchainTapscript.address(this.network.hrp, this.arkServerPublicKey);
|
|
98
108
|
}
|
|
99
|
-
getAddress() {
|
|
100
|
-
|
|
101
|
-
onchain: this.onchainAddress,
|
|
102
|
-
bip21: BIP21.create({
|
|
103
|
-
address: this.onchainAddress,
|
|
104
|
-
}),
|
|
105
|
-
};
|
|
106
|
-
// Only include Ark-related fields if Ark provider is configured and address is available
|
|
107
|
-
if (this.arkProvider &&
|
|
108
|
-
this.offchainTapscript &&
|
|
109
|
-
this.boardingTapscript &&
|
|
110
|
-
this.arkServerPublicKey) {
|
|
111
|
-
const offchainAddress = this.offchainAddress.encode();
|
|
112
|
-
addressInfo.offchain = offchainAddress;
|
|
113
|
-
addressInfo.bip21 = BIP21.create({
|
|
114
|
-
address: this.onchainP2TR.address,
|
|
115
|
-
ark: offchainAddress,
|
|
116
|
-
});
|
|
117
|
-
addressInfo.boarding = this.boardingOnchainAddress;
|
|
118
|
-
}
|
|
119
|
-
return Promise.resolve(addressInfo);
|
|
109
|
+
async getAddress() {
|
|
110
|
+
return this.arkAddress.encode();
|
|
120
111
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
!this.offchainTapscript ||
|
|
124
|
-
!this.boardingTapscript ||
|
|
125
|
-
!this.arkServerPublicKey) {
|
|
126
|
-
throw new Error("Ark provider not configured");
|
|
127
|
-
}
|
|
128
|
-
const offchainAddress = this.offchainAddress.encode();
|
|
129
|
-
const boardingAddress = this.boardingOnchainAddress;
|
|
130
|
-
return Promise.resolve({
|
|
131
|
-
offchain: {
|
|
132
|
-
address: offchainAddress,
|
|
133
|
-
scripts: {
|
|
134
|
-
exit: [this.offchainTapscript.exitScript],
|
|
135
|
-
forfeit: [this.offchainTapscript.forfeitScript],
|
|
136
|
-
},
|
|
137
|
-
},
|
|
138
|
-
boarding: {
|
|
139
|
-
address: boardingAddress,
|
|
140
|
-
scripts: {
|
|
141
|
-
exit: [this.boardingTapscript.exitScript],
|
|
142
|
-
forfeit: [this.boardingTapscript.forfeitScript],
|
|
143
|
-
},
|
|
144
|
-
},
|
|
145
|
-
});
|
|
112
|
+
async getBoardingAddress() {
|
|
113
|
+
return this.boardingTapscript.onchainAddress(this.network);
|
|
146
114
|
}
|
|
147
115
|
async getBalance() {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
116
|
+
const [boardingUtxos, vtxos] = await Promise.all([
|
|
117
|
+
this.getBoardingUtxos(),
|
|
118
|
+
this.getVtxos(),
|
|
119
|
+
]);
|
|
120
|
+
// boarding
|
|
121
|
+
let confirmed = 0;
|
|
122
|
+
let unconfirmed = 0;
|
|
123
|
+
for (const utxo of boardingUtxos) {
|
|
124
|
+
if (utxo.status.confirmed) {
|
|
125
|
+
confirmed += utxo.value;
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
unconfirmed += utxo.value;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// offchain
|
|
132
|
+
let settled = 0;
|
|
133
|
+
let preconfirmed = 0;
|
|
134
|
+
let recoverable = 0;
|
|
135
|
+
settled = vtxos
|
|
136
|
+
.filter((coin) => coin.virtualStatus.state === "settled")
|
|
152
137
|
.reduce((sum, coin) => sum + coin.value, 0);
|
|
153
|
-
|
|
154
|
-
.filter((coin) =>
|
|
138
|
+
preconfirmed = vtxos
|
|
139
|
+
.filter((coin) => coin.virtualStatus.state === "preconfirmed")
|
|
155
140
|
.reduce((sum, coin) => sum + coin.value, 0);
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
if (this.arkProvider) {
|
|
162
|
-
const vtxos = await this.getVirtualCoins();
|
|
163
|
-
offchainSettled = vtxos
|
|
164
|
-
.filter((coin) => coin.virtualStatus.state === "settled")
|
|
165
|
-
.reduce((sum, coin) => sum + coin.value, 0);
|
|
166
|
-
offchainPending = vtxos
|
|
167
|
-
.filter((coin) => coin.virtualStatus.state === "pending")
|
|
168
|
-
.reduce((sum, coin) => sum + coin.value, 0);
|
|
169
|
-
offchainSwept = vtxos
|
|
170
|
-
.filter((coin) => coin.virtualStatus.state === "swept")
|
|
171
|
-
.reduce((sum, coin) => sum + coin.value, 0);
|
|
172
|
-
}
|
|
173
|
-
const offchainTotal = offchainSettled + offchainPending;
|
|
141
|
+
recoverable = vtxos
|
|
142
|
+
.filter((coin) => isSpendable(coin) && coin.virtualStatus.state === "swept")
|
|
143
|
+
.reduce((sum, coin) => sum + coin.value, 0);
|
|
144
|
+
const totalBoarding = confirmed + unconfirmed;
|
|
145
|
+
const totalOffchain = settled + preconfirmed + recoverable;
|
|
174
146
|
return {
|
|
175
|
-
|
|
176
|
-
confirmed
|
|
177
|
-
unconfirmed
|
|
178
|
-
total:
|
|
179
|
-
},
|
|
180
|
-
offchain: {
|
|
181
|
-
swept: offchainSwept,
|
|
182
|
-
settled: offchainSettled,
|
|
183
|
-
pending: offchainPending,
|
|
184
|
-
total: offchainTotal,
|
|
147
|
+
boarding: {
|
|
148
|
+
confirmed,
|
|
149
|
+
unconfirmed,
|
|
150
|
+
total: totalBoarding,
|
|
185
151
|
},
|
|
186
|
-
|
|
152
|
+
settled,
|
|
153
|
+
preconfirmed,
|
|
154
|
+
available: settled + preconfirmed,
|
|
155
|
+
recoverable,
|
|
156
|
+
total: totalBoarding + totalOffchain,
|
|
187
157
|
};
|
|
188
158
|
}
|
|
189
|
-
async
|
|
190
|
-
|
|
191
|
-
const address = await this.getAddress();
|
|
192
|
-
return this.onchainProvider.getCoins(address.onchain);
|
|
193
|
-
}
|
|
194
|
-
async getVtxos() {
|
|
195
|
-
if (!this.arkProvider || !this.offchainTapscript) {
|
|
196
|
-
return [];
|
|
197
|
-
}
|
|
198
|
-
const address = await this.getAddress();
|
|
199
|
-
if (!address.offchain) {
|
|
200
|
-
return [];
|
|
201
|
-
}
|
|
202
|
-
const { spendableVtxos } = await this.arkProvider.getVirtualCoins(address.offchain);
|
|
159
|
+
async getVtxos(filter) {
|
|
160
|
+
const spendableVtxos = await this.getVirtualCoins(filter);
|
|
203
161
|
const encodedOffchainTapscript = this.offchainTapscript.encode();
|
|
204
162
|
const forfeit = this.offchainTapscript.forfeit();
|
|
163
|
+
const exit = this.offchainTapscript.exit();
|
|
205
164
|
return spendableVtxos.map((vtxo) => ({
|
|
206
165
|
...vtxo,
|
|
207
|
-
|
|
208
|
-
|
|
166
|
+
forfeitTapLeafScript: forfeit,
|
|
167
|
+
intentTapLeafScript: exit,
|
|
168
|
+
tapTree: encodedOffchainTapscript,
|
|
209
169
|
}));
|
|
210
170
|
}
|
|
211
|
-
async getVirtualCoins() {
|
|
212
|
-
|
|
213
|
-
|
|
171
|
+
async getVirtualCoins(filter = { withRecoverable: true, withUnrolled: false }) {
|
|
172
|
+
const scripts = [hex.encode(this.offchainTapscript.pkScript)];
|
|
173
|
+
const response = await this.indexerProvider.getVtxos({
|
|
174
|
+
scripts,
|
|
175
|
+
spendableOnly: true,
|
|
176
|
+
});
|
|
177
|
+
const vtxos = response.vtxos;
|
|
178
|
+
if (filter.withRecoverable) {
|
|
179
|
+
const response = await this.indexerProvider.getVtxos({
|
|
180
|
+
scripts,
|
|
181
|
+
recoverableOnly: true,
|
|
182
|
+
});
|
|
183
|
+
vtxos.push(...response.vtxos);
|
|
214
184
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
185
|
+
if (filter.withUnrolled) {
|
|
186
|
+
const response = await this.indexerProvider.getVtxos({
|
|
187
|
+
scripts,
|
|
188
|
+
spentOnly: true,
|
|
189
|
+
});
|
|
190
|
+
vtxos.push(...response.vtxos.filter((vtxo) => vtxo.isUnrolled));
|
|
218
191
|
}
|
|
219
|
-
return
|
|
220
|
-
.getVirtualCoins(address.offchain)
|
|
221
|
-
.then(({ spendableVtxos }) => spendableVtxos);
|
|
192
|
+
return vtxos;
|
|
222
193
|
}
|
|
223
194
|
async getTransactionHistory() {
|
|
224
|
-
if (!this.
|
|
195
|
+
if (!this.indexerProvider) {
|
|
225
196
|
return [];
|
|
226
197
|
}
|
|
227
|
-
const
|
|
228
|
-
|
|
198
|
+
const response = await this.indexerProvider.getVtxos({
|
|
199
|
+
scripts: [hex.encode(this.offchainTapscript.pkScript)],
|
|
200
|
+
});
|
|
201
|
+
const { boardingTxs, commitmentsToIgnore } = await this.getBoardingTxs();
|
|
202
|
+
const spendableVtxos = [];
|
|
203
|
+
const spentVtxos = [];
|
|
204
|
+
for (const vtxo of response.vtxos) {
|
|
205
|
+
if (isSpendable(vtxo)) {
|
|
206
|
+
spendableVtxos.push(vtxo);
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
spentVtxos.push(vtxo);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
229
212
|
// convert VTXOs to offchain transactions
|
|
230
|
-
const offchainTxs = vtxosToTxs(spendableVtxos, spentVtxos,
|
|
213
|
+
const offchainTxs = vtxosToTxs(spendableVtxos, spentVtxos, commitmentsToIgnore);
|
|
231
214
|
const txs = [...boardingTxs, ...offchainTxs];
|
|
232
215
|
// sort transactions by creation time in descending order (newest first)
|
|
233
216
|
txs.sort(
|
|
@@ -242,13 +225,10 @@ export class Wallet {
|
|
|
242
225
|
return txs;
|
|
243
226
|
}
|
|
244
227
|
async getBoardingTxs() {
|
|
245
|
-
|
|
246
|
-
return { boardingTxs: [], roundsToIgnore: new Set() };
|
|
247
|
-
}
|
|
248
|
-
const boardingAddress = this.boardingOnchainAddress;
|
|
228
|
+
const boardingAddress = await this.getBoardingAddress();
|
|
249
229
|
const txs = await this.onchainProvider.getTransactions(boardingAddress);
|
|
250
230
|
const utxos = [];
|
|
251
|
-
const
|
|
231
|
+
const commitmentsToIgnore = new Set();
|
|
252
232
|
for (const tx of txs) {
|
|
253
233
|
for (let i = 0; i < tx.vout.length; i++) {
|
|
254
234
|
const vout = tx.vout[i];
|
|
@@ -256,7 +236,7 @@ export class Wallet {
|
|
|
256
236
|
const spentStatuses = await this.onchainProvider.getTxOutspends(tx.txid);
|
|
257
237
|
const spentStatus = spentStatuses[i];
|
|
258
238
|
if (spentStatus?.spent) {
|
|
259
|
-
|
|
239
|
+
commitmentsToIgnore.add(spentStatus.txid);
|
|
260
240
|
}
|
|
261
241
|
utxos.push({
|
|
262
242
|
txid: tx.txid,
|
|
@@ -266,10 +246,11 @@ export class Wallet {
|
|
|
266
246
|
confirmed: tx.status.confirmed,
|
|
267
247
|
block_time: tx.status.block_time,
|
|
268
248
|
},
|
|
249
|
+
isUnrolled: true,
|
|
269
250
|
virtualStatus: {
|
|
270
|
-
state: spentStatus?.spent ? "
|
|
271
|
-
|
|
272
|
-
? spentStatus.txid
|
|
251
|
+
state: spentStatus?.spent ? "spent" : "settled",
|
|
252
|
+
commitmentTxIds: spentStatus?.spent
|
|
253
|
+
? [spentStatus.txid]
|
|
273
254
|
: undefined,
|
|
274
255
|
},
|
|
275
256
|
createdAt: tx.status.confirmed
|
|
@@ -285,12 +266,12 @@ export class Wallet {
|
|
|
285
266
|
const tx = {
|
|
286
267
|
key: {
|
|
287
268
|
boardingTxid: utxo.txid,
|
|
288
|
-
|
|
289
|
-
|
|
269
|
+
commitmentTxid: "",
|
|
270
|
+
arkTxid: "",
|
|
290
271
|
},
|
|
291
272
|
amount: utxo.value,
|
|
292
273
|
type: TxType.TxReceived,
|
|
293
|
-
settled: utxo.virtualStatus.state === "
|
|
274
|
+
settled: utxo.virtualStatus.state === "spent",
|
|
294
275
|
createdAt: utxo.status.block_time
|
|
295
276
|
? new Date(utxo.status.block_time * 1000).getTime()
|
|
296
277
|
: 0,
|
|
@@ -304,129 +285,80 @@ export class Wallet {
|
|
|
304
285
|
}
|
|
305
286
|
return {
|
|
306
287
|
boardingTxs: [...unconfirmedTxs, ...confirmedTxs],
|
|
307
|
-
|
|
288
|
+
commitmentsToIgnore,
|
|
308
289
|
};
|
|
309
290
|
}
|
|
310
291
|
async getBoardingUtxos() {
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
}
|
|
314
|
-
const boardingUtxos = await this.onchainProvider.getCoins(this.boardingOnchainAddress);
|
|
292
|
+
const boardingAddress = await this.getBoardingAddress();
|
|
293
|
+
const boardingUtxos = await this.onchainProvider.getCoins(boardingAddress);
|
|
315
294
|
const encodedBoardingTapscript = this.boardingTapscript.encode();
|
|
316
295
|
const forfeit = this.boardingTapscript.forfeit();
|
|
296
|
+
const exit = this.boardingTapscript.exit();
|
|
317
297
|
return boardingUtxos.map((utxo) => ({
|
|
318
298
|
...utxo,
|
|
319
|
-
|
|
320
|
-
|
|
299
|
+
forfeitTapLeafScript: forfeit,
|
|
300
|
+
intentTapLeafScript: exit,
|
|
301
|
+
tapTree: encodedBoardingTapscript,
|
|
321
302
|
}));
|
|
322
303
|
}
|
|
323
|
-
async sendBitcoin(params
|
|
304
|
+
async sendBitcoin(params) {
|
|
324
305
|
if (params.amount <= 0) {
|
|
325
306
|
throw new Error("Amount must be positive");
|
|
326
307
|
}
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
return this.sendOffchain(params, zeroFee);
|
|
330
|
-
}
|
|
331
|
-
// Otherwise, send via onchain
|
|
332
|
-
return this.sendOnchain(params);
|
|
333
|
-
}
|
|
334
|
-
isOffchainSuitable(address) {
|
|
335
|
-
try {
|
|
336
|
-
ArkAddress.decode(address);
|
|
337
|
-
return true;
|
|
338
|
-
}
|
|
339
|
-
catch (e) {
|
|
340
|
-
return false;
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
async sendOnchain(params) {
|
|
344
|
-
const coins = await this.getCoins();
|
|
345
|
-
const feeRate = params.feeRate || Wallet.FEE_RATE;
|
|
346
|
-
// Ensure fee is an integer by rounding up
|
|
347
|
-
const estimatedFee = Math.ceil(174 * feeRate);
|
|
348
|
-
const totalNeeded = params.amount + estimatedFee;
|
|
349
|
-
// Select coins
|
|
350
|
-
const selected = selectCoins(coins, totalNeeded);
|
|
351
|
-
if (!selected.inputs) {
|
|
352
|
-
throw new Error("Insufficient funds");
|
|
353
|
-
}
|
|
354
|
-
// Create transaction
|
|
355
|
-
let tx = new Transaction();
|
|
356
|
-
// Add inputs
|
|
357
|
-
for (const input of selected.inputs) {
|
|
358
|
-
tx.addInput({
|
|
359
|
-
txid: input.txid,
|
|
360
|
-
index: input.vout,
|
|
361
|
-
witnessUtxo: {
|
|
362
|
-
script: this.onchainP2TR.script,
|
|
363
|
-
amount: BigInt(input.value),
|
|
364
|
-
},
|
|
365
|
-
tapInternalKey: this.onchainP2TR.tapInternalKey,
|
|
366
|
-
});
|
|
367
|
-
}
|
|
368
|
-
// Add payment output
|
|
369
|
-
tx.addOutputAddress(params.address, BigInt(params.amount), this.network);
|
|
370
|
-
// Add change output if needed
|
|
371
|
-
if (selected.changeAmount > 0) {
|
|
372
|
-
tx.addOutputAddress(this.onchainAddress, BigInt(selected.changeAmount), this.network);
|
|
373
|
-
}
|
|
374
|
-
// Sign inputs and Finalize
|
|
375
|
-
tx = await this.identity.sign(tx);
|
|
376
|
-
tx.finalize();
|
|
377
|
-
// Broadcast
|
|
378
|
-
const txid = await this.onchainProvider.broadcastTransaction(tx.hex);
|
|
379
|
-
return txid;
|
|
380
|
-
}
|
|
381
|
-
async sendOffchain(params, zeroFee = true) {
|
|
382
|
-
if (!this.arkProvider ||
|
|
383
|
-
!this.offchainAddress ||
|
|
384
|
-
!this.offchainTapscript) {
|
|
385
|
-
throw new Error("wallet not initialized");
|
|
386
|
-
}
|
|
387
|
-
const virtualCoins = await this.getVirtualCoins();
|
|
388
|
-
const estimatedFee = zeroFee
|
|
389
|
-
? 0
|
|
390
|
-
: Math.ceil(174 * (params.feeRate || Wallet.FEE_RATE));
|
|
391
|
-
const totalNeeded = params.amount + estimatedFee;
|
|
392
|
-
const selected = selectVirtualCoins(virtualCoins, totalNeeded);
|
|
393
|
-
if (!selected || !selected.inputs) {
|
|
394
|
-
throw new Error("Insufficient funds");
|
|
308
|
+
if (!isValidArkAddress(params.address)) {
|
|
309
|
+
throw new Error("Invalid Ark address " + params.address);
|
|
395
310
|
}
|
|
311
|
+
// recoverable and subdust coins can't be spent in offchain tx
|
|
312
|
+
const virtualCoins = await this.getVirtualCoins({
|
|
313
|
+
withRecoverable: false,
|
|
314
|
+
});
|
|
315
|
+
const selected = selectVirtualCoins(virtualCoins, params.amount);
|
|
396
316
|
const selectedLeaf = this.offchainTapscript.forfeit();
|
|
397
317
|
if (!selectedLeaf) {
|
|
398
318
|
throw new Error("Selected leaf not found");
|
|
399
319
|
}
|
|
320
|
+
const outputAddress = ArkAddress.decode(params.address);
|
|
321
|
+
const outputScript = BigInt(params.amount) < this.dustAmount
|
|
322
|
+
? outputAddress.subdustPkScript
|
|
323
|
+
: outputAddress.pkScript;
|
|
400
324
|
const outputs = [
|
|
401
325
|
{
|
|
402
|
-
|
|
326
|
+
script: outputScript,
|
|
403
327
|
amount: BigInt(params.amount),
|
|
404
328
|
},
|
|
405
329
|
];
|
|
406
330
|
// add change output if needed
|
|
407
|
-
if (selected.changeAmount >
|
|
331
|
+
if (selected.changeAmount > 0n) {
|
|
332
|
+
const changeOutputScript = selected.changeAmount < this.dustAmount
|
|
333
|
+
? this.arkAddress.subdustPkScript
|
|
334
|
+
: this.arkAddress.pkScript;
|
|
408
335
|
outputs.push({
|
|
409
|
-
|
|
336
|
+
script: changeOutputScript,
|
|
410
337
|
amount: BigInt(selected.changeAmount),
|
|
411
338
|
});
|
|
412
339
|
}
|
|
413
|
-
const
|
|
414
|
-
let
|
|
340
|
+
const tapTree = this.offchainTapscript.encode();
|
|
341
|
+
let offchainTx = buildOffchainTx(selected.inputs.map((input) => ({
|
|
415
342
|
...input,
|
|
416
343
|
tapLeafScript: selectedLeaf,
|
|
417
|
-
|
|
418
|
-
})), outputs);
|
|
419
|
-
|
|
420
|
-
const
|
|
421
|
-
|
|
344
|
+
tapTree,
|
|
345
|
+
})), outputs, this.serverUnrollScript);
|
|
346
|
+
const signedVirtualTx = await this.identity.sign(offchainTx.arkTx);
|
|
347
|
+
const { arkTxid, signedCheckpointTxs } = await this.arkProvider.submitTx(base64.encode(signedVirtualTx.toPSBT()), offchainTx.checkpoints.map((c) => base64.encode(c.toPSBT())));
|
|
348
|
+
// TODO persist final virtual tx and checkpoints to repository
|
|
349
|
+
// sign the checkpoints
|
|
350
|
+
const finalCheckpoints = await Promise.all(signedCheckpointTxs.map(async (c) => {
|
|
351
|
+
const tx = Transaction.fromPSBT(base64.decode(c));
|
|
352
|
+
const signedCheckpoint = await this.identity.sign(tx);
|
|
353
|
+
return base64.encode(signedCheckpoint.toPSBT());
|
|
354
|
+
}));
|
|
355
|
+
await this.arkProvider.finalizeTx(arkTxid, finalCheckpoints);
|
|
356
|
+
return arkTxid;
|
|
422
357
|
}
|
|
423
358
|
async settle(params, eventCallback) {
|
|
424
|
-
if (!this.arkProvider) {
|
|
425
|
-
throw new Error("Ark provider not configured");
|
|
426
|
-
}
|
|
427
|
-
// validate arknotes inputs
|
|
428
359
|
if (params?.inputs) {
|
|
429
360
|
for (const input of params.inputs) {
|
|
361
|
+
// validate arknotes inputs
|
|
430
362
|
if (typeof input === "string") {
|
|
431
363
|
try {
|
|
432
364
|
ArkNote.fromString(input);
|
|
@@ -440,9 +372,6 @@ export class Wallet {
|
|
|
440
372
|
// if no params are provided, use all boarding and offchain utxos as inputs
|
|
441
373
|
// and send all to the offchain address
|
|
442
374
|
if (!params) {
|
|
443
|
-
if (!this.offchainAddress) {
|
|
444
|
-
throw new Error("Offchain address not configured");
|
|
445
|
-
}
|
|
446
375
|
let amount = 0;
|
|
447
376
|
const boardingUtxos = await this.getBoardingUtxos();
|
|
448
377
|
amount += boardingUtxos.reduce((sum, input) => sum + input.value, 0);
|
|
@@ -456,23 +385,34 @@ export class Wallet {
|
|
|
456
385
|
inputs,
|
|
457
386
|
outputs: [
|
|
458
387
|
{
|
|
459
|
-
address: this.
|
|
388
|
+
address: await this.getAddress(),
|
|
460
389
|
amount: BigInt(amount),
|
|
461
390
|
},
|
|
462
391
|
],
|
|
463
392
|
};
|
|
464
393
|
}
|
|
465
|
-
|
|
466
|
-
const
|
|
467
|
-
|
|
468
|
-
|
|
394
|
+
const onchainOutputIndexes = [];
|
|
395
|
+
const outputs = [];
|
|
396
|
+
let hasOffchainOutputs = false;
|
|
397
|
+
for (const [index, output] of params.outputs.entries()) {
|
|
398
|
+
let script;
|
|
399
|
+
try {
|
|
400
|
+
// offchain
|
|
401
|
+
const addr = ArkAddress.decode(output.address);
|
|
402
|
+
script = addr.pkScript;
|
|
403
|
+
hasOffchainOutputs = true;
|
|
469
404
|
}
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
405
|
+
catch {
|
|
406
|
+
// onchain
|
|
407
|
+
const addr = Address(this.network).decode(output.address);
|
|
408
|
+
script = OutScript.encode(addr);
|
|
409
|
+
onchainOutputIndexes.push(index);
|
|
410
|
+
}
|
|
411
|
+
outputs.push({
|
|
412
|
+
amount: output.amount,
|
|
413
|
+
script,
|
|
414
|
+
});
|
|
415
|
+
}
|
|
476
416
|
// session holds the state of the musig2 signing process of the vtxo tree
|
|
477
417
|
let session;
|
|
478
418
|
const signingPublicKeys = [];
|
|
@@ -480,189 +420,287 @@ export class Wallet {
|
|
|
480
420
|
session = this.identity.signerSession();
|
|
481
421
|
signingPublicKeys.push(hex.encode(session.getPublicKey()));
|
|
482
422
|
}
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
}, 1000);
|
|
489
|
-
let pingRunning = true;
|
|
490
|
-
const stopPing = () => {
|
|
491
|
-
if (pingRunning) {
|
|
492
|
-
pingRunning = false;
|
|
493
|
-
clearInterval(interval);
|
|
494
|
-
}
|
|
495
|
-
};
|
|
423
|
+
const [intent, deleteIntent] = await Promise.all([
|
|
424
|
+
this.makeRegisterIntentSignature(params.inputs, outputs, onchainOutputIndexes, signingPublicKeys),
|
|
425
|
+
this.makeDeleteIntentSignature(params.inputs),
|
|
426
|
+
]);
|
|
427
|
+
const intentId = await this.arkProvider.registerIntent(intent);
|
|
496
428
|
const abortController = new AbortController();
|
|
497
429
|
// listen to settlement events
|
|
498
430
|
try {
|
|
499
|
-
const settlementStream = this.arkProvider.getEventStream(abortController.signal);
|
|
500
431
|
let step;
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
}).script;
|
|
514
|
-
const sweepTapTreeRoot = tapLeafHash(sweepTapscript);
|
|
432
|
+
const topics = [
|
|
433
|
+
...signingPublicKeys,
|
|
434
|
+
...params.inputs.map((input) => `${input.txid}:${input.vout}`),
|
|
435
|
+
];
|
|
436
|
+
const settlementStream = this.arkProvider.getEventStream(abortController.signal, topics);
|
|
437
|
+
// roundId, sweepTapTreeRoot and forfeitOutputScript are set once the BatchStarted event is received
|
|
438
|
+
let roundId;
|
|
439
|
+
let sweepTapTreeRoot;
|
|
440
|
+
const vtxoChunks = [];
|
|
441
|
+
const connectorsChunks = [];
|
|
442
|
+
let vtxoGraph;
|
|
443
|
+
let connectorsGraph;
|
|
515
444
|
for await (const event of settlementStream) {
|
|
516
445
|
if (eventCallback) {
|
|
517
446
|
eventCallback(event);
|
|
518
447
|
}
|
|
519
448
|
switch (event.type) {
|
|
520
449
|
// the settlement failed
|
|
521
|
-
case SettlementEventType.
|
|
522
|
-
if
|
|
450
|
+
case SettlementEventType.BatchFailed:
|
|
451
|
+
// fail if the roundId is the one joined
|
|
452
|
+
if (event.id === roundId) {
|
|
453
|
+
throw new Error(event.reason);
|
|
454
|
+
}
|
|
455
|
+
break;
|
|
456
|
+
case SettlementEventType.BatchStarted:
|
|
457
|
+
if (step !== undefined) {
|
|
523
458
|
continue;
|
|
524
459
|
}
|
|
525
|
-
|
|
526
|
-
|
|
460
|
+
const res = await this.handleBatchStartedEvent(event, intentId, this.arkServerPublicKey, this.forfeitOutputScript);
|
|
461
|
+
if (!res.skip) {
|
|
462
|
+
step = event.type;
|
|
463
|
+
sweepTapTreeRoot = res.sweepTapTreeRoot;
|
|
464
|
+
roundId = res.roundId;
|
|
465
|
+
if (!hasOffchainOutputs) {
|
|
466
|
+
// if there are no offchain outputs, we don't have to handle musig2 tree signatures
|
|
467
|
+
// we can directly advance to the finalization step
|
|
468
|
+
step = SettlementEventType.TreeNoncesAggregated;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
break;
|
|
472
|
+
case SettlementEventType.TreeTx:
|
|
473
|
+
if (step !== SettlementEventType.BatchStarted &&
|
|
474
|
+
step !== SettlementEventType.TreeNoncesAggregated) {
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
// index 0 = vtxo tree
|
|
478
|
+
if (event.batchIndex === 0) {
|
|
479
|
+
vtxoChunks.push(event.chunk);
|
|
480
|
+
// index 1 = connectors tree
|
|
481
|
+
}
|
|
482
|
+
else if (event.batchIndex === 1) {
|
|
483
|
+
connectorsChunks.push(event.chunk);
|
|
484
|
+
}
|
|
485
|
+
else {
|
|
486
|
+
throw new Error(`Invalid batch index: ${event.batchIndex}`);
|
|
487
|
+
}
|
|
488
|
+
break;
|
|
489
|
+
case SettlementEventType.TreeSignature:
|
|
490
|
+
if (step !== SettlementEventType.TreeNoncesAggregated) {
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
if (!hasOffchainOutputs) {
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
if (!vtxoGraph) {
|
|
497
|
+
throw new Error("Vtxo graph not set, something went wrong");
|
|
498
|
+
}
|
|
499
|
+
// index 0 = vtxo graph
|
|
500
|
+
if (event.batchIndex === 0) {
|
|
501
|
+
const tapKeySig = hex.decode(event.signature);
|
|
502
|
+
vtxoGraph.update(event.txid, (tx) => {
|
|
503
|
+
tx.updateInput(0, {
|
|
504
|
+
tapKeySig,
|
|
505
|
+
});
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
break;
|
|
527
509
|
// the server has started the signing process of the vtxo tree transactions
|
|
528
510
|
// the server expects the partial musig2 nonces for each tx
|
|
529
|
-
case SettlementEventType.
|
|
530
|
-
if (step !==
|
|
511
|
+
case SettlementEventType.TreeSigningStarted:
|
|
512
|
+
if (step !== SettlementEventType.BatchStarted) {
|
|
531
513
|
continue;
|
|
532
514
|
}
|
|
533
|
-
stopPing();
|
|
534
515
|
if (hasOffchainOutputs) {
|
|
535
516
|
if (!session) {
|
|
536
|
-
throw new Error("Signing session not
|
|
517
|
+
throw new Error("Signing session not set");
|
|
518
|
+
}
|
|
519
|
+
if (!sweepTapTreeRoot) {
|
|
520
|
+
throw new Error("Sweep tap tree root not set");
|
|
537
521
|
}
|
|
538
|
-
|
|
522
|
+
if (vtxoChunks.length === 0) {
|
|
523
|
+
throw new Error("unsigned vtxo graph not received");
|
|
524
|
+
}
|
|
525
|
+
vtxoGraph = TxTree.create(vtxoChunks);
|
|
526
|
+
await this.handleSettlementSigningEvent(event, sweepTapTreeRoot, session, vtxoGraph);
|
|
539
527
|
}
|
|
528
|
+
step = event.type;
|
|
540
529
|
break;
|
|
541
530
|
// the musig2 nonces of the vtxo tree transactions are generated
|
|
542
531
|
// the server expects now the partial musig2 signatures
|
|
543
|
-
case SettlementEventType.
|
|
544
|
-
if (step !== SettlementEventType.
|
|
532
|
+
case SettlementEventType.TreeNoncesAggregated:
|
|
533
|
+
if (step !== SettlementEventType.TreeSigningStarted) {
|
|
545
534
|
continue;
|
|
546
535
|
}
|
|
547
|
-
stopPing();
|
|
548
536
|
if (hasOffchainOutputs) {
|
|
549
537
|
if (!session) {
|
|
550
|
-
throw new Error("Signing session not
|
|
538
|
+
throw new Error("Signing session not set");
|
|
551
539
|
}
|
|
552
540
|
await this.handleSettlementSigningNoncesGeneratedEvent(event, session);
|
|
553
541
|
}
|
|
542
|
+
step = event.type;
|
|
554
543
|
break;
|
|
555
544
|
// the vtxo tree is signed, craft, sign and submit forfeit transactions
|
|
556
545
|
// if any boarding utxos are involved, the settlement tx is also signed
|
|
557
|
-
case SettlementEventType.
|
|
558
|
-
if (step !== SettlementEventType.
|
|
546
|
+
case SettlementEventType.BatchFinalization:
|
|
547
|
+
if (step !== SettlementEventType.TreeNoncesAggregated) {
|
|
559
548
|
continue;
|
|
560
549
|
}
|
|
561
|
-
|
|
562
|
-
|
|
550
|
+
if (!this.forfeitOutputScript) {
|
|
551
|
+
throw new Error("Forfeit output script not set");
|
|
552
|
+
}
|
|
553
|
+
if (connectorsChunks.length > 0) {
|
|
554
|
+
connectorsGraph = TxTree.create(connectorsChunks);
|
|
555
|
+
validateConnectorsTxGraph(event.commitmentTx, connectorsGraph);
|
|
556
|
+
}
|
|
557
|
+
await this.handleSettlementFinalizationEvent(event, params.inputs, this.forfeitOutputScript, connectorsGraph);
|
|
558
|
+
step = event.type;
|
|
563
559
|
break;
|
|
564
560
|
// the settlement is done, last event to be received
|
|
565
|
-
case SettlementEventType.
|
|
566
|
-
if (step !== SettlementEventType.
|
|
561
|
+
case SettlementEventType.BatchFinalized:
|
|
562
|
+
if (step !== SettlementEventType.BatchFinalization) {
|
|
567
563
|
continue;
|
|
568
564
|
}
|
|
569
565
|
abortController.abort();
|
|
570
|
-
return event.
|
|
566
|
+
return event.commitmentTxid;
|
|
571
567
|
}
|
|
572
|
-
step = event.type;
|
|
573
568
|
}
|
|
574
569
|
}
|
|
575
570
|
catch (error) {
|
|
571
|
+
// close the stream
|
|
576
572
|
abortController.abort();
|
|
573
|
+
try {
|
|
574
|
+
// delete the intent to not be stuck in the queue
|
|
575
|
+
await this.arkProvider.deleteIntent(deleteIntent);
|
|
576
|
+
}
|
|
577
|
+
catch { }
|
|
577
578
|
throw error;
|
|
578
579
|
}
|
|
579
580
|
throw new Error("Settlement failed");
|
|
580
581
|
}
|
|
581
|
-
async
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
582
|
+
async notifyIncomingFunds(eventCallback) {
|
|
583
|
+
const arkAddress = await this.getAddress();
|
|
584
|
+
const boardingAddress = await this.getBoardingAddress();
|
|
585
|
+
let onchainStopFunc;
|
|
586
|
+
let indexerStopFunc;
|
|
587
|
+
if (this.onchainProvider && boardingAddress) {
|
|
588
|
+
onchainStopFunc = await this.onchainProvider.watchAddresses([boardingAddress], (txs) => {
|
|
589
|
+
const coins = txs
|
|
590
|
+
.map((tx) => {
|
|
591
|
+
const vout = tx.vout.findIndex((v) => v.scriptpubkey_address === boardingAddress);
|
|
592
|
+
if (vout === -1) {
|
|
593
|
+
console.warn(`No vout found for address ${boardingAddress} in transaction ${tx.txid}`);
|
|
594
|
+
return null;
|
|
595
|
+
}
|
|
596
|
+
return {
|
|
597
|
+
txid: tx.txid,
|
|
598
|
+
vout,
|
|
599
|
+
value: Number(tx.vout[vout].value),
|
|
600
|
+
status: tx.status,
|
|
601
|
+
};
|
|
602
|
+
})
|
|
603
|
+
.filter((coin) => coin !== null);
|
|
604
|
+
eventCallback({
|
|
605
|
+
type: "utxo",
|
|
606
|
+
coins,
|
|
607
|
+
});
|
|
608
|
+
});
|
|
591
609
|
}
|
|
592
|
-
if (
|
|
593
|
-
|
|
610
|
+
if (this.indexerProvider && arkAddress) {
|
|
611
|
+
const offchainScript = this.offchainTapscript;
|
|
612
|
+
const subscriptionId = await this.indexerProvider.subscribeForScripts([
|
|
613
|
+
hex.encode(offchainScript.pkScript),
|
|
614
|
+
]);
|
|
615
|
+
const abortController = new AbortController();
|
|
616
|
+
const subscription = this.indexerProvider.getSubscription(subscriptionId, abortController.signal);
|
|
617
|
+
indexerStopFunc = async () => {
|
|
618
|
+
abortController.abort();
|
|
619
|
+
await this.indexerProvider?.unsubscribeForScripts(subscriptionId);
|
|
620
|
+
};
|
|
621
|
+
// Handle subscription updates asynchronously without blocking
|
|
622
|
+
(async () => {
|
|
623
|
+
try {
|
|
624
|
+
for await (const update of subscription) {
|
|
625
|
+
if (update.newVtxos?.length > 0) {
|
|
626
|
+
eventCallback({
|
|
627
|
+
type: "vtxo",
|
|
628
|
+
vtxos: update.newVtxos,
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
catch (error) {
|
|
634
|
+
console.error("Subscription error:", error);
|
|
635
|
+
}
|
|
636
|
+
})();
|
|
594
637
|
}
|
|
595
|
-
const
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
638
|
+
const stopFunc = () => {
|
|
639
|
+
onchainStopFunc?.();
|
|
640
|
+
indexerStopFunc?.();
|
|
641
|
+
};
|
|
642
|
+
return stopFunc;
|
|
643
|
+
}
|
|
644
|
+
async handleBatchStartedEvent(event, intentId, serverPubKey, forfeitOutputScript) {
|
|
645
|
+
const utf8IntentId = new TextEncoder().encode(intentId);
|
|
646
|
+
const intentIdHash = sha256(utf8IntentId);
|
|
647
|
+
const intentIdHashStr = hex.encode(new Uint8Array(intentIdHash));
|
|
648
|
+
let skip = true;
|
|
649
|
+
// check if our intent ID hash matches any in the event
|
|
650
|
+
for (const idHash of event.intentIdHashes) {
|
|
651
|
+
if (idHash === intentIdHashStr) {
|
|
652
|
+
if (!this.arkProvider) {
|
|
653
|
+
throw new Error("Ark provider not configured");
|
|
654
|
+
}
|
|
655
|
+
await this.arkProvider.confirmRegistration(intentId);
|
|
656
|
+
skip = false;
|
|
608
657
|
}
|
|
609
|
-
const exitBranch = await tree.exitBranch(vtxo.txid, async (txid) => {
|
|
610
|
-
const status = await this.onchainProvider.getTxStatus(txid);
|
|
611
|
-
return status.confirmed;
|
|
612
|
-
});
|
|
613
|
-
transactions.push(...exitBranch);
|
|
614
658
|
}
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
if (broadcastedTxs.has(tx))
|
|
618
|
-
continue;
|
|
619
|
-
const txid = await this.onchainProvider.broadcastTransaction(tx);
|
|
620
|
-
broadcastedTxs.set(txid, true);
|
|
659
|
+
if (skip) {
|
|
660
|
+
return { skip };
|
|
621
661
|
}
|
|
662
|
+
const sweepTapscript = CSVMultisigTapscript.encode({
|
|
663
|
+
timelock: {
|
|
664
|
+
value: event.batchExpiry,
|
|
665
|
+
type: event.batchExpiry >= 512n ? "seconds" : "blocks",
|
|
666
|
+
},
|
|
667
|
+
pubkeys: [serverPubKey],
|
|
668
|
+
}).script;
|
|
669
|
+
const sweepTapTreeRoot = tapLeafHash(sweepTapscript);
|
|
670
|
+
return {
|
|
671
|
+
roundId: event.id,
|
|
672
|
+
sweepTapTreeRoot,
|
|
673
|
+
forfeitOutputScript,
|
|
674
|
+
skip: false,
|
|
675
|
+
};
|
|
622
676
|
}
|
|
623
677
|
// validates the vtxo tree, creates a signing session and generates the musig2 nonces
|
|
624
|
-
async handleSettlementSigningEvent(event, sweepTapTreeRoot, session) {
|
|
625
|
-
const vtxoTree = event.unsignedVtxoTree;
|
|
626
|
-
if (!this.arkProvider) {
|
|
627
|
-
throw new Error("Ark provider not configured");
|
|
628
|
-
}
|
|
678
|
+
async handleSettlementSigningEvent(event, sweepTapTreeRoot, session, vtxoGraph) {
|
|
629
679
|
// validate the unsigned vtxo tree
|
|
630
|
-
|
|
680
|
+
const commitmentTx = Transaction.fromPSBT(base64.decode(event.unsignedCommitmentTx));
|
|
681
|
+
validateVtxoTxGraph(vtxoGraph, commitmentTx, sweepTapTreeRoot);
|
|
631
682
|
// TODO check if our registered outputs are in the vtxo tree
|
|
632
|
-
const
|
|
633
|
-
const settlementTx = Transaction.fromPSBT(settlementPsbt);
|
|
634
|
-
const sharedOutput = settlementTx.getOutput(0);
|
|
683
|
+
const sharedOutput = commitmentTx.getOutput(0);
|
|
635
684
|
if (!sharedOutput?.amount) {
|
|
636
685
|
throw new Error("Shared output not found");
|
|
637
686
|
}
|
|
638
|
-
session.init(
|
|
687
|
+
session.init(vtxoGraph, sweepTapTreeRoot, sharedOutput.amount);
|
|
639
688
|
await this.arkProvider.submitTreeNonces(event.id, hex.encode(session.getPublicKey()), session.getNonces());
|
|
640
689
|
}
|
|
641
690
|
async handleSettlementSigningNoncesGeneratedEvent(event, session) {
|
|
642
|
-
if (!this.arkProvider) {
|
|
643
|
-
throw new Error("Ark provider not configured");
|
|
644
|
-
}
|
|
645
691
|
session.setAggregatedNonces(event.treeNonces);
|
|
646
692
|
const signatures = session.sign();
|
|
647
693
|
await this.arkProvider.submitTreeSignatures(event.id, hex.encode(session.getPublicKey()), signatures);
|
|
648
694
|
}
|
|
649
|
-
async handleSettlementFinalizationEvent(event, inputs,
|
|
650
|
-
if (!this.arkProvider) {
|
|
651
|
-
throw new Error("Ark provider not configured");
|
|
652
|
-
}
|
|
653
|
-
// parse the server forfeit address
|
|
654
|
-
// server is expecting funds to be sent to this address
|
|
655
|
-
const forfeitAddress = Address(this.network).decode(infos.forfeitAddress);
|
|
656
|
-
const serverPkScript = OutScript.encode(forfeitAddress);
|
|
695
|
+
async handleSettlementFinalizationEvent(event, inputs, forfeitOutputScript, connectorsGraph) {
|
|
657
696
|
// the signed forfeits transactions to submit
|
|
658
697
|
const signedForfeits = [];
|
|
659
698
|
const vtxos = await this.getVirtualCoins();
|
|
660
|
-
let settlementPsbt = Transaction.fromPSBT(base64.decode(event.
|
|
699
|
+
let settlementPsbt = Transaction.fromPSBT(base64.decode(event.commitmentTx));
|
|
661
700
|
let hasBoardingUtxos = false;
|
|
662
|
-
let
|
|
701
|
+
let connectorIndex = 0;
|
|
702
|
+
const connectorsLeaves = connectorsGraph?.leaves() || [];
|
|
663
703
|
for (const input of inputs) {
|
|
664
|
-
if (typeof input === "string")
|
|
665
|
-
continue; // skip notes
|
|
666
704
|
// check if the input is an offchain "virtual" coin
|
|
667
705
|
const vtxo = vtxos.find((vtxo) => vtxo.txid === input.txid && vtxo.vout === input.vout);
|
|
668
706
|
// boarding utxo, we need to sign the settlement tx
|
|
@@ -682,71 +720,246 @@ export class Wallet {
|
|
|
682
720
|
continue;
|
|
683
721
|
// input found in the settlement tx, sign it
|
|
684
722
|
settlementPsbt.updateInput(i, {
|
|
685
|
-
tapLeafScript: [input.
|
|
723
|
+
tapLeafScript: [input.forfeitTapLeafScript],
|
|
686
724
|
});
|
|
687
725
|
inputIndexes.push(i);
|
|
688
726
|
}
|
|
689
727
|
settlementPsbt = await this.identity.sign(settlementPsbt, inputIndexes);
|
|
690
728
|
continue;
|
|
691
729
|
}
|
|
692
|
-
if (
|
|
693
|
-
//
|
|
694
|
-
|
|
695
|
-
connectorsTreeValid = true;
|
|
730
|
+
if (isRecoverable(vtxo) || isSubdust(vtxo, this.dustAmount)) {
|
|
731
|
+
// recoverable or subdust coin, we don't need to create a forfeit tx
|
|
732
|
+
continue;
|
|
696
733
|
}
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
const fees = TxWeightEstimator.create()
|
|
700
|
-
.addKeySpendInput() // connector
|
|
701
|
-
.addTapscriptInput(tapscript.witnessSize(100), // TODO: handle conditional script
|
|
702
|
-
input.tapLeafScript[1].length - 1, forfeitControlBlock.length)
|
|
703
|
-
.addP2WKHOutput()
|
|
704
|
-
.vsize()
|
|
705
|
-
.fee(event.minRelayFeeRate);
|
|
706
|
-
const connectorsLeaves = event.connectors.leaves();
|
|
707
|
-
const connectorOutpoint = event.connectorsIndex.get(`${vtxo.txid}:${vtxo.vout}`);
|
|
708
|
-
if (!connectorOutpoint) {
|
|
709
|
-
throw new Error("Connector outpoint not found");
|
|
734
|
+
if (connectorsLeaves.length === 0) {
|
|
735
|
+
throw new Error("connectors not received");
|
|
710
736
|
}
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
if (leaf.txid === connectorOutpoint.txid) {
|
|
714
|
-
try {
|
|
715
|
-
const connectorTx = Transaction.fromPSBT(base64.decode(leaf.tx));
|
|
716
|
-
connectorOutput = connectorTx.getOutput(connectorOutpoint.vout);
|
|
717
|
-
break;
|
|
718
|
-
}
|
|
719
|
-
catch {
|
|
720
|
-
throw new Error("Invalid connector tx");
|
|
721
|
-
}
|
|
722
|
-
}
|
|
737
|
+
if (connectorIndex >= connectorsLeaves.length) {
|
|
738
|
+
throw new Error("not enough connectors received");
|
|
723
739
|
}
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
740
|
+
const connectorLeaf = connectorsLeaves[connectorIndex];
|
|
741
|
+
const connectorTxId = hex.encode(sha256x2(connectorLeaf.toBytes(true)).reverse());
|
|
742
|
+
const connectorOutput = connectorLeaf.getOutput(0);
|
|
743
|
+
if (!connectorOutput) {
|
|
744
|
+
throw new Error("connector output not found");
|
|
728
745
|
}
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
746
|
+
const connectorAmount = connectorOutput.amount;
|
|
747
|
+
const connectorPkScript = connectorOutput.script;
|
|
748
|
+
if (!connectorAmount || !connectorPkScript) {
|
|
749
|
+
throw new Error("invalid connector output");
|
|
750
|
+
}
|
|
751
|
+
connectorIndex++;
|
|
752
|
+
let forfeitTx = buildForfeitTx([
|
|
753
|
+
{
|
|
754
|
+
txid: input.txid,
|
|
755
|
+
index: input.vout,
|
|
756
|
+
witnessUtxo: {
|
|
757
|
+
amount: BigInt(vtxo.value),
|
|
758
|
+
script: VtxoScript.decode(input.tapTree).pkScript,
|
|
759
|
+
},
|
|
760
|
+
sighashType: SigHash.DEFAULT,
|
|
761
|
+
tapLeafScript: [input.forfeitTapLeafScript],
|
|
762
|
+
},
|
|
763
|
+
{
|
|
764
|
+
txid: connectorTxId,
|
|
765
|
+
index: 0,
|
|
766
|
+
witnessUtxo: {
|
|
767
|
+
amount: connectorAmount,
|
|
768
|
+
script: connectorPkScript,
|
|
769
|
+
},
|
|
770
|
+
},
|
|
771
|
+
], forfeitOutputScript);
|
|
743
772
|
// do not sign the connector input
|
|
744
|
-
forfeitTx = await this.identity.sign(forfeitTx, [
|
|
773
|
+
forfeitTx = await this.identity.sign(forfeitTx, [0]);
|
|
745
774
|
signedForfeits.push(base64.encode(forfeitTx.toPSBT()));
|
|
746
775
|
}
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
776
|
+
if (signedForfeits.length > 0 || hasBoardingUtxos) {
|
|
777
|
+
await this.arkProvider.submitSignedForfeitTxs(signedForfeits, hasBoardingUtxos
|
|
778
|
+
? base64.encode(settlementPsbt.toPSBT())
|
|
779
|
+
: undefined);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
async makeRegisterIntentSignature(bip322Inputs, outputs, onchainOutputsIndexes, cosignerPubKeys) {
|
|
783
|
+
const nowSeconds = Math.floor(Date.now() / 1000);
|
|
784
|
+
const { inputs, inputTapTrees, finalizer } = this.prepareBIP322Inputs(bip322Inputs);
|
|
785
|
+
const message = {
|
|
786
|
+
type: "register",
|
|
787
|
+
input_tap_trees: inputTapTrees,
|
|
788
|
+
onchain_output_indexes: onchainOutputsIndexes,
|
|
789
|
+
valid_at: nowSeconds,
|
|
790
|
+
expire_at: nowSeconds + 2 * 60, // valid for 2 minutes
|
|
791
|
+
cosigners_public_keys: cosignerPubKeys,
|
|
792
|
+
};
|
|
793
|
+
const encodedMessage = JSON.stringify(message, null, 0);
|
|
794
|
+
const signature = await this.makeBIP322Signature(encodedMessage, inputs, finalizer, outputs);
|
|
795
|
+
return {
|
|
796
|
+
signature,
|
|
797
|
+
message: encodedMessage,
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
async makeDeleteIntentSignature(bip322Inputs) {
|
|
801
|
+
const nowSeconds = Math.floor(Date.now() / 1000);
|
|
802
|
+
const { inputs, finalizer } = this.prepareBIP322Inputs(bip322Inputs);
|
|
803
|
+
const message = {
|
|
804
|
+
type: "delete",
|
|
805
|
+
expire_at: nowSeconds + 2 * 60, // valid for 2 minutes
|
|
806
|
+
};
|
|
807
|
+
const encodedMessage = JSON.stringify(message, null, 0);
|
|
808
|
+
const signature = await this.makeBIP322Signature(encodedMessage, inputs, finalizer);
|
|
809
|
+
return {
|
|
810
|
+
signature,
|
|
811
|
+
message: encodedMessage,
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
prepareBIP322Inputs(bip322Inputs) {
|
|
815
|
+
const inputs = [];
|
|
816
|
+
const inputTapTrees = [];
|
|
817
|
+
const inputExtraWitnesses = [];
|
|
818
|
+
for (const bip322Input of bip322Inputs) {
|
|
819
|
+
const vtxoScript = VtxoScript.decode(bip322Input.tapTree);
|
|
820
|
+
const sequence = getSequence(bip322Input);
|
|
821
|
+
inputs.push({
|
|
822
|
+
txid: hex.decode(bip322Input.txid),
|
|
823
|
+
index: bip322Input.vout,
|
|
824
|
+
witnessUtxo: {
|
|
825
|
+
amount: BigInt(bip322Input.value),
|
|
826
|
+
script: vtxoScript.pkScript,
|
|
827
|
+
},
|
|
828
|
+
sequence,
|
|
829
|
+
tapLeafScript: [bip322Input.intentTapLeafScript],
|
|
830
|
+
});
|
|
831
|
+
inputTapTrees.push(hex.encode(bip322Input.tapTree));
|
|
832
|
+
inputExtraWitnesses.push(bip322Input.extraWitness || []);
|
|
833
|
+
}
|
|
834
|
+
return {
|
|
835
|
+
inputs,
|
|
836
|
+
inputTapTrees,
|
|
837
|
+
finalizer: finalizeWithExtraWitnesses(inputExtraWitnesses),
|
|
838
|
+
};
|
|
750
839
|
}
|
|
840
|
+
async makeBIP322Signature(message, inputs, finalizer, outputs) {
|
|
841
|
+
const proof = BIP322.create(message, inputs, outputs);
|
|
842
|
+
const signedProof = await this.identity.sign(proof);
|
|
843
|
+
return BIP322.signature(signedProof, finalizer);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
Wallet.MIN_FEE_RATE = 1; // sats/vbyte
|
|
847
|
+
function finalizeWithExtraWitnesses(inputExtraWitnesses) {
|
|
848
|
+
return function (tx) {
|
|
849
|
+
for (let i = 0; i < tx.inputsLength; i++) {
|
|
850
|
+
try {
|
|
851
|
+
tx.finalizeIdx(i);
|
|
852
|
+
}
|
|
853
|
+
catch (e) {
|
|
854
|
+
// handle empty witness error
|
|
855
|
+
if (e instanceof Error &&
|
|
856
|
+
e.message.includes("finalize/taproot: empty witness")) {
|
|
857
|
+
const tapLeaves = tx.getInput(i).tapLeafScript;
|
|
858
|
+
if (!tapLeaves || tapLeaves.length <= 0)
|
|
859
|
+
throw e;
|
|
860
|
+
const [cb, s] = tapLeaves[0];
|
|
861
|
+
const script = s.slice(0, -1);
|
|
862
|
+
tx.updateInput(i, {
|
|
863
|
+
finalScriptWitness: [
|
|
864
|
+
script,
|
|
865
|
+
TaprootControlBlock.encode(cb),
|
|
866
|
+
],
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
const finalScriptWitness = tx.getInput(i).finalScriptWitness;
|
|
871
|
+
if (!finalScriptWitness)
|
|
872
|
+
throw new Error("input not finalized");
|
|
873
|
+
// input 0 and 1 spend the same pkscript
|
|
874
|
+
const extra = inputExtraWitnesses[i === 0 ? 0 : i - 1];
|
|
875
|
+
if (extra && extra.length > 0) {
|
|
876
|
+
tx.updateInput(i, {
|
|
877
|
+
finalScriptWitness: [...extra, ...finalScriptWitness],
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
function getSequence(bip322Input) {
|
|
884
|
+
let sequence = undefined;
|
|
885
|
+
try {
|
|
886
|
+
const scriptWithLeafVersion = bip322Input.intentTapLeafScript[1];
|
|
887
|
+
const script = scriptWithLeafVersion.subarray(0, scriptWithLeafVersion.length - 1);
|
|
888
|
+
const params = CSVMultisigTapscript.decode(script).params;
|
|
889
|
+
sequence = bip68.encode(params.timelock.type === "blocks"
|
|
890
|
+
? { blocks: Number(params.timelock.value) }
|
|
891
|
+
: { seconds: Number(params.timelock.value) });
|
|
892
|
+
}
|
|
893
|
+
catch { }
|
|
894
|
+
return sequence;
|
|
895
|
+
}
|
|
896
|
+
function isValidArkAddress(address) {
|
|
897
|
+
try {
|
|
898
|
+
ArkAddress.decode(address);
|
|
899
|
+
return true;
|
|
900
|
+
}
|
|
901
|
+
catch (e) {
|
|
902
|
+
return false;
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
/**
|
|
906
|
+
* Select virtual coins to reach a target amount, prioritizing those closer to expiry
|
|
907
|
+
* @param coins List of virtual coins to select from
|
|
908
|
+
* @param targetAmount Target amount to reach in satoshis
|
|
909
|
+
* @returns Selected coins and change amount
|
|
910
|
+
*/
|
|
911
|
+
function selectVirtualCoins(coins, targetAmount) {
|
|
912
|
+
// Sort VTXOs by expiry (ascending) and amount (descending)
|
|
913
|
+
const sortedCoins = [...coins].sort((a, b) => {
|
|
914
|
+
// First sort by expiry if available
|
|
915
|
+
const expiryA = a.virtualStatus.batchExpiry || Number.MAX_SAFE_INTEGER;
|
|
916
|
+
const expiryB = b.virtualStatus.batchExpiry || Number.MAX_SAFE_INTEGER;
|
|
917
|
+
if (expiryA !== expiryB) {
|
|
918
|
+
return expiryA - expiryB; // Earlier expiry first
|
|
919
|
+
}
|
|
920
|
+
// Then sort by amount
|
|
921
|
+
return b.value - a.value; // Larger amount first
|
|
922
|
+
});
|
|
923
|
+
const selectedCoins = [];
|
|
924
|
+
let selectedAmount = 0;
|
|
925
|
+
// Select coins until we have enough
|
|
926
|
+
for (const coin of sortedCoins) {
|
|
927
|
+
selectedCoins.push(coin);
|
|
928
|
+
selectedAmount += coin.value;
|
|
929
|
+
if (selectedAmount >= targetAmount) {
|
|
930
|
+
break;
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
if (selectedAmount === targetAmount) {
|
|
934
|
+
return { inputs: selectedCoins, changeAmount: 0n };
|
|
935
|
+
}
|
|
936
|
+
// Check if we have enough
|
|
937
|
+
if (selectedAmount < targetAmount) {
|
|
938
|
+
throw new Error("Insufficient funds");
|
|
939
|
+
}
|
|
940
|
+
const changeAmount = BigInt(selectedAmount - targetAmount);
|
|
941
|
+
return {
|
|
942
|
+
inputs: selectedCoins,
|
|
943
|
+
changeAmount,
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
/**
|
|
947
|
+
* Wait for incoming funds to the wallet
|
|
948
|
+
* @param wallet - The wallet to wait for incoming funds
|
|
949
|
+
* @returns A promise that resolves the next new coins received by the wallet's address
|
|
950
|
+
*/
|
|
951
|
+
export async function waitForIncomingFunds(wallet) {
|
|
952
|
+
let stopFunc;
|
|
953
|
+
const promise = new Promise((resolve) => {
|
|
954
|
+
wallet
|
|
955
|
+
.notifyIncomingFunds((coins) => {
|
|
956
|
+
resolve(coins);
|
|
957
|
+
if (stopFunc)
|
|
958
|
+
stopFunc();
|
|
959
|
+
})
|
|
960
|
+
.then((stop) => {
|
|
961
|
+
stopFunc = stop;
|
|
962
|
+
});
|
|
963
|
+
});
|
|
964
|
+
return promise;
|
|
751
965
|
}
|
|
752
|
-
Wallet.FEE_RATE = 1; // sats/vbyte
|