@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.
Files changed (114) hide show
  1. package/README.md +157 -174
  2. package/dist/cjs/arknote/index.js +61 -58
  3. package/dist/cjs/bip322/errors.js +13 -0
  4. package/dist/cjs/bip322/index.js +178 -0
  5. package/dist/cjs/forfeit.js +14 -25
  6. package/dist/cjs/identity/singleKey.js +68 -0
  7. package/dist/cjs/index.js +43 -17
  8. package/dist/cjs/providers/ark.js +261 -321
  9. package/dist/cjs/providers/indexer.js +525 -0
  10. package/dist/cjs/providers/onchain.js +193 -15
  11. package/dist/cjs/script/address.js +48 -17
  12. package/dist/cjs/script/base.js +120 -3
  13. package/dist/cjs/script/default.js +18 -4
  14. package/dist/cjs/script/tapscript.js +61 -20
  15. package/dist/cjs/script/vhtlc.js +85 -7
  16. package/dist/cjs/tree/signingSession.js +63 -106
  17. package/dist/cjs/tree/txTree.js +193 -0
  18. package/dist/cjs/tree/validation.js +79 -155
  19. package/dist/cjs/utils/anchor.js +35 -0
  20. package/dist/cjs/utils/arkTransaction.js +108 -0
  21. package/dist/cjs/utils/transactionHistory.js +84 -72
  22. package/dist/cjs/utils/txSizeEstimator.js +12 -0
  23. package/dist/cjs/utils/unknownFields.js +211 -0
  24. package/dist/cjs/wallet/index.js +12 -0
  25. package/dist/cjs/wallet/onchain.js +201 -0
  26. package/dist/cjs/wallet/ramps.js +95 -0
  27. package/dist/cjs/wallet/serviceWorker/db/vtxo/idb.js +32 -0
  28. package/dist/cjs/wallet/serviceWorker/request.js +15 -12
  29. package/dist/cjs/wallet/serviceWorker/response.js +22 -27
  30. package/dist/cjs/wallet/serviceWorker/utils.js +8 -0
  31. package/dist/cjs/wallet/serviceWorker/wallet.js +61 -34
  32. package/dist/cjs/wallet/serviceWorker/worker.js +120 -108
  33. package/dist/cjs/wallet/unroll.js +270 -0
  34. package/dist/cjs/wallet/wallet.js +701 -454
  35. package/dist/esm/arknote/index.js +61 -57
  36. package/dist/esm/bip322/errors.js +9 -0
  37. package/dist/esm/bip322/index.js +174 -0
  38. package/dist/esm/forfeit.js +15 -26
  39. package/dist/esm/identity/singleKey.js +64 -0
  40. package/dist/esm/index.js +31 -12
  41. package/dist/esm/providers/ark.js +259 -320
  42. package/dist/esm/providers/indexer.js +521 -0
  43. package/dist/esm/providers/onchain.js +193 -15
  44. package/dist/esm/script/address.js +48 -17
  45. package/dist/esm/script/base.js +120 -3
  46. package/dist/esm/script/default.js +18 -4
  47. package/dist/esm/script/tapscript.js +61 -20
  48. package/dist/esm/script/vhtlc.js +85 -7
  49. package/dist/esm/tree/signingSession.js +65 -108
  50. package/dist/esm/tree/txTree.js +189 -0
  51. package/dist/esm/tree/validation.js +75 -152
  52. package/dist/esm/utils/anchor.js +31 -0
  53. package/dist/esm/utils/arkTransaction.js +105 -0
  54. package/dist/esm/utils/transactionHistory.js +84 -72
  55. package/dist/esm/utils/txSizeEstimator.js +12 -0
  56. package/dist/esm/utils/unknownFields.js +173 -0
  57. package/dist/esm/wallet/index.js +9 -0
  58. package/dist/esm/wallet/onchain.js +196 -0
  59. package/dist/esm/wallet/ramps.js +91 -0
  60. package/dist/esm/wallet/serviceWorker/db/vtxo/idb.js +32 -0
  61. package/dist/esm/wallet/serviceWorker/request.js +15 -12
  62. package/dist/esm/wallet/serviceWorker/response.js +22 -27
  63. package/dist/esm/wallet/serviceWorker/utils.js +8 -0
  64. package/dist/esm/wallet/serviceWorker/wallet.js +62 -35
  65. package/dist/esm/wallet/serviceWorker/worker.js +120 -108
  66. package/dist/esm/wallet/unroll.js +267 -0
  67. package/dist/esm/wallet/wallet.js +674 -461
  68. package/dist/types/arknote/index.d.ts +40 -13
  69. package/dist/types/bip322/errors.d.ts +6 -0
  70. package/dist/types/bip322/index.d.ts +57 -0
  71. package/dist/types/forfeit.d.ts +2 -14
  72. package/dist/types/identity/singleKey.d.ts +27 -0
  73. package/dist/types/index.d.ts +24 -12
  74. package/dist/types/providers/ark.d.ts +114 -95
  75. package/dist/types/providers/indexer.d.ts +186 -0
  76. package/dist/types/providers/onchain.d.ts +41 -11
  77. package/dist/types/script/address.d.ts +26 -2
  78. package/dist/types/script/base.d.ts +13 -3
  79. package/dist/types/script/default.d.ts +22 -0
  80. package/dist/types/script/tapscript.d.ts +61 -5
  81. package/dist/types/script/vhtlc.d.ts +27 -0
  82. package/dist/types/tree/signingSession.d.ts +5 -5
  83. package/dist/types/tree/txTree.d.ts +28 -0
  84. package/dist/types/tree/validation.d.ts +15 -22
  85. package/dist/types/utils/anchor.d.ts +19 -0
  86. package/dist/types/utils/arkTransaction.d.ts +27 -0
  87. package/dist/types/utils/transactionHistory.d.ts +7 -1
  88. package/dist/types/utils/txSizeEstimator.d.ts +3 -0
  89. package/dist/types/utils/unknownFields.d.ts +83 -0
  90. package/dist/types/wallet/index.d.ts +51 -50
  91. package/dist/types/wallet/onchain.d.ts +49 -0
  92. package/dist/types/wallet/ramps.d.ts +32 -0
  93. package/dist/types/wallet/serviceWorker/db/vtxo/idb.d.ts +2 -0
  94. package/dist/types/wallet/serviceWorker/db/vtxo/index.d.ts +2 -0
  95. package/dist/types/wallet/serviceWorker/request.d.ts +14 -16
  96. package/dist/types/wallet/serviceWorker/response.d.ts +17 -19
  97. package/dist/types/wallet/serviceWorker/utils.d.ts +8 -0
  98. package/dist/types/wallet/serviceWorker/wallet.d.ts +36 -8
  99. package/dist/types/wallet/serviceWorker/worker.d.ts +7 -3
  100. package/dist/types/wallet/unroll.d.ts +102 -0
  101. package/dist/types/wallet/wallet.d.ts +71 -25
  102. package/package.json +37 -35
  103. package/dist/cjs/identity/inMemoryKey.js +0 -40
  104. package/dist/cjs/tree/vtxoTree.js +0 -231
  105. package/dist/cjs/utils/coinselect.js +0 -73
  106. package/dist/cjs/utils/psbt.js +0 -137
  107. package/dist/esm/identity/inMemoryKey.js +0 -36
  108. package/dist/esm/tree/vtxoTree.js +0 -191
  109. package/dist/esm/utils/coinselect.js +0 -69
  110. package/dist/esm/utils/psbt.js +0 -131
  111. package/dist/types/identity/inMemoryKey.d.ts +0 -12
  112. package/dist/types/tree/vtxoTree.d.ts +0 -33
  113. package/dist/types/utils/coinselect.d.ts +0 -21
  114. 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 psbt_2 = require("../utils/psbt");
55
+ const arkTransaction_1 = require("../utils/arkTransaction");
23
56
  const arknote_1 = require("../arknote");
24
- // Wallet does not store any data and rely on the Ark and onchain providers to fetch utxos and vtxos
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, onchainProvider, onchainP2TR, arkProvider, arkServerPublicKey, offchainTapscript, boardingTapscript) {
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
- let arkProvider;
45
- if (config.arkServerUrl) {
46
- arkProvider = new ark_1.RestArkProvider(config.arkServerUrl);
47
- }
48
- // Save onchain Taproot address key-path only
49
- const onchainP2TR = (0, payment_1.p2tr)(pubkey, undefined, network);
50
- if (arkProvider) {
51
- const info = await arkProvider.getInfo();
52
- if (info.network !== config.network) {
53
- throw new Error(`The Ark Server URL expects ${info.network} but ${config.network} was configured`);
54
- }
55
- const exitTimelock = {
56
- value: info.unilateralExitDelay,
57
- type: info.unilateralExitDelay < 512n ? "blocks" : "seconds",
58
- };
59
- const boardingTimelock = {
60
- value: info.unilateralExitDelay * 2n,
61
- type: info.unilateralExitDelay * 2n < 512n ? "blocks" : "seconds",
62
- };
63
- // Generate tapscripts for offchain and boarding address
64
- const serverPubKey = base_1.hex.decode(info.pubkey).slice(1);
65
- const bareVtxoTapscript = new default_1.DefaultVtxo.Script({
66
- pubKey: pubkey,
67
- serverPubKey,
68
- csvTimelock: exitTimelock,
69
- });
70
- const boardingTapscript = new default_1.DefaultVtxo.Script({
71
- pubKey: pubkey,
72
- serverPubKey,
73
- csvTimelock: boardingTimelock,
74
- });
75
- // Save tapscripts
76
- const offchainTapscript = bareVtxoTapscript;
77
- return new Wallet(config.identity, network, onchainProvider, onchainP2TR, arkProvider, serverPubKey, offchainTapscript, boardingTapscript);
78
- }
79
- return new Wallet(config.identity, network, onchainProvider, onchainP2TR);
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 offchainAddress() {
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
- const addressInfo = {
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
- getAddressInfo() {
125
- if (!this.arkProvider ||
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
- // Get onchain coins
152
- const coins = await this.getCoins();
153
- const onchainConfirmed = coins
154
- .filter((coin) => coin.status.confirmed)
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
- const onchainUnconfirmed = coins
157
- .filter((coin) => !coin.status.confirmed)
175
+ preconfirmed = vtxos
176
+ .filter((coin) => coin.virtualStatus.state === "preconfirmed")
158
177
  .reduce((sum, coin) => sum + coin.value, 0);
159
- const onchainTotal = onchainConfirmed + onchainUnconfirmed;
160
- // Get offchain coins if Ark provider is configured
161
- let offchainSettled = 0;
162
- let offchainPending = 0;
163
- let offchainSwept = 0;
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
- onchain: {
179
- confirmed: onchainConfirmed,
180
- unconfirmed: onchainUnconfirmed,
181
- total: onchainTotal,
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
- total: onchainTotal + offchainTotal,
189
+ settled,
190
+ preconfirmed,
191
+ available: settled + preconfirmed,
192
+ recoverable,
193
+ total: totalBoarding + totalOffchain,
190
194
  };
191
195
  }
192
- async getCoins() {
193
- // TODO: add caching logic to lower the number of requests to provider
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
- tapLeafScript: forfeit,
211
- scripts: encodedOffchainTapscript,
203
+ forfeitTapLeafScript: forfeit,
204
+ intentTapLeafScript: exit,
205
+ tapTree: encodedOffchainTapscript,
212
206
  }));
213
207
  }
214
- async getVirtualCoins() {
215
- if (!this.arkProvider) {
216
- return [];
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
- const address = await this.getAddress();
219
- if (!address.offchain) {
220
- return [];
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 this.arkProvider
223
- .getVirtualCoins(address.offchain)
224
- .then(({ spendableVtxos }) => spendableVtxos);
229
+ return vtxos;
225
230
  }
226
231
  async getTransactionHistory() {
227
- if (!this.arkProvider) {
232
+ if (!this.indexerProvider) {
228
233
  return [];
229
234
  }
230
- const { spendableVtxos, spentVtxos } = await this.arkProvider.getVirtualCoins(this.offchainAddress.encode());
231
- const { boardingTxs, roundsToIgnore } = await this.getBoardingTxs();
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, roundsToIgnore);
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
- if (!this.boardingAddress) {
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 roundsToIgnore = new Set();
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
- roundsToIgnore.add(spentStatus.txid);
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 ? "swept" : "pending",
274
- batchTxID: spentStatus?.spent
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
- roundTxid: "",
292
- redeemTxid: "",
306
+ commitmentTxid: "",
307
+ arkTxid: "",
293
308
  },
294
309
  amount: utxo.value,
295
310
  type: _1.TxType.TxReceived,
296
- settled: utxo.virtualStatus.state === "swept",
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
- roundsToIgnore,
325
+ commitmentsToIgnore,
311
326
  };
312
327
  }
313
328
  async getBoardingUtxos() {
314
- if (!this.boardingAddress || !this.boardingTapscript) {
315
- throw new Error("Boarding address not configured");
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
- tapLeafScript: forfeit,
323
- scripts: encodedBoardingTapscript,
336
+ forfeitTapLeafScript: forfeit,
337
+ intentTapLeafScript: exit,
338
+ tapTree: encodedBoardingTapscript,
324
339
  }));
325
340
  }
326
- async sendBitcoin(params, zeroFee = true) {
341
+ async sendBitcoin(params) {
327
342
  if (params.amount <= 0) {
328
343
  throw new Error("Amount must be positive");
329
344
  }
330
- // If Ark is configured and amount is suitable, send via offchain
331
- if (this.arkProvider && this.isOffchainSuitable(params.address)) {
332
- return this.sendOffchain(params, zeroFee);
333
- }
334
- // Otherwise, send via onchain
335
- return this.sendOnchain(params);
336
- }
337
- isOffchainSuitable(address) {
338
- try {
339
- 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
- address: params.address,
363
+ script: outputScript,
406
364
  amount: BigInt(params.amount),
407
365
  },
408
366
  ];
409
367
  // add change output if needed
410
- if (selected.changeAmount > 0) {
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
- address: this.offchainAddress.encode(),
373
+ script: changeOutputScript,
413
374
  amount: BigInt(selected.changeAmount),
414
375
  });
415
376
  }
416
- const scripts = this.offchainTapscript.encode();
417
- let tx = (0, psbt_2.createVirtualTx)(selected.inputs.map((input) => ({
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
- scripts,
421
- })), outputs);
422
- tx = await this.identity.sign(tx);
423
- const psbt = base_1.base64.encode(tx.toPSBT());
424
- return this.arkProvider.submitVirtualTx(psbt);
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.offchainAddress.encode(),
425
+ address: await this.getAddress(),
463
426
  amount: BigInt(amount),
464
427
  },
465
428
  ],
466
429
  };
467
430
  }
468
- // register inputs
469
- const { requestId } = await this.arkProvider.registerInputsForNextRound(params.inputs.map((input) => {
470
- if (typeof input === "string") {
471
- return input;
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
- return {
474
- outpoint: input,
475
- tapscripts: input.scripts,
476
- };
477
- }));
478
- const hasOffchainOutputs = params.outputs.some((output) => this.isOffchainSuitable(output.address));
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
- // register outputs
487
- await this.arkProvider.registerOutputsForNextRound(requestId, params.outputs, signingPublicKeys);
488
- // start pinging every seconds
489
- const interval = setInterval(() => {
490
- this.arkProvider?.ping(requestId).catch(stopPing);
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
- if (!hasOffchainOutputs) {
505
- // if there are no offchain outputs, we don't have to handle musig2 tree signatures
506
- // we can directly advance to the finalization step
507
- step = ark_1.SettlementEventType.SigningNoncesGenerated;
508
- }
509
- const info = await this.arkProvider.getInfo();
510
- const sweepTapscript = tapscript_1.CSVMultisigTapscript.encode({
511
- timelock: {
512
- value: info.batchExpiry,
513
- type: info.batchExpiry >= 512n ? "seconds" : "blocks",
514
- },
515
- pubkeys: [base_1.hex.decode(info.pubkey).slice(1)],
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.Failed:
525
- if (step === undefined) {
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
- stopPing();
529
- throw new Error(event.reason);
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.SigningStart:
533
- if (step !== undefined) {
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 found");
554
+ throw new Error("Signing session not set");
540
555
  }
541
- await this.handleSettlementSigningEvent(event, sweepTapTreeRoot, session);
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.SigningNoncesGenerated:
547
- if (step !== ark_1.SettlementEventType.SigningStart) {
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 found");
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.Finalization:
561
- if (step !== ark_1.SettlementEventType.SigningNoncesGenerated) {
583
+ case ark_1.SettlementEventType.BatchFinalization:
584
+ if (step !== ark_1.SettlementEventType.TreeNoncesAggregated) {
562
585
  continue;
563
586
  }
564
- stopPing();
565
- await this.handleSettlementFinalizationEvent(event, params.inputs, info);
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.Finalized:
569
- if (step !== ark_1.SettlementEventType.Finalization) {
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.roundTxid;
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 exit(outpoints) {
585
- // TODO store the exit branches in repository
586
- // exit should not depend on the ark provider
587
- if (!this.arkProvider) {
588
- throw new Error("Ark provider not configured");
589
- }
590
- let vtxos = await this.getVtxos();
591
- if (outpoints && outpoints.length > 0) {
592
- vtxos = vtxos.filter((vtxo) => outpoints.some((outpoint) => vtxo.txid === outpoint.txid &&
593
- vtxo.vout === outpoint.vout));
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 (vtxos.length === 0) {
596
- throw new Error("No vtxos to exit");
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 trees = new Map();
599
- const transactions = [];
600
- for (const vtxo of vtxos) {
601
- const batchTxid = vtxo.virtualStatus.batchTxID;
602
- if (!batchTxid)
603
- continue;
604
- if (!trees.has(batchTxid)) {
605
- const round = await this.arkProvider.getRound(batchTxid);
606
- trees.set(batchTxid, round.vtxoTree);
607
- }
608
- const tree = trees.get(batchTxid);
609
- if (!tree) {
610
- throw new Error("Tree not found");
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
- const broadcastedTxs = new Map();
619
- for (const tx of transactions) {
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
- (0, validation_1.validateVtxoTree)(event.unsignedSettlementTx, vtxoTree, sweepTapTreeRoot);
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 settlementPsbt = base_1.base64.decode(event.unsignedSettlementTx);
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(vtxoTree, sweepTapTreeRoot, sharedOutput.amount);
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, infos) {
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.roundTx));
736
+ let settlementPsbt = btc_signer_1.Transaction.fromPSBT(base_1.base64.decode(event.commitmentTx));
664
737
  let hasBoardingUtxos = false;
665
- let connectorsTreeValid = false;
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.tapLeafScript],
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 (!connectorsTreeValid) {
696
- // validate that the connectors tree is valid and contains our expected connectors
697
- (0, validation_1.validateConnectorsTree)(event.roundTx, event.connectors);
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
- const forfeitControlBlock = psbt_1.TaprootControlBlock.encode(input.tapLeafScript[0]);
701
- const tapscript = (0, tapscript_1.decodeTapscript)((0, base_2.scriptFromTapLeafScript)(input.tapLeafScript));
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
- let connectorOutput;
715
- for (const leaf of connectorsLeaves) {
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
- if (!connectorOutput ||
728
- !connectorOutput.amount ||
729
- !connectorOutput.script) {
730
- throw new Error("Connector output not found");
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
- let forfeitTx = (0, forfeit_1.buildForfeitTx)({
733
- connectorInput: connectorOutpoint,
734
- connectorAmount: connectorOutput.amount,
735
- feeAmount: fees,
736
- serverPkScript,
737
- connectorPkScript: connectorOutput.script,
738
- vtxoAmount: BigInt(vtxo.value),
739
- vtxoInput: input,
740
- vtxoPkScript: base_2.VtxoScript.decode(input.scripts).pkScript,
741
- });
742
- // add the tapscript
743
- forfeitTx.updateInput(1, {
744
- tapLeafScript: [input.tapLeafScript],
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, [1]);
810
+ forfeitTx = await this.identity.sign(forfeitTx, [0]);
748
811
  signedForfeits.push(base_1.base64.encode(forfeitTx.toPSBT()));
749
812
  }
750
- await this.arkProvider.submitSignedForfeitTxs(signedForfeits, hasBoardingUtxos
751
- ? base_1.base64.encode(settlementPsbt.toPSBT())
752
- : undefined);
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.FEE_RATE = 1; // sats/vbyte
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
+ }