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