@arkade-os/sdk 0.1.4 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +157 -174
- package/dist/cjs/arknote/index.js +61 -58
- package/dist/cjs/bip322/errors.js +13 -0
- package/dist/cjs/bip322/index.js +178 -0
- package/dist/cjs/forfeit.js +14 -25
- package/dist/cjs/identity/singleKey.js +68 -0
- package/dist/cjs/index.js +43 -17
- package/dist/cjs/providers/ark.js +261 -321
- package/dist/cjs/providers/indexer.js +525 -0
- package/dist/cjs/providers/onchain.js +193 -15
- package/dist/cjs/script/address.js +48 -17
- package/dist/cjs/script/base.js +120 -3
- package/dist/cjs/script/default.js +18 -4
- package/dist/cjs/script/tapscript.js +61 -20
- package/dist/cjs/script/vhtlc.js +85 -7
- package/dist/cjs/tree/signingSession.js +63 -106
- package/dist/cjs/tree/txTree.js +193 -0
- package/dist/cjs/tree/validation.js +79 -155
- package/dist/cjs/utils/anchor.js +35 -0
- package/dist/cjs/utils/arkTransaction.js +108 -0
- package/dist/cjs/utils/transactionHistory.js +84 -72
- package/dist/cjs/utils/txSizeEstimator.js +12 -0
- package/dist/cjs/utils/unknownFields.js +211 -0
- package/dist/cjs/wallet/index.js +12 -0
- package/dist/cjs/wallet/onchain.js +201 -0
- package/dist/cjs/wallet/ramps.js +95 -0
- package/dist/cjs/wallet/serviceWorker/db/vtxo/idb.js +32 -0
- package/dist/cjs/wallet/serviceWorker/request.js +15 -12
- package/dist/cjs/wallet/serviceWorker/response.js +22 -27
- package/dist/cjs/wallet/serviceWorker/utils.js +8 -0
- package/dist/cjs/wallet/serviceWorker/wallet.js +61 -34
- package/dist/cjs/wallet/serviceWorker/worker.js +120 -108
- package/dist/cjs/wallet/unroll.js +270 -0
- package/dist/cjs/wallet/wallet.js +701 -454
- package/dist/esm/arknote/index.js +61 -57
- package/dist/esm/bip322/errors.js +9 -0
- package/dist/esm/bip322/index.js +174 -0
- package/dist/esm/forfeit.js +15 -26
- package/dist/esm/identity/singleKey.js +64 -0
- package/dist/esm/index.js +31 -12
- package/dist/esm/providers/ark.js +259 -320
- package/dist/esm/providers/indexer.js +521 -0
- package/dist/esm/providers/onchain.js +193 -15
- package/dist/esm/script/address.js +48 -17
- package/dist/esm/script/base.js +120 -3
- package/dist/esm/script/default.js +18 -4
- package/dist/esm/script/tapscript.js +61 -20
- package/dist/esm/script/vhtlc.js +85 -7
- package/dist/esm/tree/signingSession.js +65 -108
- package/dist/esm/tree/txTree.js +189 -0
- package/dist/esm/tree/validation.js +75 -152
- package/dist/esm/utils/anchor.js +31 -0
- package/dist/esm/utils/arkTransaction.js +105 -0
- package/dist/esm/utils/transactionHistory.js +84 -72
- package/dist/esm/utils/txSizeEstimator.js +12 -0
- package/dist/esm/utils/unknownFields.js +173 -0
- package/dist/esm/wallet/index.js +9 -0
- package/dist/esm/wallet/onchain.js +196 -0
- package/dist/esm/wallet/ramps.js +91 -0
- package/dist/esm/wallet/serviceWorker/db/vtxo/idb.js +32 -0
- package/dist/esm/wallet/serviceWorker/request.js +15 -12
- package/dist/esm/wallet/serviceWorker/response.js +22 -27
- package/dist/esm/wallet/serviceWorker/utils.js +8 -0
- package/dist/esm/wallet/serviceWorker/wallet.js +62 -35
- package/dist/esm/wallet/serviceWorker/worker.js +120 -108
- package/dist/esm/wallet/unroll.js +267 -0
- package/dist/esm/wallet/wallet.js +674 -461
- package/dist/types/arknote/index.d.ts +40 -13
- package/dist/types/bip322/errors.d.ts +6 -0
- package/dist/types/bip322/index.d.ts +57 -0
- package/dist/types/forfeit.d.ts +2 -14
- package/dist/types/identity/singleKey.d.ts +27 -0
- package/dist/types/index.d.ts +24 -12
- package/dist/types/providers/ark.d.ts +114 -95
- package/dist/types/providers/indexer.d.ts +186 -0
- package/dist/types/providers/onchain.d.ts +41 -11
- package/dist/types/script/address.d.ts +26 -2
- package/dist/types/script/base.d.ts +13 -3
- package/dist/types/script/default.d.ts +22 -0
- package/dist/types/script/tapscript.d.ts +61 -5
- package/dist/types/script/vhtlc.d.ts +27 -0
- package/dist/types/tree/signingSession.d.ts +5 -5
- package/dist/types/tree/txTree.d.ts +28 -0
- package/dist/types/tree/validation.d.ts +15 -22
- package/dist/types/utils/anchor.d.ts +19 -0
- package/dist/types/utils/arkTransaction.d.ts +27 -0
- package/dist/types/utils/transactionHistory.d.ts +7 -1
- package/dist/types/utils/txSizeEstimator.d.ts +3 -0
- package/dist/types/utils/unknownFields.d.ts +83 -0
- package/dist/types/wallet/index.d.ts +51 -50
- package/dist/types/wallet/onchain.d.ts +49 -0
- package/dist/types/wallet/ramps.d.ts +32 -0
- package/dist/types/wallet/serviceWorker/db/vtxo/idb.d.ts +2 -0
- package/dist/types/wallet/serviceWorker/db/vtxo/index.d.ts +2 -0
- package/dist/types/wallet/serviceWorker/request.d.ts +14 -16
- package/dist/types/wallet/serviceWorker/response.d.ts +17 -19
- package/dist/types/wallet/serviceWorker/utils.d.ts +8 -0
- package/dist/types/wallet/serviceWorker/wallet.d.ts +36 -8
- package/dist/types/wallet/serviceWorker/worker.d.ts +7 -3
- package/dist/types/wallet/unroll.d.ts +102 -0
- package/dist/types/wallet/wallet.d.ts +71 -25
- package/package.json +37 -35
- package/dist/cjs/identity/inMemoryKey.js +0 -40
- package/dist/cjs/tree/vtxoTree.js +0 -231
- package/dist/cjs/utils/coinselect.js +0 -73
- package/dist/cjs/utils/psbt.js +0 -137
- package/dist/esm/identity/inMemoryKey.js +0 -36
- package/dist/esm/tree/vtxoTree.js +0 -191
- package/dist/esm/utils/coinselect.js +0 -69
- package/dist/esm/utils/psbt.js +0 -131
- package/dist/types/identity/inMemoryKey.d.ts +0 -12
- package/dist/types/tree/vtxoTree.d.ts +0 -33
- package/dist/types/utils/coinselect.d.ts +0 -21
- package/dist/types/utils/psbt.d.ts +0 -11
|
@@ -1,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,129 +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
|
-
|
|
331
|
-
|
|
332
|
-
return this.sendOffchain(params, zeroFee);
|
|
333
|
-
}
|
|
334
|
-
// Otherwise, send via onchain
|
|
335
|
-
return this.sendOnchain(params);
|
|
336
|
-
}
|
|
337
|
-
isOffchainSuitable(address) {
|
|
338
|
-
try {
|
|
339
|
-
address_1.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 = (0, coinselect_1.selectCoins)(coins, totalNeeded);
|
|
354
|
-
if (!selected.inputs) {
|
|
355
|
-
throw new Error("Insufficient funds");
|
|
356
|
-
}
|
|
357
|
-
// Create transaction
|
|
358
|
-
let tx = new btc_signer_1.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 = (0, coinselect_1.selectVirtualCoins)(virtualCoins, totalNeeded);
|
|
396
|
-
if (!selected || !selected.inputs) {
|
|
397
|
-
throw new Error("Insufficient funds");
|
|
345
|
+
if (!isValidArkAddress(params.address)) {
|
|
346
|
+
throw new Error("Invalid Ark address " + params.address);
|
|
398
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);
|
|
399
353
|
const selectedLeaf = this.offchainTapscript.forfeit();
|
|
400
354
|
if (!selectedLeaf) {
|
|
401
355
|
throw new Error("Selected leaf not found");
|
|
402
356
|
}
|
|
357
|
+
const outputAddress = address_1.ArkAddress.decode(params.address);
|
|
358
|
+
const outputScript = BigInt(params.amount) < this.dustAmount
|
|
359
|
+
? outputAddress.subdustPkScript
|
|
360
|
+
: outputAddress.pkScript;
|
|
403
361
|
const outputs = [
|
|
404
362
|
{
|
|
405
|
-
|
|
363
|
+
script: outputScript,
|
|
406
364
|
amount: BigInt(params.amount),
|
|
407
365
|
},
|
|
408
366
|
];
|
|
409
367
|
// add change output if needed
|
|
410
|
-
if (selected.changeAmount >
|
|
368
|
+
if (selected.changeAmount > 0n) {
|
|
369
|
+
const changeOutputScript = selected.changeAmount < this.dustAmount
|
|
370
|
+
? this.arkAddress.subdustPkScript
|
|
371
|
+
: this.arkAddress.pkScript;
|
|
411
372
|
outputs.push({
|
|
412
|
-
|
|
373
|
+
script: changeOutputScript,
|
|
413
374
|
amount: BigInt(selected.changeAmount),
|
|
414
375
|
});
|
|
415
376
|
}
|
|
416
|
-
const
|
|
417
|
-
let
|
|
377
|
+
const tapTree = this.offchainTapscript.encode();
|
|
378
|
+
let offchainTx = (0, arkTransaction_1.buildOffchainTx)(selected.inputs.map((input) => ({
|
|
418
379
|
...input,
|
|
419
380
|
tapLeafScript: selectedLeaf,
|
|
420
|
-
|
|
421
|
-
})), outputs);
|
|
422
|
-
|
|
423
|
-
const
|
|
424
|
-
|
|
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;
|
|
425
394
|
}
|
|
426
395
|
async settle(params, eventCallback) {
|
|
427
|
-
if (!this.arkProvider) {
|
|
428
|
-
throw new Error("Ark provider not configured");
|
|
429
|
-
}
|
|
430
|
-
// validate arknotes inputs
|
|
431
396
|
if (params?.inputs) {
|
|
432
397
|
for (const input of params.inputs) {
|
|
398
|
+
// validate arknotes inputs
|
|
433
399
|
if (typeof input === "string") {
|
|
434
400
|
try {
|
|
435
401
|
arknote_1.ArkNote.fromString(input);
|
|
@@ -443,9 +409,6 @@ class Wallet {
|
|
|
443
409
|
// if no params are provided, use all boarding and offchain utxos as inputs
|
|
444
410
|
// and send all to the offchain address
|
|
445
411
|
if (!params) {
|
|
446
|
-
if (!this.offchainAddress) {
|
|
447
|
-
throw new Error("Offchain address not configured");
|
|
448
|
-
}
|
|
449
412
|
let amount = 0;
|
|
450
413
|
const boardingUtxos = await this.getBoardingUtxos();
|
|
451
414
|
amount += boardingUtxos.reduce((sum, input) => sum + input.value, 0);
|
|
@@ -459,23 +422,34 @@ class Wallet {
|
|
|
459
422
|
inputs,
|
|
460
423
|
outputs: [
|
|
461
424
|
{
|
|
462
|
-
address: this.
|
|
425
|
+
address: await this.getAddress(),
|
|
463
426
|
amount: BigInt(amount),
|
|
464
427
|
},
|
|
465
428
|
],
|
|
466
429
|
};
|
|
467
430
|
}
|
|
468
|
-
|
|
469
|
-
const
|
|
470
|
-
|
|
471
|
-
|
|
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;
|
|
472
441
|
}
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
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
|
+
}
|
|
479
453
|
// session holds the state of the musig2 signing process of the vtxo tree
|
|
480
454
|
let session;
|
|
481
455
|
const signingPublicKeys = [];
|
|
@@ -483,189 +457,287 @@ class Wallet {
|
|
|
483
457
|
session = this.identity.signerSession();
|
|
484
458
|
signingPublicKeys.push(base_1.hex.encode(session.getPublicKey()));
|
|
485
459
|
}
|
|
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
|
-
};
|
|
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);
|
|
499
465
|
const abortController = new AbortController();
|
|
500
466
|
// listen to settlement events
|
|
501
467
|
try {
|
|
502
|
-
const settlementStream = this.arkProvider.getEventStream(abortController.signal);
|
|
503
468
|
let step;
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
}).script;
|
|
517
|
-
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;
|
|
518
481
|
for await (const event of settlementStream) {
|
|
519
482
|
if (eventCallback) {
|
|
520
483
|
eventCallback(event);
|
|
521
484
|
}
|
|
522
485
|
switch (event.type) {
|
|
523
486
|
// the settlement failed
|
|
524
|
-
case ark_1.SettlementEventType.
|
|
525
|
-
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) {
|
|
526
495
|
continue;
|
|
527
496
|
}
|
|
528
|
-
|
|
529
|
-
|
|
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) {
|
|
531
|
+
continue;
|
|
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;
|
|
530
546
|
// the server has started the signing process of the vtxo tree transactions
|
|
531
547
|
// the server expects the partial musig2 nonces for each tx
|
|
532
|
-
case ark_1.SettlementEventType.
|
|
533
|
-
if (step !==
|
|
548
|
+
case ark_1.SettlementEventType.TreeSigningStarted:
|
|
549
|
+
if (step !== ark_1.SettlementEventType.BatchStarted) {
|
|
534
550
|
continue;
|
|
535
551
|
}
|
|
536
|
-
stopPing();
|
|
537
552
|
if (hasOffchainOutputs) {
|
|
538
553
|
if (!session) {
|
|
539
|
-
throw new Error("Signing session not
|
|
554
|
+
throw new Error("Signing session not set");
|
|
540
555
|
}
|
|
541
|
-
|
|
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");
|
|
561
|
+
}
|
|
562
|
+
vtxoGraph = txTree_1.TxTree.create(vtxoChunks);
|
|
563
|
+
await this.handleSettlementSigningEvent(event, sweepTapTreeRoot, session, vtxoGraph);
|
|
542
564
|
}
|
|
565
|
+
step = event.type;
|
|
543
566
|
break;
|
|
544
567
|
// the musig2 nonces of the vtxo tree transactions are generated
|
|
545
568
|
// the server expects now the partial musig2 signatures
|
|
546
|
-
case ark_1.SettlementEventType.
|
|
547
|
-
if (step !== ark_1.SettlementEventType.
|
|
569
|
+
case ark_1.SettlementEventType.TreeNoncesAggregated:
|
|
570
|
+
if (step !== ark_1.SettlementEventType.TreeSigningStarted) {
|
|
548
571
|
continue;
|
|
549
572
|
}
|
|
550
|
-
stopPing();
|
|
551
573
|
if (hasOffchainOutputs) {
|
|
552
574
|
if (!session) {
|
|
553
|
-
throw new Error("Signing session not
|
|
575
|
+
throw new Error("Signing session not set");
|
|
554
576
|
}
|
|
555
577
|
await this.handleSettlementSigningNoncesGeneratedEvent(event, session);
|
|
556
578
|
}
|
|
579
|
+
step = event.type;
|
|
557
580
|
break;
|
|
558
581
|
// the vtxo tree is signed, craft, sign and submit forfeit transactions
|
|
559
582
|
// if any boarding utxos are involved, the settlement tx is also signed
|
|
560
|
-
case ark_1.SettlementEventType.
|
|
561
|
-
if (step !== ark_1.SettlementEventType.
|
|
583
|
+
case ark_1.SettlementEventType.BatchFinalization:
|
|
584
|
+
if (step !== ark_1.SettlementEventType.TreeNoncesAggregated) {
|
|
562
585
|
continue;
|
|
563
586
|
}
|
|
564
|
-
|
|
565
|
-
|
|
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;
|
|
566
596
|
break;
|
|
567
597
|
// the settlement is done, last event to be received
|
|
568
|
-
case ark_1.SettlementEventType.
|
|
569
|
-
if (step !== ark_1.SettlementEventType.
|
|
598
|
+
case ark_1.SettlementEventType.BatchFinalized:
|
|
599
|
+
if (step !== ark_1.SettlementEventType.BatchFinalization) {
|
|
570
600
|
continue;
|
|
571
601
|
}
|
|
572
602
|
abortController.abort();
|
|
573
|
-
return event.
|
|
603
|
+
return event.commitmentTxid;
|
|
574
604
|
}
|
|
575
|
-
step = event.type;
|
|
576
605
|
}
|
|
577
606
|
}
|
|
578
607
|
catch (error) {
|
|
608
|
+
// close the stream
|
|
579
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 { }
|
|
580
615
|
throw error;
|
|
581
616
|
}
|
|
582
617
|
throw new Error("Settlement failed");
|
|
583
618
|
}
|
|
584
|
-
async
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
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
|
+
});
|
|
594
646
|
}
|
|
595
|
-
if (
|
|
596
|
-
|
|
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
|
+
})();
|
|
597
674
|
}
|
|
598
|
-
const
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
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;
|
|
611
694
|
}
|
|
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
695
|
}
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
if (broadcastedTxs.has(tx))
|
|
621
|
-
continue;
|
|
622
|
-
const txid = await this.onchainProvider.broadcastTransaction(tx);
|
|
623
|
-
broadcastedTxs.set(txid, true);
|
|
696
|
+
if (skip) {
|
|
697
|
+
return { skip };
|
|
624
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
|
+
};
|
|
625
713
|
}
|
|
626
714
|
// 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
|
-
}
|
|
715
|
+
async handleSettlementSigningEvent(event, sweepTapTreeRoot, session, vtxoGraph) {
|
|
632
716
|
// validate the unsigned vtxo tree
|
|
633
|
-
|
|
717
|
+
const commitmentTx = btc_signer_1.Transaction.fromPSBT(base_1.base64.decode(event.unsignedCommitmentTx));
|
|
718
|
+
(0, validation_1.validateVtxoTxGraph)(vtxoGraph, commitmentTx, sweepTapTreeRoot);
|
|
634
719
|
// TODO check if our registered outputs are in the vtxo tree
|
|
635
|
-
const
|
|
636
|
-
const settlementTx = btc_signer_1.Transaction.fromPSBT(settlementPsbt);
|
|
637
|
-
const sharedOutput = settlementTx.getOutput(0);
|
|
720
|
+
const sharedOutput = commitmentTx.getOutput(0);
|
|
638
721
|
if (!sharedOutput?.amount) {
|
|
639
722
|
throw new Error("Shared output not found");
|
|
640
723
|
}
|
|
641
|
-
session.init(
|
|
724
|
+
session.init(vtxoGraph, sweepTapTreeRoot, sharedOutput.amount);
|
|
642
725
|
await this.arkProvider.submitTreeNonces(event.id, base_1.hex.encode(session.getPublicKey()), session.getNonces());
|
|
643
726
|
}
|
|
644
727
|
async handleSettlementSigningNoncesGeneratedEvent(event, session) {
|
|
645
|
-
if (!this.arkProvider) {
|
|
646
|
-
throw new Error("Ark provider not configured");
|
|
647
|
-
}
|
|
648
728
|
session.setAggregatedNonces(event.treeNonces);
|
|
649
729
|
const signatures = session.sign();
|
|
650
730
|
await this.arkProvider.submitTreeSignatures(event.id, base_1.hex.encode(session.getPublicKey()), signatures);
|
|
651
731
|
}
|
|
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 = (0, payment_1.Address)(this.network).decode(infos.forfeitAddress);
|
|
659
|
-
const serverPkScript = payment_1.OutScript.encode(forfeitAddress);
|
|
732
|
+
async handleSettlementFinalizationEvent(event, inputs, forfeitOutputScript, connectorsGraph) {
|
|
660
733
|
// the signed forfeits transactions to submit
|
|
661
734
|
const signedForfeits = [];
|
|
662
735
|
const vtxos = await this.getVirtualCoins();
|
|
663
|
-
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));
|
|
664
737
|
let hasBoardingUtxos = false;
|
|
665
|
-
let
|
|
738
|
+
let connectorIndex = 0;
|
|
739
|
+
const connectorsLeaves = connectorsGraph?.leaves() || [];
|
|
666
740
|
for (const input of inputs) {
|
|
667
|
-
if (typeof input === "string")
|
|
668
|
-
continue; // skip notes
|
|
669
741
|
// check if the input is an offchain "virtual" coin
|
|
670
742
|
const vtxo = vtxos.find((vtxo) => vtxo.txid === input.txid && vtxo.vout === input.vout);
|
|
671
743
|
// boarding utxo, we need to sign the settlement tx
|
|
@@ -685,72 +757,247 @@ class Wallet {
|
|
|
685
757
|
continue;
|
|
686
758
|
// input found in the settlement tx, sign it
|
|
687
759
|
settlementPsbt.updateInput(i, {
|
|
688
|
-
tapLeafScript: [input.
|
|
760
|
+
tapLeafScript: [input.forfeitTapLeafScript],
|
|
689
761
|
});
|
|
690
762
|
inputIndexes.push(i);
|
|
691
763
|
}
|
|
692
764
|
settlementPsbt = await this.identity.sign(settlementPsbt, inputIndexes);
|
|
693
765
|
continue;
|
|
694
766
|
}
|
|
695
|
-
if (
|
|
696
|
-
//
|
|
697
|
-
|
|
698
|
-
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;
|
|
699
770
|
}
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
const fees = txSizeEstimator_1.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");
|
|
771
|
+
if (connectorsLeaves.length === 0) {
|
|
772
|
+
throw new Error("connectors not received");
|
|
713
773
|
}
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
if (leaf.txid === connectorOutpoint.txid) {
|
|
717
|
-
try {
|
|
718
|
-
const connectorTx = btc_signer_1.Transaction.fromPSBT(base_1.base64.decode(leaf.tx));
|
|
719
|
-
connectorOutput = connectorTx.getOutput(connectorOutpoint.vout);
|
|
720
|
-
break;
|
|
721
|
-
}
|
|
722
|
-
catch {
|
|
723
|
-
throw new Error("Invalid connector tx");
|
|
724
|
-
}
|
|
725
|
-
}
|
|
774
|
+
if (connectorIndex >= connectorsLeaves.length) {
|
|
775
|
+
throw new Error("not enough connectors received");
|
|
726
776
|
}
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
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");
|
|
731
782
|
}
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
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);
|
|
746
809
|
// do not sign the connector input
|
|
747
|
-
forfeitTx = await this.identity.sign(forfeitTx, [
|
|
810
|
+
forfeitTx = await this.identity.sign(forfeitTx, [0]);
|
|
748
811
|
signedForfeits.push(base_1.base64.encode(forfeitTx.toPSBT()));
|
|
749
812
|
}
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
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);
|
|
753
881
|
}
|
|
754
882
|
}
|
|
755
883
|
exports.Wallet = Wallet;
|
|
756
|
-
Wallet.
|
|
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
|
+
}
|