@arkade-os/sdk 0.4.14 → 0.4.16
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 +287 -215
- package/dist/cjs/arkfee/estimator.js +1 -1
- package/dist/cjs/arkfee/types.js +2 -1
- package/dist/cjs/arknote/index.js +43 -4
- package/dist/cjs/bip322/index.js +1 -1
- package/dist/cjs/contracts/arkcontract.js +1 -1
- package/dist/cjs/contracts/contractManager.js +40 -24
- package/dist/cjs/contracts/contractWatcher.js +29 -22
- package/dist/cjs/contracts/handlers/default.js +1 -1
- package/dist/cjs/contracts/handlers/delegate.js +1 -1
- package/dist/cjs/contracts/handlers/helpers.js +25 -1
- package/dist/cjs/contracts/handlers/vhtlc.js +2 -4
- package/dist/cjs/extension/asset/assetGroup.js +92 -5
- package/dist/cjs/extension/asset/assetId.js +67 -3
- package/dist/cjs/extension/asset/assetInput.js +18 -0
- package/dist/cjs/extension/asset/assetOutput.js +15 -0
- package/dist/cjs/extension/asset/assetRef.js +66 -0
- package/dist/cjs/extension/asset/metadata.js +15 -0
- package/dist/cjs/extension/asset/packet.js +4 -1
- package/dist/cjs/extension/index.js +1 -1
- package/dist/cjs/forfeit.js +14 -0
- package/dist/cjs/identity/index.js +6 -0
- package/dist/cjs/identity/seedIdentity.js +5 -5
- package/dist/cjs/identity/singleKey.js +4 -0
- package/dist/cjs/index.js +5 -3
- package/dist/cjs/intent/index.js +28 -12
- package/dist/cjs/providers/ark.js +3 -2
- package/dist/cjs/providers/delegator.js +20 -1
- package/dist/cjs/providers/expoArk.js +2 -2
- package/dist/cjs/providers/indexer.js +2 -2
- package/dist/cjs/providers/onchain.js +2 -1
- package/dist/cjs/repositories/realm/schemas.js +2 -2
- package/dist/cjs/repositories/realm/types.js +1 -1
- package/dist/cjs/script/address.js +37 -6
- package/dist/cjs/script/base.js +70 -1
- package/dist/cjs/script/default.js +3 -0
- package/dist/cjs/script/delegate.js +4 -0
- package/dist/cjs/script/tapscript.js +25 -4
- package/dist/cjs/script/vhtlc.js +35 -27
- package/dist/cjs/storage/fileSystem.js +1 -1
- package/dist/cjs/storage/inMemory.js +1 -1
- package/dist/cjs/storage/indexedDB.js +1 -1
- package/dist/cjs/storage/localStorage.js +1 -1
- package/dist/cjs/tree/validation.js +1 -1
- package/dist/cjs/utils/arkTransaction.js +5 -5
- package/dist/cjs/utils/bip21.js +16 -3
- package/dist/cjs/utils/syncCursors.js +4 -4
- package/dist/cjs/utils/transaction.js +1 -1
- package/dist/cjs/utils/transactionHistory.js +11 -11
- package/dist/cjs/utils/unknownFields.js +3 -3
- package/dist/cjs/wallet/asset-manager.js +4 -4
- package/dist/cjs/wallet/batch.js +5 -5
- package/dist/cjs/wallet/delegator.js +9 -8
- package/dist/cjs/wallet/expo/background.js +3 -3
- package/dist/cjs/wallet/expo/wallet.js +7 -7
- package/dist/cjs/wallet/index.js +43 -0
- package/dist/cjs/wallet/onchain.js +43 -5
- package/dist/cjs/wallet/ramps.js +44 -14
- package/dist/cjs/wallet/serviceWorker/wallet-message-handler.js +22 -22
- package/dist/cjs/wallet/serviceWorker/wallet.js +28 -24
- package/dist/cjs/wallet/unroll.js +12 -8
- package/dist/cjs/wallet/utils.js +1 -1
- package/dist/cjs/wallet/vtxo-manager.js +123 -82
- package/dist/cjs/wallet/wallet.js +231 -98
- package/dist/cjs/worker/expo/asyncStorageTaskQueue.js +1 -1
- package/dist/cjs/worker/expo/processors/contractPollProcessor.js +2 -2
- package/dist/cjs/worker/expo/taskRunner.js +3 -3
- package/dist/cjs/worker/messageBus.js +3 -0
- package/dist/esm/arkfee/estimator.js +1 -1
- package/dist/esm/arkfee/types.js +2 -1
- package/dist/esm/arknote/index.js +43 -4
- package/dist/esm/bip322/index.js +1 -1
- package/dist/esm/contracts/arkcontract.js +1 -1
- package/dist/esm/contracts/contractManager.js +40 -24
- package/dist/esm/contracts/contractWatcher.js +29 -22
- package/dist/esm/contracts/handlers/default.js +1 -1
- package/dist/esm/contracts/handlers/delegate.js +1 -1
- package/dist/esm/contracts/handlers/helpers.js +24 -1
- package/dist/esm/contracts/handlers/vhtlc.js +3 -5
- package/dist/esm/extension/asset/assetGroup.js +92 -5
- package/dist/esm/extension/asset/assetId.js +67 -3
- package/dist/esm/extension/asset/assetInput.js +18 -0
- package/dist/esm/extension/asset/assetOutput.js +15 -0
- package/dist/esm/extension/asset/assetRef.js +66 -0
- package/dist/esm/extension/asset/metadata.js +15 -0
- package/dist/esm/extension/asset/packet.js +4 -1
- package/dist/esm/extension/index.js +1 -1
- package/dist/esm/forfeit.js +14 -0
- package/dist/esm/identity/index.js +5 -0
- package/dist/esm/identity/seedIdentity.js +5 -5
- package/dist/esm/identity/singleKey.js +4 -0
- package/dist/esm/index.js +3 -2
- package/dist/esm/intent/index.js +28 -12
- package/dist/esm/providers/ark.js +3 -2
- package/dist/esm/providers/delegator.js +20 -1
- package/dist/esm/providers/expoArk.js +2 -2
- package/dist/esm/providers/indexer.js +2 -2
- package/dist/esm/providers/onchain.js +2 -1
- package/dist/esm/repositories/realm/schemas.js +2 -2
- package/dist/esm/repositories/realm/types.js +1 -1
- package/dist/esm/script/address.js +37 -6
- package/dist/esm/script/base.js +70 -1
- package/dist/esm/script/default.js +3 -0
- package/dist/esm/script/delegate.js +4 -0
- package/dist/esm/script/tapscript.js +25 -4
- package/dist/esm/script/vhtlc.js +35 -27
- package/dist/esm/storage/fileSystem.js +1 -1
- package/dist/esm/storage/inMemory.js +1 -1
- package/dist/esm/storage/indexedDB.js +1 -1
- package/dist/esm/storage/localStorage.js +1 -1
- package/dist/esm/tree/validation.js +1 -1
- package/dist/esm/utils/arkTransaction.js +5 -5
- package/dist/esm/utils/bip21.js +16 -3
- package/dist/esm/utils/syncCursors.js +4 -4
- package/dist/esm/utils/transaction.js +1 -1
- package/dist/esm/utils/transactionHistory.js +11 -11
- package/dist/esm/utils/unknownFields.js +3 -3
- package/dist/esm/wallet/asset-manager.js +4 -4
- package/dist/esm/wallet/batch.js +5 -5
- package/dist/esm/wallet/delegator.js +9 -8
- package/dist/esm/wallet/expo/background.js +3 -3
- package/dist/esm/wallet/expo/wallet.js +7 -7
- package/dist/esm/wallet/index.js +43 -0
- package/dist/esm/wallet/onchain.js +43 -5
- package/dist/esm/wallet/ramps.js +44 -14
- package/dist/esm/wallet/serviceWorker/wallet-message-handler.js +22 -22
- package/dist/esm/wallet/serviceWorker/wallet.js +28 -24
- package/dist/esm/wallet/unroll.js +12 -8
- package/dist/esm/wallet/utils.js +1 -1
- package/dist/esm/wallet/vtxo-manager.js +122 -81
- package/dist/esm/wallet/wallet.js +232 -99
- package/dist/esm/worker/expo/asyncStorageTaskQueue.js +1 -1
- package/dist/esm/worker/expo/processors/contractPollProcessor.js +2 -2
- package/dist/esm/worker/expo/taskRunner.js +3 -3
- package/dist/esm/worker/messageBus.js +3 -0
- package/dist/types/arkfee/estimator.d.ts +1 -1
- package/dist/types/arkfee/types.d.ts +2 -1
- package/dist/types/arknote/index.d.ts +44 -4
- package/dist/types/bip322/index.d.ts +1 -1
- package/dist/types/contracts/arkcontract.d.ts +1 -1
- package/dist/types/contracts/contractManager.d.ts +40 -63
- package/dist/types/contracts/contractWatcher.d.ts +39 -18
- package/dist/types/contracts/handlers/default.d.ts +1 -1
- package/dist/types/contracts/handlers/delegate.d.ts +1 -1
- package/dist/types/contracts/handlers/helpers.d.ts +11 -1
- package/dist/types/contracts/types.d.ts +36 -26
- package/dist/types/extension/asset/assetGroup.d.ts +92 -1
- package/dist/types/extension/asset/assetId.d.ts +67 -3
- package/dist/types/extension/asset/assetInput.d.ts +18 -0
- package/dist/types/extension/asset/assetOutput.d.ts +15 -0
- package/dist/types/extension/asset/assetRef.d.ts +66 -0
- package/dist/types/extension/asset/metadata.d.ts +15 -0
- package/dist/types/extension/asset/packet.d.ts +4 -1
- package/dist/types/extension/index.d.ts +1 -1
- package/dist/types/forfeit.d.ts +14 -0
- package/dist/types/identity/index.d.ts +36 -0
- package/dist/types/identity/seedIdentity.d.ts +10 -8
- package/dist/types/identity/singleKey.d.ts +4 -0
- package/dist/types/index.d.ts +3 -3
- package/dist/types/intent/index.d.ts +19 -6
- package/dist/types/providers/ark.d.ts +40 -2
- package/dist/types/providers/delegator.d.ts +54 -1
- package/dist/types/providers/expoArk.d.ts +2 -2
- package/dist/types/providers/indexer.d.ts +105 -2
- package/dist/types/providers/onchain.d.ts +62 -1
- package/dist/types/repositories/realm/schemas.d.ts +2 -2
- package/dist/types/repositories/realm/types.d.ts +2 -2
- package/dist/types/repositories/walletRepository.d.ts +16 -0
- package/dist/types/script/address.d.ts +35 -2
- package/dist/types/script/base.d.ts +66 -1
- package/dist/types/script/default.d.ts +3 -0
- package/dist/types/script/delegate.d.ts +4 -0
- package/dist/types/script/tapscript.d.ts +17 -2
- package/dist/types/script/vhtlc.d.ts +35 -27
- package/dist/types/storage/fileSystem.d.ts +1 -1
- package/dist/types/storage/inMemory.d.ts +1 -1
- package/dist/types/storage/index.d.ts +1 -1
- package/dist/types/storage/indexedDB.d.ts +1 -1
- package/dist/types/storage/localStorage.d.ts +1 -1
- package/dist/types/utils/arkTransaction.d.ts +3 -3
- package/dist/types/utils/bip21.d.ts +17 -0
- package/dist/types/utils/syncCursors.d.ts +4 -4
- package/dist/types/utils/transaction.d.ts +1 -1
- package/dist/types/utils/transactionHistory.d.ts +3 -3
- package/dist/types/utils/unknownFields.d.ts +5 -5
- package/dist/types/wallet/asset-manager.d.ts +3 -3
- package/dist/types/wallet/batch.d.ts +27 -7
- package/dist/types/wallet/delegator.d.ts +10 -0
- package/dist/types/wallet/expo/background.d.ts +4 -4
- package/dist/types/wallet/expo/wallet.d.ts +10 -10
- package/dist/types/wallet/index.d.ts +457 -25
- package/dist/types/wallet/onchain.d.ts +42 -4
- package/dist/types/wallet/ramps.d.ts +40 -10
- package/dist/types/wallet/serviceWorker/wallet-message-handler.d.ts +4 -4
- package/dist/types/wallet/serviceWorker/wallet.d.ts +71 -33
- package/dist/types/wallet/unroll.d.ts +8 -6
- package/dist/types/wallet/vtxo-manager.d.ts +146 -93
- package/dist/types/wallet/wallet.d.ts +91 -33
- package/dist/types/worker/expo/asyncStorageTaskQueue.d.ts +1 -1
- package/dist/types/worker/expo/processors/contractPollProcessor.d.ts +1 -1
- package/dist/types/worker/expo/taskRunner.d.ts +6 -6
- package/dist/types/worker/messageBus.d.ts +5 -3
- package/package.json +18 -10
|
@@ -10,11 +10,12 @@ import { RestArkProvider, } from '../providers/ark.js';
|
|
|
10
10
|
import { buildForfeitTx } from '../forfeit.js';
|
|
11
11
|
import { validateConnectorsTxGraph, validateVtxoTxGraph, } from '../tree/validation.js';
|
|
12
12
|
import { validateBatchRecipients } from './validation.js';
|
|
13
|
+
import { isBatchSignable } from '../identity/index.js';
|
|
13
14
|
import { isExpired, isRecoverable, isSpendable, isSubdust, TxType, } from './index.js';
|
|
14
15
|
import { createAssetPacket, selectedCoinsToAssetInputs, selectCoinsWithAsset, } from './asset.js';
|
|
15
16
|
import { VtxoScript } from '../script/base.js';
|
|
16
17
|
import { CSVMultisigTapscript } from '../script/tapscript.js';
|
|
17
|
-
import { buildOffchainTx, hasBoardingTxExpired, isValidArkAddress, } from '../utils/arkTransaction.js';
|
|
18
|
+
import { buildOffchainTx, combineTapscriptSigs, hasBoardingTxExpired, isValidArkAddress, } from '../utils/arkTransaction.js';
|
|
18
19
|
import { DEFAULT_RENEWAL_CONFIG, DEFAULT_SETTLEMENT_CONFIG, VtxoManager, } from './vtxo-manager.js';
|
|
19
20
|
import { ArkNote } from '../arknote/index.js';
|
|
20
21
|
import { Intent } from '../intent/index.js';
|
|
@@ -107,12 +108,12 @@ export class ReadonlyWallet {
|
|
|
107
108
|
const serverIsMainnet = info.network === "bitcoin";
|
|
108
109
|
if (identityIsMainnet && !serverIsMainnet) {
|
|
109
110
|
throw new Error(`Network mismatch: identity uses mainnet derivation (coin type 0) ` +
|
|
110
|
-
`but
|
|
111
|
+
`but the Arkade server is on ${info.network}. ` +
|
|
111
112
|
`Create identity with { isMainnet: false } to use testnet derivation.`);
|
|
112
113
|
}
|
|
113
114
|
if (!identityIsMainnet && serverIsMainnet) {
|
|
114
115
|
throw new Error(`Network mismatch: identity uses testnet derivation (coin type 1) ` +
|
|
115
|
-
`but
|
|
116
|
+
`but the Arkade server is on mainnet. ` +
|
|
116
117
|
`Create identity with { isMainnet: true } or omit isMainnet (defaults to mainnet).`);
|
|
117
118
|
}
|
|
118
119
|
}
|
|
@@ -184,6 +185,12 @@ export class ReadonlyWallet {
|
|
|
184
185
|
delegatorProvider: config.delegatorProvider,
|
|
185
186
|
};
|
|
186
187
|
}
|
|
188
|
+
/**
|
|
189
|
+
* Create a readonly wallet for querying balances, addresses, and history.
|
|
190
|
+
*
|
|
191
|
+
* @param config - Readonly wallet configuration
|
|
192
|
+
* @returns A readonly wallet instance
|
|
193
|
+
*/
|
|
187
194
|
static async create(config) {
|
|
188
195
|
const pubkey = await config.identity.xOnlyPublicKey();
|
|
189
196
|
if (!pubkey) {
|
|
@@ -202,12 +209,17 @@ export class ReadonlyWallet {
|
|
|
202
209
|
get defaultContractScript() {
|
|
203
210
|
return hex.encode(this.offchainTapscript.pkScript);
|
|
204
211
|
}
|
|
212
|
+
/** Returns the wallet's Arkade address. */
|
|
205
213
|
async getAddress() {
|
|
206
214
|
return this.arkAddress.encode();
|
|
207
215
|
}
|
|
216
|
+
/** Returns the onchain boarding address used to move funds into Arkade. */
|
|
208
217
|
async getBoardingAddress() {
|
|
209
218
|
return this.boardingTapscript.onchainAddress(this.network);
|
|
210
219
|
}
|
|
220
|
+
/**
|
|
221
|
+
* Return the wallet's combined onchain and offchain balances.
|
|
222
|
+
*/
|
|
211
223
|
async getBalance() {
|
|
212
224
|
const [boardingUtxos, vtxos] = await Promise.all([
|
|
213
225
|
this.getBoardingUtxos(),
|
|
@@ -239,7 +251,7 @@ export class ReadonlyWallet {
|
|
|
239
251
|
.reduce((sum, coin) => sum + coin.value, 0);
|
|
240
252
|
const totalBoarding = confirmed + unconfirmed;
|
|
241
253
|
const totalOffchain = settled + preconfirmed + recoverable;
|
|
242
|
-
// aggregate asset balances from spendable
|
|
254
|
+
// aggregate asset balances from spendable virtual outputs
|
|
243
255
|
const assetBalances = new Map();
|
|
244
256
|
for (const vtxo of vtxos) {
|
|
245
257
|
if (!isSpendable(vtxo))
|
|
@@ -269,11 +281,16 @@ export class ReadonlyWallet {
|
|
|
269
281
|
assets,
|
|
270
282
|
};
|
|
271
283
|
}
|
|
284
|
+
/**
|
|
285
|
+
* Return virtual outputs tracked by the wallet.
|
|
286
|
+
*
|
|
287
|
+
* @param filter - Optional flags controlling whether recoverable or unrolled VTXOs are included
|
|
288
|
+
*/
|
|
272
289
|
async getVtxos(filter) {
|
|
273
290
|
const { isDelta, fetchedExtended, address } = await this.syncVtxos();
|
|
274
291
|
const f = filter ?? { withRecoverable: true, withUnrolled: false };
|
|
275
292
|
// For delta syncs, read the full merged set from cache so old
|
|
276
|
-
//
|
|
293
|
+
// Virtual outputs that weren't in the delta are still returned.
|
|
277
294
|
const vtxos = isDelta
|
|
278
295
|
? await this.walletRepository.getVtxos(address)
|
|
279
296
|
: fetchedExtended;
|
|
@@ -288,8 +305,11 @@ export class ReadonlyWallet {
|
|
|
288
305
|
return !!(f.withUnrolled && vtxo.isUnrolled);
|
|
289
306
|
});
|
|
290
307
|
}
|
|
308
|
+
/**
|
|
309
|
+
* Return wallet transaction history derived from Arkade state and boarding transactions.
|
|
310
|
+
*/
|
|
291
311
|
async getTransactionHistory() {
|
|
292
|
-
// Delta-sync
|
|
312
|
+
// Delta-sync virtual outputs into cache, then build history from the cache.
|
|
293
313
|
const { isDelta, fetchedExtended, address } = await this.syncVtxos();
|
|
294
314
|
const allVtxos = isDelta
|
|
295
315
|
? await this.walletRepository.getVtxos(address)
|
|
@@ -301,7 +321,7 @@ export class ReadonlyWallet {
|
|
|
301
321
|
return buildTransactionHistory(allVtxos, boardingTxs, commitmentsToIgnore, getTxCreatedAt);
|
|
302
322
|
}
|
|
303
323
|
/**
|
|
304
|
-
* Delta-sync wallet
|
|
324
|
+
* Delta-sync wallet virtual outputs: fetch only changed virtual outputs since the last
|
|
305
325
|
* cursor, or do a full bootstrap when no cursor exists. Upserts
|
|
306
326
|
* the result into the cache and advances the sync cursors.
|
|
307
327
|
*
|
|
@@ -340,6 +360,19 @@ export class ReadonlyWallet {
|
|
|
340
360
|
}
|
|
341
361
|
const requestStartedAt = Date.now();
|
|
342
362
|
const allVtxos = [];
|
|
363
|
+
const extendWithScript = (vtxo) => {
|
|
364
|
+
const vtxoScript = vtxo.script
|
|
365
|
+
? scriptMap.get(vtxo.script)
|
|
366
|
+
: undefined;
|
|
367
|
+
if (!vtxoScript)
|
|
368
|
+
return undefined;
|
|
369
|
+
return {
|
|
370
|
+
...vtxo,
|
|
371
|
+
forfeitTapLeafScript: vtxoScript.forfeit(),
|
|
372
|
+
intentTapLeafScript: vtxoScript.forfeit(),
|
|
373
|
+
tapTree: vtxoScript.encode(),
|
|
374
|
+
};
|
|
375
|
+
};
|
|
343
376
|
// Full fetch for scripts with no cursor.
|
|
344
377
|
if (bootstrapScripts.length > 0) {
|
|
345
378
|
const response = await this.indexerProvider.getVtxos({
|
|
@@ -361,49 +394,79 @@ export class ReadonlyWallet {
|
|
|
361
394
|
allVtxos.push(...response.vtxos);
|
|
362
395
|
}
|
|
363
396
|
}
|
|
364
|
-
// Extend every fetched
|
|
397
|
+
// Extend every fetched virtual output and upsert into the cache.
|
|
365
398
|
const fetchedExtended = [];
|
|
366
399
|
for (const vtxo of allVtxos) {
|
|
367
|
-
const
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
if (!vtxoScript)
|
|
371
|
-
continue;
|
|
372
|
-
fetchedExtended.push({
|
|
373
|
-
...vtxo,
|
|
374
|
-
forfeitTapLeafScript: vtxoScript.forfeit(),
|
|
375
|
-
intentTapLeafScript: vtxoScript.forfeit(),
|
|
376
|
-
tapTree: vtxoScript.encode(),
|
|
377
|
-
});
|
|
400
|
+
const extended = extendWithScript(vtxo);
|
|
401
|
+
if (extended)
|
|
402
|
+
fetchedExtended.push(extended);
|
|
378
403
|
}
|
|
379
|
-
// Save
|
|
404
|
+
// Save virtual outputs first, then advance cursors only on success.
|
|
380
405
|
const cutoff = cursorCutoff(requestStartedAt);
|
|
381
406
|
await this.walletRepository.saveVtxos(address, fetchedExtended);
|
|
382
407
|
await advanceSyncCursors(this.walletRepository, Object.fromEntries(allScripts.map((s) => [s, cutoff])));
|
|
383
|
-
//
|
|
384
|
-
//
|
|
385
|
-
//
|
|
408
|
+
// Delta-sync reconciliation: full re-fetch for delta scripts.
|
|
409
|
+
//
|
|
410
|
+
// The delta fetch (above) only returns virtual outputs changed after the
|
|
411
|
+
// cursor, so it can miss preconfirmed virtual outputs that were consumed
|
|
412
|
+
// by a round between syncs. Rather than layering targeted
|
|
413
|
+
// queries (pendingOnly, spendableOnly) with pagination guards
|
|
414
|
+
// and set algebra, we perform a single unfiltered re-fetch for
|
|
415
|
+
// delta scripts. This is slightly more data over the wire but
|
|
416
|
+
// gives us complete, authoritative state in one call and keeps
|
|
417
|
+
// the reconciliation logic simple.
|
|
418
|
+
//
|
|
419
|
+
// Any cached non-spent virtual output that is absent from the full
|
|
420
|
+
// result set is marked spent; any virtual output whose state changed
|
|
421
|
+
// (e.g. preconfirmed → settled) is updated in place.
|
|
386
422
|
if (hasDelta) {
|
|
387
|
-
const { vtxos:
|
|
423
|
+
const { vtxos: fullVtxos, page: fullPage } = await this.indexerProvider.getVtxos({
|
|
388
424
|
scripts: deltaScripts,
|
|
389
|
-
pendingOnly: true,
|
|
390
425
|
});
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
426
|
+
// Reconciliation is best-effort: if the response is
|
|
427
|
+
// paginated we don't have a complete picture, so we skip
|
|
428
|
+
// rather than act on partial data. Wallets with enough
|
|
429
|
+
// virtual outputs to exceed a single page rely solely on the
|
|
430
|
+
// cursor-based delta mechanism for state updates.
|
|
431
|
+
const fullSetComplete = !fullPage || fullPage.total <= 1;
|
|
432
|
+
if (fullSetComplete) {
|
|
433
|
+
const fullOutpoints = new Map(fullVtxos.map((v) => [`${v.txid}:${v.vout}`, v]));
|
|
434
|
+
const deltaScriptSet = new Set(deltaScripts);
|
|
435
|
+
const cachedVtxos = await this.walletRepository.getVtxos(address);
|
|
436
|
+
const reconciledExtended = [];
|
|
437
|
+
for (const cached of cachedVtxos) {
|
|
438
|
+
if (!cached.script ||
|
|
439
|
+
!deltaScriptSet.has(cached.script) ||
|
|
440
|
+
cached.isSpent) {
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
const outpoint = `${cached.txid}:${cached.vout}`;
|
|
444
|
+
const fresh = fullOutpoints.get(outpoint);
|
|
445
|
+
if (!fresh) {
|
|
446
|
+
// Server no longer knows about this virtual output —
|
|
447
|
+
// it was spent between syncs.
|
|
448
|
+
reconciledExtended.push({
|
|
449
|
+
...cached,
|
|
450
|
+
isSpent: true,
|
|
451
|
+
});
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
const extended = extendWithScript(fresh);
|
|
455
|
+
if (extended &&
|
|
456
|
+
extended.virtualStatus.state !==
|
|
457
|
+
cached.virtualStatus.state) {
|
|
458
|
+
// State transitioned (e.g. preconfirmed →
|
|
459
|
+
// settled) — update the cached entry.
|
|
460
|
+
reconciledExtended.push(extended);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
if (reconciledExtended.length > 0) {
|
|
464
|
+
console.warn(`[ark-sdk] delta sync: reconciled ${reconciledExtended.length} stale VTXO(s) via full re-fetch`);
|
|
465
|
+
await this.walletRepository.saveVtxos(address, reconciledExtended);
|
|
466
|
+
}
|
|
404
467
|
}
|
|
405
|
-
|
|
406
|
-
|
|
468
|
+
else {
|
|
469
|
+
console.warn("[ark-sdk] delta sync: skipping reconciliation — full re-fetch was paginated");
|
|
407
470
|
}
|
|
408
471
|
}
|
|
409
472
|
return {
|
|
@@ -413,12 +476,15 @@ export class ReadonlyWallet {
|
|
|
413
476
|
};
|
|
414
477
|
}
|
|
415
478
|
/**
|
|
416
|
-
* Clear all
|
|
479
|
+
* Clear all virtual output sync cursors, forcing a full re-bootstrap on next sync.
|
|
417
480
|
* Useful for recovery after indexer reprocessing or debugging.
|
|
418
481
|
*/
|
|
419
482
|
async clearSyncCursors() {
|
|
420
483
|
await clearSyncCursors(this.walletRepository);
|
|
421
484
|
}
|
|
485
|
+
/**
|
|
486
|
+
* Build a transaction history view for the wallet's boarding address.
|
|
487
|
+
*/
|
|
422
488
|
async getBoardingTxs() {
|
|
423
489
|
const utxos = [];
|
|
424
490
|
const commitmentsToIgnore = new Set();
|
|
@@ -489,16 +555,25 @@ export class ReadonlyWallet {
|
|
|
489
555
|
commitmentsToIgnore,
|
|
490
556
|
};
|
|
491
557
|
}
|
|
558
|
+
/**
|
|
559
|
+
* Fetch and cache onchain inputs (UTXOs) received at the boarding address.
|
|
560
|
+
*/
|
|
492
561
|
async getBoardingUtxos() {
|
|
493
562
|
const boardingAddress = await this.getBoardingAddress();
|
|
494
563
|
const boardingUtxos = await this.onchainProvider.getCoins(boardingAddress);
|
|
495
564
|
const utxos = boardingUtxos.map((utxo) => {
|
|
496
565
|
return extendCoin(this, utxo);
|
|
497
566
|
});
|
|
498
|
-
// Save
|
|
567
|
+
// Save boarding inputs using unified repository
|
|
499
568
|
await this.walletRepository.saveUtxos(boardingAddress, utxos);
|
|
500
569
|
return utxos;
|
|
501
570
|
}
|
|
571
|
+
/**
|
|
572
|
+
* Subscribe to onchain and offchain notifications for newly received funds.
|
|
573
|
+
*
|
|
574
|
+
* @param eventCallback - Callback invoked when matching funds are detected
|
|
575
|
+
* @returns A function that stops the subscriptions
|
|
576
|
+
*/
|
|
502
577
|
async notifyIncomingFunds(eventCallback) {
|
|
503
578
|
const arkAddress = await this.getAddress();
|
|
504
579
|
const boardingAddress = await this.getBoardingAddress();
|
|
@@ -509,11 +584,11 @@ export class ReadonlyWallet {
|
|
|
509
584
|
return tx.vout.findIndex((v) => v.scriptpubkey_address === boardingAddress);
|
|
510
585
|
};
|
|
511
586
|
onchainStopFunc = await this.onchainProvider.watchAddresses([boardingAddress], (txs) => {
|
|
512
|
-
// find all
|
|
587
|
+
// find all onchain outputs belonging to our boarding address
|
|
513
588
|
const coins = txs
|
|
514
589
|
// filter txs where address is in output
|
|
515
590
|
.filter((tx) => findVoutOnTx(tx) !== -1)
|
|
516
|
-
// return
|
|
591
|
+
// return boarding input as Coin
|
|
517
592
|
.map((tx) => {
|
|
518
593
|
const { txid, status } = tx;
|
|
519
594
|
const vout = findVoutOnTx(tx);
|
|
@@ -538,8 +613,8 @@ export class ReadonlyWallet {
|
|
|
538
613
|
};
|
|
539
614
|
// Handle subscription updates asynchronously without blocking.
|
|
540
615
|
// Note: subscription covers all wallet scripts (default + delegate),
|
|
541
|
-
// but we can't determine which script each
|
|
542
|
-
// subscription event.
|
|
616
|
+
// but we can't determine which script each virtual output belongs to from the
|
|
617
|
+
// subscription event. Virtual outputs are extended with the current offchainTapscript;
|
|
543
618
|
// this is for notification/display only — not for spending.
|
|
544
619
|
// For correct extension metadata, use getVtxos() which queries per-script.
|
|
545
620
|
(async () => {
|
|
@@ -566,8 +641,9 @@ export class ReadonlyWallet {
|
|
|
566
641
|
};
|
|
567
642
|
return stopFunc;
|
|
568
643
|
}
|
|
644
|
+
/** Fetch Arkade transaction ids that are still pending final settlement. */
|
|
569
645
|
async fetchPendingTxs() {
|
|
570
|
-
// get non-swept
|
|
646
|
+
// get non-swept virtual outputs, rely on the indexer only in case DB doesn't have the right state
|
|
571
647
|
const scripts = await this.getWalletScripts();
|
|
572
648
|
let { vtxos } = await this.indexerProvider.getVtxos({
|
|
573
649
|
scripts,
|
|
@@ -608,7 +684,7 @@ export class ReadonlyWallet {
|
|
|
608
684
|
}
|
|
609
685
|
/**
|
|
610
686
|
* Build a map of scriptHex → VtxoScript for all wallet contracts,
|
|
611
|
-
* so
|
|
687
|
+
* so virtual outputs can be extended with the correct tapscript per contract.
|
|
612
688
|
*/
|
|
613
689
|
async getScriptMap() {
|
|
614
690
|
const map = new Map();
|
|
@@ -721,7 +797,7 @@ export class ReadonlyWallet {
|
|
|
721
797
|
address: await this.getAddress(),
|
|
722
798
|
state: "active",
|
|
723
799
|
});
|
|
724
|
-
// Also register the non-delegate version so old
|
|
800
|
+
// Also register the non-delegate version so old virtual outputs remain visible
|
|
725
801
|
const nonDelegateScript = new DefaultVtxo.Script({
|
|
726
802
|
pubKey: delegateScript.options.pubKey,
|
|
727
803
|
serverPubKey: delegateScript.options.serverPubKey,
|
|
@@ -759,6 +835,7 @@ export class ReadonlyWallet {
|
|
|
759
835
|
}
|
|
760
836
|
return manager;
|
|
761
837
|
}
|
|
838
|
+
/** Dispose wallet-owned managers and release background resources. */
|
|
762
839
|
async dispose() {
|
|
763
840
|
const manager = this._contractManager ??
|
|
764
841
|
(this._contractManagerInitializing
|
|
@@ -768,29 +845,30 @@ export class ReadonlyWallet {
|
|
|
768
845
|
this._contractManager = undefined;
|
|
769
846
|
this._contractManagerInitializing = undefined;
|
|
770
847
|
}
|
|
848
|
+
/** Async-dispose hook that forwards to `dispose()`. */
|
|
771
849
|
async [Symbol.asyncDispose]() {
|
|
772
850
|
await this.dispose();
|
|
773
851
|
}
|
|
774
852
|
}
|
|
775
853
|
/**
|
|
776
|
-
* Main wallet implementation for Bitcoin transactions with
|
|
777
|
-
* The wallet does not store any data locally and relies on
|
|
778
|
-
* providers to fetch
|
|
854
|
+
* Main wallet implementation for Bitcoin transactions with Arkade protocol support.
|
|
855
|
+
* The wallet does not store any data locally and relies on Arkade and onchain
|
|
856
|
+
* providers to fetch onchain and virtual outputs.
|
|
779
857
|
*
|
|
780
858
|
* @example
|
|
781
859
|
* ```typescript
|
|
782
860
|
* // Create a wallet with URL configuration
|
|
783
861
|
* const wallet = await Wallet.create({
|
|
784
|
-
* identity:
|
|
785
|
-
* arkServerUrl: 'https://
|
|
862
|
+
* identity: MnemonicIdentity.fromMnemonic('abandon abandon...'),
|
|
863
|
+
* arkServerUrl: 'https://arkade.computer',
|
|
786
864
|
* esploraUrl: 'https://mempool.space/api'
|
|
787
865
|
* });
|
|
788
866
|
*
|
|
789
867
|
* // Or with custom provider instances (e.g., for Expo/React Native)
|
|
790
868
|
* const wallet = await Wallet.create({
|
|
791
|
-
* identity:
|
|
792
|
-
* arkProvider: new ExpoArkProvider('https://
|
|
793
|
-
* indexerProvider: new ExpoIndexerProvider('https://
|
|
869
|
+
* identity: MnemonicIdentity.fromMnemonic('abandon abandon...'),
|
|
870
|
+
* arkProvider: new ExpoArkProvider('https://arkade.computer'),
|
|
871
|
+
* indexerProvider: new ExpoIndexerProvider('https://arkade.computer'),
|
|
794
872
|
* esploraUrl: 'https://mempool.space/api'
|
|
795
873
|
* });
|
|
796
874
|
*
|
|
@@ -799,9 +877,9 @@ export class ReadonlyWallet {
|
|
|
799
877
|
* const boardingAddress = await wallet.getBoardingAddress();
|
|
800
878
|
*
|
|
801
879
|
* // Send bitcoin
|
|
802
|
-
* const txid = await wallet.
|
|
803
|
-
* address: '
|
|
804
|
-
* amount: 50000
|
|
880
|
+
* const txid = await wallet.send({
|
|
881
|
+
* address: 'ark1q...',
|
|
882
|
+
* amount: 50000,
|
|
805
883
|
* });
|
|
806
884
|
* ```
|
|
807
885
|
*/
|
|
@@ -830,7 +908,7 @@ export class Wallet extends ReadonlyWallet {
|
|
|
830
908
|
this.forfeitOutputScript = forfeitOutputScript;
|
|
831
909
|
this.forfeitPubkey = forfeitPubkey;
|
|
832
910
|
/**
|
|
833
|
-
* Async mutex that serializes all operations submitting VTXOs to the
|
|
911
|
+
* Async mutex that serializes all operations submitting VTXOs to the Arkade
|
|
834
912
|
* server (`settle`, `send`, `sendBitcoin`). This prevents VtxoManager's
|
|
835
913
|
* background renewal from racing with user-initiated transactions for the
|
|
836
914
|
* same VTXO inputs.
|
|
@@ -910,6 +988,19 @@ export class Wallet extends ReadonlyWallet {
|
|
|
910
988
|
await super.dispose();
|
|
911
989
|
}
|
|
912
990
|
}
|
|
991
|
+
/**
|
|
992
|
+
* Create a full wallet and initialize its background managers.
|
|
993
|
+
*
|
|
994
|
+
* @param config - Wallet configuration
|
|
995
|
+
* @returns A wallet ready to query balances and send transactions
|
|
996
|
+
* @example
|
|
997
|
+
* ```typescript
|
|
998
|
+
* const wallet = await Wallet.create({
|
|
999
|
+
* identity,
|
|
1000
|
+
* arkServerUrl: 'https://arkade.computer',
|
|
1001
|
+
* });
|
|
1002
|
+
* ```
|
|
1003
|
+
*/
|
|
913
1004
|
static async create(config) {
|
|
914
1005
|
const pubkey = await config.identity.xOnlyPublicKey();
|
|
915
1006
|
if (!pubkey) {
|
|
@@ -941,7 +1032,7 @@ export class Wallet extends ReadonlyWallet {
|
|
|
941
1032
|
* @returns A readonly wallet with the same configuration but readonly identity
|
|
942
1033
|
* @example
|
|
943
1034
|
* ```typescript
|
|
944
|
-
* const wallet = await Wallet.create({ identity:
|
|
1035
|
+
* const wallet = await Wallet.create({ identity: MnemonicIdentity.fromMnemonic('abandon abandon...'), ... });
|
|
945
1036
|
* const readonlyWallet = await wallet.toReadonly();
|
|
946
1037
|
*
|
|
947
1038
|
* // Can query balance and addresses
|
|
@@ -949,7 +1040,7 @@ export class Wallet extends ReadonlyWallet {
|
|
|
949
1040
|
* const address = await readonlyWallet.getAddress();
|
|
950
1041
|
*
|
|
951
1042
|
* // But cannot send transactions (type error)
|
|
952
|
-
* // readonlyWallet.
|
|
1043
|
+
* // readonlyWallet.send(...); // TypeScript error
|
|
953
1044
|
* ```
|
|
954
1045
|
*/
|
|
955
1046
|
async toReadonly() {
|
|
@@ -959,19 +1050,22 @@ export class Wallet extends ReadonlyWallet {
|
|
|
959
1050
|
: this.identity; // Identity extends ReadonlyIdentity, so this is safe
|
|
960
1051
|
return new ReadonlyWallet(readonlyIdentity, this.network, this.onchainProvider, this.indexerProvider, this.arkServerPublicKey, this.offchainTapscript, this.boardingTapscript, this.dustAmount, this.walletRepository, this.contractRepository, this.delegatorProvider, this.watcherConfig);
|
|
961
1052
|
}
|
|
1053
|
+
/** Returns the delegator manager when delegation support is configured. */
|
|
962
1054
|
async getDelegatorManager() {
|
|
963
1055
|
return this._delegatorManager;
|
|
964
1056
|
}
|
|
965
1057
|
/**
|
|
966
|
-
*
|
|
967
|
-
*
|
|
1058
|
+
* Send bitcoin to an Arkade address.
|
|
1059
|
+
*
|
|
1060
|
+
* @deprecated Use `send`.
|
|
1061
|
+
* @param params - Send parameters
|
|
968
1062
|
*/
|
|
969
1063
|
async sendBitcoin(params) {
|
|
970
1064
|
if (params.amount <= 0) {
|
|
971
1065
|
throw new Error("Amount must be positive");
|
|
972
1066
|
}
|
|
973
1067
|
if (!isValidArkAddress(params.address)) {
|
|
974
|
-
throw new Error("Invalid
|
|
1068
|
+
throw new Error("Invalid Arkade address " + params.address);
|
|
975
1069
|
}
|
|
976
1070
|
if (params.selectedVtxos && params.selectedVtxos.length > 0) {
|
|
977
1071
|
return this._withTxLock(async () => {
|
|
@@ -1016,6 +1110,13 @@ export class Wallet extends ReadonlyWallet {
|
|
|
1016
1110
|
amount: params.amount,
|
|
1017
1111
|
});
|
|
1018
1112
|
}
|
|
1113
|
+
/**
|
|
1114
|
+
* Settle boarding inputs and/or virtual outputs into a finalized mainnet transaction.
|
|
1115
|
+
*
|
|
1116
|
+
* @param params - Optional settlement inputs and outputs. When omitted, the wallet settles all eligible funds.
|
|
1117
|
+
* @param eventCallback - Optional callback invoked for settlement stream events.
|
|
1118
|
+
* @returns The finalized Arkade transaction id
|
|
1119
|
+
*/
|
|
1019
1120
|
async settle(params, eventCallback) {
|
|
1020
1121
|
return this._withTxLock(() => this._settleImpl(params, eventCallback));
|
|
1021
1122
|
}
|
|
@@ -1033,7 +1134,7 @@ export class Wallet extends ReadonlyWallet {
|
|
|
1033
1134
|
}
|
|
1034
1135
|
}
|
|
1035
1136
|
}
|
|
1036
|
-
// if no params are provided, use all non
|
|
1137
|
+
// if no params are provided, use all non-expired boarding inputs and offchain virtual outputs as inputs
|
|
1037
1138
|
// and send all to the offchain address
|
|
1038
1139
|
if (!params) {
|
|
1039
1140
|
const { fees } = await this.arkProvider.getInfo();
|
|
@@ -1054,7 +1155,7 @@ export class Wallet extends ReadonlyWallet {
|
|
|
1054
1155
|
amount: BigInt(utxo.value),
|
|
1055
1156
|
});
|
|
1056
1157
|
if (inputFee.value >= utxo.value) {
|
|
1057
|
-
// skip if fees are greater than the
|
|
1158
|
+
// skip if fees are greater than the boarding input value
|
|
1058
1159
|
continue;
|
|
1059
1160
|
}
|
|
1060
1161
|
filteredBoardingUtxos.push(utxo);
|
|
@@ -1075,7 +1176,7 @@ export class Wallet extends ReadonlyWallet {
|
|
|
1075
1176
|
: new Date(),
|
|
1076
1177
|
});
|
|
1077
1178
|
if (inputFee.value >= vtxo.value) {
|
|
1078
|
-
// skip if fees are greater than the
|
|
1179
|
+
// skip if fees are greater than the virtual output value
|
|
1079
1180
|
continue;
|
|
1080
1181
|
}
|
|
1081
1182
|
filteredVtxos.push(vtxo);
|
|
@@ -1166,7 +1267,7 @@ export class Wallet extends ReadonlyWallet {
|
|
|
1166
1267
|
const assetPacket = createAssetPacket(assetInputs, recipients);
|
|
1167
1268
|
outputs.push(Extension.create([assetPacket]).txOut());
|
|
1168
1269
|
}
|
|
1169
|
-
// session holds the state of the musig2 signing process of the
|
|
1270
|
+
// session holds the state of the musig2 signing process of the virtual output tree
|
|
1170
1271
|
let session;
|
|
1171
1272
|
const signingPublicKeys = [];
|
|
1172
1273
|
if (hasOffchainOutputs) {
|
|
@@ -1217,7 +1318,7 @@ export class Wallet extends ReadonlyWallet {
|
|
|
1217
1318
|
for (const input of inputs) {
|
|
1218
1319
|
// check if the input is an offchain "virtual" coin
|
|
1219
1320
|
const vtxo = vtxos.find((vtxo) => vtxo.txid === input.txid && vtxo.vout === input.vout);
|
|
1220
|
-
// boarding
|
|
1321
|
+
// boarding input, we need to sign the settlement tx
|
|
1221
1322
|
if (!vtxo) {
|
|
1222
1323
|
for (let i = 0; i < settlementPsbt.inputsLength; i++) {
|
|
1223
1324
|
const settlementInput = settlementPsbt.getInput(i);
|
|
@@ -1295,11 +1396,12 @@ export class Wallet extends ReadonlyWallet {
|
|
|
1295
1396
|
}
|
|
1296
1397
|
}
|
|
1297
1398
|
/**
|
|
1298
|
-
*
|
|
1399
|
+
* Create a batch event handler for settlement flows.
|
|
1400
|
+
*
|
|
1299
1401
|
* @param intentId - The intent ID.
|
|
1300
|
-
* @param inputs -
|
|
1301
|
-
* @param
|
|
1302
|
-
* @param
|
|
1402
|
+
* @param inputs - Inputs used by the intent.
|
|
1403
|
+
* @param expectedRecipients - Expected recipients to validate in the virtual output tree.
|
|
1404
|
+
* @param session - Optional musig2 signing session. When omitted, signing steps are skipped.
|
|
1303
1405
|
*/
|
|
1304
1406
|
createBatchHandler(intentId, inputs, expectedRecipients, session) {
|
|
1305
1407
|
let sweepTapTreeRoot;
|
|
@@ -1313,7 +1415,7 @@ export class Wallet extends ReadonlyWallet {
|
|
|
1313
1415
|
for (const idHash of event.intentIdHashes) {
|
|
1314
1416
|
if (idHash === intentIdHashStr) {
|
|
1315
1417
|
if (!this.arkProvider) {
|
|
1316
|
-
throw new Error("
|
|
1418
|
+
throw new Error("Arkade provider not configured");
|
|
1317
1419
|
}
|
|
1318
1420
|
await this.arkProvider.confirmRegistration(intentId);
|
|
1319
1421
|
skip = false;
|
|
@@ -1346,10 +1448,10 @@ export class Wallet extends ReadonlyWallet {
|
|
|
1346
1448
|
// not a cosigner, skip the signing
|
|
1347
1449
|
return { skip: true };
|
|
1348
1450
|
}
|
|
1349
|
-
// validate the unsigned
|
|
1451
|
+
// validate the unsigned virtual output tree
|
|
1350
1452
|
const commitmentTx = Transaction.fromPSBT(base64.decode(event.unsignedCommitmentTx));
|
|
1351
1453
|
validateVtxoTxGraph(vtxoTree, commitmentTx, sweepTapTreeRoot);
|
|
1352
|
-
// validate that all expected receivers are in the
|
|
1454
|
+
// validate that all expected receivers are in the virtual output tree with correct amounts and assets
|
|
1353
1455
|
if (expectedRecipients && expectedRecipients.length > 0) {
|
|
1354
1456
|
validateBatchRecipients(commitmentTx, vtxoTree.leaves(), expectedRecipients, this.network);
|
|
1355
1457
|
}
|
|
@@ -1450,7 +1552,7 @@ export class Wallet extends ReadonlyWallet {
|
|
|
1450
1552
|
/**
|
|
1451
1553
|
* Finalizes pending transactions by retrieving them from the server and finalizing each one.
|
|
1452
1554
|
* Skips the server check entirely when no send was interrupted (no pending tx flag set).
|
|
1453
|
-
* @param vtxos - Optional list of
|
|
1555
|
+
* @param vtxos - Optional list of virtual outputs to use instead of retrieving them from the server
|
|
1454
1556
|
* @returns Array of transaction IDs that were finalized
|
|
1455
1557
|
*/
|
|
1456
1558
|
async finalizePendingTxs(vtxos) {
|
|
@@ -1549,13 +1651,13 @@ export class Wallet extends ReadonlyWallet {
|
|
|
1549
1651
|
/**
|
|
1550
1652
|
* Send BTC and/or assets to one or more recipients.
|
|
1551
1653
|
*
|
|
1552
|
-
* @param
|
|
1553
|
-
* @returns Promise resolving to the
|
|
1654
|
+
* @param args - Recipients with their addresses, BTC amounts, and assets
|
|
1655
|
+
* @returns Promise resolving to the Arkade transaction ID
|
|
1554
1656
|
*
|
|
1555
1657
|
* @example
|
|
1556
1658
|
* ```typescript
|
|
1557
1659
|
* const txid = await wallet.send({
|
|
1558
|
-
* address: '
|
|
1660
|
+
* address: 'ark1q...',
|
|
1559
1661
|
* amount: 1000, // (optional, default to dust) btc amount to send to the output
|
|
1560
1662
|
* assets: [{ assetId: 'abc123...', amount: 50 }] // (optional) list of assets to send
|
|
1561
1663
|
* });
|
|
@@ -1703,8 +1805,8 @@ export class Wallet extends ReadonlyWallet {
|
|
|
1703
1805
|
}
|
|
1704
1806
|
/**
|
|
1705
1807
|
* Build an offchain transaction from the given inputs and outputs,
|
|
1706
|
-
* sign it, submit to the
|
|
1707
|
-
* @returns The
|
|
1808
|
+
* sign it, submit to the Arkade provider, and finalize.
|
|
1809
|
+
* @returns The Arkade transaction id and server-signed checkpoint PSBTs (for bookkeeping)
|
|
1708
1810
|
*/
|
|
1709
1811
|
async buildAndSubmitOffchainTx(inputs, outputs) {
|
|
1710
1812
|
const offchainTx = buildOffchainTx(inputs.map((input) => {
|
|
@@ -1713,16 +1815,47 @@ export class Wallet extends ReadonlyWallet {
|
|
|
1713
1815
|
tapLeafScript: input.forfeitTapLeafScript,
|
|
1714
1816
|
};
|
|
1715
1817
|
}), outputs, this.serverUnrollScript);
|
|
1716
|
-
|
|
1818
|
+
let signedVirtualTx;
|
|
1819
|
+
let userSignedCheckpoints;
|
|
1820
|
+
if (isBatchSignable(this.identity)) {
|
|
1821
|
+
// Batch-sign arkTx + all checkpoints in one wallet popup.
|
|
1822
|
+
// Clone so the provider can't mutate originals before submitTx.
|
|
1823
|
+
const requests = [
|
|
1824
|
+
{ tx: offchainTx.arkTx.clone() },
|
|
1825
|
+
...offchainTx.checkpoints.map((c) => ({ tx: c.clone() })),
|
|
1826
|
+
];
|
|
1827
|
+
const signed = await this.identity.signMultiple(requests);
|
|
1828
|
+
if (signed.length !== requests.length) {
|
|
1829
|
+
throw new Error(`signMultiple returned ${signed.length} transactions, expected ${requests.length}`);
|
|
1830
|
+
}
|
|
1831
|
+
const [firstSignedTx, ...signedCheckpoints] = signed;
|
|
1832
|
+
signedVirtualTx = firstSignedTx;
|
|
1833
|
+
userSignedCheckpoints = signedCheckpoints;
|
|
1834
|
+
}
|
|
1835
|
+
else {
|
|
1836
|
+
signedVirtualTx = await this.identity.sign(offchainTx.arkTx);
|
|
1837
|
+
}
|
|
1717
1838
|
// Mark pending before submitting — if we crash between submit and
|
|
1718
1839
|
// finalize, the next init will recover via finalizePendingTxs.
|
|
1719
1840
|
await this.setPendingTxFlag(true);
|
|
1720
1841
|
const { arkTxid, signedCheckpointTxs } = await this.arkProvider.submitTx(base64.encode(signedVirtualTx.toPSBT()), offchainTx.checkpoints.map((c) => base64.encode(c.toPSBT())));
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1842
|
+
let finalCheckpoints;
|
|
1843
|
+
if (userSignedCheckpoints) {
|
|
1844
|
+
// Merge pre-signed user signatures onto server-signed checkpoints
|
|
1845
|
+
finalCheckpoints = signedCheckpointTxs.map((c, i) => {
|
|
1846
|
+
const serverSigned = Transaction.fromPSBT(base64.decode(c));
|
|
1847
|
+
combineTapscriptSigs(userSignedCheckpoints[i], serverSigned);
|
|
1848
|
+
return base64.encode(serverSigned.toPSBT());
|
|
1849
|
+
});
|
|
1850
|
+
}
|
|
1851
|
+
else {
|
|
1852
|
+
// Legacy: sign each checkpoint individually (N popups)
|
|
1853
|
+
finalCheckpoints = await Promise.all(signedCheckpointTxs.map(async (c) => {
|
|
1854
|
+
const tx = Transaction.fromPSBT(base64.decode(c));
|
|
1855
|
+
const signedCheckpoint = await this.identity.sign(tx);
|
|
1856
|
+
return base64.encode(signedCheckpoint.toPSBT());
|
|
1857
|
+
}));
|
|
1858
|
+
}
|
|
1726
1859
|
await this.arkProvider.finalizeTx(arkTxid, finalCheckpoints);
|
|
1727
1860
|
try {
|
|
1728
1861
|
await this.setPendingTxFlag(false);
|
|
@@ -1732,7 +1865,7 @@ export class Wallet extends ReadonlyWallet {
|
|
|
1732
1865
|
}
|
|
1733
1866
|
return { arkTxid, signedCheckpointTxs };
|
|
1734
1867
|
}
|
|
1735
|
-
// mark
|
|
1868
|
+
// mark virtual outputs as spent, save change outputs if any
|
|
1736
1869
|
async updateDbAfterOffchainTx(inputs, arkTxid, signedCheckpointTxs, sentAmount, changeAmount, changeVout, changeAssets) {
|
|
1737
1870
|
try {
|
|
1738
1871
|
const spentVtxos = [];
|
|
@@ -1780,7 +1913,7 @@ export class Wallet extends ReadonlyWallet {
|
|
|
1780
1913
|
}
|
|
1781
1914
|
const createdAt = Date.now();
|
|
1782
1915
|
const addr = this.arkAddress.encode();
|
|
1783
|
-
// Only save a change
|
|
1916
|
+
// Only save a change virtual output for preconfirmed coins (those with a batchExpiry).
|
|
1784
1917
|
// Inputs without a batchExpiry are already settled/unrolled and don't need tracking.
|
|
1785
1918
|
let changeVtxo;
|
|
1786
1919
|
if (changeAmount > 0n && batchExpiry !== Number.MAX_SAFE_INTEGER) {
|
|
@@ -1824,7 +1957,7 @@ export class Wallet extends ReadonlyWallet {
|
|
|
1824
1957
|
console.warn("error saving offchain tx to repository", e);
|
|
1825
1958
|
}
|
|
1826
1959
|
}
|
|
1827
|
-
// mark
|
|
1960
|
+
// mark virtual outputs as spent/settled, remove boarding inputs
|
|
1828
1961
|
async updateDbAfterSettle(inputs, commitmentTxid) {
|
|
1829
1962
|
try {
|
|
1830
1963
|
const addr = this.arkAddress.encode();
|
|
@@ -1835,7 +1968,7 @@ export class Wallet extends ReadonlyWallet {
|
|
|
1835
1968
|
const isVtxo = (input) => "virtualStatus" in input;
|
|
1836
1969
|
for (const input of inputs) {
|
|
1837
1970
|
if (isVtxo(input)) {
|
|
1838
|
-
//
|
|
1971
|
+
// virtual output = mark it settled
|
|
1839
1972
|
const vtxo = extendVirtualCoin(this, input);
|
|
1840
1973
|
if (vtxo.arkTxId) {
|
|
1841
1974
|
inputArkTxIds.add(vtxo.arkTxId);
|
|
@@ -1851,7 +1984,7 @@ export class Wallet extends ReadonlyWallet {
|
|
|
1851
1984
|
});
|
|
1852
1985
|
}
|
|
1853
1986
|
else {
|
|
1854
|
-
// boarding
|
|
1987
|
+
// boarding input = remove it
|
|
1855
1988
|
boardingUtxoToRemove.add(`${input.txid}:${input.vout}`);
|
|
1856
1989
|
}
|
|
1857
1990
|
}
|
|
@@ -1875,13 +2008,13 @@ export class Wallet extends ReadonlyWallet {
|
|
|
1875
2008
|
}
|
|
1876
2009
|
Wallet.MIN_FEE_RATE = 1; // sats/vbyte
|
|
1877
2010
|
/**
|
|
1878
|
-
* Select virtual
|
|
1879
|
-
* @param coins List of virtual
|
|
2011
|
+
* Select virtual outputs to reach a target amount, prioritizing those closer to expiry
|
|
2012
|
+
* @param coins List of virtual outputs to select from
|
|
1880
2013
|
* @param targetAmount Target amount to reach in satoshis
|
|
1881
|
-
* @returns Selected
|
|
2014
|
+
* @returns Selected virtual outputs and change amount
|
|
1882
2015
|
*/
|
|
1883
2016
|
export function selectVirtualCoins(coins, targetAmount) {
|
|
1884
|
-
// Sort
|
|
2017
|
+
// Sort virtual outputs by expiry (ascending) and amount (descending)
|
|
1885
2018
|
const sortedCoins = [...coins].sort((a, b) => {
|
|
1886
2019
|
// First sort by expiry if available
|
|
1887
2020
|
const expiryA = a.virtualStatus.batchExpiry || Number.MAX_SAFE_INTEGER;
|