@arkade-os/sdk 0.3.12 → 0.4.0-next.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +483 -54
- package/dist/cjs/adapters/expo-db.js +35 -0
- package/dist/cjs/asset/assetGroup.js +141 -0
- package/dist/cjs/asset/assetId.js +88 -0
- package/dist/cjs/asset/assetInput.js +204 -0
- package/dist/cjs/asset/assetOutput.js +159 -0
- package/dist/cjs/asset/assetRef.js +82 -0
- package/dist/cjs/asset/index.js +24 -0
- package/dist/cjs/asset/metadata.js +172 -0
- package/dist/cjs/asset/packet.js +164 -0
- package/dist/cjs/asset/types.js +25 -0
- package/dist/cjs/asset/utils.js +105 -0
- package/dist/cjs/contracts/arkcontract.js +148 -0
- package/dist/cjs/contracts/contractManager.js +436 -0
- package/dist/cjs/contracts/contractWatcher.js +567 -0
- package/dist/cjs/contracts/handlers/default.js +85 -0
- package/dist/cjs/contracts/handlers/delegate.js +89 -0
- package/dist/cjs/contracts/handlers/helpers.js +105 -0
- package/dist/cjs/contracts/handlers/index.js +19 -0
- package/dist/cjs/contracts/handlers/registry.js +89 -0
- package/dist/cjs/contracts/handlers/vhtlc.js +193 -0
- package/dist/cjs/contracts/index.js +41 -0
- package/dist/cjs/contracts/types.js +2 -0
- package/dist/cjs/db/manager.js +97 -0
- package/dist/cjs/forfeit.js +12 -8
- package/dist/cjs/identity/index.js +1 -0
- package/dist/cjs/identity/seedIdentity.js +255 -0
- package/dist/cjs/index.js +70 -14
- package/dist/cjs/intent/index.js +28 -2
- package/dist/cjs/providers/ark.js +7 -0
- package/dist/cjs/providers/delegator.js +66 -0
- package/dist/cjs/providers/expoIndexer.js +5 -0
- package/dist/cjs/providers/indexer.js +68 -1
- package/dist/cjs/providers/onchain.js +2 -2
- package/dist/cjs/providers/utils.js +1 -0
- package/dist/cjs/repositories/contractRepository.js +0 -103
- package/dist/cjs/repositories/inMemory/contractRepository.js +55 -0
- package/dist/cjs/repositories/inMemory/walletRepository.js +80 -0
- package/dist/cjs/repositories/index.js +16 -0
- package/dist/cjs/repositories/indexedDB/contractRepository.js +187 -0
- package/dist/cjs/repositories/indexedDB/db.js +57 -0
- package/dist/cjs/repositories/indexedDB/schema.js +159 -0
- package/dist/cjs/repositories/indexedDB/walletRepository.js +338 -0
- package/dist/cjs/repositories/indexedDB/websqlAdapter.js +144 -0
- package/dist/cjs/repositories/migrations/contractRepositoryImpl.js +127 -0
- package/dist/cjs/repositories/migrations/fromStorageAdapter.js +66 -0
- package/dist/cjs/repositories/migrations/walletRepositoryImpl.js +180 -0
- package/dist/cjs/repositories/walletRepository.js +0 -169
- package/dist/cjs/script/base.js +54 -0
- package/dist/cjs/script/delegate.js +49 -0
- package/dist/cjs/storage/asyncStorage.js +4 -1
- package/dist/cjs/storage/fileSystem.js +3 -0
- package/dist/cjs/storage/inMemory.js +3 -0
- package/dist/cjs/storage/indexedDB.js +5 -1
- package/dist/cjs/storage/localStorage.js +3 -0
- package/dist/cjs/utils/arkTransaction.js +16 -0
- package/dist/cjs/utils/transactionHistory.js +50 -0
- package/dist/cjs/utils/txSizeEstimator.js +39 -14
- package/dist/cjs/wallet/asset-manager.js +338 -0
- package/dist/cjs/wallet/asset.js +117 -0
- package/dist/cjs/wallet/batch.js +1 -1
- package/dist/cjs/wallet/delegator.js +235 -0
- package/dist/cjs/wallet/expo/background.js +133 -0
- package/dist/cjs/wallet/expo/index.js +9 -0
- package/dist/cjs/wallet/expo/wallet.js +231 -0
- package/dist/cjs/wallet/onchain.js +57 -12
- package/dist/cjs/wallet/serviceWorker/wallet-message-handler.js +568 -0
- package/dist/cjs/wallet/serviceWorker/wallet.js +383 -102
- package/dist/cjs/wallet/unroll.js +7 -2
- package/dist/cjs/wallet/utils.js +60 -0
- package/dist/cjs/wallet/validation.js +151 -0
- package/dist/cjs/wallet/vtxo-manager.js +1 -1
- package/dist/cjs/wallet/wallet.js +702 -260
- package/dist/cjs/worker/browser/service-worker-manager.js +82 -0
- package/dist/cjs/{wallet/serviceWorker → worker/browser}/utils.js +2 -1
- package/dist/cjs/worker/expo/asyncStorageTaskQueue.js +78 -0
- package/dist/cjs/worker/expo/index.js +12 -0
- package/dist/cjs/worker/expo/processors/contractPollProcessor.js +61 -0
- package/dist/cjs/worker/expo/processors/index.js +6 -0
- package/dist/cjs/worker/expo/taskQueue.js +41 -0
- package/dist/cjs/worker/expo/taskRunner.js +57 -0
- package/dist/cjs/worker/messageBus.js +252 -0
- package/dist/esm/adapters/expo-db.js +27 -0
- package/dist/esm/asset/assetGroup.js +137 -0
- package/dist/esm/asset/assetId.js +84 -0
- package/dist/esm/asset/assetInput.js +199 -0
- package/dist/esm/asset/assetOutput.js +154 -0
- package/dist/esm/asset/assetRef.js +78 -0
- package/dist/esm/asset/index.js +8 -0
- package/dist/esm/asset/metadata.js +167 -0
- package/dist/esm/asset/packet.js +159 -0
- package/dist/esm/asset/types.js +22 -0
- package/dist/esm/asset/utils.js +99 -0
- package/dist/esm/contracts/arkcontract.js +141 -0
- package/dist/esm/contracts/contractManager.js +432 -0
- package/dist/esm/contracts/contractWatcher.js +563 -0
- package/dist/esm/contracts/handlers/default.js +82 -0
- package/dist/esm/contracts/handlers/delegate.js +86 -0
- package/dist/esm/contracts/handlers/helpers.js +66 -0
- package/dist/esm/contracts/handlers/index.js +12 -0
- package/dist/esm/contracts/handlers/registry.js +86 -0
- package/dist/esm/contracts/handlers/vhtlc.js +190 -0
- package/dist/esm/contracts/index.js +13 -0
- package/dist/esm/contracts/types.js +1 -0
- package/dist/esm/db/manager.js +92 -0
- package/dist/esm/forfeit.js +11 -8
- package/dist/esm/identity/index.js +1 -0
- package/dist/esm/identity/seedIdentity.js +249 -0
- package/dist/esm/index.js +25 -15
- package/dist/esm/intent/index.js +28 -2
- package/dist/esm/providers/ark.js +7 -0
- package/dist/esm/providers/delegator.js +62 -0
- package/dist/esm/providers/expoIndexer.js +5 -0
- package/dist/esm/providers/indexer.js +68 -1
- package/dist/esm/providers/onchain.js +2 -2
- package/dist/esm/providers/utils.js +1 -0
- package/dist/esm/repositories/contractRepository.js +1 -101
- package/dist/esm/repositories/inMemory/contractRepository.js +51 -0
- package/dist/esm/repositories/inMemory/walletRepository.js +76 -0
- package/dist/esm/repositories/index.js +8 -0
- package/dist/esm/repositories/indexedDB/contractRepository.js +183 -0
- package/dist/esm/repositories/indexedDB/db.js +42 -0
- package/dist/esm/repositories/indexedDB/schema.js +155 -0
- package/dist/esm/repositories/indexedDB/walletRepository.js +334 -0
- package/dist/esm/repositories/indexedDB/websqlAdapter.js +138 -0
- package/dist/esm/repositories/migrations/contractRepositoryImpl.js +121 -0
- package/dist/esm/repositories/migrations/fromStorageAdapter.js +58 -0
- package/dist/esm/repositories/migrations/walletRepositoryImpl.js +176 -0
- package/dist/esm/repositories/walletRepository.js +1 -167
- package/dist/esm/script/base.js +21 -1
- package/dist/esm/script/delegate.js +46 -0
- package/dist/esm/storage/asyncStorage.js +4 -1
- package/dist/esm/storage/fileSystem.js +3 -0
- package/dist/esm/storage/inMemory.js +3 -0
- package/dist/esm/storage/indexedDB.js +5 -1
- package/dist/esm/storage/localStorage.js +3 -0
- package/dist/esm/utils/arkTransaction.js +15 -0
- package/dist/esm/utils/transactionHistory.js +50 -0
- package/dist/esm/utils/txSizeEstimator.js +39 -14
- package/dist/esm/wallet/asset-manager.js +333 -0
- package/dist/esm/wallet/asset.js +111 -0
- package/dist/esm/wallet/batch.js +1 -1
- package/dist/esm/wallet/delegator.js +231 -0
- package/dist/esm/wallet/expo/background.js +128 -0
- package/dist/esm/wallet/expo/index.js +2 -0
- package/dist/esm/wallet/expo/wallet.js +194 -0
- package/dist/esm/wallet/onchain.js +57 -12
- package/dist/esm/wallet/serviceWorker/wallet-message-handler.js +564 -0
- package/dist/esm/wallet/serviceWorker/wallet.js +382 -101
- package/dist/esm/wallet/unroll.js +7 -2
- package/dist/esm/wallet/utils.js +55 -0
- package/dist/esm/wallet/validation.js +139 -0
- package/dist/esm/wallet/vtxo-manager.js +1 -1
- package/dist/esm/wallet/wallet.js +704 -229
- package/dist/esm/worker/browser/service-worker-manager.js +76 -0
- package/dist/esm/{wallet/serviceWorker → worker/browser}/utils.js +2 -1
- package/dist/esm/worker/expo/asyncStorageTaskQueue.js +74 -0
- package/dist/esm/worker/expo/index.js +4 -0
- package/dist/esm/worker/expo/processors/contractPollProcessor.js +58 -0
- package/dist/esm/worker/expo/processors/index.js +1 -0
- package/dist/esm/worker/expo/taskQueue.js +37 -0
- package/dist/esm/worker/expo/taskRunner.js +54 -0
- package/dist/esm/worker/messageBus.js +248 -0
- package/dist/types/adapters/expo-db.d.ts +7 -0
- package/dist/types/asset/assetGroup.d.ts +28 -0
- package/dist/types/asset/assetId.d.ts +19 -0
- package/dist/types/asset/assetInput.d.ts +46 -0
- package/dist/types/asset/assetOutput.d.ts +39 -0
- package/dist/types/asset/assetRef.d.ts +25 -0
- package/dist/types/asset/index.d.ts +8 -0
- package/dist/types/asset/metadata.d.ts +37 -0
- package/dist/types/asset/packet.d.ts +27 -0
- package/dist/types/asset/types.d.ts +18 -0
- package/dist/types/asset/utils.d.ts +21 -0
- package/dist/types/contracts/arkcontract.d.ts +101 -0
- package/dist/types/contracts/contractManager.d.ts +331 -0
- package/dist/types/contracts/contractWatcher.d.ts +192 -0
- package/dist/types/contracts/handlers/default.d.ts +19 -0
- package/dist/types/contracts/handlers/delegate.d.ts +21 -0
- package/dist/types/contracts/handlers/helpers.d.ts +18 -0
- package/dist/types/contracts/handlers/index.d.ts +7 -0
- package/dist/types/contracts/handlers/registry.d.ts +65 -0
- package/dist/types/contracts/handlers/vhtlc.d.ts +32 -0
- package/dist/types/contracts/index.d.ts +14 -0
- package/dist/types/contracts/types.d.ts +222 -0
- package/dist/types/db/manager.d.ts +22 -0
- package/dist/types/forfeit.d.ts +2 -1
- package/dist/types/identity/index.d.ts +1 -0
- package/dist/types/identity/seedIdentity.d.ts +128 -0
- package/dist/types/index.d.ts +21 -12
- package/dist/types/intent/index.d.ts +2 -1
- package/dist/types/providers/ark.d.ts +11 -2
- package/dist/types/providers/delegator.d.ts +29 -0
- package/dist/types/providers/indexer.d.ts +11 -1
- package/dist/types/repositories/contractRepository.d.ts +30 -19
- package/dist/types/repositories/inMemory/contractRepository.d.ts +17 -0
- package/dist/types/repositories/inMemory/walletRepository.d.ts +26 -0
- package/dist/types/repositories/index.d.ts +7 -0
- package/dist/types/repositories/indexedDB/contractRepository.d.ts +21 -0
- package/dist/types/repositories/indexedDB/db.d.ts +56 -0
- package/dist/types/repositories/indexedDB/schema.d.ts +8 -0
- package/dist/types/repositories/indexedDB/walletRepository.d.ts +25 -0
- package/dist/types/repositories/indexedDB/websqlAdapter.d.ts +49 -0
- package/dist/types/repositories/migrations/contractRepositoryImpl.d.ts +24 -0
- package/dist/types/repositories/migrations/fromStorageAdapter.d.ts +19 -0
- package/dist/types/repositories/migrations/walletRepositoryImpl.d.ts +27 -0
- package/dist/types/repositories/walletRepository.d.ts +13 -24
- package/dist/types/script/base.d.ts +1 -0
- package/dist/types/script/delegate.d.ts +36 -0
- package/dist/types/storage/asyncStorage.d.ts +4 -0
- package/dist/types/storage/fileSystem.d.ts +3 -0
- package/dist/types/storage/inMemory.d.ts +3 -0
- package/dist/types/storage/index.d.ts +3 -0
- package/dist/types/storage/indexedDB.d.ts +3 -0
- package/dist/types/storage/localStorage.d.ts +3 -0
- package/dist/types/utils/arkTransaction.d.ts +6 -0
- package/dist/types/utils/txSizeEstimator.d.ts +12 -2
- package/dist/types/wallet/asset-manager.d.ts +78 -0
- package/dist/types/wallet/asset.d.ts +21 -0
- package/dist/types/wallet/batch.d.ts +1 -1
- package/dist/types/wallet/delegator.d.ts +24 -0
- package/dist/types/wallet/expo/background.d.ts +66 -0
- package/dist/types/wallet/expo/index.d.ts +4 -0
- package/dist/types/wallet/expo/wallet.d.ts +97 -0
- package/dist/types/wallet/index.d.ts +75 -2
- package/dist/types/wallet/onchain.d.ts +22 -1
- package/dist/types/wallet/serviceWorker/wallet-message-handler.d.ts +366 -0
- package/dist/types/wallet/serviceWorker/wallet.d.ts +20 -11
- package/dist/types/wallet/utils.d.ts +13 -1
- package/dist/types/wallet/validation.d.ts +24 -0
- package/dist/types/wallet/wallet.d.ts +111 -17
- package/dist/types/worker/browser/service-worker-manager.d.ts +21 -0
- package/dist/types/{wallet/serviceWorker → worker/browser}/utils.d.ts +2 -1
- package/dist/types/worker/expo/asyncStorageTaskQueue.d.ts +46 -0
- package/dist/types/worker/expo/index.d.ts +7 -0
- package/dist/types/worker/expo/processors/contractPollProcessor.d.ts +14 -0
- package/dist/types/worker/expo/processors/index.d.ts +1 -0
- package/dist/types/worker/expo/taskQueue.d.ts +50 -0
- package/dist/types/worker/expo/taskRunner.d.ts +42 -0
- package/dist/types/worker/messageBus.d.ts +109 -0
- package/package.json +71 -17
- package/dist/cjs/wallet/serviceWorker/request.js +0 -78
- package/dist/cjs/wallet/serviceWorker/response.js +0 -222
- package/dist/cjs/wallet/serviceWorker/worker.js +0 -655
- package/dist/esm/wallet/serviceWorker/request.js +0 -75
- package/dist/esm/wallet/serviceWorker/response.js +0 -219
- package/dist/esm/wallet/serviceWorker/worker.js +0 -651
- package/dist/types/wallet/serviceWorker/request.d.ts +0 -74
- package/dist/types/wallet/serviceWorker/response.d.ts +0 -123
- package/dist/types/wallet/serviceWorker/worker.d.ts +0 -53
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { base64, hex } from "@scure/base";
|
|
2
|
-
import * as bip68 from "bip68";
|
|
3
2
|
import { tapLeafHash } from "@scure/btc-signer/payment.js";
|
|
4
|
-
import {
|
|
3
|
+
import { Address, OutScript, SigHash, Transaction } from "@scure/btc-signer";
|
|
5
4
|
import { sha256 } from "@scure/btc-signer/utils.js";
|
|
6
5
|
import { ArkAddress } from '../script/address.js';
|
|
7
6
|
import { DefaultVtxo } from '../script/default.js';
|
|
@@ -10,23 +9,28 @@ import { ESPLORA_URL, EsploraProvider, } from '../providers/onchain.js';
|
|
|
10
9
|
import { RestArkProvider, } from '../providers/ark.js';
|
|
11
10
|
import { buildForfeitTx } from '../forfeit.js';
|
|
12
11
|
import { validateConnectorsTxGraph, validateVtxoTxGraph, } from '../tree/validation.js';
|
|
12
|
+
import { validateBatchRecipients } from './validation.js';
|
|
13
13
|
import { isExpired, isRecoverable, isSpendable, isSubdust, TxType, } from './index.js';
|
|
14
|
+
import { createAssetPacket, selectedCoinsToAssetInputs, selectCoinsWithAsset, } from './asset.js';
|
|
14
15
|
import { VtxoScript } from '../script/base.js';
|
|
15
|
-
import {
|
|
16
|
-
import { buildOffchainTx, hasBoardingTxExpired } from '../utils/arkTransaction.js';
|
|
16
|
+
import { CSVMultisigTapscript } from '../script/tapscript.js';
|
|
17
|
+
import { buildOffchainTx, hasBoardingTxExpired, isValidArkAddress, } from '../utils/arkTransaction.js';
|
|
17
18
|
import { DEFAULT_RENEWAL_CONFIG } from './vtxo-manager.js';
|
|
18
19
|
import { ArkNote } from '../arknote/index.js';
|
|
19
20
|
import { Intent } from '../intent/index.js';
|
|
20
21
|
import { RestIndexerProvider } from '../providers/indexer.js';
|
|
21
|
-
import {
|
|
22
|
-
import { InMemoryStorageAdapter } from '../storage/inMemory.js';
|
|
23
|
-
import { WalletRepositoryImpl, } from '../repositories/walletRepository.js';
|
|
24
|
-
import { ContractRepositoryImpl, } from '../repositories/contractRepository.js';
|
|
25
|
-
import { extendCoin, extendVirtualCoin } from './utils.js';
|
|
22
|
+
import { extendCoin, extendVirtualCoin, validateRecipients } from './utils.js';
|
|
26
23
|
import { ArkError } from '../providers/errors.js';
|
|
27
24
|
import { Batch } from './batch.js';
|
|
28
25
|
import { Estimator } from '../arkfee/index.js';
|
|
29
26
|
import { buildTransactionHistory } from '../utils/transactionHistory.js';
|
|
27
|
+
import { AssetManager, ReadonlyAssetManager } from './asset-manager.js';
|
|
28
|
+
import { DelegateVtxo } from '../script/delegate.js';
|
|
29
|
+
import { DelegatorManagerImpl } from './delegator.js';
|
|
30
|
+
import { IndexedDBContractRepository, IndexedDBWalletRepository, } from '../repositories/index.js';
|
|
31
|
+
import { ContractManager } from '../contracts/contractManager.js';
|
|
32
|
+
import { contractHandlers } from '../contracts/handlers/index.js';
|
|
33
|
+
import { timelockToSequence } from '../contracts/handlers/helpers.js';
|
|
30
34
|
/**
|
|
31
35
|
* Type guard function to check if an identity has a toReadonly method.
|
|
32
36
|
*/
|
|
@@ -37,7 +41,10 @@ function hasToReadonly(identity) {
|
|
|
37
41
|
typeof identity.toReadonly === "function");
|
|
38
42
|
}
|
|
39
43
|
export class ReadonlyWallet {
|
|
40
|
-
|
|
44
|
+
get assetManager() {
|
|
45
|
+
return this._assetManager;
|
|
46
|
+
}
|
|
47
|
+
constructor(identity, network, onchainProvider, indexerProvider, arkServerPublicKey, offchainTapscript, boardingTapscript, dustAmount, walletRepository, contractRepository, delegatorProvider, watcherConfig) {
|
|
41
48
|
this.identity = identity;
|
|
42
49
|
this.network = network;
|
|
43
50
|
this.onchainProvider = onchainProvider;
|
|
@@ -48,12 +55,15 @@ export class ReadonlyWallet {
|
|
|
48
55
|
this.dustAmount = dustAmount;
|
|
49
56
|
this.walletRepository = walletRepository;
|
|
50
57
|
this.contractRepository = contractRepository;
|
|
58
|
+
this.delegatorProvider = delegatorProvider;
|
|
59
|
+
this.watcherConfig = watcherConfig;
|
|
60
|
+
this._assetManager = new ReadonlyAssetManager(this.indexerProvider);
|
|
51
61
|
}
|
|
52
62
|
/**
|
|
53
63
|
* Protected helper to set up shared wallet configuration.
|
|
54
64
|
* Extracts common logic used by both ReadonlyWallet.create() and Wallet.create().
|
|
55
65
|
*/
|
|
56
|
-
static async setupWalletConfig(config,
|
|
66
|
+
static async setupWalletConfig(config, pubKey) {
|
|
57
67
|
// Use provided arkProvider instance or create a new one from arkServerUrl
|
|
58
68
|
const arkProvider = config.arkProvider ||
|
|
59
69
|
(() => {
|
|
@@ -105,22 +115,26 @@ export class ReadonlyWallet {
|
|
|
105
115
|
};
|
|
106
116
|
// Generate tapscripts for offchain and boarding address
|
|
107
117
|
const serverPubKey = hex.decode(info.signerPubkey).slice(1);
|
|
108
|
-
const
|
|
109
|
-
|
|
118
|
+
const delegatePubKey = config.delegatorProvider
|
|
119
|
+
? await config.delegatorProvider
|
|
120
|
+
.getDelegateInfo()
|
|
121
|
+
.then((info) => hex.decode(info.pubkey).slice(1))
|
|
122
|
+
: undefined;
|
|
123
|
+
const offchainOptions = {
|
|
124
|
+
pubKey,
|
|
110
125
|
serverPubKey,
|
|
111
126
|
csvTimelock: exitTimelock,
|
|
112
|
-
}
|
|
127
|
+
};
|
|
128
|
+
const offchainTapscript = !delegatePubKey
|
|
129
|
+
? new DefaultVtxo.Script(offchainOptions)
|
|
130
|
+
: new DelegateVtxo.Script({ ...offchainOptions, delegatePubKey });
|
|
113
131
|
const boardingTapscript = new DefaultVtxo.Script({
|
|
114
|
-
|
|
115
|
-
serverPubKey,
|
|
132
|
+
...offchainOptions,
|
|
116
133
|
csvTimelock: boardingTimelock,
|
|
117
134
|
});
|
|
118
|
-
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
const storage = config.storage || new InMemoryStorageAdapter();
|
|
122
|
-
const walletRepository = new WalletRepositoryImpl(storage);
|
|
123
|
-
const contractRepository = new ContractRepositoryImpl(storage);
|
|
135
|
+
const walletRepository = config.storage?.walletRepository ?? new IndexedDBWalletRepository();
|
|
136
|
+
const contractRepository = config.storage?.contractRepository ??
|
|
137
|
+
new IndexedDBContractRepository();
|
|
124
138
|
return {
|
|
125
139
|
arkProvider,
|
|
126
140
|
indexerProvider,
|
|
@@ -134,6 +148,7 @@ export class ReadonlyWallet {
|
|
|
134
148
|
walletRepository,
|
|
135
149
|
contractRepository,
|
|
136
150
|
info,
|
|
151
|
+
delegatorProvider: config.delegatorProvider,
|
|
137
152
|
};
|
|
138
153
|
}
|
|
139
154
|
static async create(config) {
|
|
@@ -142,11 +157,18 @@ export class ReadonlyWallet {
|
|
|
142
157
|
throw new Error("Invalid configured public key");
|
|
143
158
|
}
|
|
144
159
|
const setup = await ReadonlyWallet.setupWalletConfig(config, pubkey);
|
|
145
|
-
return new ReadonlyWallet(config.identity, setup.network, setup.onchainProvider, setup.indexerProvider, setup.serverPubKey, setup.offchainTapscript, setup.boardingTapscript, setup.dustAmount, setup.walletRepository, setup.contractRepository);
|
|
160
|
+
return new ReadonlyWallet(config.identity, setup.network, setup.onchainProvider, setup.indexerProvider, setup.serverPubKey, setup.offchainTapscript, setup.boardingTapscript, setup.dustAmount, setup.walletRepository, setup.contractRepository, setup.delegatorProvider, config.watcherConfig);
|
|
146
161
|
}
|
|
147
162
|
get arkAddress() {
|
|
148
163
|
return this.offchainTapscript.address(this.network.hrp, this.arkServerPublicKey);
|
|
149
164
|
}
|
|
165
|
+
/**
|
|
166
|
+
* Get the contract script for the wallet's default address.
|
|
167
|
+
* This is the pkScript hex, used to identify the wallet in ContractManager.
|
|
168
|
+
*/
|
|
169
|
+
get defaultContractScript() {
|
|
170
|
+
return hex.encode(this.offchainTapscript.pkScript);
|
|
171
|
+
}
|
|
150
172
|
async getAddress() {
|
|
151
173
|
return this.arkAddress.encode();
|
|
152
174
|
}
|
|
@@ -184,6 +206,22 @@ export class ReadonlyWallet {
|
|
|
184
206
|
.reduce((sum, coin) => sum + coin.value, 0);
|
|
185
207
|
const totalBoarding = confirmed + unconfirmed;
|
|
186
208
|
const totalOffchain = settled + preconfirmed + recoverable;
|
|
209
|
+
// aggregate asset balances from spendable vtxos
|
|
210
|
+
const assetBalances = new Map();
|
|
211
|
+
for (const vtxo of vtxos) {
|
|
212
|
+
if (!isSpendable(vtxo))
|
|
213
|
+
continue;
|
|
214
|
+
if (vtxo.assets) {
|
|
215
|
+
for (const a of vtxo.assets) {
|
|
216
|
+
const current = assetBalances.get(a.assetId) ?? 0;
|
|
217
|
+
assetBalances.set(a.assetId, current + a.amount);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
const assets = Array.from(assetBalances.entries()).map(([assetId, amount]) => ({
|
|
222
|
+
assetId,
|
|
223
|
+
amount,
|
|
224
|
+
}));
|
|
187
225
|
return {
|
|
188
226
|
boarding: {
|
|
189
227
|
confirmed,
|
|
@@ -195,20 +233,39 @@ export class ReadonlyWallet {
|
|
|
195
233
|
available: settled + preconfirmed,
|
|
196
234
|
recoverable,
|
|
197
235
|
total: totalBoarding + totalOffchain,
|
|
236
|
+
assets,
|
|
198
237
|
};
|
|
199
238
|
}
|
|
200
239
|
async getVtxos(filter) {
|
|
201
240
|
const address = await this.getAddress();
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
//
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
241
|
+
const scriptMap = await this.getScriptMap();
|
|
242
|
+
const f = filter ?? { withRecoverable: true, withUnrolled: false };
|
|
243
|
+
const allExtended = [];
|
|
244
|
+
// Query each script separately so we can extend VTXOs with the correct tapscript
|
|
245
|
+
for (const [scriptHex, vtxoScript] of scriptMap) {
|
|
246
|
+
const response = await this.indexerProvider.getVtxos({
|
|
247
|
+
scripts: [scriptHex],
|
|
248
|
+
});
|
|
249
|
+
let vtxos = response.vtxos.filter(isSpendable);
|
|
250
|
+
if (!f.withRecoverable) {
|
|
251
|
+
vtxos = vtxos.filter((vtxo) => !isRecoverable(vtxo) && !isExpired(vtxo));
|
|
252
|
+
}
|
|
253
|
+
if (f.withUnrolled) {
|
|
254
|
+
const spentVtxos = response.vtxos.filter((vtxo) => !isSpendable(vtxo));
|
|
255
|
+
vtxos.push(...spentVtxos.filter((vtxo) => vtxo.isUnrolled));
|
|
256
|
+
}
|
|
257
|
+
for (const vtxo of vtxos) {
|
|
258
|
+
allExtended.push({
|
|
259
|
+
...vtxo,
|
|
260
|
+
forfeitTapLeafScript: vtxoScript.forfeit(),
|
|
261
|
+
intentTapLeafScript: vtxoScript.forfeit(),
|
|
262
|
+
tapTree: vtxoScript.encode(),
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
}
|
|
209
266
|
// Update cache with fresh data
|
|
210
|
-
await this.walletRepository.saveVtxos(address,
|
|
211
|
-
return
|
|
267
|
+
await this.walletRepository.saveVtxos(address, allExtended);
|
|
268
|
+
return allExtended;
|
|
212
269
|
}
|
|
213
270
|
async getVirtualCoins(filter = { withRecoverable: true, withUnrolled: false }) {
|
|
214
271
|
const scripts = [hex.encode(this.offchainTapscript.pkScript)];
|
|
@@ -226,9 +283,8 @@ export class ReadonlyWallet {
|
|
|
226
283
|
return vtxos;
|
|
227
284
|
}
|
|
228
285
|
async getTransactionHistory() {
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
});
|
|
286
|
+
const scripts = await this.getWalletScripts();
|
|
287
|
+
const response = await this.indexerProvider.getVtxos({ scripts });
|
|
232
288
|
const { boardingTxs, commitmentsToIgnore } = await this.getBoardingTxs();
|
|
233
289
|
const getTxCreatedAt = (txid) => this.indexerProvider
|
|
234
290
|
.getVtxos({ outpoints: [{ txid, vout: 0 }] })
|
|
@@ -338,17 +394,20 @@ export class ReadonlyWallet {
|
|
|
338
394
|
});
|
|
339
395
|
}
|
|
340
396
|
if (this.indexerProvider && arkAddress) {
|
|
341
|
-
const
|
|
342
|
-
const subscriptionId = await this.indexerProvider.subscribeForScripts(
|
|
343
|
-
hex.encode(offchainScript.pkScript),
|
|
344
|
-
]);
|
|
397
|
+
const walletScripts = await this.getWalletScripts();
|
|
398
|
+
const subscriptionId = await this.indexerProvider.subscribeForScripts(walletScripts);
|
|
345
399
|
const abortController = new AbortController();
|
|
346
400
|
const subscription = this.indexerProvider.getSubscription(subscriptionId, abortController.signal);
|
|
347
401
|
indexerStopFunc = async () => {
|
|
348
402
|
abortController.abort();
|
|
349
403
|
await this.indexerProvider?.unsubscribeForScripts(subscriptionId);
|
|
350
404
|
};
|
|
351
|
-
// Handle subscription updates asynchronously without blocking
|
|
405
|
+
// Handle subscription updates asynchronously without blocking.
|
|
406
|
+
// Note: subscription covers all wallet scripts (default + delegate),
|
|
407
|
+
// but we can't determine which script each VTXO belongs to from the
|
|
408
|
+
// subscription event. VTXOs are extended with the current offchainTapscript;
|
|
409
|
+
// this is for notification/display only — not for spending.
|
|
410
|
+
// For correct extension metadata, use getVtxos() which queries per-script.
|
|
352
411
|
(async () => {
|
|
353
412
|
try {
|
|
354
413
|
for await (const update of subscription) {
|
|
@@ -375,7 +434,7 @@ export class ReadonlyWallet {
|
|
|
375
434
|
}
|
|
376
435
|
async fetchPendingTxs() {
|
|
377
436
|
// get non-swept VTXOs, rely on the indexer only in case DB doesn't have the right state
|
|
378
|
-
const scripts =
|
|
437
|
+
const scripts = await this.getWalletScripts();
|
|
379
438
|
let { vtxos } = await this.indexerProvider.getVtxos({
|
|
380
439
|
scripts,
|
|
381
440
|
});
|
|
@@ -385,6 +444,183 @@ export class ReadonlyWallet {
|
|
|
385
444
|
vtxo.arkTxId !== undefined)
|
|
386
445
|
.map((_) => _.arkTxId);
|
|
387
446
|
}
|
|
447
|
+
// ========================================================================
|
|
448
|
+
// Multi-script support (default + delegate addresses)
|
|
449
|
+
// ========================================================================
|
|
450
|
+
/**
|
|
451
|
+
* Get all pkScript hex strings for the wallet's own addresses
|
|
452
|
+
* (both delegate and non-delegate, current and historical).
|
|
453
|
+
* Falls back to only the current script if ContractManager is not yet initialized.
|
|
454
|
+
*/
|
|
455
|
+
async getWalletScripts() {
|
|
456
|
+
if (this._contractManager) {
|
|
457
|
+
try {
|
|
458
|
+
const contracts = await this._contractManager.getContracts({
|
|
459
|
+
type: ["default", "delegate"],
|
|
460
|
+
});
|
|
461
|
+
if (contracts.length > 0) {
|
|
462
|
+
return contracts.map((c) => c.script);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
catch {
|
|
466
|
+
// fall through to current script only
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
return [hex.encode(this.offchainTapscript.pkScript)];
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Build a map of scriptHex → VtxoScript for all wallet contracts,
|
|
473
|
+
* so VTXOs can be extended with the correct tapscript per contract.
|
|
474
|
+
*/
|
|
475
|
+
async getScriptMap() {
|
|
476
|
+
const map = new Map();
|
|
477
|
+
// Always include the current script
|
|
478
|
+
const currentScriptHex = hex.encode(this.offchainTapscript.pkScript);
|
|
479
|
+
map.set(currentScriptHex, this.offchainTapscript);
|
|
480
|
+
if (this._contractManager) {
|
|
481
|
+
try {
|
|
482
|
+
const contracts = await this._contractManager.getContracts({
|
|
483
|
+
type: ["default", "delegate"],
|
|
484
|
+
});
|
|
485
|
+
for (const contract of contracts) {
|
|
486
|
+
if (map.has(contract.script))
|
|
487
|
+
continue;
|
|
488
|
+
const handler = contractHandlers.get(contract.type);
|
|
489
|
+
if (handler) {
|
|
490
|
+
const script = handler.createScript(contract.params);
|
|
491
|
+
map.set(contract.script, script);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
catch {
|
|
496
|
+
// ContractManager error — only current script in map
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
return map;
|
|
500
|
+
}
|
|
501
|
+
// ========================================================================
|
|
502
|
+
// Contract Management
|
|
503
|
+
// ========================================================================
|
|
504
|
+
/**
|
|
505
|
+
* Get the ContractManager for managing contracts including the wallet's default address.
|
|
506
|
+
*
|
|
507
|
+
* The ContractManager handles:
|
|
508
|
+
* - The wallet's default receiving address (as a "default" contract)
|
|
509
|
+
* - External contracts (Boltz swaps, HTLCs, etc.)
|
|
510
|
+
* - Multi-contract watching with resilient connections
|
|
511
|
+
*
|
|
512
|
+
* @example
|
|
513
|
+
* ```typescript
|
|
514
|
+
* const manager = await wallet.getContractManager();
|
|
515
|
+
*
|
|
516
|
+
* // Create a contract for a Boltz swap
|
|
517
|
+
* const contract = await manager.createContract({
|
|
518
|
+
* label: "Boltz Swap",
|
|
519
|
+
* type: "vhtlc",
|
|
520
|
+
* params: { ... },
|
|
521
|
+
* script: swapScript,
|
|
522
|
+
* address: swapAddress,
|
|
523
|
+
* });
|
|
524
|
+
*
|
|
525
|
+
* // Start watching for events (includes wallet's default address)
|
|
526
|
+
* const stop = await manager.onContractEvent((event) => {
|
|
527
|
+
* console.log(`${event.type} on ${event.contractScript}`);
|
|
528
|
+
* });
|
|
529
|
+
* ```
|
|
530
|
+
*/
|
|
531
|
+
async getContractManager() {
|
|
532
|
+
// Return existing manager if already initialized
|
|
533
|
+
if (this._contractManager) {
|
|
534
|
+
return this._contractManager;
|
|
535
|
+
}
|
|
536
|
+
// If initialization is in progress, wait for it
|
|
537
|
+
if (this._contractManagerInitializing) {
|
|
538
|
+
return this._contractManagerInitializing;
|
|
539
|
+
}
|
|
540
|
+
// Start initialization and store the promise
|
|
541
|
+
this._contractManagerInitializing = this.initializeContractManager();
|
|
542
|
+
try {
|
|
543
|
+
const manager = await this._contractManagerInitializing;
|
|
544
|
+
this._contractManager = manager;
|
|
545
|
+
return manager;
|
|
546
|
+
}
|
|
547
|
+
catch (error) {
|
|
548
|
+
// Clear the initializing promise so subsequent calls can retry
|
|
549
|
+
this._contractManagerInitializing = undefined;
|
|
550
|
+
throw error;
|
|
551
|
+
}
|
|
552
|
+
finally {
|
|
553
|
+
// Clear the initializing promise after completion
|
|
554
|
+
this._contractManagerInitializing = undefined;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
async initializeContractManager() {
|
|
558
|
+
const manager = await ContractManager.create({
|
|
559
|
+
indexerProvider: this.indexerProvider,
|
|
560
|
+
contractRepository: this.contractRepository,
|
|
561
|
+
walletRepository: this.walletRepository,
|
|
562
|
+
getDefaultAddress: () => this.getAddress(),
|
|
563
|
+
watcherConfig: this.watcherConfig,
|
|
564
|
+
});
|
|
565
|
+
// Register the wallet's current address as a contract
|
|
566
|
+
const csvTimelock = this.offchainTapscript.options.csvTimelock ??
|
|
567
|
+
DefaultVtxo.Script.DEFAULT_TIMELOCK;
|
|
568
|
+
const csvTimelockStr = timelockToSequence(csvTimelock).toString();
|
|
569
|
+
const isDelegateScript = this.offchainTapscript instanceof DelegateVtxo.Script;
|
|
570
|
+
if (isDelegateScript) {
|
|
571
|
+
const delegateScript = this
|
|
572
|
+
.offchainTapscript;
|
|
573
|
+
// Register the delegate contract (current address)
|
|
574
|
+
await manager.createContract({
|
|
575
|
+
type: "delegate",
|
|
576
|
+
params: {
|
|
577
|
+
pubKey: hex.encode(delegateScript.options.pubKey),
|
|
578
|
+
serverPubKey: hex.encode(delegateScript.options.serverPubKey),
|
|
579
|
+
delegatePubKey: hex.encode(delegateScript.options.delegatePubKey),
|
|
580
|
+
csvTimelock: csvTimelockStr,
|
|
581
|
+
},
|
|
582
|
+
script: this.defaultContractScript,
|
|
583
|
+
address: await this.getAddress(),
|
|
584
|
+
state: "active",
|
|
585
|
+
});
|
|
586
|
+
// Also register the non-delegate version so old VTXOs remain visible
|
|
587
|
+
const nonDelegateScript = new DefaultVtxo.Script({
|
|
588
|
+
pubKey: delegateScript.options.pubKey,
|
|
589
|
+
serverPubKey: delegateScript.options.serverPubKey,
|
|
590
|
+
csvTimelock,
|
|
591
|
+
});
|
|
592
|
+
await manager.createContract({
|
|
593
|
+
type: "default",
|
|
594
|
+
params: {
|
|
595
|
+
pubKey: hex.encode(delegateScript.options.pubKey),
|
|
596
|
+
serverPubKey: hex.encode(delegateScript.options.serverPubKey),
|
|
597
|
+
csvTimelock: csvTimelockStr,
|
|
598
|
+
},
|
|
599
|
+
script: hex.encode(nonDelegateScript.pkScript),
|
|
600
|
+
address: nonDelegateScript
|
|
601
|
+
.address(this.network.hrp, this.arkServerPublicKey)
|
|
602
|
+
.encode(),
|
|
603
|
+
state: "active",
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
else {
|
|
607
|
+
// Register the default contract (current address)
|
|
608
|
+
await manager.createContract({
|
|
609
|
+
type: "default",
|
|
610
|
+
params: {
|
|
611
|
+
pubKey: hex.encode(this.offchainTapscript.options.pubKey),
|
|
612
|
+
serverPubKey: hex.encode(this.offchainTapscript.options.serverPubKey),
|
|
613
|
+
csvTimelock: csvTimelockStr,
|
|
614
|
+
},
|
|
615
|
+
script: this.defaultContractScript,
|
|
616
|
+
address: await this.getAddress(),
|
|
617
|
+
state: "active",
|
|
618
|
+
});
|
|
619
|
+
// Any old "delegate" contract from a prior wallet incarnation
|
|
620
|
+
// is already loaded by ContractManager.initialize() from ContractRepository
|
|
621
|
+
}
|
|
622
|
+
return manager;
|
|
623
|
+
}
|
|
388
624
|
}
|
|
389
625
|
/**
|
|
390
626
|
* Main wallet implementation for Bitcoin transactions with Ark protocol support.
|
|
@@ -420,8 +656,8 @@ export class ReadonlyWallet {
|
|
|
420
656
|
* ```
|
|
421
657
|
*/
|
|
422
658
|
export class Wallet extends ReadonlyWallet {
|
|
423
|
-
constructor(identity, network, networkName, onchainProvider, arkProvider, indexerProvider, arkServerPublicKey, offchainTapscript, boardingTapscript, serverUnrollScript, forfeitOutputScript, forfeitPubkey, dustAmount, walletRepository, contractRepository, renewalConfig) {
|
|
424
|
-
super(identity, network, onchainProvider, indexerProvider, arkServerPublicKey, offchainTapscript, boardingTapscript, dustAmount, walletRepository, contractRepository);
|
|
659
|
+
constructor(identity, network, networkName, onchainProvider, arkProvider, indexerProvider, arkServerPublicKey, offchainTapscript, boardingTapscript, serverUnrollScript, forfeitOutputScript, forfeitPubkey, dustAmount, walletRepository, contractRepository, renewalConfig, delegatorProvider, watcherConfig) {
|
|
660
|
+
super(identity, network, onchainProvider, indexerProvider, arkServerPublicKey, offchainTapscript, boardingTapscript, dustAmount, walletRepository, contractRepository, delegatorProvider, watcherConfig);
|
|
425
661
|
this.networkName = networkName;
|
|
426
662
|
this.arkProvider = arkProvider;
|
|
427
663
|
this.serverUnrollScript = serverUnrollScript;
|
|
@@ -433,6 +669,13 @@ export class Wallet extends ReadonlyWallet {
|
|
|
433
669
|
...DEFAULT_RENEWAL_CONFIG,
|
|
434
670
|
...renewalConfig,
|
|
435
671
|
};
|
|
672
|
+
this.delegatorManager = delegatorProvider
|
|
673
|
+
? new DelegatorManagerImpl(delegatorProvider, arkProvider, identity)
|
|
674
|
+
: undefined;
|
|
675
|
+
}
|
|
676
|
+
get assetManager() {
|
|
677
|
+
this._walletAssetManager ?? (this._walletAssetManager = new AssetManager(this));
|
|
678
|
+
return this._walletAssetManager;
|
|
436
679
|
}
|
|
437
680
|
static async create(config) {
|
|
438
681
|
const pubkey = await config.identity.xOnlyPublicKey();
|
|
@@ -455,7 +698,7 @@ export class Wallet extends ReadonlyWallet {
|
|
|
455
698
|
const forfeitPubkey = hex.decode(setup.info.forfeitPubkey).slice(1);
|
|
456
699
|
const forfeitAddress = Address(setup.network).decode(setup.info.forfeitAddress);
|
|
457
700
|
const forfeitOutputScript = OutScript.encode(forfeitAddress);
|
|
458
|
-
return new Wallet(config.identity, setup.network, setup.networkName, setup.onchainProvider, setup.arkProvider, setup.indexerProvider, setup.serverPubKey, setup.offchainTapscript, setup.boardingTapscript, serverUnrollScript, forfeitOutputScript, forfeitPubkey, setup.dustAmount, setup.walletRepository, setup.contractRepository, config.renewalConfig);
|
|
701
|
+
return new Wallet(config.identity, setup.network, setup.networkName, setup.onchainProvider, setup.arkProvider, setup.indexerProvider, setup.serverPubKey, setup.offchainTapscript, setup.boardingTapscript, serverUnrollScript, forfeitOutputScript, forfeitPubkey, setup.dustAmount, setup.walletRepository, setup.contractRepository, config.renewalConfig, config.delegatorProvider, config.watcherConfig);
|
|
459
702
|
}
|
|
460
703
|
/**
|
|
461
704
|
* Convert this wallet to a readonly wallet.
|
|
@@ -479,7 +722,7 @@ export class Wallet extends ReadonlyWallet {
|
|
|
479
722
|
const readonlyIdentity = hasToReadonly(this.identity)
|
|
480
723
|
? await this.identity.toReadonly()
|
|
481
724
|
: this.identity; // Identity extends ReadonlyIdentity, so this is safe
|
|
482
|
-
return new ReadonlyWallet(readonlyIdentity, this.network, this.onchainProvider, this.indexerProvider, this.arkServerPublicKey, this.offchainTapscript, this.boardingTapscript, this.dustAmount, this.walletRepository, this.contractRepository);
|
|
725
|
+
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);
|
|
483
726
|
}
|
|
484
727
|
async sendBitcoin(params) {
|
|
485
728
|
if (params.amount <= 0) {
|
|
@@ -488,12 +731,7 @@ export class Wallet extends ReadonlyWallet {
|
|
|
488
731
|
if (!isValidArkAddress(params.address)) {
|
|
489
732
|
throw new Error("Invalid Ark address " + params.address);
|
|
490
733
|
}
|
|
491
|
-
|
|
492
|
-
const virtualCoins = await this.getVirtualCoins({
|
|
493
|
-
withRecoverable: false,
|
|
494
|
-
});
|
|
495
|
-
let selected;
|
|
496
|
-
if (params.selectedVtxos) {
|
|
734
|
+
if (params.selectedVtxos && params.selectedVtxos.length > 0) {
|
|
497
735
|
const selectedVtxoSum = params.selectedVtxos
|
|
498
736
|
.map((v) => v.value)
|
|
499
737
|
.reduce((a, b) => a + b, 0);
|
|
@@ -501,125 +739,38 @@ export class Wallet extends ReadonlyWallet {
|
|
|
501
739
|
throw new Error("Selected VTXOs do not cover specified amount");
|
|
502
740
|
}
|
|
503
741
|
const changeAmount = selectedVtxoSum - params.amount;
|
|
504
|
-
selected = {
|
|
742
|
+
const selected = {
|
|
505
743
|
inputs: params.selectedVtxos,
|
|
506
744
|
changeAmount: BigInt(changeAmount),
|
|
507
745
|
};
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
if (!selectedLeaf) {
|
|
514
|
-
throw new Error("Selected leaf not found");
|
|
515
|
-
}
|
|
516
|
-
const outputAddress = ArkAddress.decode(params.address);
|
|
517
|
-
const outputScript = BigInt(params.amount) < this.dustAmount
|
|
518
|
-
? outputAddress.subdustPkScript
|
|
519
|
-
: outputAddress.pkScript;
|
|
520
|
-
const outputs = [
|
|
521
|
-
{
|
|
522
|
-
script: outputScript,
|
|
523
|
-
amount: BigInt(params.amount),
|
|
524
|
-
},
|
|
525
|
-
];
|
|
526
|
-
// add change output if needed
|
|
527
|
-
if (selected.changeAmount > 0n) {
|
|
528
|
-
const changeOutputScript = selected.changeAmount < this.dustAmount
|
|
529
|
-
? this.arkAddress.subdustPkScript
|
|
530
|
-
: this.arkAddress.pkScript;
|
|
531
|
-
outputs.push({
|
|
532
|
-
script: changeOutputScript,
|
|
533
|
-
amount: BigInt(selected.changeAmount),
|
|
534
|
-
});
|
|
535
|
-
}
|
|
536
|
-
const tapTree = this.offchainTapscript.encode();
|
|
537
|
-
const offchainTx = buildOffchainTx(selected.inputs.map((input) => ({
|
|
538
|
-
...input,
|
|
539
|
-
tapLeafScript: selectedLeaf,
|
|
540
|
-
tapTree,
|
|
541
|
-
})), outputs, this.serverUnrollScript);
|
|
542
|
-
const signedVirtualTx = await this.identity.sign(offchainTx.arkTx);
|
|
543
|
-
const { arkTxid, signedCheckpointTxs } = await this.arkProvider.submitTx(base64.encode(signedVirtualTx.toPSBT()), offchainTx.checkpoints.map((c) => base64.encode(c.toPSBT())));
|
|
544
|
-
// sign the checkpoints
|
|
545
|
-
const finalCheckpoints = await Promise.all(signedCheckpointTxs.map(async (c) => {
|
|
546
|
-
const tx = Transaction.fromPSBT(base64.decode(c));
|
|
547
|
-
const signedCheckpoint = await this.identity.sign(tx);
|
|
548
|
-
return base64.encode(signedCheckpoint.toPSBT());
|
|
549
|
-
}));
|
|
550
|
-
await this.arkProvider.finalizeTx(arkTxid, finalCheckpoints);
|
|
551
|
-
try {
|
|
552
|
-
// mark VTXOs as spent and optionally add the change VTXO
|
|
553
|
-
const spentVtxos = [];
|
|
554
|
-
const commitmentTxIds = new Set();
|
|
555
|
-
let batchExpiry = Number.MAX_SAFE_INTEGER;
|
|
556
|
-
for (const [inputIndex, input] of selected.inputs.entries()) {
|
|
557
|
-
const vtxo = extendVirtualCoin(this, input);
|
|
558
|
-
const checkpointB64 = signedCheckpointTxs[inputIndex];
|
|
559
|
-
const checkpoint = Transaction.fromPSBT(base64.decode(checkpointB64));
|
|
560
|
-
spentVtxos.push({
|
|
561
|
-
...vtxo,
|
|
562
|
-
virtualStatus: { ...vtxo.virtualStatus, state: "spent" },
|
|
563
|
-
spentBy: checkpoint.id,
|
|
564
|
-
arkTxId: arkTxid,
|
|
565
|
-
isSpent: true,
|
|
566
|
-
});
|
|
567
|
-
if (vtxo.virtualStatus.commitmentTxIds) {
|
|
568
|
-
for (const commitmentTxId of vtxo.virtualStatus
|
|
569
|
-
.commitmentTxIds) {
|
|
570
|
-
commitmentTxIds.add(commitmentTxId);
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
if (vtxo.virtualStatus.batchExpiry) {
|
|
574
|
-
batchExpiry = Math.min(batchExpiry, vtxo.virtualStatus.batchExpiry);
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
const createdAt = Date.now();
|
|
578
|
-
const addr = this.arkAddress.encode();
|
|
579
|
-
if (selected.changeAmount > 0n &&
|
|
580
|
-
batchExpiry !== Number.MAX_SAFE_INTEGER) {
|
|
581
|
-
const changeVtxo = {
|
|
582
|
-
txid: arkTxid,
|
|
583
|
-
vout: outputs.length - 1,
|
|
584
|
-
createdAt: new Date(createdAt),
|
|
585
|
-
forfeitTapLeafScript: this.offchainTapscript.forfeit(),
|
|
586
|
-
intentTapLeafScript: this.offchainTapscript.forfeit(),
|
|
587
|
-
isUnrolled: false,
|
|
588
|
-
isSpent: false,
|
|
589
|
-
tapTree: this.offchainTapscript.encode(),
|
|
590
|
-
value: Number(selected.changeAmount),
|
|
591
|
-
virtualStatus: {
|
|
592
|
-
state: "preconfirmed",
|
|
593
|
-
commitmentTxIds: Array.from(commitmentTxIds),
|
|
594
|
-
batchExpiry,
|
|
595
|
-
},
|
|
596
|
-
status: {
|
|
597
|
-
confirmed: false,
|
|
598
|
-
},
|
|
599
|
-
};
|
|
600
|
-
await this.walletRepository.saveVtxos(addr, [changeVtxo]);
|
|
601
|
-
}
|
|
602
|
-
await this.walletRepository.saveVtxos(addr, spentVtxos);
|
|
603
|
-
await this.walletRepository.saveTransactions(addr, [
|
|
746
|
+
const outputAddress = ArkAddress.decode(params.address);
|
|
747
|
+
const outputScript = BigInt(params.amount) < this.dustAmount
|
|
748
|
+
? outputAddress.subdustPkScript
|
|
749
|
+
: outputAddress.pkScript;
|
|
750
|
+
const outputs = [
|
|
604
751
|
{
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
commitmentTxid: "",
|
|
608
|
-
arkTxid: arkTxid,
|
|
609
|
-
},
|
|
610
|
-
amount: params.amount,
|
|
611
|
-
type: TxType.TxSent,
|
|
612
|
-
settled: false,
|
|
613
|
-
createdAt: Date.now(),
|
|
752
|
+
script: outputScript,
|
|
753
|
+
amount: BigInt(params.amount),
|
|
614
754
|
},
|
|
615
|
-
]
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
755
|
+
];
|
|
756
|
+
// add change output if needed
|
|
757
|
+
if (selected.changeAmount > 0n) {
|
|
758
|
+
const changeOutputScript = selected.changeAmount < this.dustAmount
|
|
759
|
+
? this.arkAddress.subdustPkScript
|
|
760
|
+
: this.arkAddress.pkScript;
|
|
761
|
+
outputs.push({
|
|
762
|
+
script: changeOutputScript,
|
|
763
|
+
amount: BigInt(selected.changeAmount),
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
const { arkTxid, signedCheckpointTxs } = await this.buildAndSubmitOffchainTx(selected.inputs, outputs);
|
|
767
|
+
await this.updateDbAfterOffchainTx(selected.inputs, arkTxid, signedCheckpointTxs, params.amount, selected.changeAmount, selected.changeAmount > 0n ? outputs.length - 1 : 0);
|
|
621
768
|
return arkTxid;
|
|
622
769
|
}
|
|
770
|
+
return this.send({
|
|
771
|
+
address: params.address,
|
|
772
|
+
amount: params.amount,
|
|
773
|
+
});
|
|
623
774
|
}
|
|
624
775
|
async settle(params, eventCallback) {
|
|
625
776
|
if (params?.inputs) {
|
|
@@ -720,6 +871,49 @@ export class Wallet extends ReadonlyWallet {
|
|
|
720
871
|
script,
|
|
721
872
|
});
|
|
722
873
|
}
|
|
874
|
+
// if some of the inputs hold assets, build the asset packet and append as output
|
|
875
|
+
// in the intent proof tx, there is a "fake" input at index 0
|
|
876
|
+
// so the real coin indices are offset by +1
|
|
877
|
+
const assetInputs = new Map();
|
|
878
|
+
for (let i = 0; i < params.inputs.length; i++) {
|
|
879
|
+
if ("assets" in params.inputs[i]) {
|
|
880
|
+
const assets = params.inputs[i]
|
|
881
|
+
.assets;
|
|
882
|
+
if (assets && assets.length > 0) {
|
|
883
|
+
assetInputs.set(i + 1, assets);
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
let outputAssets;
|
|
888
|
+
let assetOutputIndex; // where to send the assets
|
|
889
|
+
if (assetInputs.size > 0) {
|
|
890
|
+
// collect all input assets and assign them to the first offchain output
|
|
891
|
+
const allAssets = new Map();
|
|
892
|
+
for (const [, assets] of assetInputs) {
|
|
893
|
+
for (const asset of assets) {
|
|
894
|
+
const existing = allAssets.get(asset.assetId) ?? 0n;
|
|
895
|
+
allAssets.set(asset.assetId, existing + BigInt(asset.amount));
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
outputAssets = [];
|
|
899
|
+
for (const [assetId, amount] of allAssets) {
|
|
900
|
+
outputAssets.push({ assetId, amount: Number(amount) });
|
|
901
|
+
}
|
|
902
|
+
const firstOffchainIndex = params.outputs.findIndex((_, i) => !onchainOutputIndexes.includes(i));
|
|
903
|
+
if (firstOffchainIndex === -1) {
|
|
904
|
+
throw new Error("Cannot settle assets without an offchain output");
|
|
905
|
+
}
|
|
906
|
+
assetOutputIndex = firstOffchainIndex;
|
|
907
|
+
}
|
|
908
|
+
const recipients = params.outputs.map((output, i) => ({
|
|
909
|
+
address: output.address,
|
|
910
|
+
amount: Number(output.amount),
|
|
911
|
+
assets: i === assetOutputIndex ? outputAssets : undefined,
|
|
912
|
+
}));
|
|
913
|
+
if (outputAssets && outputAssets.length > 0) {
|
|
914
|
+
const assetPacket = createAssetPacket(assetInputs, recipients);
|
|
915
|
+
outputs.push(assetPacket.txOut());
|
|
916
|
+
}
|
|
723
917
|
// session holds the state of the musig2 signing process of the vtxo tree
|
|
724
918
|
let session;
|
|
725
919
|
const signingPublicKeys = [];
|
|
@@ -736,17 +930,19 @@ export class Wallet extends ReadonlyWallet {
|
|
|
736
930
|
...signingPublicKeys,
|
|
737
931
|
...params.inputs.map((input) => `${input.txid}:${input.vout}`),
|
|
738
932
|
];
|
|
739
|
-
const handler = this.createBatchHandler(intentId, params.inputs, session);
|
|
933
|
+
const handler = this.createBatchHandler(intentId, params.inputs, recipients, session);
|
|
740
934
|
const abortController = new AbortController();
|
|
741
935
|
try {
|
|
742
936
|
const stream = this.arkProvider.getEventStream(abortController.signal, topics);
|
|
743
|
-
|
|
937
|
+
const commitmentTxid = await Batch.join(stream, handler, {
|
|
744
938
|
abortController,
|
|
745
939
|
skipVtxoTreeSigning: !hasOffchainOutputs,
|
|
746
940
|
eventCallback: eventCallback
|
|
747
941
|
? (event) => Promise.resolve(eventCallback(event))
|
|
748
942
|
: undefined,
|
|
749
943
|
});
|
|
944
|
+
await this.updateDbAfterSettle(params.inputs, commitmentTxid);
|
|
945
|
+
return commitmentTxid;
|
|
750
946
|
}
|
|
751
947
|
catch (error) {
|
|
752
948
|
// delete the intent to not be stuck in the queue
|
|
@@ -851,8 +1047,9 @@ export class Wallet extends ReadonlyWallet {
|
|
|
851
1047
|
* @param intentId - The intent ID.
|
|
852
1048
|
* @param inputs - The inputs of the intent.
|
|
853
1049
|
* @param session - The musig2 signing session, if not provided, the signing will be skipped.
|
|
1050
|
+
* @param expectedRecipients - Expected recipients to validate in the vtxo tree.
|
|
854
1051
|
*/
|
|
855
|
-
createBatchHandler(intentId, inputs, session) {
|
|
1052
|
+
createBatchHandler(intentId, inputs, expectedRecipients, session) {
|
|
856
1053
|
let sweepTapTreeRoot;
|
|
857
1054
|
return {
|
|
858
1055
|
onBatchStarted: async (event) => {
|
|
@@ -900,7 +1097,10 @@ export class Wallet extends ReadonlyWallet {
|
|
|
900
1097
|
// validate the unsigned vtxo tree
|
|
901
1098
|
const commitmentTx = Transaction.fromPSBT(base64.decode(event.unsignedCommitmentTx));
|
|
902
1099
|
validateVtxoTxGraph(vtxoTree, commitmentTx, sweepTapTreeRoot);
|
|
903
|
-
//
|
|
1100
|
+
// validate that all expected receivers are in the vtxo tree with correct amounts and assets
|
|
1101
|
+
if (expectedRecipients && expectedRecipients.length > 0) {
|
|
1102
|
+
validateBatchRecipients(commitmentTx, vtxoTree.leaves(), expectedRecipients, this.network);
|
|
1103
|
+
}
|
|
904
1104
|
const sharedOutput = commitmentTx.getOutput(0);
|
|
905
1105
|
if (!sharedOutput?.amount) {
|
|
906
1106
|
throw new Error("Shared output not found");
|
|
@@ -956,16 +1156,15 @@ export class Wallet extends ReadonlyWallet {
|
|
|
956
1156
|
throw error;
|
|
957
1157
|
}
|
|
958
1158
|
}
|
|
959
|
-
async makeRegisterIntentSignature(coins, outputs, onchainOutputsIndexes, cosignerPubKeys) {
|
|
960
|
-
const inputs = this.prepareIntentProofInputs(coins);
|
|
1159
|
+
async makeRegisterIntentSignature(coins, outputs, onchainOutputsIndexes, cosignerPubKeys, validAt) {
|
|
961
1160
|
const message = {
|
|
962
1161
|
type: "register",
|
|
963
1162
|
onchain_output_indexes: onchainOutputsIndexes,
|
|
964
|
-
valid_at: 0,
|
|
1163
|
+
valid_at: validAt ? Math.floor(validAt) : 0,
|
|
965
1164
|
expire_at: 0,
|
|
966
1165
|
cosigners_public_keys: cosignerPubKeys,
|
|
967
1166
|
};
|
|
968
|
-
const proof = Intent.create(message,
|
|
1167
|
+
const proof = Intent.create(message, coins, outputs);
|
|
969
1168
|
const signedProof = await this.identity.sign(proof);
|
|
970
1169
|
return {
|
|
971
1170
|
proof: base64.encode(signedProof.toPSBT()),
|
|
@@ -973,25 +1172,23 @@ export class Wallet extends ReadonlyWallet {
|
|
|
973
1172
|
};
|
|
974
1173
|
}
|
|
975
1174
|
async makeDeleteIntentSignature(coins) {
|
|
976
|
-
const inputs = this.prepareIntentProofInputs(coins);
|
|
977
1175
|
const message = {
|
|
978
1176
|
type: "delete",
|
|
979
1177
|
expire_at: 0,
|
|
980
1178
|
};
|
|
981
|
-
const proof = Intent.create(message,
|
|
1179
|
+
const proof = Intent.create(message, coins, []);
|
|
982
1180
|
const signedProof = await this.identity.sign(proof);
|
|
983
1181
|
return {
|
|
984
1182
|
proof: base64.encode(signedProof.toPSBT()),
|
|
985
1183
|
message,
|
|
986
1184
|
};
|
|
987
1185
|
}
|
|
988
|
-
async makeGetPendingTxIntentSignature(
|
|
989
|
-
const inputs = this.prepareIntentProofInputs(vtxos);
|
|
1186
|
+
async makeGetPendingTxIntentSignature(coins) {
|
|
990
1187
|
const message = {
|
|
991
1188
|
type: "get-pending-tx",
|
|
992
1189
|
expire_at: 0,
|
|
993
1190
|
};
|
|
994
|
-
const proof = Intent.create(message,
|
|
1191
|
+
const proof = Intent.create(message, coins, []);
|
|
995
1192
|
const signedProof = await this.identity.sign(proof);
|
|
996
1193
|
return {
|
|
997
1194
|
proof: base64.encode(signedProof.toPSBT()),
|
|
@@ -1006,17 +1203,28 @@ export class Wallet extends ReadonlyWallet {
|
|
|
1006
1203
|
async finalizePendingTxs(vtxos) {
|
|
1007
1204
|
const MAX_INPUTS_PER_INTENT = 20;
|
|
1008
1205
|
if (!vtxos || vtxos.length === 0) {
|
|
1009
|
-
//
|
|
1010
|
-
const
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1206
|
+
// Query per-script so each VTXO is extended with the correct tapscript
|
|
1207
|
+
const scriptMap = await this.getScriptMap();
|
|
1208
|
+
const allExtended = [];
|
|
1209
|
+
for (const [scriptHex, vtxoScript] of scriptMap) {
|
|
1210
|
+
const { vtxos: fetchedVtxos } = await this.indexerProvider.getVtxos({
|
|
1211
|
+
scripts: [scriptHex],
|
|
1212
|
+
});
|
|
1213
|
+
const pending = fetchedVtxos.filter((vtxo) => vtxo.virtualStatus.state !== "swept" &&
|
|
1214
|
+
vtxo.virtualStatus.state !== "settled");
|
|
1215
|
+
for (const vtxo of pending) {
|
|
1216
|
+
allExtended.push({
|
|
1217
|
+
...vtxo,
|
|
1218
|
+
forfeitTapLeafScript: vtxoScript.forfeit(),
|
|
1219
|
+
intentTapLeafScript: vtxoScript.forfeit(),
|
|
1220
|
+
tapTree: vtxoScript.encode(),
|
|
1221
|
+
});
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
if (allExtended.length === 0) {
|
|
1017
1225
|
return { finalized: [], pending: [] };
|
|
1018
1226
|
}
|
|
1019
|
-
vtxos =
|
|
1227
|
+
vtxos = allExtended;
|
|
1020
1228
|
}
|
|
1021
1229
|
const finalized = [];
|
|
1022
1230
|
const pending = [];
|
|
@@ -1045,66 +1253,333 @@ export class Wallet extends ReadonlyWallet {
|
|
|
1045
1253
|
}
|
|
1046
1254
|
return { finalized, pending };
|
|
1047
1255
|
}
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1256
|
+
/**
|
|
1257
|
+
* Send BTC and/or assets to one or more recipients.
|
|
1258
|
+
*
|
|
1259
|
+
* @param recipients - Array of recipients with their addresses, BTC amounts, and assets
|
|
1260
|
+
* @returns Promise resolving to the ark transaction ID
|
|
1261
|
+
*
|
|
1262
|
+
* @example
|
|
1263
|
+
* ```typescript
|
|
1264
|
+
* const txid = await wallet.send({
|
|
1265
|
+
* address: 'ark1...',
|
|
1266
|
+
* amount: 1000, // (optional, default to dust) btc amount to send to the output
|
|
1267
|
+
* assets: [{ assetId: 'abc123...', amount: 50 }] // (optional) list of assets to send
|
|
1268
|
+
* });
|
|
1269
|
+
* ```
|
|
1270
|
+
*/
|
|
1271
|
+
async send(...args) {
|
|
1272
|
+
if (args.length === 0) {
|
|
1273
|
+
throw new Error("At least one receiver is required");
|
|
1274
|
+
}
|
|
1275
|
+
// validate recipients and populate undefined amount with dust amount
|
|
1276
|
+
const recipients = validateRecipients(args, Number(this.dustAmount));
|
|
1277
|
+
const address = await this.getAddress();
|
|
1278
|
+
const outputAddress = ArkAddress.decode(address);
|
|
1279
|
+
const virtualCoins = await this.getVirtualCoins({
|
|
1280
|
+
withRecoverable: false,
|
|
1281
|
+
});
|
|
1282
|
+
// keep track of asset changes
|
|
1283
|
+
const assetChanges = new Map();
|
|
1284
|
+
let selectedCoins = [];
|
|
1285
|
+
let btcAmountToSelect = 0;
|
|
1286
|
+
for (const recipient of recipients) {
|
|
1287
|
+
btcAmountToSelect += Math.max(recipient.amount, Number(this.dustAmount));
|
|
1288
|
+
}
|
|
1289
|
+
// select assets
|
|
1290
|
+
for (const recipient of recipients) {
|
|
1291
|
+
if (!recipient.assets) {
|
|
1292
|
+
continue;
|
|
1056
1293
|
}
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1294
|
+
for (const receiverAsset of recipient.assets) {
|
|
1295
|
+
let amountToSelect = BigInt(receiverAsset.amount);
|
|
1296
|
+
// check if existing change covers the needed amount
|
|
1297
|
+
const existingChange = assetChanges.get(receiverAsset.assetId) ?? 0n;
|
|
1298
|
+
if (existingChange >= amountToSelect) {
|
|
1299
|
+
assetChanges.set(receiverAsset.assetId, existingChange - amountToSelect);
|
|
1300
|
+
if (assetChanges.get(receiverAsset.assetId) === 0n) {
|
|
1301
|
+
assetChanges.delete(receiverAsset.assetId);
|
|
1302
|
+
}
|
|
1303
|
+
continue;
|
|
1304
|
+
}
|
|
1305
|
+
if (existingChange > 0n) {
|
|
1306
|
+
amountToSelect -= existingChange;
|
|
1307
|
+
assetChanges.delete(receiverAsset.assetId);
|
|
1308
|
+
}
|
|
1309
|
+
const availableCoins = virtualCoins.filter((c) => !selectedCoins.find((sc) => sc.txid === c.txid && sc.vout === c.vout));
|
|
1310
|
+
const { selected, totalAssetAmount } = selectCoinsWithAsset(availableCoins, receiverAsset.assetId, amountToSelect);
|
|
1311
|
+
for (const coin of selected) {
|
|
1312
|
+
selectedCoins.push(coin);
|
|
1313
|
+
// asset coins contain btc, subtract from total amount to select
|
|
1314
|
+
btcAmountToSelect -= coin.value;
|
|
1315
|
+
// coin may contain other assets, add them to asset changes
|
|
1316
|
+
if (coin.assets) {
|
|
1317
|
+
for (const a of coin.assets) {
|
|
1318
|
+
if (a.assetId === receiverAsset.assetId) {
|
|
1319
|
+
continue;
|
|
1320
|
+
}
|
|
1321
|
+
const existing = assetChanges.get(a.assetId) ?? 0n;
|
|
1322
|
+
assetChanges.set(a.assetId, existing + BigInt(a.amount));
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
const assetChangeAmount = totalAssetAmount - amountToSelect;
|
|
1327
|
+
if (assetChangeAmount > 0n) {
|
|
1328
|
+
const existing = assetChanges.get(receiverAsset.assetId) ?? 0n;
|
|
1329
|
+
assetChanges.set(receiverAsset.assetId, existing + assetChangeAmount);
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
// select remaining btc
|
|
1334
|
+
if (btcAmountToSelect > 0) {
|
|
1335
|
+
const availableCoins = virtualCoins.filter((c) => !selectedCoins.find((sc) => sc.txid === c.txid && sc.vout === c.vout));
|
|
1336
|
+
const { inputs: btcCoins } = selectVirtualCoins(availableCoins, btcAmountToSelect);
|
|
1337
|
+
// some coins may contain assets, add them to asset changes
|
|
1338
|
+
for (const coin of btcCoins) {
|
|
1339
|
+
if (coin.assets) {
|
|
1340
|
+
for (const asset of coin.assets) {
|
|
1341
|
+
const existing = assetChanges.get(asset.assetId) ?? 0n;
|
|
1342
|
+
assetChanges.set(asset.assetId, existing + BigInt(asset.amount));
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
selectedCoins = [...selectedCoins, ...btcCoins];
|
|
1347
|
+
}
|
|
1348
|
+
let totalBtcSelected = selectedCoins.reduce((sum, c) => sum + c.value, 0);
|
|
1349
|
+
// build tx outputs
|
|
1350
|
+
const outputs = recipients.map((recipient) => ({
|
|
1351
|
+
script: recipient.script,
|
|
1352
|
+
amount: BigInt(recipient.amount),
|
|
1353
|
+
}));
|
|
1354
|
+
const totalBtcOutput = outputs.reduce((sum, o) => sum + Number(o.amount), 0);
|
|
1355
|
+
let changeAmount = totalBtcSelected - totalBtcOutput;
|
|
1356
|
+
// enforce minimum change amount when there are asset changes
|
|
1357
|
+
if (assetChanges.size > 0 && changeAmount < Number(this.dustAmount)) {
|
|
1358
|
+
const availableCoins = virtualCoins.filter((c) => !selectedCoins.find((sc) => sc.txid === c.txid && sc.vout === c.vout));
|
|
1359
|
+
const { inputs: extraCoins } = selectVirtualCoins(availableCoins, Number(this.dustAmount) - changeAmount);
|
|
1360
|
+
for (const coin of extraCoins) {
|
|
1361
|
+
if (coin.assets) {
|
|
1362
|
+
for (const asset of coin.assets) {
|
|
1363
|
+
const existing = assetChanges.get(asset.assetId) ?? 0n;
|
|
1364
|
+
assetChanges.set(asset.assetId, existing + BigInt(asset.amount));
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
selectedCoins = [...selectedCoins, ...extraCoins];
|
|
1369
|
+
totalBtcSelected += extraCoins.reduce((sum, c) => sum + c.value, 0);
|
|
1370
|
+
changeAmount = totalBtcSelected - totalBtcOutput;
|
|
1371
|
+
}
|
|
1372
|
+
// build change receiver with BTC change and all asset changes
|
|
1373
|
+
let changeReceiver;
|
|
1374
|
+
let changeIndex = 0;
|
|
1375
|
+
if (changeAmount > 0) {
|
|
1376
|
+
const changeAssets = [];
|
|
1377
|
+
for (const [assetId, amount] of assetChanges) {
|
|
1378
|
+
if (amount > 0n) {
|
|
1379
|
+
changeAssets.push({ assetId, amount: Number(amount) });
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
changeIndex = outputs.length;
|
|
1383
|
+
outputs.push({
|
|
1384
|
+
script: BigInt(changeAmount) < this.dustAmount
|
|
1385
|
+
? outputAddress.subdustPkScript
|
|
1386
|
+
: outputAddress.pkScript,
|
|
1387
|
+
amount: BigInt(changeAmount),
|
|
1067
1388
|
});
|
|
1389
|
+
changeReceiver = {
|
|
1390
|
+
address: address,
|
|
1391
|
+
amount: changeAmount,
|
|
1392
|
+
assets: changeAssets.length > 0 ? changeAssets : undefined,
|
|
1393
|
+
};
|
|
1394
|
+
}
|
|
1395
|
+
// create asset packet only if there are assets involved
|
|
1396
|
+
const assetInputs = selectedCoinsToAssetInputs(selectedCoins);
|
|
1397
|
+
const hasAssets = assetInputs.size > 0 ||
|
|
1398
|
+
recipients.some((r) => r.assets && r.assets.length > 0);
|
|
1399
|
+
if (hasAssets) {
|
|
1400
|
+
const assetPacket = createAssetPacket(assetInputs, recipients, changeReceiver);
|
|
1401
|
+
outputs.push(assetPacket.txOut());
|
|
1068
1402
|
}
|
|
1069
|
-
|
|
1403
|
+
const sentAmount = recipients.reduce((sum, r) => sum + r.amount, 0);
|
|
1404
|
+
const { arkTxid, signedCheckpointTxs } = await this.buildAndSubmitOffchainTx(selectedCoins, outputs);
|
|
1405
|
+
await this.updateDbAfterOffchainTx(selectedCoins, arkTxid, signedCheckpointTxs, sentAmount, BigInt(changeAmount), changeReceiver ? changeIndex : 0, changeReceiver?.assets);
|
|
1406
|
+
return arkTxid;
|
|
1070
1407
|
}
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
const
|
|
1408
|
+
/**
|
|
1409
|
+
* Build an offchain transaction from the given inputs and outputs,
|
|
1410
|
+
* sign it, submit to the ark provider, and finalize.
|
|
1411
|
+
* @returns The ark transaction id and server-signed checkpoint PSBTs (for bookkeeping)
|
|
1412
|
+
*/
|
|
1413
|
+
async buildAndSubmitOffchainTx(inputs, outputs) {
|
|
1414
|
+
const tapLeafScript = this.offchainTapscript.forfeit();
|
|
1415
|
+
if (!tapLeafScript) {
|
|
1416
|
+
throw new Error("Selected leaf not found");
|
|
1417
|
+
}
|
|
1418
|
+
const tapTree = this.offchainTapscript.encode();
|
|
1419
|
+
const offchainTx = buildOffchainTx(inputs.map((input) => ({
|
|
1420
|
+
...input,
|
|
1421
|
+
tapLeafScript,
|
|
1422
|
+
tapTree,
|
|
1423
|
+
})), outputs, this.serverUnrollScript);
|
|
1424
|
+
const signedVirtualTx = await this.identity.sign(offchainTx.arkTx);
|
|
1425
|
+
const { arkTxid, signedCheckpointTxs } = await this.arkProvider.submitTx(base64.encode(signedVirtualTx.toPSBT()), offchainTx.checkpoints.map((c) => base64.encode(c.toPSBT())));
|
|
1426
|
+
const finalCheckpoints = await Promise.all(signedCheckpointTxs.map(async (c) => {
|
|
1427
|
+
const tx = Transaction.fromPSBT(base64.decode(c));
|
|
1428
|
+
const signedCheckpoint = await this.identity.sign(tx);
|
|
1429
|
+
return base64.encode(signedCheckpoint.toPSBT());
|
|
1430
|
+
}));
|
|
1431
|
+
await this.arkProvider.finalizeTx(arkTxid, finalCheckpoints);
|
|
1432
|
+
return { arkTxid, signedCheckpointTxs };
|
|
1433
|
+
}
|
|
1434
|
+
// mark vtxo spent and save change vtxo if any
|
|
1435
|
+
async updateDbAfterOffchainTx(inputs, arkTxid, signedCheckpointTxs, sentAmount, changeAmount, changeVout, changeAssets) {
|
|
1078
1436
|
try {
|
|
1079
|
-
const
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1437
|
+
const spentVtxos = [];
|
|
1438
|
+
const commitmentTxIds = new Set();
|
|
1439
|
+
let batchExpiry = Number.MAX_SAFE_INTEGER;
|
|
1440
|
+
if (inputs.length !== signedCheckpointTxs.length) {
|
|
1441
|
+
console.warn(`updateDbAfterOffchainTx: inputs length (${inputs.length}) differs from signedCheckpointTxs length (${signedCheckpointTxs.length})`);
|
|
1442
|
+
}
|
|
1443
|
+
const safeLength = Math.min(inputs.length, signedCheckpointTxs.length);
|
|
1444
|
+
for (const [inputIndex, input] of inputs.entries()) {
|
|
1445
|
+
const vtxo = extendVirtualCoin(this, input);
|
|
1446
|
+
if (inputIndex < safeLength &&
|
|
1447
|
+
signedCheckpointTxs[inputIndex]) {
|
|
1448
|
+
const checkpoint = Transaction.fromPSBT(base64.decode(signedCheckpointTxs[inputIndex]));
|
|
1449
|
+
spentVtxos.push({
|
|
1450
|
+
...vtxo,
|
|
1451
|
+
virtualStatus: {
|
|
1452
|
+
...vtxo.virtualStatus,
|
|
1453
|
+
state: "spent",
|
|
1454
|
+
},
|
|
1455
|
+
spentBy: checkpoint.id,
|
|
1456
|
+
arkTxId: arkTxid,
|
|
1457
|
+
isSpent: true,
|
|
1458
|
+
});
|
|
1459
|
+
}
|
|
1460
|
+
else {
|
|
1461
|
+
spentVtxos.push({
|
|
1462
|
+
...vtxo,
|
|
1463
|
+
virtualStatus: {
|
|
1464
|
+
...vtxo.virtualStatus,
|
|
1465
|
+
state: "spent",
|
|
1466
|
+
},
|
|
1467
|
+
arkTxId: arkTxid,
|
|
1468
|
+
isSpent: true,
|
|
1469
|
+
});
|
|
1470
|
+
}
|
|
1471
|
+
if (vtxo.virtualStatus.commitmentTxIds) {
|
|
1472
|
+
for (const id of vtxo.virtualStatus.commitmentTxIds) {
|
|
1473
|
+
commitmentTxIds.add(id);
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
if (vtxo.virtualStatus.batchExpiry) {
|
|
1477
|
+
batchExpiry = Math.min(batchExpiry, vtxo.virtualStatus.batchExpiry);
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
const createdAt = Date.now();
|
|
1481
|
+
const addr = this.arkAddress.encode();
|
|
1482
|
+
// Only save a change VTXO for preconfirmed coins (those with a batchExpiry).
|
|
1483
|
+
// Inputs without a batchExpiry are already settled/unrolled and don't need tracking.
|
|
1484
|
+
let changeVtxo;
|
|
1485
|
+
if (changeAmount > 0n && batchExpiry !== Number.MAX_SAFE_INTEGER) {
|
|
1486
|
+
changeVtxo = {
|
|
1487
|
+
txid: arkTxid,
|
|
1488
|
+
vout: changeVout,
|
|
1489
|
+
createdAt: new Date(createdAt),
|
|
1490
|
+
forfeitTapLeafScript: this.offchainTapscript.forfeit(),
|
|
1491
|
+
intentTapLeafScript: this.offchainTapscript.forfeit(),
|
|
1492
|
+
isUnrolled: false,
|
|
1493
|
+
isSpent: false,
|
|
1494
|
+
tapTree: this.offchainTapscript.encode(),
|
|
1495
|
+
value: Number(changeAmount),
|
|
1496
|
+
virtualStatus: {
|
|
1497
|
+
state: "preconfirmed",
|
|
1498
|
+
commitmentTxIds: Array.from(commitmentTxIds),
|
|
1499
|
+
batchExpiry,
|
|
1500
|
+
},
|
|
1501
|
+
status: {
|
|
1502
|
+
confirmed: false,
|
|
1503
|
+
},
|
|
1504
|
+
assets: changeAssets,
|
|
1505
|
+
};
|
|
1506
|
+
}
|
|
1507
|
+
await this.walletRepository.saveVtxos(addr, changeVtxo ? [...spentVtxos, changeVtxo] : spentVtxos);
|
|
1508
|
+
await this.walletRepository.saveTransactions(addr, [
|
|
1509
|
+
{
|
|
1510
|
+
key: {
|
|
1511
|
+
boardingTxid: "",
|
|
1512
|
+
commitmentTxid: "",
|
|
1513
|
+
arkTxid: arkTxid,
|
|
1514
|
+
},
|
|
1515
|
+
amount: sentAmount,
|
|
1516
|
+
type: TxType.TxSent,
|
|
1517
|
+
settled: false,
|
|
1518
|
+
createdAt,
|
|
1519
|
+
},
|
|
1520
|
+
]);
|
|
1083
1521
|
}
|
|
1084
|
-
catch {
|
|
1085
|
-
|
|
1086
|
-
sequence = Number(params.absoluteTimelock);
|
|
1522
|
+
catch (e) {
|
|
1523
|
+
console.warn("error saving offchain tx to repository", e);
|
|
1087
1524
|
}
|
|
1088
1525
|
}
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1526
|
+
// mark vtxo spent & settled, remove boarding utxo
|
|
1527
|
+
async updateDbAfterSettle(inputs, commitmentTxid) {
|
|
1528
|
+
try {
|
|
1529
|
+
const addr = this.arkAddress.encode();
|
|
1530
|
+
const boardingAddress = await this.getBoardingAddress();
|
|
1531
|
+
const spentVtxos = [];
|
|
1532
|
+
const inputArkTxIds = new Set();
|
|
1533
|
+
const boardingUtxoToRemove = new Set();
|
|
1534
|
+
const isVtxo = (input) => "virtualStatus" in input;
|
|
1535
|
+
for (const input of inputs) {
|
|
1536
|
+
if (isVtxo(input)) {
|
|
1537
|
+
// vtxo = mark it settled
|
|
1538
|
+
const vtxo = extendVirtualCoin(this, input);
|
|
1539
|
+
if (vtxo.arkTxId) {
|
|
1540
|
+
inputArkTxIds.add(vtxo.arkTxId);
|
|
1541
|
+
}
|
|
1542
|
+
spentVtxos.push({
|
|
1543
|
+
...vtxo,
|
|
1544
|
+
virtualStatus: {
|
|
1545
|
+
...vtxo.virtualStatus,
|
|
1546
|
+
state: "settled",
|
|
1547
|
+
},
|
|
1548
|
+
settledBy: commitmentTxid,
|
|
1549
|
+
isSpent: true,
|
|
1550
|
+
});
|
|
1551
|
+
}
|
|
1552
|
+
else {
|
|
1553
|
+
// boarding utxo = remove it
|
|
1554
|
+
boardingUtxoToRemove.add(`${input.txid}:${input.vout}`);
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
if (spentVtxos.length > 0) {
|
|
1558
|
+
await this.walletRepository.saveVtxos(addr, spentVtxos);
|
|
1559
|
+
}
|
|
1560
|
+
if (boardingUtxoToRemove.size > 0) {
|
|
1561
|
+
const currentUtxos = await this.walletRepository.getUtxos(boardingAddress);
|
|
1562
|
+
const filtered = currentUtxos.filter((u) => !boardingUtxoToRemove.has(`${u.txid}:${u.vout}`));
|
|
1563
|
+
// Clear and re-save the filtered list
|
|
1564
|
+
await this.walletRepository.deleteUtxos(boardingAddress);
|
|
1565
|
+
if (filtered.length > 0) {
|
|
1566
|
+
await this.walletRepository.saveUtxos(boardingAddress, filtered);
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
catch (e) {
|
|
1571
|
+
console.warn("error updating repository after settle", e);
|
|
1572
|
+
}
|
|
1099
1573
|
}
|
|
1100
1574
|
}
|
|
1575
|
+
Wallet.MIN_FEE_RATE = 1; // sats/vbyte
|
|
1101
1576
|
/**
|
|
1102
1577
|
* Select virtual coins to reach a target amount, prioritizing those closer to expiry
|
|
1103
1578
|
* @param coins List of virtual coins to select from
|
|
1104
1579
|
* @param targetAmount Target amount to reach in satoshis
|
|
1105
1580
|
* @returns Selected coins and change amount
|
|
1106
1581
|
*/
|
|
1107
|
-
function selectVirtualCoins(coins, targetAmount) {
|
|
1582
|
+
export function selectVirtualCoins(coins, targetAmount) {
|
|
1108
1583
|
// Sort VTXOs by expiry (ascending) and amount (descending)
|
|
1109
1584
|
const sortedCoins = [...coins].sort((a, b) => {
|
|
1110
1585
|
// First sort by expiry if available
|