@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
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { ArkAddress, decodeTapscript, Estimator, Intent, isRecoverable, MultisigTapscript, VtxoScript, } from '../index.js';
|
|
2
|
+
import { base64, hex } from "@scure/base";
|
|
3
|
+
import { scriptFromTapLeafScript } from '../script/base.js';
|
|
4
|
+
import { buildForfeitTxWithOutput } from '../forfeit.js';
|
|
5
|
+
import { Address, OutScript, SigHash } from "@scure/btc-signer";
|
|
6
|
+
import { getNetwork } from '../networks.js';
|
|
7
|
+
export class DelegatorManagerImpl {
|
|
8
|
+
constructor(delegatorProvider, arkInfoProvider, identity) {
|
|
9
|
+
this.delegatorProvider = delegatorProvider;
|
|
10
|
+
this.arkInfoProvider = arkInfoProvider;
|
|
11
|
+
this.identity = identity;
|
|
12
|
+
}
|
|
13
|
+
async delegate(vtxos, destination, delegateAt) {
|
|
14
|
+
if (vtxos.length === 0) {
|
|
15
|
+
return { delegated: [], failed: [] };
|
|
16
|
+
}
|
|
17
|
+
const destinationScript = ArkAddress.decode(destination).pkScript;
|
|
18
|
+
// if explicit delegateAt is provided, delegate all vtxos at once without sorting
|
|
19
|
+
if (delegateAt) {
|
|
20
|
+
try {
|
|
21
|
+
await delegate(this.identity, this.delegatorProvider, this.arkInfoProvider, vtxos, destinationScript, delegateAt);
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
return { delegated: [], failed: [{ outpoints: vtxos, error }] };
|
|
25
|
+
}
|
|
26
|
+
return { delegated: vtxos, failed: [] };
|
|
27
|
+
}
|
|
28
|
+
// if no explicit delegateAt is provided, sort vtxos by expiry and delegate in groups of the same expiry day
|
|
29
|
+
const groupByExpiry = new Map();
|
|
30
|
+
let recoverableVtxos = [];
|
|
31
|
+
for (const vtxo of vtxos) {
|
|
32
|
+
if (isRecoverable(vtxo)) {
|
|
33
|
+
recoverableVtxos.push(vtxo);
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
const expiry = vtxo.virtualStatus.batchExpiry;
|
|
37
|
+
if (!expiry)
|
|
38
|
+
continue;
|
|
39
|
+
const dayKey = getDayTimestamp(expiry);
|
|
40
|
+
groupByExpiry.set(dayKey, [
|
|
41
|
+
...(groupByExpiry.get(dayKey) ?? []),
|
|
42
|
+
vtxo,
|
|
43
|
+
]);
|
|
44
|
+
}
|
|
45
|
+
// if no groups, it means we only need to delegate the recoverable vtxos
|
|
46
|
+
if (groupByExpiry.size === 0) {
|
|
47
|
+
try {
|
|
48
|
+
await delegate(this.identity, this.delegatorProvider, this.arkInfoProvider, recoverableVtxos, destinationScript, delegateAt);
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
return {
|
|
52
|
+
delegated: [],
|
|
53
|
+
failed: [{ outpoints: recoverableVtxos, error }],
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
return { delegated: recoverableVtxos, failed: [] };
|
|
57
|
+
}
|
|
58
|
+
// search for the earliest group, include recoverable vtxos into it
|
|
59
|
+
const earliestGroup = Math.min(...groupByExpiry.keys());
|
|
60
|
+
groupByExpiry.set(earliestGroup, [
|
|
61
|
+
...(groupByExpiry.get(earliestGroup) ?? []),
|
|
62
|
+
...recoverableVtxos,
|
|
63
|
+
]);
|
|
64
|
+
const groupsList = Array.from(groupByExpiry.entries());
|
|
65
|
+
const result = await Promise.allSettled(groupsList.map(async ([, vtxosGroup]) => delegate(this.identity, this.delegatorProvider, this.arkInfoProvider, vtxosGroup, destinationScript)));
|
|
66
|
+
const delegated = [];
|
|
67
|
+
const failed = [];
|
|
68
|
+
for (const [index, resultItem] of result.entries()) {
|
|
69
|
+
const vtxos = groupsList[index][1];
|
|
70
|
+
if (resultItem.status === "rejected") {
|
|
71
|
+
failed.push({ outpoints: vtxos, error: resultItem.reason });
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
delegated.push(...vtxos);
|
|
75
|
+
}
|
|
76
|
+
return { delegated, failed };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Delegates virtual coins to a delegator provider, allowing them to manage the coins renewal
|
|
81
|
+
* on behalf of the wallet.
|
|
82
|
+
* @param vtxos - Array of extended virtual coins to delegate. Must not be empty.
|
|
83
|
+
* @param delegateAt - Optional Date specifying when the delegation
|
|
84
|
+
* should occur. If not provided, defaults to 12 hours before the earliest
|
|
85
|
+
* expiry time of the provided vtxos.
|
|
86
|
+
*/
|
|
87
|
+
async function delegate(identity, delegatorProvider, arkInfoProvider, vtxos, destinationScript, delegateAt) {
|
|
88
|
+
if (vtxos.length === 0) {
|
|
89
|
+
throw new Error("unable to delegate: no vtxos provided");
|
|
90
|
+
}
|
|
91
|
+
if (!delegatorProvider) {
|
|
92
|
+
throw new Error("unable to delegate: delegator provider not configured");
|
|
93
|
+
}
|
|
94
|
+
if (!delegateAt) {
|
|
95
|
+
const expiryTimestamp = vtxos
|
|
96
|
+
.filter((coin) => !isRecoverable(coin) && coin.virtualStatus.batchExpiry)
|
|
97
|
+
.reduce((min, coin) => Math.min(min, coin.virtualStatus.batchExpiry), Number.MAX_SAFE_INTEGER);
|
|
98
|
+
if (!expiryTimestamp || expiryTimestamp === Number.MAX_SAFE_INTEGER) {
|
|
99
|
+
// if no expiry (recoverable vtxos), delegate 1 minute from now
|
|
100
|
+
delegateAt = new Date(Date.now() + 1 * 60 * 1000);
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
const remainingTimeMs = expiryTimestamp - Date.now();
|
|
104
|
+
if (remainingTimeMs <= 0) {
|
|
105
|
+
delegateAt = new Date(Date.now() + 1 * 60 * 1000);
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
// delegate 10% before the expiry
|
|
109
|
+
delegateAt = new Date(expiryTimestamp - remainingTimeMs * 0.1);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const { fees, dust, forfeitAddress, network } = await arkInfoProvider.getInfo();
|
|
114
|
+
const delegateAtSeconds = delegateAt.getTime() / 1000;
|
|
115
|
+
const estimator = new Estimator({
|
|
116
|
+
...fees.intentFee,
|
|
117
|
+
// replace now() function with the delegateAt timestamp
|
|
118
|
+
offchainInput: fees.intentFee.offchainInput?.replace("now()", `double(${delegateAtSeconds})`),
|
|
119
|
+
offchainOutput: fees.intentFee.offchainOutput?.replace("now()", `double(${delegateAtSeconds})`),
|
|
120
|
+
});
|
|
121
|
+
let amount = 0n;
|
|
122
|
+
for (const coin of vtxos) {
|
|
123
|
+
const inputFee = estimator.evalOffchainInput({
|
|
124
|
+
amount: BigInt(coin.value),
|
|
125
|
+
type: "vtxo",
|
|
126
|
+
weight: 0,
|
|
127
|
+
birth: coin.createdAt,
|
|
128
|
+
expiry: coin.virtualStatus.batchExpiry
|
|
129
|
+
? new Date(coin.virtualStatus.batchExpiry)
|
|
130
|
+
: undefined,
|
|
131
|
+
});
|
|
132
|
+
if (inputFee.value >= coin.value) {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
amount += BigInt(coin.value) - BigInt(inputFee.value);
|
|
136
|
+
}
|
|
137
|
+
const { delegatorAddress, pubkey, fee } = await delegatorProvider.getDelegateInfo();
|
|
138
|
+
const outputs = [];
|
|
139
|
+
const delegatorFee = BigInt(Number(fee));
|
|
140
|
+
if (delegatorFee > 0n) {
|
|
141
|
+
outputs.push({
|
|
142
|
+
script: ArkAddress.decode(delegatorAddress).pkScript,
|
|
143
|
+
amount: delegatorFee,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
const outputFee = outputs.reduce((fee, output) => {
|
|
147
|
+
if (!output.amount || !output.script)
|
|
148
|
+
return fee;
|
|
149
|
+
return (fee +
|
|
150
|
+
estimator.evalOffchainOutput({
|
|
151
|
+
amount: output.amount,
|
|
152
|
+
script: hex.encode(output.script),
|
|
153
|
+
}).satoshis);
|
|
154
|
+
}, 0);
|
|
155
|
+
if (amount - BigInt(outputFee) <= dust) {
|
|
156
|
+
throw new Error("Amount is below dust limit, cannot delegate");
|
|
157
|
+
}
|
|
158
|
+
amount -= BigInt(outputFee);
|
|
159
|
+
amount -= delegatorFee;
|
|
160
|
+
if (amount <= dust) {
|
|
161
|
+
throw new Error("Amount is below dust limit, cannot delegate");
|
|
162
|
+
}
|
|
163
|
+
outputs.push({
|
|
164
|
+
script: destinationScript,
|
|
165
|
+
amount: amount,
|
|
166
|
+
});
|
|
167
|
+
const registerIntent = await makeSignedDelegateIntent(identity, vtxos, outputs, [], [pubkey], delegateAtSeconds);
|
|
168
|
+
const forfeitOutputScript = OutScript.encode(Address(getNetwork(network)).decode(forfeitAddress));
|
|
169
|
+
const forfeits = await Promise.all(vtxos
|
|
170
|
+
.filter((v) => !isRecoverable(v))
|
|
171
|
+
.map(async (coin) => {
|
|
172
|
+
const forfeit = await makeDelegateForfeitTx(coin, dust, pubkey, forfeitOutputScript, identity);
|
|
173
|
+
return base64.encode(forfeit.toPSBT());
|
|
174
|
+
}));
|
|
175
|
+
await delegatorProvider.delegate(registerIntent, forfeits);
|
|
176
|
+
}
|
|
177
|
+
async function makeDelegateForfeitTx(input, connectorAmount, delegatePubkey, forfeitOutputScript, identity) {
|
|
178
|
+
if (delegatePubkey.length === 66) {
|
|
179
|
+
delegatePubkey = delegatePubkey.slice(2);
|
|
180
|
+
}
|
|
181
|
+
const vtxoScript = VtxoScript.decode(input.tapTree);
|
|
182
|
+
const delegateTapLeaf = vtxoScript.leaves.find((tapLeaf) => {
|
|
183
|
+
const arkTapscript = decodeTapscript(scriptFromTapLeafScript(tapLeaf));
|
|
184
|
+
if (!MultisigTapscript.is(arkTapscript))
|
|
185
|
+
return false;
|
|
186
|
+
if (!arkTapscript.params.pubkeys
|
|
187
|
+
.map(hex.encode)
|
|
188
|
+
.includes(delegatePubkey))
|
|
189
|
+
return false;
|
|
190
|
+
return true;
|
|
191
|
+
});
|
|
192
|
+
if (!delegateTapLeaf) {
|
|
193
|
+
throw new Error(`delegate tap leaf not found for input: ${input.txid}:${input.vout}`);
|
|
194
|
+
}
|
|
195
|
+
const tx = buildForfeitTxWithOutput([
|
|
196
|
+
{
|
|
197
|
+
txid: input.txid,
|
|
198
|
+
index: input.vout,
|
|
199
|
+
witnessUtxo: {
|
|
200
|
+
amount: BigInt(input.value),
|
|
201
|
+
script: VtxoScript.decode(input.tapTree).pkScript,
|
|
202
|
+
},
|
|
203
|
+
sighashType: SigHash.ALL_ANYONECANPAY,
|
|
204
|
+
tapLeafScript: [delegateTapLeaf],
|
|
205
|
+
},
|
|
206
|
+
], {
|
|
207
|
+
script: forfeitOutputScript,
|
|
208
|
+
amount: BigInt(input.value) + connectorAmount,
|
|
209
|
+
});
|
|
210
|
+
return identity.sign(tx);
|
|
211
|
+
}
|
|
212
|
+
async function makeSignedDelegateIntent(identity, coins, outputs, onchainOutputsIndexes, cosignerPubKeys, validAt) {
|
|
213
|
+
const message = {
|
|
214
|
+
type: "register",
|
|
215
|
+
onchain_output_indexes: onchainOutputsIndexes,
|
|
216
|
+
valid_at: Math.floor(validAt),
|
|
217
|
+
expire_at: 0,
|
|
218
|
+
cosigners_public_keys: cosignerPubKeys,
|
|
219
|
+
};
|
|
220
|
+
const proof = Intent.create(message, coins, outputs);
|
|
221
|
+
const signedProof = await identity.sign(proof);
|
|
222
|
+
return {
|
|
223
|
+
proof: base64.encode(signedProof.toPSBT()),
|
|
224
|
+
message,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
function getDayTimestamp(timestamp) {
|
|
228
|
+
const date = new Date(timestamp);
|
|
229
|
+
date.setUTCHours(0, 0, 0, 0);
|
|
230
|
+
return date.getTime();
|
|
231
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { hex } from "@scure/base";
|
|
2
|
+
import { runTasks } from '../../worker/expo/taskRunner.js';
|
|
3
|
+
import { contractPollProcessor, CONTRACT_POLL_TASK_TYPE, } from '../../worker/expo/processors/index.js';
|
|
4
|
+
import { DefaultVtxo } from '../../script/default.js';
|
|
5
|
+
import { ExpoArkProvider } from '../../providers/expoArk.js';
|
|
6
|
+
import { ExpoIndexerProvider } from '../../providers/expoIndexer.js';
|
|
7
|
+
import { extendVirtualCoin, extendVtxoFromContract, getRandomId, } from '../utils.js';
|
|
8
|
+
function requireTaskManager() {
|
|
9
|
+
try {
|
|
10
|
+
return require("expo-task-manager");
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
throw new Error("expo-task-manager is required for background tasks. " +
|
|
14
|
+
"Install it with: npx expo install expo-task-manager");
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function requireBackgroundTask() {
|
|
18
|
+
try {
|
|
19
|
+
return require("expo-background-task");
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
throw new Error("expo-background-task is required for background tasks. " +
|
|
23
|
+
"Install it with: npx expo install expo-background-task");
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Define the Expo background task handler.
|
|
28
|
+
*
|
|
29
|
+
* **Must be called at module/global scope** (before React mounts).
|
|
30
|
+
* Internally calls `TaskManager.defineTask()`.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```ts
|
|
34
|
+
* // At the top of your app entry file
|
|
35
|
+
* import { defineExpoBackgroundTask } from "@arkade-os/sdk/wallet/expo";
|
|
36
|
+
* import { AsyncStorageTaskQueue } from "@arkade-os/sdk/worker/expo";
|
|
37
|
+
* import AsyncStorage from "@react-native-async-storage/async-storage";
|
|
38
|
+
*
|
|
39
|
+
* const taskQueue = new AsyncStorageTaskQueue(AsyncStorage);
|
|
40
|
+
* defineExpoBackgroundTask("ark-background-poll", {
|
|
41
|
+
* taskQueue,
|
|
42
|
+
* walletRepository: new IndexedDBWalletRepository(),
|
|
43
|
+
* contractRepository: new IndexedDBContractRepository(),
|
|
44
|
+
* });
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export function defineExpoBackgroundTask(taskName, options) {
|
|
48
|
+
const TaskManager = requireTaskManager();
|
|
49
|
+
const BackgroundTask = requireBackgroundTask();
|
|
50
|
+
const { taskQueue, walletRepository, contractRepository, processors = [contractPollProcessor], } = options;
|
|
51
|
+
TaskManager.defineTask(taskName, async () => {
|
|
52
|
+
try {
|
|
53
|
+
const config = await taskQueue.loadConfig();
|
|
54
|
+
if (!config) {
|
|
55
|
+
// No config persisted yet — ExpoWallet.setup() hasn't run.
|
|
56
|
+
// Nothing to do.
|
|
57
|
+
return BackgroundTask.BackgroundTaskResult.Success;
|
|
58
|
+
}
|
|
59
|
+
// Reconstruct providers
|
|
60
|
+
const indexerProvider = new ExpoIndexerProvider(config.arkServerUrl);
|
|
61
|
+
const arkProvider = new ExpoArkProvider(config.arkServerUrl);
|
|
62
|
+
// Reconstruct default offchainTapscript as fallback
|
|
63
|
+
// for VTXOs not associated with a contract.
|
|
64
|
+
const defaultTapscript = new DefaultVtxo.Script({
|
|
65
|
+
pubKey: hex.decode(config.pubkeyHex),
|
|
66
|
+
serverPubKey: hex.decode(config.serverPubKeyHex),
|
|
67
|
+
csvTimelock: {
|
|
68
|
+
value: BigInt(config.exitTimelockValue),
|
|
69
|
+
type: config.exitTimelockType,
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
await runTasks(taskQueue, processors, {
|
|
73
|
+
walletRepository,
|
|
74
|
+
contractRepository,
|
|
75
|
+
indexerProvider,
|
|
76
|
+
arkProvider,
|
|
77
|
+
extendVtxo: (vtxo, contract) => {
|
|
78
|
+
if (contract) {
|
|
79
|
+
return extendVtxoFromContract(vtxo, contract);
|
|
80
|
+
}
|
|
81
|
+
return extendVirtualCoin({ offchainTapscript: defaultTapscript }, vtxo);
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
// Acknowledge outbox results (no foreground to consume them)
|
|
85
|
+
const results = await taskQueue.getResults();
|
|
86
|
+
if (results.length > 0) {
|
|
87
|
+
await taskQueue.acknowledgeResults(results.map((r) => r.id));
|
|
88
|
+
}
|
|
89
|
+
// Re-seed the contract-poll task for the next OS wake
|
|
90
|
+
const existing = await taskQueue.getTasks(CONTRACT_POLL_TASK_TYPE);
|
|
91
|
+
if (existing.length === 0) {
|
|
92
|
+
const task = {
|
|
93
|
+
id: getRandomId(),
|
|
94
|
+
type: CONTRACT_POLL_TASK_TYPE,
|
|
95
|
+
data: {},
|
|
96
|
+
createdAt: Date.now(),
|
|
97
|
+
};
|
|
98
|
+
await taskQueue.addTask(task);
|
|
99
|
+
}
|
|
100
|
+
return BackgroundTask.BackgroundTaskResult.Success;
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
console.error("[ark-sdk] Background task failed:", error instanceof Error ? error.message : error);
|
|
104
|
+
return BackgroundTask.BackgroundTaskResult.Failed;
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Activate the OS-level background task scheduler.
|
|
110
|
+
*
|
|
111
|
+
* Call this after {@link defineExpoBackgroundTask} (typically inside
|
|
112
|
+
* {@link ExpoWallet.setup} or in a React component after wallet init).
|
|
113
|
+
*
|
|
114
|
+
* @param minimumInterval - Minimum interval in minutes (default 15).
|
|
115
|
+
*/
|
|
116
|
+
export async function registerExpoBackgroundTask(taskName, options) {
|
|
117
|
+
const BackgroundTask = requireBackgroundTask();
|
|
118
|
+
await BackgroundTask.registerTaskAsync(taskName, {
|
|
119
|
+
minimumInterval: (options?.minimumInterval ?? 15) * 60,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Unregister the background task from the OS scheduler.
|
|
124
|
+
*/
|
|
125
|
+
export async function unregisterExpoBackgroundTask(taskName) {
|
|
126
|
+
const BackgroundTask = requireBackgroundTask();
|
|
127
|
+
await BackgroundTask.unregisterTaskAsync(taskName);
|
|
128
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { hex } from "@scure/base";
|
|
2
|
+
import { Wallet } from '../wallet.js';
|
|
3
|
+
import { RestArkProvider } from '../../providers/ark.js';
|
|
4
|
+
import { runTasks } from '../../worker/expo/taskRunner.js';
|
|
5
|
+
import { contractPollProcessor, CONTRACT_POLL_TASK_TYPE, } from '../../worker/expo/processors/index.js';
|
|
6
|
+
import { extendVirtualCoin, extendVtxoFromContract, getRandomId, } from '../utils.js';
|
|
7
|
+
import { DefaultVtxo } from '../../script/default.js';
|
|
8
|
+
/**
|
|
9
|
+
* Expo/React Native wallet with built-in background task processing.
|
|
10
|
+
*
|
|
11
|
+
* Wraps a standard {@link Wallet} and adds a lightweight task queue
|
|
12
|
+
* for keeping contract/VTXO state fresh while the app is active and
|
|
13
|
+
* across Expo BackgroundTask wakes.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* import { ExpoWallet } from "@arkade-os/sdk/wallet/expo";
|
|
18
|
+
* import { AsyncStorageTaskQueue } from "@arkade-os/sdk/worker/expo";
|
|
19
|
+
*
|
|
20
|
+
* const wallet = await ExpoWallet.setup({
|
|
21
|
+
* identity: SingleKey.fromHex(privateKey),
|
|
22
|
+
* arkServerUrl,
|
|
23
|
+
* esploraUrl,
|
|
24
|
+
* storage: { walletRepository, contractRepository },
|
|
25
|
+
* background: {
|
|
26
|
+
* taskName: "ark-background-poll",
|
|
27
|
+
* taskQueue: new AsyncStorageTaskQueue(AsyncStorage),
|
|
28
|
+
* foregroundIntervalMs: 20_000,
|
|
29
|
+
* minimumBackgroundInterval: 15,
|
|
30
|
+
* },
|
|
31
|
+
* });
|
|
32
|
+
*
|
|
33
|
+
* const balance = await wallet.getBalance();
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export class ExpoWallet {
|
|
37
|
+
constructor(wallet, taskQueue, processors, deps, taskName, foregroundIntervalMs) {
|
|
38
|
+
this.wallet = wallet;
|
|
39
|
+
this.taskQueue = taskQueue;
|
|
40
|
+
this.processors = processors;
|
|
41
|
+
this.deps = deps;
|
|
42
|
+
this.identity = wallet.identity;
|
|
43
|
+
this.arkProvider = wallet.arkProvider;
|
|
44
|
+
this.indexerProvider = wallet.indexerProvider;
|
|
45
|
+
this.taskName = taskName;
|
|
46
|
+
if (foregroundIntervalMs && foregroundIntervalMs > 0) {
|
|
47
|
+
this.startForegroundPolling(foregroundIntervalMs);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Create an ExpoWallet with background task support.
|
|
52
|
+
*
|
|
53
|
+
* 1. Creates the inner {@link Wallet} via `Wallet.create()`.
|
|
54
|
+
* 2. Wires up processors (defaults to {@link contractPollProcessor}).
|
|
55
|
+
* 3. Persists background config for the background handler (if the queue supports it).
|
|
56
|
+
* 4. Seeds the task queue with a `contract-poll` task.
|
|
57
|
+
* 5. Registers the background task with the OS scheduler (if `minimumBackgroundInterval` is set).
|
|
58
|
+
* 6. Starts foreground polling if `foregroundIntervalMs` is set.
|
|
59
|
+
*/
|
|
60
|
+
static async setup(config) {
|
|
61
|
+
const wallet = await Wallet.create(config);
|
|
62
|
+
const processors = config.background.processors ?? [
|
|
63
|
+
contractPollProcessor,
|
|
64
|
+
];
|
|
65
|
+
const deps = {
|
|
66
|
+
walletRepository: wallet.walletRepository,
|
|
67
|
+
contractRepository: wallet.contractRepository,
|
|
68
|
+
indexerProvider: wallet.indexerProvider,
|
|
69
|
+
arkProvider: wallet.arkProvider,
|
|
70
|
+
extendVtxo: (vtxo, contract) => {
|
|
71
|
+
if (contract) {
|
|
72
|
+
return extendVtxoFromContract(vtxo, contract);
|
|
73
|
+
}
|
|
74
|
+
return extendVirtualCoin(wallet, vtxo);
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
const { taskQueue } = config.background;
|
|
78
|
+
// Persist wallet params so the background handler can rehydrate
|
|
79
|
+
// without a network call. Only works with AsyncStorageTaskQueue.
|
|
80
|
+
if ("persistConfig" in taskQueue) {
|
|
81
|
+
const arkServerUrl = config.arkServerUrl ??
|
|
82
|
+
(wallet.arkProvider instanceof RestArkProvider
|
|
83
|
+
? wallet.arkProvider.serverUrl
|
|
84
|
+
: undefined);
|
|
85
|
+
if (arkServerUrl) {
|
|
86
|
+
const timelock = wallet.offchainTapscript.options.csvTimelock ??
|
|
87
|
+
DefaultVtxo.Script.DEFAULT_TIMELOCK;
|
|
88
|
+
const bgConfig = {
|
|
89
|
+
arkServerUrl,
|
|
90
|
+
pubkeyHex: hex.encode(wallet.offchainTapscript.options.pubKey),
|
|
91
|
+
serverPubKeyHex: hex.encode(wallet.offchainTapscript.options.serverPubKey),
|
|
92
|
+
exitTimelockValue: timelock.value.toString(),
|
|
93
|
+
exitTimelockType: timelock.type,
|
|
94
|
+
};
|
|
95
|
+
await taskQueue.persistConfig(bgConfig);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const expoWallet = new ExpoWallet(wallet, taskQueue, processors, deps, config.background.taskName, config.background.foregroundIntervalMs);
|
|
99
|
+
// Seed the queue so the first tick (or background wake) has work to do
|
|
100
|
+
await expoWallet.seedContractPollTask();
|
|
101
|
+
// Activate OS-level background scheduling
|
|
102
|
+
if (config.background.minimumBackgroundInterval) {
|
|
103
|
+
try {
|
|
104
|
+
const { registerExpoBackgroundTask } = await import("./background");
|
|
105
|
+
await registerExpoBackgroundTask(config.background.taskName, {
|
|
106
|
+
minimumInterval: config.background.minimumBackgroundInterval,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
// expo-background-task not installed — foreground-only mode
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return expoWallet;
|
|
114
|
+
}
|
|
115
|
+
// ── Foreground polling ───────────────────────────────────────────
|
|
116
|
+
startForegroundPolling(intervalMs) {
|
|
117
|
+
this.foregroundIntervalId = setInterval(() => {
|
|
118
|
+
this.runForegroundPoll().catch(console.error);
|
|
119
|
+
}, intervalMs);
|
|
120
|
+
}
|
|
121
|
+
async runForegroundPoll() {
|
|
122
|
+
await runTasks(this.taskQueue, this.processors, this.deps);
|
|
123
|
+
// Consume results immediately (no background handoff needed)
|
|
124
|
+
const results = await this.taskQueue.getResults();
|
|
125
|
+
if (results.length > 0) {
|
|
126
|
+
await this.taskQueue.acknowledgeResults(results.map((r) => r.id));
|
|
127
|
+
}
|
|
128
|
+
// Re-seed for the next tick
|
|
129
|
+
await this.seedContractPollTask();
|
|
130
|
+
}
|
|
131
|
+
async seedContractPollTask() {
|
|
132
|
+
const existing = await this.taskQueue.getTasks(CONTRACT_POLL_TASK_TYPE);
|
|
133
|
+
if (existing.length > 0)
|
|
134
|
+
return;
|
|
135
|
+
const task = {
|
|
136
|
+
id: getRandomId(),
|
|
137
|
+
type: CONTRACT_POLL_TASK_TYPE,
|
|
138
|
+
data: {},
|
|
139
|
+
createdAt: Date.now(),
|
|
140
|
+
};
|
|
141
|
+
await this.taskQueue.addTask(task);
|
|
142
|
+
}
|
|
143
|
+
// ── Lifecycle ────────────────────────────────────────────────────
|
|
144
|
+
/**
|
|
145
|
+
* Stop foreground polling and unregister the background task.
|
|
146
|
+
*/
|
|
147
|
+
async dispose() {
|
|
148
|
+
if (this.foregroundIntervalId) {
|
|
149
|
+
clearInterval(this.foregroundIntervalId);
|
|
150
|
+
this.foregroundIntervalId = undefined;
|
|
151
|
+
}
|
|
152
|
+
try {
|
|
153
|
+
const { unregisterExpoBackgroundTask } = await import("./background");
|
|
154
|
+
await unregisterExpoBackgroundTask(this.taskName);
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
// expo-background-task not installed — nothing to unregister
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// ── IWallet delegation ───────────────────────────────────────────
|
|
161
|
+
getAddress() {
|
|
162
|
+
return this.wallet.getAddress();
|
|
163
|
+
}
|
|
164
|
+
getBoardingAddress() {
|
|
165
|
+
return this.wallet.getBoardingAddress();
|
|
166
|
+
}
|
|
167
|
+
getBalance() {
|
|
168
|
+
return this.wallet.getBalance();
|
|
169
|
+
}
|
|
170
|
+
getVtxos(filter) {
|
|
171
|
+
return this.wallet.getVtxos(filter);
|
|
172
|
+
}
|
|
173
|
+
getBoardingUtxos() {
|
|
174
|
+
return this.wallet.getBoardingUtxos();
|
|
175
|
+
}
|
|
176
|
+
getTransactionHistory() {
|
|
177
|
+
return this.wallet.getTransactionHistory();
|
|
178
|
+
}
|
|
179
|
+
getContractManager() {
|
|
180
|
+
return this.wallet.getContractManager();
|
|
181
|
+
}
|
|
182
|
+
sendBitcoin(params) {
|
|
183
|
+
return this.wallet.sendBitcoin(params);
|
|
184
|
+
}
|
|
185
|
+
settle(params, eventCallback) {
|
|
186
|
+
return this.wallet.settle(params, eventCallback);
|
|
187
|
+
}
|
|
188
|
+
send(...recipients) {
|
|
189
|
+
return this.wallet.send(...recipients);
|
|
190
|
+
}
|
|
191
|
+
get assetManager() {
|
|
192
|
+
return this.wallet.assetManager;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
@@ -4,6 +4,7 @@ import { ESPLORA_URL, EsploraProvider, } from '../providers/onchain.js';
|
|
|
4
4
|
import { findP2AOutput, P2A } from '../utils/anchor.js';
|
|
5
5
|
import { TxWeightEstimator } from '../utils/txSizeEstimator.js';
|
|
6
6
|
import { Transaction } from '../utils/transaction.js';
|
|
7
|
+
import { DUST_AMOUNT } from './utils.js';
|
|
7
8
|
/**
|
|
8
9
|
* Onchain Bitcoin wallet implementation for traditional Bitcoin transactions.
|
|
9
10
|
*
|
|
@@ -55,11 +56,58 @@ export class OnchainWallet {
|
|
|
55
56
|
const onchainTotal = onchainConfirmed + onchainUnconfirmed;
|
|
56
57
|
return onchainTotal;
|
|
57
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* Iteratively selects coins and estimates transaction fees until convergence.
|
|
61
|
+
*
|
|
62
|
+
* This method handles the circular dependency between coin selection and fee
|
|
63
|
+
* estimation: the fee depends on transaction size, which depends on the number
|
|
64
|
+
* of inputs (selected coins) and whether a change output is needed.
|
|
65
|
+
*
|
|
66
|
+
* The algorithm iterates up to 10 times, refining the fee estimate based on
|
|
67
|
+
* the actual transaction structure. It resolves dust oscillation loops that
|
|
68
|
+
* occur when the change amount hovers near the dust threshold—adding/removing
|
|
69
|
+
* the change output causes the fee to fluctuate, preventing convergence.
|
|
70
|
+
* When a lower fee is computed (indicating the change output was dropped),
|
|
71
|
+
* the function accepts this state to guarantee termination.
|
|
72
|
+
*
|
|
73
|
+
* @param coins - Available coins to select from
|
|
74
|
+
* @param amount - Target send amount in satoshis
|
|
75
|
+
* @param feeRate - Fee rate in sat/vbyte
|
|
76
|
+
* @param recipientAddress - Destination address for size estimation
|
|
77
|
+
* @returns Selected inputs, change amount, and calculated fee
|
|
78
|
+
* @throws Error if fee estimation fails to converge within max iterations
|
|
79
|
+
*/
|
|
80
|
+
estimateFeesAndSelectCoins(coins, amount, feeRate, recipientAddress) {
|
|
81
|
+
const MAX_ITERATIONS = 10;
|
|
82
|
+
let fee = 0;
|
|
83
|
+
for (let i = 0; i < MAX_ITERATIONS; i++) {
|
|
84
|
+
const totalNeeded = amount + fee;
|
|
85
|
+
const selected = selectCoins(coins, totalNeeded);
|
|
86
|
+
const estimator = TxWeightEstimator.create();
|
|
87
|
+
for (const _ of selected.inputs) {
|
|
88
|
+
estimator.addKeySpendInput();
|
|
89
|
+
}
|
|
90
|
+
estimator.addOutputAddress(recipientAddress, this.network);
|
|
91
|
+
if (selected.changeAmount >= BigInt(DUST_AMOUNT)) {
|
|
92
|
+
estimator.addOutputAddress(this.address, this.network);
|
|
93
|
+
}
|
|
94
|
+
const newFee = Number(estimator.vsize().value) * feeRate;
|
|
95
|
+
const roundedNewFee = Math.ceil(newFee);
|
|
96
|
+
// Prevent oscillation loops when change falls just below the dust limit.
|
|
97
|
+
// If removing the change output reduces the fee below our budget,
|
|
98
|
+
// we accept the valid transaction state to guarantee convergence.
|
|
99
|
+
if (roundedNewFee <= fee) {
|
|
100
|
+
return { ...selected, fee: roundedNewFee };
|
|
101
|
+
}
|
|
102
|
+
fee = roundedNewFee;
|
|
103
|
+
}
|
|
104
|
+
throw new Error("Fee estimation failed: could not converge");
|
|
105
|
+
}
|
|
58
106
|
async send(params) {
|
|
59
107
|
if (params.amount <= 0) {
|
|
60
108
|
throw new Error("Amount must be positive");
|
|
61
109
|
}
|
|
62
|
-
if (params.amount <
|
|
110
|
+
if (params.amount < DUST_AMOUNT) {
|
|
63
111
|
throw new Error("Amount is below dust limit");
|
|
64
112
|
}
|
|
65
113
|
const coins = await this.getCoins();
|
|
@@ -70,15 +118,14 @@ export class OnchainWallet {
|
|
|
70
118
|
if (!feeRate || feeRate < OnchainWallet.MIN_FEE_RATE) {
|
|
71
119
|
feeRate = OnchainWallet.MIN_FEE_RATE;
|
|
72
120
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const selected = selectCoins(coins, totalNeeded);
|
|
121
|
+
const { inputs, changeAmount } = this.estimateFeesAndSelectCoins(coins, params.amount, feeRate, params.address);
|
|
122
|
+
if (!inputs) {
|
|
123
|
+
throw new Error("Fee estimation failed");
|
|
124
|
+
}
|
|
78
125
|
// Create transaction
|
|
79
126
|
let tx = new Transaction();
|
|
80
127
|
// Add inputs
|
|
81
|
-
for (const input of
|
|
128
|
+
for (const input of inputs) {
|
|
82
129
|
tx.addInput({
|
|
83
130
|
txid: input.txid,
|
|
84
131
|
index: input.vout,
|
|
@@ -91,9 +138,8 @@ export class OnchainWallet {
|
|
|
91
138
|
}
|
|
92
139
|
// Add payment output
|
|
93
140
|
tx.addOutputAddress(params.address, BigInt(params.amount), this.network);
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
tx.addOutputAddress(this.address, selected.changeAmount, this.network);
|
|
141
|
+
if (changeAmount >= BigInt(DUST_AMOUNT)) {
|
|
142
|
+
tx.addOutputAddress(this.address, changeAmount, this.network);
|
|
97
143
|
}
|
|
98
144
|
// Sign inputs and Finalize
|
|
99
145
|
tx = await this.identity.sign(tx);
|
|
@@ -112,7 +158,7 @@ export class OnchainWallet {
|
|
|
112
158
|
const childVsize = TxWeightEstimator.create()
|
|
113
159
|
.addKeySpendInput(true)
|
|
114
160
|
.addP2AInput()
|
|
115
|
-
.
|
|
161
|
+
.addOutputAddress(this.address, this.network)
|
|
116
162
|
.vsize().value;
|
|
117
163
|
const packageVSize = parentVsize + Number(childVsize);
|
|
118
164
|
let feeRate = await this.provider.getFeeRate();
|
|
@@ -155,7 +201,6 @@ export class OnchainWallet {
|
|
|
155
201
|
}
|
|
156
202
|
}
|
|
157
203
|
OnchainWallet.MIN_FEE_RATE = 1; // sat/vbyte
|
|
158
|
-
OnchainWallet.DUST_AMOUNT = 546; // sats
|
|
159
204
|
/**
|
|
160
205
|
* Select coins to reach a target amount, prioritizing those closer to expiry
|
|
161
206
|
* @param coins List of coins to select from
|