@arkade-os/sdk 0.1.3 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +156 -174
- package/dist/cjs/arknote/index.js +61 -58
- package/dist/cjs/bip322/errors.js +13 -0
- package/dist/cjs/bip322/index.js +178 -0
- package/dist/cjs/forfeit.js +14 -25
- package/dist/cjs/identity/singleKey.js +68 -0
- package/dist/cjs/index.js +41 -17
- package/dist/cjs/providers/ark.js +253 -317
- package/dist/cjs/providers/indexer.js +525 -0
- package/dist/cjs/providers/onchain.js +193 -15
- package/dist/cjs/script/address.js +48 -17
- package/dist/cjs/script/base.js +120 -3
- package/dist/cjs/script/default.js +18 -4
- package/dist/cjs/script/tapscript.js +46 -14
- package/dist/cjs/script/vhtlc.js +27 -7
- package/dist/cjs/tree/signingSession.js +63 -106
- package/dist/cjs/tree/txTree.js +193 -0
- package/dist/cjs/tree/validation.js +79 -155
- package/dist/cjs/utils/anchor.js +35 -0
- package/dist/cjs/utils/arkTransaction.js +108 -0
- package/dist/cjs/utils/transactionHistory.js +84 -72
- package/dist/cjs/utils/txSizeEstimator.js +12 -0
- package/dist/cjs/utils/unknownFields.js +211 -0
- package/dist/cjs/wallet/index.js +12 -0
- package/dist/cjs/wallet/onchain.js +201 -0
- package/dist/cjs/wallet/ramps.js +95 -0
- package/dist/cjs/wallet/serviceWorker/db/vtxo/idb.js +32 -0
- package/dist/cjs/wallet/serviceWorker/request.js +15 -12
- package/dist/cjs/wallet/serviceWorker/response.js +22 -27
- package/dist/cjs/wallet/serviceWorker/utils.js +8 -0
- package/dist/cjs/wallet/serviceWorker/wallet.js +58 -34
- package/dist/cjs/wallet/serviceWorker/worker.js +117 -108
- package/dist/cjs/wallet/unroll.js +270 -0
- package/dist/cjs/wallet/wallet.js +701 -459
- package/dist/esm/arknote/index.js +61 -57
- package/dist/esm/bip322/errors.js +9 -0
- package/dist/esm/bip322/index.js +174 -0
- package/dist/esm/forfeit.js +15 -26
- package/dist/esm/identity/singleKey.js +64 -0
- package/dist/esm/index.js +30 -12
- package/dist/esm/providers/ark.js +252 -317
- package/dist/esm/providers/indexer.js +521 -0
- package/dist/esm/providers/onchain.js +193 -15
- package/dist/esm/script/address.js +48 -17
- package/dist/esm/script/base.js +120 -3
- package/dist/esm/script/default.js +18 -4
- package/dist/esm/script/tapscript.js +46 -14
- package/dist/esm/script/vhtlc.js +27 -7
- package/dist/esm/tree/signingSession.js +65 -108
- package/dist/esm/tree/txTree.js +189 -0
- package/dist/esm/tree/validation.js +75 -152
- package/dist/esm/utils/anchor.js +31 -0
- package/dist/esm/utils/arkTransaction.js +105 -0
- package/dist/esm/utils/transactionHistory.js +84 -72
- package/dist/esm/utils/txSizeEstimator.js +12 -0
- package/dist/esm/utils/unknownFields.js +173 -0
- package/dist/esm/wallet/index.js +9 -0
- package/dist/esm/wallet/onchain.js +196 -0
- package/dist/esm/wallet/ramps.js +91 -0
- package/dist/esm/wallet/serviceWorker/db/vtxo/idb.js +32 -0
- package/dist/esm/wallet/serviceWorker/request.js +15 -12
- package/dist/esm/wallet/serviceWorker/response.js +22 -27
- package/dist/esm/wallet/serviceWorker/utils.js +8 -0
- package/dist/esm/wallet/serviceWorker/wallet.js +59 -35
- package/dist/esm/wallet/serviceWorker/worker.js +117 -108
- package/dist/esm/wallet/unroll.js +267 -0
- package/dist/esm/wallet/wallet.js +674 -466
- package/dist/types/arknote/index.d.ts +40 -13
- package/dist/types/bip322/errors.d.ts +6 -0
- package/dist/types/bip322/index.d.ts +57 -0
- package/dist/types/forfeit.d.ts +2 -14
- package/dist/types/identity/singleKey.d.ts +27 -0
- package/dist/types/index.d.ts +23 -12
- package/dist/types/providers/ark.d.ts +114 -95
- package/dist/types/providers/indexer.d.ts +186 -0
- package/dist/types/providers/onchain.d.ts +41 -11
- package/dist/types/script/address.d.ts +26 -2
- package/dist/types/script/base.d.ts +13 -3
- package/dist/types/script/default.d.ts +22 -0
- package/dist/types/script/tapscript.d.ts +61 -5
- package/dist/types/script/vhtlc.d.ts +27 -0
- package/dist/types/tree/signingSession.d.ts +5 -5
- package/dist/types/tree/txTree.d.ts +28 -0
- package/dist/types/tree/validation.d.ts +15 -22
- package/dist/types/utils/anchor.d.ts +19 -0
- package/dist/types/utils/arkTransaction.d.ts +27 -0
- package/dist/types/utils/transactionHistory.d.ts +7 -1
- package/dist/types/utils/txSizeEstimator.d.ts +3 -0
- package/dist/types/utils/unknownFields.d.ts +83 -0
- package/dist/types/wallet/index.d.ts +51 -50
- package/dist/types/wallet/onchain.d.ts +49 -0
- package/dist/types/wallet/ramps.d.ts +32 -0
- package/dist/types/wallet/serviceWorker/db/vtxo/idb.d.ts +2 -0
- package/dist/types/wallet/serviceWorker/db/vtxo/index.d.ts +2 -0
- package/dist/types/wallet/serviceWorker/request.d.ts +14 -16
- package/dist/types/wallet/serviceWorker/response.d.ts +17 -19
- package/dist/types/wallet/serviceWorker/utils.d.ts +8 -0
- package/dist/types/wallet/serviceWorker/wallet.d.ts +36 -8
- package/dist/types/wallet/serviceWorker/worker.d.ts +7 -3
- package/dist/types/wallet/unroll.d.ts +102 -0
- package/dist/types/wallet/wallet.d.ts +71 -26
- package/package.json +14 -15
- package/dist/cjs/identity/inMemoryKey.js +0 -40
- package/dist/cjs/tree/vtxoTree.js +0 -231
- package/dist/cjs/utils/coinselect.js +0 -73
- package/dist/cjs/utils/psbt.js +0 -137
- package/dist/esm/identity/inMemoryKey.js +0 -36
- package/dist/esm/tree/vtxoTree.js +0 -191
- package/dist/esm/utils/coinselect.js +0 -69
- package/dist/esm/utils/psbt.js +0 -131
- package/dist/types/identity/inMemoryKey.d.ts +0 -12
- package/dist/types/tree/vtxoTree.d.ts +0 -33
- package/dist/types/utils/coinselect.d.ts +0 -21
- package/dist/types/utils/psbt.d.ts +0 -11
|
@@ -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,132 +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
|
-
if (params.
|
|
328
|
-
throw new Error("
|
|
329
|
-
}
|
|
330
|
-
// If Ark is configured and amount is suitable, send via offchain
|
|
331
|
-
if (this.arkProvider && this.isOffchainSuitable(params.address)) {
|
|
332
|
-
return this.sendOffchain(params, zeroFee);
|
|
333
|
-
}
|
|
334
|
-
// Otherwise, send via onchain
|
|
335
|
-
return this.sendOnchain(params);
|
|
336
|
-
}
|
|
337
|
-
isOffchainSuitable(address) {
|
|
338
|
-
try {
|
|
339
|
-
ArkAddress.decode(address);
|
|
340
|
-
return true;
|
|
341
|
-
}
|
|
342
|
-
catch (e) {
|
|
343
|
-
return false;
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
async sendOnchain(params) {
|
|
347
|
-
const coins = await this.getCoins();
|
|
348
|
-
const feeRate = params.feeRate || Wallet.FEE_RATE;
|
|
349
|
-
// Ensure fee is an integer by rounding up
|
|
350
|
-
const estimatedFee = Math.ceil(174 * feeRate);
|
|
351
|
-
const totalNeeded = params.amount + estimatedFee;
|
|
352
|
-
// Select coins
|
|
353
|
-
const selected = selectCoins(coins, totalNeeded);
|
|
354
|
-
if (!selected.inputs) {
|
|
355
|
-
throw new Error("Insufficient funds");
|
|
356
|
-
}
|
|
357
|
-
// Create transaction
|
|
358
|
-
let tx = new Transaction();
|
|
359
|
-
// Add inputs
|
|
360
|
-
for (const input of selected.inputs) {
|
|
361
|
-
tx.addInput({
|
|
362
|
-
txid: input.txid,
|
|
363
|
-
index: input.vout,
|
|
364
|
-
witnessUtxo: {
|
|
365
|
-
script: this.onchainP2TR.script,
|
|
366
|
-
amount: BigInt(input.value),
|
|
367
|
-
},
|
|
368
|
-
tapInternalKey: this.onchainP2TR.tapInternalKey,
|
|
369
|
-
});
|
|
370
|
-
}
|
|
371
|
-
// Add payment output
|
|
372
|
-
tx.addOutputAddress(params.address, BigInt(params.amount), this.network);
|
|
373
|
-
// Add change output if needed
|
|
374
|
-
if (selected.changeAmount > 0) {
|
|
375
|
-
tx.addOutputAddress(this.onchainAddress, BigInt(selected.changeAmount), this.network);
|
|
376
|
-
}
|
|
377
|
-
// Sign inputs and Finalize
|
|
378
|
-
tx = await this.identity.sign(tx);
|
|
379
|
-
tx.finalize();
|
|
380
|
-
// Broadcast
|
|
381
|
-
const txid = await this.onchainProvider.broadcastTransaction(tx.hex);
|
|
382
|
-
return txid;
|
|
383
|
-
}
|
|
384
|
-
async sendOffchain(params, zeroFee = true) {
|
|
385
|
-
if (!this.arkProvider ||
|
|
386
|
-
!this.offchainAddress ||
|
|
387
|
-
!this.offchainTapscript) {
|
|
388
|
-
throw new Error("wallet not initialized");
|
|
389
|
-
}
|
|
390
|
-
const virtualCoins = await this.getVirtualCoins();
|
|
391
|
-
const estimatedFee = zeroFee
|
|
392
|
-
? 0
|
|
393
|
-
: Math.ceil(174 * (params.feeRate || Wallet.FEE_RATE));
|
|
394
|
-
const totalNeeded = params.amount + estimatedFee;
|
|
395
|
-
const selected = selectVirtualCoins(virtualCoins, totalNeeded);
|
|
396
|
-
if (!selected || !selected.inputs) {
|
|
397
|
-
throw new Error("Insufficient funds");
|
|
308
|
+
if (!isValidArkAddress(params.address)) {
|
|
309
|
+
throw new Error("Invalid Ark address " + params.address);
|
|
398
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);
|
|
399
316
|
const selectedLeaf = this.offchainTapscript.forfeit();
|
|
400
317
|
if (!selectedLeaf) {
|
|
401
318
|
throw new Error("Selected leaf not found");
|
|
402
319
|
}
|
|
320
|
+
const outputAddress = ArkAddress.decode(params.address);
|
|
321
|
+
const outputScript = BigInt(params.amount) < this.dustAmount
|
|
322
|
+
? outputAddress.subdustPkScript
|
|
323
|
+
: outputAddress.pkScript;
|
|
403
324
|
const outputs = [
|
|
404
325
|
{
|
|
405
|
-
|
|
326
|
+
script: outputScript,
|
|
406
327
|
amount: BigInt(params.amount),
|
|
407
328
|
},
|
|
408
329
|
];
|
|
409
330
|
// add change output if needed
|
|
410
|
-
if (selected.changeAmount >
|
|
331
|
+
if (selected.changeAmount > 0n) {
|
|
332
|
+
const changeOutputScript = selected.changeAmount < this.dustAmount
|
|
333
|
+
? this.arkAddress.subdustPkScript
|
|
334
|
+
: this.arkAddress.pkScript;
|
|
411
335
|
outputs.push({
|
|
412
|
-
|
|
336
|
+
script: changeOutputScript,
|
|
413
337
|
amount: BigInt(selected.changeAmount),
|
|
414
338
|
});
|
|
415
339
|
}
|
|
416
|
-
const
|
|
417
|
-
let
|
|
340
|
+
const tapTree = this.offchainTapscript.encode();
|
|
341
|
+
let offchainTx = buildOffchainTx(selected.inputs.map((input) => ({
|
|
418
342
|
...input,
|
|
419
343
|
tapLeafScript: selectedLeaf,
|
|
420
|
-
|
|
421
|
-
})), outputs);
|
|
422
|
-
|
|
423
|
-
const
|
|
424
|
-
|
|
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;
|
|
425
357
|
}
|
|
426
358
|
async settle(params, eventCallback) {
|
|
427
|
-
if (!this.arkProvider) {
|
|
428
|
-
throw new Error("Ark provider not configured");
|
|
429
|
-
}
|
|
430
|
-
// validate arknotes inputs
|
|
431
359
|
if (params?.inputs) {
|
|
432
360
|
for (const input of params.inputs) {
|
|
361
|
+
// validate arknotes inputs
|
|
433
362
|
if (typeof input === "string") {
|
|
434
363
|
try {
|
|
435
364
|
ArkNote.fromString(input);
|
|
@@ -443,9 +372,6 @@ export class Wallet {
|
|
|
443
372
|
// if no params are provided, use all boarding and offchain utxos as inputs
|
|
444
373
|
// and send all to the offchain address
|
|
445
374
|
if (!params) {
|
|
446
|
-
if (!this.offchainAddress) {
|
|
447
|
-
throw new Error("Offchain address not configured");
|
|
448
|
-
}
|
|
449
375
|
let amount = 0;
|
|
450
376
|
const boardingUtxos = await this.getBoardingUtxos();
|
|
451
377
|
amount += boardingUtxos.reduce((sum, input) => sum + input.value, 0);
|
|
@@ -459,23 +385,34 @@ export class Wallet {
|
|
|
459
385
|
inputs,
|
|
460
386
|
outputs: [
|
|
461
387
|
{
|
|
462
|
-
address: this.
|
|
388
|
+
address: await this.getAddress(),
|
|
463
389
|
amount: BigInt(amount),
|
|
464
390
|
},
|
|
465
391
|
],
|
|
466
392
|
};
|
|
467
393
|
}
|
|
468
|
-
|
|
469
|
-
const
|
|
470
|
-
|
|
471
|
-
|
|
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;
|
|
472
404
|
}
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
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
|
+
}
|
|
479
416
|
// session holds the state of the musig2 signing process of the vtxo tree
|
|
480
417
|
let session;
|
|
481
418
|
const signingPublicKeys = [];
|
|
@@ -483,189 +420,287 @@ export class Wallet {
|
|
|
483
420
|
session = this.identity.signerSession();
|
|
484
421
|
signingPublicKeys.push(hex.encode(session.getPublicKey()));
|
|
485
422
|
}
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
}, 1000);
|
|
492
|
-
let pingRunning = true;
|
|
493
|
-
const stopPing = () => {
|
|
494
|
-
if (pingRunning) {
|
|
495
|
-
pingRunning = false;
|
|
496
|
-
clearInterval(interval);
|
|
497
|
-
}
|
|
498
|
-
};
|
|
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);
|
|
499
428
|
const abortController = new AbortController();
|
|
500
429
|
// listen to settlement events
|
|
501
430
|
try {
|
|
502
|
-
const settlementStream = this.arkProvider.getEventStream(abortController.signal);
|
|
503
431
|
let step;
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
}).script;
|
|
517
|
-
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;
|
|
518
444
|
for await (const event of settlementStream) {
|
|
519
445
|
if (eventCallback) {
|
|
520
446
|
eventCallback(event);
|
|
521
447
|
}
|
|
522
448
|
switch (event.type) {
|
|
523
449
|
// the settlement failed
|
|
524
|
-
case SettlementEventType.
|
|
525
|
-
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) {
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
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) {
|
|
526
491
|
continue;
|
|
527
492
|
}
|
|
528
|
-
|
|
529
|
-
|
|
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;
|
|
530
509
|
// the server has started the signing process of the vtxo tree transactions
|
|
531
510
|
// the server expects the partial musig2 nonces for each tx
|
|
532
|
-
case SettlementEventType.
|
|
533
|
-
if (step !==
|
|
511
|
+
case SettlementEventType.TreeSigningStarted:
|
|
512
|
+
if (step !== SettlementEventType.BatchStarted) {
|
|
534
513
|
continue;
|
|
535
514
|
}
|
|
536
|
-
stopPing();
|
|
537
515
|
if (hasOffchainOutputs) {
|
|
538
516
|
if (!session) {
|
|
539
|
-
throw new Error("Signing session not
|
|
517
|
+
throw new Error("Signing session not set");
|
|
540
518
|
}
|
|
541
|
-
|
|
519
|
+
if (!sweepTapTreeRoot) {
|
|
520
|
+
throw new Error("Sweep tap tree root not set");
|
|
521
|
+
}
|
|
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);
|
|
542
527
|
}
|
|
528
|
+
step = event.type;
|
|
543
529
|
break;
|
|
544
530
|
// the musig2 nonces of the vtxo tree transactions are generated
|
|
545
531
|
// the server expects now the partial musig2 signatures
|
|
546
|
-
case SettlementEventType.
|
|
547
|
-
if (step !== SettlementEventType.
|
|
532
|
+
case SettlementEventType.TreeNoncesAggregated:
|
|
533
|
+
if (step !== SettlementEventType.TreeSigningStarted) {
|
|
548
534
|
continue;
|
|
549
535
|
}
|
|
550
|
-
stopPing();
|
|
551
536
|
if (hasOffchainOutputs) {
|
|
552
537
|
if (!session) {
|
|
553
|
-
throw new Error("Signing session not
|
|
538
|
+
throw new Error("Signing session not set");
|
|
554
539
|
}
|
|
555
540
|
await this.handleSettlementSigningNoncesGeneratedEvent(event, session);
|
|
556
541
|
}
|
|
542
|
+
step = event.type;
|
|
557
543
|
break;
|
|
558
544
|
// the vtxo tree is signed, craft, sign and submit forfeit transactions
|
|
559
545
|
// if any boarding utxos are involved, the settlement tx is also signed
|
|
560
|
-
case SettlementEventType.
|
|
561
|
-
if (step !== SettlementEventType.
|
|
546
|
+
case SettlementEventType.BatchFinalization:
|
|
547
|
+
if (step !== SettlementEventType.TreeNoncesAggregated) {
|
|
562
548
|
continue;
|
|
563
549
|
}
|
|
564
|
-
|
|
565
|
-
|
|
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;
|
|
566
559
|
break;
|
|
567
560
|
// the settlement is done, last event to be received
|
|
568
|
-
case SettlementEventType.
|
|
569
|
-
if (step !== SettlementEventType.
|
|
561
|
+
case SettlementEventType.BatchFinalized:
|
|
562
|
+
if (step !== SettlementEventType.BatchFinalization) {
|
|
570
563
|
continue;
|
|
571
564
|
}
|
|
572
565
|
abortController.abort();
|
|
573
|
-
return event.
|
|
566
|
+
return event.commitmentTxid;
|
|
574
567
|
}
|
|
575
|
-
step = event.type;
|
|
576
568
|
}
|
|
577
569
|
}
|
|
578
570
|
catch (error) {
|
|
571
|
+
// close the stream
|
|
579
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 { }
|
|
580
578
|
throw error;
|
|
581
579
|
}
|
|
582
580
|
throw new Error("Settlement failed");
|
|
583
581
|
}
|
|
584
|
-
async
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
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
|
+
});
|
|
594
609
|
}
|
|
595
|
-
if (
|
|
596
|
-
|
|
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
|
+
})();
|
|
597
637
|
}
|
|
598
|
-
const
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
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;
|
|
611
657
|
}
|
|
612
|
-
const exitBranch = await tree.exitBranch(vtxo.txid, async (txid) => {
|
|
613
|
-
const status = await this.onchainProvider.getTxStatus(txid);
|
|
614
|
-
return status.confirmed;
|
|
615
|
-
});
|
|
616
|
-
transactions.push(...exitBranch);
|
|
617
658
|
}
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
if (broadcastedTxs.has(tx))
|
|
621
|
-
continue;
|
|
622
|
-
const txid = await this.onchainProvider.broadcastTransaction(tx);
|
|
623
|
-
broadcastedTxs.set(txid, true);
|
|
659
|
+
if (skip) {
|
|
660
|
+
return { skip };
|
|
624
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
|
+
};
|
|
625
676
|
}
|
|
626
677
|
// validates the vtxo tree, creates a signing session and generates the musig2 nonces
|
|
627
|
-
async handleSettlementSigningEvent(event, sweepTapTreeRoot, session) {
|
|
628
|
-
const vtxoTree = event.unsignedVtxoTree;
|
|
629
|
-
if (!this.arkProvider) {
|
|
630
|
-
throw new Error("Ark provider not configured");
|
|
631
|
-
}
|
|
678
|
+
async handleSettlementSigningEvent(event, sweepTapTreeRoot, session, vtxoGraph) {
|
|
632
679
|
// validate the unsigned vtxo tree
|
|
633
|
-
|
|
680
|
+
const commitmentTx = Transaction.fromPSBT(base64.decode(event.unsignedCommitmentTx));
|
|
681
|
+
validateVtxoTxGraph(vtxoGraph, commitmentTx, sweepTapTreeRoot);
|
|
634
682
|
// TODO check if our registered outputs are in the vtxo tree
|
|
635
|
-
const
|
|
636
|
-
const settlementTx = Transaction.fromPSBT(settlementPsbt);
|
|
637
|
-
const sharedOutput = settlementTx.getOutput(0);
|
|
683
|
+
const sharedOutput = commitmentTx.getOutput(0);
|
|
638
684
|
if (!sharedOutput?.amount) {
|
|
639
685
|
throw new Error("Shared output not found");
|
|
640
686
|
}
|
|
641
|
-
session.init(
|
|
687
|
+
session.init(vtxoGraph, sweepTapTreeRoot, sharedOutput.amount);
|
|
642
688
|
await this.arkProvider.submitTreeNonces(event.id, hex.encode(session.getPublicKey()), session.getNonces());
|
|
643
689
|
}
|
|
644
690
|
async handleSettlementSigningNoncesGeneratedEvent(event, session) {
|
|
645
|
-
if (!this.arkProvider) {
|
|
646
|
-
throw new Error("Ark provider not configured");
|
|
647
|
-
}
|
|
648
691
|
session.setAggregatedNonces(event.treeNonces);
|
|
649
692
|
const signatures = session.sign();
|
|
650
693
|
await this.arkProvider.submitTreeSignatures(event.id, hex.encode(session.getPublicKey()), signatures);
|
|
651
694
|
}
|
|
652
|
-
async handleSettlementFinalizationEvent(event, inputs,
|
|
653
|
-
if (!this.arkProvider) {
|
|
654
|
-
throw new Error("Ark provider not configured");
|
|
655
|
-
}
|
|
656
|
-
// parse the server forfeit address
|
|
657
|
-
// server is expecting funds to be sent to this address
|
|
658
|
-
const forfeitAddress = Address(this.network).decode(infos.forfeitAddress);
|
|
659
|
-
const serverPkScript = OutScript.encode(forfeitAddress);
|
|
695
|
+
async handleSettlementFinalizationEvent(event, inputs, forfeitOutputScript, connectorsGraph) {
|
|
660
696
|
// the signed forfeits transactions to submit
|
|
661
697
|
const signedForfeits = [];
|
|
662
698
|
const vtxos = await this.getVirtualCoins();
|
|
663
|
-
let settlementPsbt = Transaction.fromPSBT(base64.decode(event.
|
|
699
|
+
let settlementPsbt = Transaction.fromPSBT(base64.decode(event.commitmentTx));
|
|
664
700
|
let hasBoardingUtxos = false;
|
|
665
|
-
let
|
|
701
|
+
let connectorIndex = 0;
|
|
702
|
+
const connectorsLeaves = connectorsGraph?.leaves() || [];
|
|
666
703
|
for (const input of inputs) {
|
|
667
|
-
if (typeof input === "string")
|
|
668
|
-
continue; // skip notes
|
|
669
704
|
// check if the input is an offchain "virtual" coin
|
|
670
705
|
const vtxo = vtxos.find((vtxo) => vtxo.txid === input.txid && vtxo.vout === input.vout);
|
|
671
706
|
// boarding utxo, we need to sign the settlement tx
|
|
@@ -685,73 +720,246 @@ export class Wallet {
|
|
|
685
720
|
continue;
|
|
686
721
|
// input found in the settlement tx, sign it
|
|
687
722
|
settlementPsbt.updateInput(i, {
|
|
688
|
-
tapLeafScript: [input.
|
|
723
|
+
tapLeafScript: [input.forfeitTapLeafScript],
|
|
689
724
|
});
|
|
690
725
|
inputIndexes.push(i);
|
|
691
726
|
}
|
|
692
727
|
settlementPsbt = await this.identity.sign(settlementPsbt, inputIndexes);
|
|
693
728
|
continue;
|
|
694
729
|
}
|
|
695
|
-
if (
|
|
696
|
-
//
|
|
697
|
-
|
|
698
|
-
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;
|
|
699
733
|
}
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
const fees = TxWeightEstimator.create()
|
|
703
|
-
.addKeySpendInput() // connector
|
|
704
|
-
.addTapscriptInput(tapscript.witnessSize(100), // TODO: handle conditional script
|
|
705
|
-
input.tapLeafScript[1].length - 1, forfeitControlBlock.length)
|
|
706
|
-
.addP2WKHOutput()
|
|
707
|
-
.vsize()
|
|
708
|
-
.fee(event.minRelayFeeRate);
|
|
709
|
-
const connectorsLeaves = event.connectors.leaves();
|
|
710
|
-
const connectorOutpoint = event.connectorsIndex.get(`${vtxo.txid}:${vtxo.vout}`);
|
|
711
|
-
if (!connectorOutpoint) {
|
|
712
|
-
throw new Error("Connector outpoint not found");
|
|
734
|
+
if (connectorsLeaves.length === 0) {
|
|
735
|
+
throw new Error("connectors not received");
|
|
713
736
|
}
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
if (leaf.txid === connectorOutpoint.txid) {
|
|
717
|
-
try {
|
|
718
|
-
const connectorTx = Transaction.fromPSBT(base64.decode(leaf.tx));
|
|
719
|
-
connectorOutput = connectorTx.getOutput(connectorOutpoint.vout);
|
|
720
|
-
break;
|
|
721
|
-
}
|
|
722
|
-
catch {
|
|
723
|
-
throw new Error("Invalid connector tx");
|
|
724
|
-
}
|
|
725
|
-
}
|
|
737
|
+
if (connectorIndex >= connectorsLeaves.length) {
|
|
738
|
+
throw new Error("not enough connectors received");
|
|
726
739
|
}
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
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");
|
|
731
745
|
}
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
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);
|
|
746
772
|
// do not sign the connector input
|
|
747
|
-
forfeitTx = await this.identity.sign(forfeitTx, [
|
|
773
|
+
forfeitTx = await this.identity.sign(forfeitTx, [0]);
|
|
748
774
|
signedForfeits.push(base64.encode(forfeitTx.toPSBT()));
|
|
749
775
|
}
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
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
|
+
};
|
|
753
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;
|
|
754
965
|
}
|
|
755
|
-
// TODO get dust from ark server?
|
|
756
|
-
Wallet.DUST_AMOUNT = BigInt(546); // Bitcoin dust limit in satoshis = 546
|
|
757
|
-
Wallet.FEE_RATE = 1; // sats/vbyte
|