@arkade-os/sdk 0.4.26 → 0.4.28
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 +36 -125
- package/dist/adapters/asyncStorage.cjs +48 -0
- package/dist/adapters/asyncStorage.cjs.map +1 -0
- package/dist/adapters/asyncStorage.d.cts +16 -0
- package/dist/{types/storage → adapters}/asyncStorage.d.ts +5 -2
- package/dist/adapters/asyncStorage.js +46 -0
- package/dist/adapters/asyncStorage.js.map +1 -0
- package/dist/adapters/expo.cjs +19 -0
- package/dist/adapters/expo.cjs.map +1 -0
- package/dist/adapters/expo.d.cts +48 -0
- package/dist/adapters/expo.d.ts +48 -0
- package/dist/adapters/expo.js +6 -0
- package/dist/adapters/expo.js.map +1 -0
- package/dist/adapters/fileSystem.cjs +116 -0
- package/dist/adapters/fileSystem.cjs.map +1 -0
- package/dist/adapters/fileSystem.d.cts +17 -0
- package/dist/{types/storage → adapters}/fileSystem.d.ts +5 -2
- package/dist/adapters/fileSystem.js +93 -0
- package/dist/adapters/fileSystem.js.map +1 -0
- package/dist/adapters/indexedDB.cjs +103 -0
- package/dist/adapters/indexedDB.cjs.map +1 -0
- package/dist/adapters/indexedDB.d.cts +18 -0
- package/dist/{types/storage → adapters}/indexedDB.d.ts +5 -2
- package/dist/adapters/indexedDB.js +101 -0
- package/dist/adapters/indexedDB.js.map +1 -0
- package/dist/adapters/localStorage.cjs +50 -0
- package/dist/adapters/localStorage.cjs.map +1 -0
- package/dist/{types/storage/inMemory.d.ts → adapters/localStorage.d.cts} +6 -3
- package/dist/{types/storage → adapters}/localStorage.d.ts +5 -2
- package/dist/adapters/localStorage.js +48 -0
- package/dist/adapters/localStorage.js.map +1 -0
- package/dist/ark-TZ1gXAXU.d.cts +3880 -0
- package/dist/ark-TZ1gXAXU.d.ts +3880 -0
- package/dist/{types/worker/expo/asyncStorageTaskQueue.d.ts → asyncStorageTaskQueue-Cb1F_Z9s.d.ts} +6 -3
- package/dist/asyncStorageTaskQueue-EFqSmYTg.d.cts +49 -0
- package/dist/chunk-5BLDMQED.cjs +18 -0
- package/dist/chunk-5BLDMQED.cjs.map +1 -0
- package/dist/chunk-5PG7DV7A.cjs +805 -0
- package/dist/chunk-5PG7DV7A.cjs.map +1 -0
- package/dist/chunk-A3EMF7RN.js +95 -0
- package/dist/chunk-A3EMF7RN.js.map +1 -0
- package/dist/chunk-ADV27S4N.cjs +2701 -0
- package/dist/chunk-ADV27S4N.cjs.map +1 -0
- package/dist/chunk-BQLHADL7.js +13805 -0
- package/dist/chunk-BQLHADL7.js.map +1 -0
- package/dist/chunk-CFZMTDWI.js +209 -0
- package/dist/chunk-CFZMTDWI.js.map +1 -0
- package/dist/chunk-FG5ACJJW.cjs +212 -0
- package/dist/chunk-FG5ACJJW.cjs.map +1 -0
- package/dist/chunk-HW3JJ323.js +768 -0
- package/dist/chunk-HW3JJ323.js.map +1 -0
- package/dist/chunk-I3DGUUCT.cjs +838 -0
- package/dist/chunk-I3DGUUCT.cjs.map +1 -0
- package/dist/chunk-IPX2R7FR.cjs +100 -0
- package/dist/chunk-IPX2R7FR.cjs.map +1 -0
- package/dist/chunk-NSBPE2FW.js +15 -0
- package/dist/chunk-NSBPE2FW.js.map +1 -0
- package/dist/chunk-T64LAI7L.js +829 -0
- package/dist/chunk-T64LAI7L.js.map +1 -0
- package/dist/chunk-ZBUDLTBO.js +2671 -0
- package/dist/chunk-ZBUDLTBO.js.map +1 -0
- package/dist/chunk-ZLO6NETT.cjs +13910 -0
- package/dist/chunk-ZLO6NETT.cjs.map +1 -0
- package/dist/contracts/handlers/index.cjs +26 -0
- package/dist/contracts/handlers/index.cjs.map +1 -0
- package/dist/contracts/handlers/index.d.cts +7 -0
- package/dist/contracts/handlers/index.d.ts +7 -0
- package/dist/contracts/handlers/index.js +5 -0
- package/dist/contracts/handlers/index.js.map +1 -0
- package/dist/delegate-BFZs69hp.d.cts +84 -0
- package/dist/delegate-aaVGfWsV.d.ts +84 -0
- package/dist/index-B22cA64m.d.cts +199 -0
- package/dist/{types/storage/index.d.ts → index-C0IanN1m.d.cts} +3 -1
- package/dist/index-C0IanN1m.d.ts +11 -0
- package/dist/index-NDla_UoJ.d.ts +199 -0
- package/dist/index.cjs +480 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +3343 -0
- package/dist/index.d.ts +3343 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/repositories/realm/index.cjs +513 -0
- package/dist/repositories/realm/index.cjs.map +1 -0
- package/dist/repositories/realm/index.d.cts +217 -0
- package/dist/{types/repositories/realm/schemas.d.ts → repositories/realm/index.d.ts} +80 -112
- package/dist/repositories/realm/index.js +507 -0
- package/dist/repositories/realm/index.js.map +1 -0
- package/dist/repositories/sqlite/index.cjs +588 -0
- package/dist/repositories/sqlite/index.cjs.map +1 -0
- package/dist/repositories/sqlite/index.d.cts +118 -0
- package/dist/{types/repositories/sqlite/walletRepository.d.ts → repositories/sqlite/index.d.ts} +58 -5
- package/dist/repositories/sqlite/index.js +585 -0
- package/dist/repositories/sqlite/index.js.map +1 -0
- package/dist/taskRunner-C6Ff4OaU.d.cts +114 -0
- package/dist/taskRunner-yvPN8Z0K.d.ts +114 -0
- package/dist/wallet/expo/background.cjs +93 -0
- package/dist/wallet/expo/background.cjs.map +1 -0
- package/dist/wallet/expo/background.d.cts +84 -0
- package/dist/wallet/expo/background.d.ts +84 -0
- package/dist/wallet/expo/background.js +68 -0
- package/dist/wallet/expo/background.js.map +1 -0
- package/dist/wallet/expo/index.cjs +171 -0
- package/dist/wallet/expo/index.cjs.map +1 -0
- package/dist/wallet/expo/index.d.cts +122 -0
- package/dist/{types/wallet/expo/wallet.d.ts → wallet/expo/index.d.ts} +45 -22
- package/dist/wallet/expo/index.js +169 -0
- package/dist/wallet/expo/index.js.map +1 -0
- package/dist/wallet-AF-p-OWj.d.cts +774 -0
- package/dist/wallet-D9NBRqvC.d.ts +774 -0
- package/dist/worker/expo/index.cjs +140 -0
- package/dist/worker/expo/index.cjs.map +1 -0
- package/dist/worker/expo/index.d.cts +29 -0
- package/dist/worker/expo/index.d.ts +29 -0
- package/dist/worker/expo/index.js +121 -0
- package/dist/worker/expo/index.js.map +1 -0
- package/package.json +110 -76
- package/dist/cjs/adapters/asyncStorage.js +0 -5
- package/dist/cjs/adapters/expo.js +0 -8
- package/dist/cjs/adapters/fileSystem.js +0 -5
- package/dist/cjs/adapters/indexedDB.js +0 -5
- package/dist/cjs/adapters/localStorage.js +0 -5
- package/dist/cjs/arkfee/celenv.js +0 -43
- package/dist/cjs/arkfee/estimator.js +0 -143
- package/dist/cjs/arkfee/index.js +0 -5
- package/dist/cjs/arkfee/types.js +0 -26
- package/dist/cjs/arknote/index.js +0 -128
- package/dist/cjs/bip322/index.js +0 -270
- package/dist/cjs/contracts/arkcontract.js +0 -147
- package/dist/cjs/contracts/contractManager.js +0 -629
- package/dist/cjs/contracts/contractWatcher.js +0 -598
- package/dist/cjs/contracts/handlers/default.js +0 -93
- package/dist/cjs/contracts/handlers/delegate.js +0 -90
- package/dist/cjs/contracts/handlers/helpers.js +0 -115
- package/dist/cjs/contracts/handlers/index.js +0 -19
- package/dist/cjs/contracts/handlers/registry.js +0 -89
- package/dist/cjs/contracts/handlers/vhtlc.js +0 -194
- package/dist/cjs/contracts/index.js +0 -41
- package/dist/cjs/contracts/types.js +0 -2
- package/dist/cjs/contracts/vtxoOwnership.js +0 -78
- package/dist/cjs/extension/asset/assetGroup.js +0 -228
- package/dist/cjs/extension/asset/assetId.js +0 -152
- package/dist/cjs/extension/asset/assetInput.js +0 -222
- package/dist/cjs/extension/asset/assetOutput.js +0 -174
- package/dist/cjs/extension/asset/assetRef.js +0 -148
- package/dist/cjs/extension/asset/index.js +0 -23
- package/dist/cjs/extension/asset/metadata.js +0 -187
- package/dist/cjs/extension/asset/packet.js +0 -114
- package/dist/cjs/extension/asset/types.js +0 -22
- package/dist/cjs/extension/asset/utils.js +0 -105
- package/dist/cjs/extension/index.js +0 -254
- package/dist/cjs/extension/packet.js +0 -20
- package/dist/cjs/forfeit.js +0 -45
- package/dist/cjs/identity/descriptor.js +0 -169
- package/dist/cjs/identity/descriptorProvider.js +0 -2
- package/dist/cjs/identity/hdCapableIdentity.js +0 -2
- package/dist/cjs/identity/index.js +0 -38
- package/dist/cjs/identity/seedIdentity.js +0 -461
- package/dist/cjs/identity/serialize.js +0 -171
- package/dist/cjs/identity/singleKey.js +0 -126
- package/dist/cjs/identity/staticDescriptorProvider.js +0 -65
- package/dist/cjs/index.js +0 -200
- package/dist/cjs/intent/index.js +0 -259
- package/dist/cjs/musig2/index.js +0 -11
- package/dist/cjs/musig2/keys.js +0 -57
- package/dist/cjs/musig2/nonces.js +0 -48
- package/dist/cjs/musig2/sign.js +0 -102
- package/dist/cjs/networks.js +0 -26
- package/dist/cjs/package.json +0 -3
- package/dist/cjs/providers/ark.js +0 -577
- package/dist/cjs/providers/delegator.js +0 -85
- package/dist/cjs/providers/electrum.js +0 -869
- package/dist/cjs/providers/errors.js +0 -59
- package/dist/cjs/providers/expoArk.js +0 -82
- package/dist/cjs/providers/expoIndexer.js +0 -111
- package/dist/cjs/providers/expoUtils.js +0 -124
- package/dist/cjs/providers/indexer.js +0 -630
- package/dist/cjs/providers/onchain.js +0 -262
- package/dist/cjs/providers/utils.js +0 -121
- package/dist/cjs/repositories/contractRepository.js +0 -2
- package/dist/cjs/repositories/inMemory/contractRepository.js +0 -55
- package/dist/cjs/repositories/inMemory/walletRepository.js +0 -115
- package/dist/cjs/repositories/index.js +0 -34
- package/dist/cjs/repositories/indexedDB/contractRepository.js +0 -187
- package/dist/cjs/repositories/indexedDB/db.js +0 -19
- package/dist/cjs/repositories/indexedDB/manager.js +0 -100
- package/dist/cjs/repositories/indexedDB/schema.js +0 -204
- package/dist/cjs/repositories/indexedDB/walletRepository.js +0 -474
- package/dist/cjs/repositories/indexedDB/websqlAdapter.js +0 -144
- package/dist/cjs/repositories/migrations/contractRepositoryImpl.js +0 -127
- package/dist/cjs/repositories/migrations/fromStorageAdapter.js +0 -66
- package/dist/cjs/repositories/migrations/walletRepositoryImpl.js +0 -184
- package/dist/cjs/repositories/realm/contractRepository.js +0 -116
- package/dist/cjs/repositories/realm/index.js +0 -11
- package/dist/cjs/repositories/realm/schemas.js +0 -157
- package/dist/cjs/repositories/realm/types.js +0 -7
- package/dist/cjs/repositories/realm/walletRepository.js +0 -305
- package/dist/cjs/repositories/scriptFromAddress.js +0 -16
- package/dist/cjs/repositories/serialization.js +0 -82
- package/dist/cjs/repositories/sqlite/contractRepository.js +0 -135
- package/dist/cjs/repositories/sqlite/index.js +0 -7
- package/dist/cjs/repositories/sqlite/types.js +0 -2
- package/dist/cjs/repositories/sqlite/walletRepository.js +0 -441
- package/dist/cjs/repositories/walletRepository.js +0 -2
- package/dist/cjs/script/address.js +0 -108
- package/dist/cjs/script/base.js +0 -185
- package/dist/cjs/script/default.js +0 -57
- package/dist/cjs/script/delegate.js +0 -53
- package/dist/cjs/script/tapscript.js +0 -619
- package/dist/cjs/script/vhtlc.js +0 -170
- package/dist/cjs/storage/asyncStorage.js +0 -50
- package/dist/cjs/storage/fileSystem.js +0 -141
- package/dist/cjs/storage/inMemory.js +0 -24
- package/dist/cjs/storage/index.js +0 -2
- package/dist/cjs/storage/indexedDB.js +0 -101
- package/dist/cjs/storage/localStorage.js +0 -51
- package/dist/cjs/tree/signingSession.js +0 -229
- package/dist/cjs/tree/txTree.js +0 -192
- package/dist/cjs/tree/validation.js +0 -107
- package/dist/cjs/utils/anchor.js +0 -35
- package/dist/cjs/utils/arkTransaction.js +0 -271
- package/dist/cjs/utils/bip21.js +0 -127
- package/dist/cjs/utils/syncCursors.js +0 -128
- package/dist/cjs/utils/timelock.js +0 -59
- package/dist/cjs/utils/transaction.js +0 -28
- package/dist/cjs/utils/transactionHistory.js +0 -183
- package/dist/cjs/utils/txSizeEstimator.js +0 -132
- package/dist/cjs/utils/unknownFields.js +0 -174
- package/dist/cjs/wallet/asset-manager.js +0 -330
- package/dist/cjs/wallet/asset.js +0 -119
- package/dist/cjs/wallet/batch.js +0 -183
- package/dist/cjs/wallet/delegator.js +0 -302
- package/dist/cjs/wallet/expo/background.js +0 -116
- package/dist/cjs/wallet/expo/index.js +0 -9
- package/dist/cjs/wallet/expo/wallet.js +0 -230
- package/dist/cjs/wallet/hdDescriptorProvider.js +0 -159
- package/dist/cjs/wallet/index.js +0 -82
- package/dist/cjs/wallet/onchain.js +0 -290
- package/dist/cjs/wallet/ramps.js +0 -216
- package/dist/cjs/wallet/serviceWorker/wallet-message-handler.js +0 -953
- package/dist/cjs/wallet/serviceWorker/wallet.js +0 -1173
- package/dist/cjs/wallet/unroll.js +0 -289
- package/dist/cjs/wallet/utils.js +0 -111
- package/dist/cjs/wallet/validation.js +0 -154
- package/dist/cjs/wallet/vtxo-manager.js +0 -1142
- package/dist/cjs/wallet/wallet.js +0 -2049
- package/dist/cjs/worker/browser/service-worker-manager.js +0 -183
- package/dist/cjs/worker/browser/utils.js +0 -67
- package/dist/cjs/worker/errors.js +0 -16
- package/dist/cjs/worker/expo/asyncStorageTaskQueue.js +0 -78
- package/dist/cjs/worker/expo/index.js +0 -13
- package/dist/cjs/worker/expo/processors/contractPollProcessor.js +0 -62
- package/dist/cjs/worker/expo/processors/index.js +0 -6
- package/dist/cjs/worker/expo/taskQueue.js +0 -41
- package/dist/cjs/worker/expo/taskRunner.js +0 -73
- package/dist/cjs/worker/messageBus.js +0 -473
- package/dist/esm/adapters/asyncStorage.js +0 -1
- package/dist/esm/adapters/expo.js +0 -3
- package/dist/esm/adapters/fileSystem.js +0 -1
- package/dist/esm/adapters/indexedDB.js +0 -1
- package/dist/esm/adapters/localStorage.js +0 -1
- package/dist/esm/arkfee/celenv.js +0 -40
- package/dist/esm/arkfee/estimator.js +0 -139
- package/dist/esm/arkfee/index.js +0 -1
- package/dist/esm/arkfee/types.js +0 -22
- package/dist/esm/arknote/index.js +0 -124
- package/dist/esm/bip322/index.js +0 -267
- package/dist/esm/contracts/arkcontract.js +0 -140
- package/dist/esm/contracts/contractManager.js +0 -625
- package/dist/esm/contracts/contractWatcher.js +0 -594
- package/dist/esm/contracts/handlers/default.js +0 -90
- package/dist/esm/contracts/handlers/delegate.js +0 -87
- package/dist/esm/contracts/handlers/helpers.js +0 -110
- package/dist/esm/contracts/handlers/index.js +0 -12
- package/dist/esm/contracts/handlers/registry.js +0 -86
- package/dist/esm/contracts/handlers/vhtlc.js +0 -191
- package/dist/esm/contracts/index.js +0 -13
- package/dist/esm/contracts/types.js +0 -1
- package/dist/esm/contracts/vtxoOwnership.js +0 -69
- package/dist/esm/extension/asset/assetGroup.js +0 -224
- package/dist/esm/extension/asset/assetId.js +0 -148
- package/dist/esm/extension/asset/assetInput.js +0 -217
- package/dist/esm/extension/asset/assetOutput.js +0 -169
- package/dist/esm/extension/asset/assetRef.js +0 -144
- package/dist/esm/extension/asset/index.js +0 -8
- package/dist/esm/extension/asset/metadata.js +0 -182
- package/dist/esm/extension/asset/packet.js +0 -110
- package/dist/esm/extension/asset/types.js +0 -19
- package/dist/esm/extension/asset/utils.js +0 -99
- package/dist/esm/extension/index.js +0 -248
- package/dist/esm/extension/packet.js +0 -16
- package/dist/esm/forfeit.js +0 -41
- package/dist/esm/identity/descriptor.js +0 -161
- package/dist/esm/identity/descriptorProvider.js +0 -1
- package/dist/esm/identity/hdCapableIdentity.js +0 -1
- package/dist/esm/identity/index.js +0 -12
- package/dist/esm/identity/seedIdentity.js +0 -453
- package/dist/esm/identity/serialize.js +0 -164
- package/dist/esm/identity/singleKey.js +0 -121
- package/dist/esm/identity/staticDescriptorProvider.js +0 -61
- package/dist/esm/index.js +0 -87
- package/dist/esm/intent/index.js +0 -255
- package/dist/esm/musig2/index.js +0 -3
- package/dist/esm/musig2/keys.js +0 -21
- package/dist/esm/musig2/nonces.js +0 -11
- package/dist/esm/musig2/sign.js +0 -63
- package/dist/esm/networks.js +0 -22
- package/dist/esm/package.json +0 -3
- package/dist/esm/providers/ark.js +0 -572
- package/dist/esm/providers/delegator.js +0 -81
- package/dist/esm/providers/electrum.js +0 -864
- package/dist/esm/providers/errors.js +0 -54
- package/dist/esm/providers/expoArk.js +0 -78
- package/dist/esm/providers/expoIndexer.js +0 -107
- package/dist/esm/providers/expoUtils.js +0 -87
- package/dist/esm/providers/indexer.js +0 -626
- package/dist/esm/providers/onchain.js +0 -258
- package/dist/esm/providers/utils.js +0 -117
- package/dist/esm/repositories/contractRepository.js +0 -1
- package/dist/esm/repositories/inMemory/contractRepository.js +0 -51
- package/dist/esm/repositories/inMemory/walletRepository.js +0 -111
- package/dist/esm/repositories/index.js +0 -10
- package/dist/esm/repositories/indexedDB/contractRepository.js +0 -183
- package/dist/esm/repositories/indexedDB/db.js +0 -4
- package/dist/esm/repositories/indexedDB/manager.js +0 -95
- package/dist/esm/repositories/indexedDB/schema.js +0 -199
- package/dist/esm/repositories/indexedDB/walletRepository.js +0 -470
- package/dist/esm/repositories/indexedDB/websqlAdapter.js +0 -138
- package/dist/esm/repositories/migrations/contractRepositoryImpl.js +0 -121
- package/dist/esm/repositories/migrations/fromStorageAdapter.js +0 -58
- package/dist/esm/repositories/migrations/walletRepositoryImpl.js +0 -180
- package/dist/esm/repositories/realm/contractRepository.js +0 -112
- package/dist/esm/repositories/realm/index.js +0 -3
- package/dist/esm/repositories/realm/schemas.js +0 -153
- package/dist/esm/repositories/realm/types.js +0 -6
- package/dist/esm/repositories/realm/walletRepository.js +0 -301
- package/dist/esm/repositories/scriptFromAddress.js +0 -13
- package/dist/esm/repositories/serialization.js +0 -67
- package/dist/esm/repositories/sqlite/contractRepository.js +0 -131
- package/dist/esm/repositories/sqlite/index.js +0 -2
- package/dist/esm/repositories/sqlite/types.js +0 -1
- package/dist/esm/repositories/sqlite/walletRepository.js +0 -437
- package/dist/esm/repositories/walletRepository.js +0 -1
- package/dist/esm/script/address.js +0 -104
- package/dist/esm/script/base.js +0 -179
- package/dist/esm/script/default.js +0 -54
- package/dist/esm/script/delegate.js +0 -50
- package/dist/esm/script/tapscript.js +0 -615
- package/dist/esm/script/vhtlc.js +0 -167
- package/dist/esm/storage/asyncStorage.js +0 -46
- package/dist/esm/storage/fileSystem.js +0 -104
- package/dist/esm/storage/inMemory.js +0 -20
- package/dist/esm/storage/index.js +0 -1
- package/dist/esm/storage/indexedDB.js +0 -97
- package/dist/esm/storage/localStorage.js +0 -47
- package/dist/esm/tree/signingSession.js +0 -191
- package/dist/esm/tree/txTree.js +0 -188
- package/dist/esm/tree/validation.js +0 -101
- package/dist/esm/utils/anchor.js +0 -31
- package/dist/esm/utils/arkTransaction.js +0 -264
- package/dist/esm/utils/bip21.js +0 -123
- package/dist/esm/utils/syncCursors.js +0 -119
- package/dist/esm/utils/timelock.js +0 -22
- package/dist/esm/utils/transaction.js +0 -24
- package/dist/esm/utils/transactionHistory.js +0 -180
- package/dist/esm/utils/txSizeEstimator.js +0 -128
- package/dist/esm/utils/unknownFields.js +0 -169
- package/dist/esm/wallet/asset-manager.js +0 -325
- package/dist/esm/wallet/asset.js +0 -113
- package/dist/esm/wallet/batch.js +0 -180
- package/dist/esm/wallet/delegator.js +0 -297
- package/dist/esm/wallet/expo/background.js +0 -111
- package/dist/esm/wallet/expo/index.js +0 -2
- package/dist/esm/wallet/expo/wallet.js +0 -193
- package/dist/esm/wallet/hdDescriptorProvider.js +0 -155
- package/dist/esm/wallet/index.js +0 -75
- package/dist/esm/wallet/onchain.js +0 -285
- package/dist/esm/wallet/ramps.js +0 -212
- package/dist/esm/wallet/serviceWorker/wallet-message-handler.js +0 -946
- package/dist/esm/wallet/serviceWorker/wallet.js +0 -1168
- package/dist/esm/wallet/unroll.js +0 -285
- package/dist/esm/wallet/utils.js +0 -103
- package/dist/esm/wallet/validation.js +0 -142
- package/dist/esm/wallet/vtxo-manager.js +0 -1136
- package/dist/esm/wallet/wallet.js +0 -2041
- package/dist/esm/worker/browser/service-worker-manager.js +0 -177
- package/dist/esm/worker/browser/utils.js +0 -63
- package/dist/esm/worker/errors.js +0 -11
- package/dist/esm/worker/expo/asyncStorageTaskQueue.js +0 -74
- package/dist/esm/worker/expo/index.js +0 -4
- package/dist/esm/worker/expo/processors/contractPollProcessor.js +0 -59
- package/dist/esm/worker/expo/processors/index.js +0 -1
- package/dist/esm/worker/expo/taskQueue.js +0 -37
- package/dist/esm/worker/expo/taskRunner.js +0 -69
- package/dist/esm/worker/messageBus.js +0 -469
- package/dist/types/adapters/asyncStorage.d.ts +0 -2
- package/dist/types/adapters/expo.d.ts +0 -4
- package/dist/types/adapters/fileSystem.d.ts +0 -2
- package/dist/types/adapters/indexedDB.d.ts +0 -2
- package/dist/types/adapters/localStorage.d.ts +0 -2
- package/dist/types/arkfee/celenv.d.ts +0 -25
- package/dist/types/arkfee/estimator.d.ts +0 -49
- package/dist/types/arkfee/index.d.ts +0 -2
- package/dist/types/arkfee/types.d.ts +0 -38
- package/dist/types/arknote/index.d.ts +0 -84
- package/dist/types/bip322/index.d.ts +0 -55
- package/dist/types/contracts/arkcontract.d.ts +0 -99
- package/dist/types/contracts/contractManager.d.ts +0 -381
- package/dist/types/contracts/contractWatcher.d.ts +0 -217
- package/dist/types/contracts/handlers/default.d.ts +0 -19
- package/dist/types/contracts/handlers/delegate.d.ts +0 -21
- package/dist/types/contracts/handlers/helpers.d.ts +0 -19
- package/dist/types/contracts/handlers/index.d.ts +0 -7
- package/dist/types/contracts/handlers/registry.d.ts +0 -65
- package/dist/types/contracts/handlers/vhtlc.d.ts +0 -32
- package/dist/types/contracts/index.d.ts +0 -14
- package/dist/types/contracts/types.d.ts +0 -233
- package/dist/types/contracts/vtxoOwnership.d.ts +0 -33
- package/dist/types/extension/asset/assetGroup.d.ts +0 -119
- package/dist/types/extension/asset/assetId.d.ts +0 -83
- package/dist/types/extension/asset/assetInput.d.ts +0 -64
- package/dist/types/extension/asset/assetOutput.d.ts +0 -54
- package/dist/types/extension/asset/assetRef.d.ts +0 -91
- package/dist/types/extension/asset/index.d.ts +0 -8
- package/dist/types/extension/asset/metadata.d.ts +0 -52
- package/dist/types/extension/asset/packet.d.ts +0 -41
- package/dist/types/extension/asset/types.d.ts +0 -16
- package/dist/types/extension/asset/utils.d.ts +0 -21
- package/dist/types/extension/index.d.ts +0 -56
- package/dist/types/extension/packet.d.ts +0 -21
- package/dist/types/forfeit.d.ts +0 -18
- package/dist/types/identity/descriptor.d.ts +0 -61
- package/dist/types/identity/descriptorProvider.d.ts +0 -35
- package/dist/types/identity/hdCapableIdentity.d.ts +0 -44
- package/dist/types/identity/index.d.ts +0 -56
- package/dist/types/identity/seedIdentity.d.ts +0 -254
- package/dist/types/identity/serialize.d.ts +0 -96
- package/dist/types/identity/singleKey.d.ts +0 -62
- package/dist/types/identity/staticDescriptorProvider.d.ts +0 -18
- package/dist/types/index.d.ts +0 -59
- package/dist/types/intent/index.d.ts +0 -86
- package/dist/types/musig2/index.d.ts +0 -4
- package/dist/types/musig2/keys.d.ts +0 -9
- package/dist/types/musig2/nonces.d.ts +0 -14
- package/dist/types/musig2/sign.d.ts +0 -27
- package/dist/types/networks.d.ts +0 -16
- package/dist/types/providers/ark.d.ts +0 -369
- package/dist/types/providers/delegator.d.ts +0 -82
- package/dist/types/providers/electrum.d.ts +0 -312
- package/dist/types/providers/errors.d.ts +0 -13
- package/dist/types/providers/expoArk.d.ts +0 -22
- package/dist/types/providers/expoIndexer.d.ts +0 -18
- package/dist/types/providers/expoUtils.d.ts +0 -18
- package/dist/types/providers/indexer.d.ts +0 -301
- package/dist/types/providers/onchain.d.ts +0 -148
- package/dist/types/providers/utils.d.ts +0 -12
- package/dist/types/repositories/contractRepository.d.ts +0 -32
- package/dist/types/repositories/inMemory/contractRepository.d.ts +0 -17
- package/dist/types/repositories/inMemory/walletRepository.d.ts +0 -29
- package/dist/types/repositories/index.d.ts +0 -9
- package/dist/types/repositories/indexedDB/contractRepository.d.ts +0 -21
- package/dist/types/repositories/indexedDB/db.d.ts +0 -4
- package/dist/types/repositories/indexedDB/manager.d.ts +0 -25
- package/dist/types/repositories/indexedDB/schema.d.ts +0 -9
- package/dist/types/repositories/indexedDB/walletRepository.d.ts +0 -28
- package/dist/types/repositories/indexedDB/websqlAdapter.d.ts +0 -49
- package/dist/types/repositories/migrations/contractRepositoryImpl.d.ts +0 -24
- package/dist/types/repositories/migrations/fromStorageAdapter.d.ts +0 -19
- package/dist/types/repositories/migrations/walletRepositoryImpl.d.ts +0 -27
- package/dist/types/repositories/realm/contractRepository.d.ts +0 -24
- package/dist/types/repositories/realm/index.d.ts +0 -4
- package/dist/types/repositories/realm/types.d.ts +0 -16
- package/dist/types/repositories/realm/walletRepository.d.ts +0 -34
- package/dist/types/repositories/scriptFromAddress.d.ts +0 -9
- package/dist/types/repositories/serialization.d.ts +0 -65
- package/dist/types/repositories/sqlite/contractRepository.d.ts +0 -33
- package/dist/types/repositories/sqlite/index.d.ts +0 -3
- package/dist/types/repositories/sqlite/types.d.ts +0 -18
- package/dist/types/repositories/walletRepository.d.ts +0 -72
- package/dist/types/script/address.d.ts +0 -67
- package/dist/types/script/base.d.ts +0 -105
- package/dist/types/script/default.d.ts +0 -44
- package/dist/types/script/delegate.d.ts +0 -40
- package/dist/types/script/tapscript.d.ts +0 -169
- package/dist/types/script/vhtlc.d.ts +0 -66
- package/dist/types/tree/signingSession.d.ts +0 -37
- package/dist/types/tree/txTree.d.ts +0 -28
- package/dist/types/tree/validation.d.ts +0 -15
- package/dist/types/utils/anchor.d.ts +0 -19
- package/dist/types/utils/arkTransaction.d.ts +0 -49
- package/dist/types/utils/bip21.d.ts +0 -38
- package/dist/types/utils/syncCursors.d.ts +0 -60
- package/dist/types/utils/timelock.d.ts +0 -9
- package/dist/types/utils/transaction.d.ts +0 -13
- package/dist/types/utils/transactionHistory.d.ts +0 -15
- package/dist/types/utils/txSizeEstimator.d.ts +0 -40
- package/dist/types/utils/unknownFields.d.ts +0 -83
- package/dist/types/wallet/asset-manager.d.ts +0 -69
- package/dist/types/wallet/asset.d.ts +0 -21
- package/dist/types/wallet/batch.d.ts +0 -107
- package/dist/types/wallet/delegator.d.ts +0 -48
- package/dist/types/wallet/expo/background.d.ts +0 -66
- package/dist/types/wallet/expo/index.d.ts +0 -4
- package/dist/types/wallet/hdDescriptorProvider.d.ts +0 -93
- package/dist/types/wallet/index.d.ts +0 -755
- package/dist/types/wallet/onchain.d.ts +0 -109
- package/dist/types/wallet/ramps.d.ts +0 -64
- package/dist/types/wallet/serviceWorker/wallet-message-handler.d.ts +0 -543
- package/dist/types/wallet/serviceWorker/wallet.d.ts +0 -238
- package/dist/types/wallet/unroll.d.ts +0 -114
- package/dist/types/wallet/utils.d.ts +0 -36
- package/dist/types/wallet/validation.d.ts +0 -24
- package/dist/types/wallet/vtxo-manager.d.ts +0 -476
- package/dist/types/wallet/wallet.d.ts +0 -360
- package/dist/types/worker/browser/service-worker-manager.d.ts +0 -32
- package/dist/types/worker/browser/utils.d.ts +0 -17
- package/dist/types/worker/errors.d.ts +0 -7
- package/dist/types/worker/expo/index.d.ts +0 -7
- package/dist/types/worker/expo/processors/contractPollProcessor.d.ts +0 -19
- package/dist/types/worker/expo/processors/index.d.ts +0 -1
- package/dist/types/worker/expo/taskQueue.d.ts +0 -50
- package/dist/types/worker/expo/taskRunner.d.ts +0 -66
- package/dist/types/worker/messageBus.d.ts +0 -188
|
@@ -1,2041 +0,0 @@
|
|
|
1
|
-
import { base64, hex } from "@scure/base";
|
|
2
|
-
import { tapLeafHash } from "@scure/btc-signer/payment.js";
|
|
3
|
-
import { Address, OutScript, SigHash, Transaction } from "@scure/btc-signer";
|
|
4
|
-
import { sha256 } from "@scure/btc-signer/utils.js";
|
|
5
|
-
import { ArkAddress } from "../script/address.js";
|
|
6
|
-
import { DefaultVtxo } from "../script/default.js";
|
|
7
|
-
import { getNetwork } from "../networks.js";
|
|
8
|
-
import { ESPLORA_URL, EsploraProvider, } from "../providers/onchain.js";
|
|
9
|
-
import { RestArkProvider, } from "../providers/ark.js";
|
|
10
|
-
import { buildForfeitTx } from "../forfeit.js";
|
|
11
|
-
import { validateConnectorsTxGraph, validateVtxoTxGraph, } from "../tree/validation.js";
|
|
12
|
-
import { validateBatchRecipients } from "./validation.js";
|
|
13
|
-
import { isBatchSignable } from "../identity/index.js";
|
|
14
|
-
import { DEFAULT_ARKADE_SERVER_URL, isExpired, isRecoverable, isSpendable, isSubdust, TxType, } from "./index.js";
|
|
15
|
-
import { createAssetPacket, selectCoinsWithAsset, selectedCoinsToAssetInputs, } from "./asset.js";
|
|
16
|
-
import { VtxoScript } from "../script/base.js";
|
|
17
|
-
import { CSVMultisigTapscript } from "../script/tapscript.js";
|
|
18
|
-
import { buildOffchainTx, combineTapscriptSigs, hasBoardingTxExpired, isValidArkAddress, } from "../utils/arkTransaction.js";
|
|
19
|
-
import { DEFAULT_RENEWAL_CONFIG, DEFAULT_SETTLEMENT_CONFIG, VtxoManager, } from "./vtxo-manager.js";
|
|
20
|
-
import { ArkNote } from "../arknote/index.js";
|
|
21
|
-
import { Intent } from "../intent/index.js";
|
|
22
|
-
import { RestIndexerProvider } from "../providers/indexer.js";
|
|
23
|
-
import { extendCoin, validateRecipients } from "./utils.js";
|
|
24
|
-
import { ArkError } from "../providers/errors.js";
|
|
25
|
-
import { Batch } from "./batch.js";
|
|
26
|
-
import { Estimator } from "../arkfee/index.js";
|
|
27
|
-
import { buildTransactionHistory } from "../utils/transactionHistory.js";
|
|
28
|
-
import { AssetManager, ReadonlyAssetManager } from "./asset-manager.js";
|
|
29
|
-
import { Extension } from "../extension/index.js";
|
|
30
|
-
import { DelegateVtxo } from "../script/delegate.js";
|
|
31
|
-
import { DelegatorManagerImpl, findDestinationOutputIndex, } from "./delegator.js";
|
|
32
|
-
import { IndexedDBContractRepository, IndexedDBWalletRepository, } from "../repositories/index.js";
|
|
33
|
-
import { ContractManager } from "../contracts/contractManager.js";
|
|
34
|
-
import { contractHandlers } from "../contracts/handlers/index.js";
|
|
35
|
-
import { timelockToSequence } from "../utils/timelock.js";
|
|
36
|
-
import { clearSyncCursor, updateWalletState } from "../utils/syncCursors.js";
|
|
37
|
-
import { validateVtxosForScript, saveVtxosForContract, } from "../contracts/vtxoOwnership.js";
|
|
38
|
-
export const getArkadeServerUrl = ({ arkServerUrl, }) => arkServerUrl || DEFAULT_ARKADE_SERVER_URL;
|
|
39
|
-
// Historical unilateral exit delay for mainnet (~7 days in seconds).
|
|
40
|
-
// Kept so existing wallets can still discover and spend VTXOs sent to the
|
|
41
|
-
// legacy address after arkd starts advertising a different delay.
|
|
42
|
-
const MAINNET_UNILATERAL_EXIT_DELAY = 605184n;
|
|
43
|
-
function delayToTimelock(delay) {
|
|
44
|
-
return {
|
|
45
|
-
value: delay,
|
|
46
|
-
type: delay < 512n ? "blocks" : "seconds",
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
function dedupeTimelocks(timelocks) {
|
|
50
|
-
const seen = new Set();
|
|
51
|
-
const deduped = [];
|
|
52
|
-
for (const timelock of timelocks) {
|
|
53
|
-
const sequence = timelockToSequence(timelock).toString();
|
|
54
|
-
if (seen.has(sequence))
|
|
55
|
-
continue;
|
|
56
|
-
seen.add(sequence);
|
|
57
|
-
deduped.push(timelock);
|
|
58
|
-
}
|
|
59
|
-
return deduped;
|
|
60
|
-
}
|
|
61
|
-
/**
|
|
62
|
-
* Type guard function to check if an identity has a toReadonly method.
|
|
63
|
-
*/
|
|
64
|
-
function hasToReadonly(identity) {
|
|
65
|
-
return (typeof identity === "object" &&
|
|
66
|
-
identity !== null &&
|
|
67
|
-
"toReadonly" in identity &&
|
|
68
|
-
typeof identity.toReadonly === "function");
|
|
69
|
-
}
|
|
70
|
-
export class ReadonlyWallet {
|
|
71
|
-
get assetManager() {
|
|
72
|
-
return this._assetManager;
|
|
73
|
-
}
|
|
74
|
-
constructor(identity, network, onchainProvider, indexerProvider, arkServerPublicKey, offchainTapscript, boardingTapscript, dustAmount, walletRepository, contractRepository, delegatorProvider, watcherConfig, walletContractTimelocks) {
|
|
75
|
-
this.identity = identity;
|
|
76
|
-
this.network = network;
|
|
77
|
-
this.onchainProvider = onchainProvider;
|
|
78
|
-
this.indexerProvider = indexerProvider;
|
|
79
|
-
this.arkServerPublicKey = arkServerPublicKey;
|
|
80
|
-
this.offchainTapscript = offchainTapscript;
|
|
81
|
-
this.boardingTapscript = boardingTapscript;
|
|
82
|
-
this.dustAmount = dustAmount;
|
|
83
|
-
this.walletRepository = walletRepository;
|
|
84
|
-
this.contractRepository = contractRepository;
|
|
85
|
-
this.delegatorProvider = delegatorProvider;
|
|
86
|
-
// Outpoints ("txid:vout") committed to an in-flight settle/send. Filtered
|
|
87
|
-
// from getVtxos() so concurrent callers (UI, VtxoManager auto-renewal,
|
|
88
|
-
// another send/settle racing the _txLock) can't reselect coins that are
|
|
89
|
-
// already on their way out. The set is in-memory only: a process crash
|
|
90
|
-
// clears it, and a stale entry only hides a VTXO (never spends one).
|
|
91
|
-
this._pendingSpendOutpoints = new Set();
|
|
92
|
-
// Guard: detect identity/server network mismatch for descriptor-based identities.
|
|
93
|
-
// This duplicates the check in setupWalletConfig() so that subclasses
|
|
94
|
-
// bypassing the factory still get the safety net.
|
|
95
|
-
if ("descriptor" in identity) {
|
|
96
|
-
const descriptor = identity.descriptor;
|
|
97
|
-
const identityIsMainnet = !descriptor.includes("tpub");
|
|
98
|
-
const serverIsMainnet = network.bech32 === "bc";
|
|
99
|
-
if (identityIsMainnet !== serverIsMainnet) {
|
|
100
|
-
throw new Error(`Network mismatch: identity uses ${identityIsMainnet ? "mainnet" : "testnet"} derivation ` +
|
|
101
|
-
`but wallet network is ${serverIsMainnet ? "mainnet" : "testnet"}. ` +
|
|
102
|
-
`Create identity with { isMainnet: ${serverIsMainnet} } to match.`);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
this.watcherConfig = watcherConfig;
|
|
106
|
-
this._assetManager = new ReadonlyAssetManager(this.indexerProvider);
|
|
107
|
-
// Defensive for direct-construction callers; setupWalletConfig already
|
|
108
|
-
// passes a deduped list through the public create() factories.
|
|
109
|
-
this.walletContractTimelocks =
|
|
110
|
-
walletContractTimelocks && walletContractTimelocks.length > 0
|
|
111
|
-
? dedupeTimelocks(walletContractTimelocks)
|
|
112
|
-
: [
|
|
113
|
-
this.offchainTapscript.options.csvTimelock ??
|
|
114
|
-
DefaultVtxo.Script.DEFAULT_TIMELOCK,
|
|
115
|
-
];
|
|
116
|
-
}
|
|
117
|
-
/**
|
|
118
|
-
* Protected helper to set up shared wallet configuration.
|
|
119
|
-
* Extracts common logic used by both ReadonlyWallet.create() and Wallet.create().
|
|
120
|
-
*/
|
|
121
|
-
static async setupWalletConfig(config, pubKey) {
|
|
122
|
-
// Use provided arkProvider instance or create a new one from arkServerUrl
|
|
123
|
-
const arkProvider = config.arkProvider ||
|
|
124
|
-
(() => {
|
|
125
|
-
return new RestArkProvider(getArkadeServerUrl(config));
|
|
126
|
-
})();
|
|
127
|
-
// Extract arkServerUrl from provider if not explicitly provided
|
|
128
|
-
const arkServerUrl = config.arkServerUrl || arkProvider.serverUrl;
|
|
129
|
-
if (!arkServerUrl) {
|
|
130
|
-
throw new Error("Could not determine arkServerUrl from provider");
|
|
131
|
-
}
|
|
132
|
-
// Use provided indexerProvider instance or create a new one
|
|
133
|
-
// indexerUrl defaults to arkServerUrl if not provided
|
|
134
|
-
const indexerUrl = config.indexerUrl || arkServerUrl;
|
|
135
|
-
const indexerProvider = config.indexerProvider || new RestIndexerProvider(indexerUrl);
|
|
136
|
-
const info = await arkProvider.getInfo();
|
|
137
|
-
const network = getNetwork(info.network);
|
|
138
|
-
// Guard: detect identity/server network mismatch for seed-based identities.
|
|
139
|
-
// A mainnet descriptor (xpub, coin type 0) connected to a testnet server
|
|
140
|
-
// (or vice versa) means wrong derivation path → wrong keys → potential fund loss.
|
|
141
|
-
if ("descriptor" in config.identity) {
|
|
142
|
-
const descriptor = config.identity.descriptor;
|
|
143
|
-
const identityIsMainnet = !descriptor.includes("tpub");
|
|
144
|
-
const serverIsMainnet = info.network === "bitcoin";
|
|
145
|
-
if (identityIsMainnet && !serverIsMainnet) {
|
|
146
|
-
throw new Error(`Network mismatch: identity uses mainnet derivation (coin type 0) ` +
|
|
147
|
-
`but the Arkade server is on ${info.network}. ` +
|
|
148
|
-
`Create identity with { isMainnet: false } to use testnet derivation.`);
|
|
149
|
-
}
|
|
150
|
-
if (!identityIsMainnet && serverIsMainnet) {
|
|
151
|
-
throw new Error(`Network mismatch: identity uses testnet derivation (coin type 1) ` +
|
|
152
|
-
`but the Arkade server is on mainnet. ` +
|
|
153
|
-
`Create identity with { isMainnet: true } or omit isMainnet (defaults to mainnet).`);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
// Extract esploraUrl from provider if not explicitly provided
|
|
157
|
-
const esploraUrl = config.esploraUrl || ESPLORA_URL[info.network];
|
|
158
|
-
// Use provided onchainProvider instance or create a new one
|
|
159
|
-
const onchainProvider = config.onchainProvider || new EsploraProvider(esploraUrl);
|
|
160
|
-
// validate unilateral exit timelock passed in config if any
|
|
161
|
-
if (config.exitTimelock) {
|
|
162
|
-
const { value, type } = config.exitTimelock;
|
|
163
|
-
if ((value < 512n && type !== "blocks") ||
|
|
164
|
-
(value >= 512n && type !== "seconds")) {
|
|
165
|
-
throw new Error("invalid exitTimelock");
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
const arkdExitTimelock = delayToTimelock(info.unilateralExitDelay);
|
|
169
|
-
// create unilateral exit timelock
|
|
170
|
-
const exitTimelock = config.exitTimelock ?? arkdExitTimelock;
|
|
171
|
-
const walletContractTimelocks = config.exitTimelock
|
|
172
|
-
? [exitTimelock]
|
|
173
|
-
: dedupeTimelocks([
|
|
174
|
-
arkdExitTimelock,
|
|
175
|
-
...(info.network === "bitcoin"
|
|
176
|
-
? [delayToTimelock(MAINNET_UNILATERAL_EXIT_DELAY)]
|
|
177
|
-
: []),
|
|
178
|
-
]);
|
|
179
|
-
// validate boarding timelock passed in config if any
|
|
180
|
-
if (config.boardingTimelock) {
|
|
181
|
-
const { value, type } = config.boardingTimelock;
|
|
182
|
-
if ((value < 512n && type !== "blocks") ||
|
|
183
|
-
(value >= 512n && type !== "seconds")) {
|
|
184
|
-
throw new Error("invalid boardingTimelock");
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
// create boarding timelock
|
|
188
|
-
const boardingTimelock = config.boardingTimelock ?? {
|
|
189
|
-
value: info.boardingExitDelay,
|
|
190
|
-
type: info.boardingExitDelay < 512n ? "blocks" : "seconds",
|
|
191
|
-
};
|
|
192
|
-
// Generate tapscripts for offchain and boarding address
|
|
193
|
-
const serverPubKey = hex.decode(info.signerPubkey).slice(1);
|
|
194
|
-
const delegatePubKey = config.delegatorProvider
|
|
195
|
-
? await config.delegatorProvider
|
|
196
|
-
.getDelegateInfo()
|
|
197
|
-
.then((info) => hex.decode(info.pubkey).slice(1))
|
|
198
|
-
: undefined;
|
|
199
|
-
const offchainOptions = {
|
|
200
|
-
pubKey,
|
|
201
|
-
serverPubKey,
|
|
202
|
-
csvTimelock: exitTimelock,
|
|
203
|
-
};
|
|
204
|
-
const offchainTapscript = !delegatePubKey
|
|
205
|
-
? new DefaultVtxo.Script(offchainOptions)
|
|
206
|
-
: new DelegateVtxo.Script({ ...offchainOptions, delegatePubKey });
|
|
207
|
-
const boardingTapscript = new DefaultVtxo.Script({
|
|
208
|
-
...offchainOptions,
|
|
209
|
-
csvTimelock: boardingTimelock,
|
|
210
|
-
});
|
|
211
|
-
const walletRepository = config.storage?.walletRepository ?? new IndexedDBWalletRepository();
|
|
212
|
-
const contractRepository = config.storage?.contractRepository ??
|
|
213
|
-
new IndexedDBContractRepository();
|
|
214
|
-
return {
|
|
215
|
-
arkProvider,
|
|
216
|
-
indexerProvider,
|
|
217
|
-
onchainProvider,
|
|
218
|
-
network,
|
|
219
|
-
networkName: info.network,
|
|
220
|
-
serverPubKey,
|
|
221
|
-
offchainTapscript,
|
|
222
|
-
boardingTapscript,
|
|
223
|
-
dustAmount: info.dust,
|
|
224
|
-
walletRepository,
|
|
225
|
-
contractRepository,
|
|
226
|
-
info,
|
|
227
|
-
delegatorProvider: config.delegatorProvider,
|
|
228
|
-
walletContractTimelocks,
|
|
229
|
-
};
|
|
230
|
-
}
|
|
231
|
-
/**
|
|
232
|
-
* Create a readonly wallet for querying balances, addresses, and history.
|
|
233
|
-
*
|
|
234
|
-
* @param config - Readonly wallet configuration
|
|
235
|
-
* @returns A readonly wallet instance
|
|
236
|
-
*/
|
|
237
|
-
static async create(config) {
|
|
238
|
-
const pubkey = await config.identity.xOnlyPublicKey();
|
|
239
|
-
if (!pubkey) {
|
|
240
|
-
throw new Error("Invalid configured public key");
|
|
241
|
-
}
|
|
242
|
-
const setup = await ReadonlyWallet.setupWalletConfig(config, pubkey);
|
|
243
|
-
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, setup.walletContractTimelocks);
|
|
244
|
-
}
|
|
245
|
-
get arkAddress() {
|
|
246
|
-
return this.offchainTapscript.address(this.network.hrp, this.arkServerPublicKey);
|
|
247
|
-
}
|
|
248
|
-
/**
|
|
249
|
-
* Get the pkScript hex for the wallet's primary offchain address.
|
|
250
|
-
* For the full wallet-owned script set registered in ContractManager, use getWalletScripts().
|
|
251
|
-
*/
|
|
252
|
-
get defaultContractScript() {
|
|
253
|
-
return hex.encode(this.offchainTapscript.pkScript);
|
|
254
|
-
}
|
|
255
|
-
/** Returns the wallet's Arkade address. */
|
|
256
|
-
async getAddress() {
|
|
257
|
-
return this.arkAddress.encode();
|
|
258
|
-
}
|
|
259
|
-
/** Returns the onchain boarding address used to move funds into Arkade. */
|
|
260
|
-
async getBoardingAddress() {
|
|
261
|
-
return this.boardingTapscript.onchainAddress(this.network);
|
|
262
|
-
}
|
|
263
|
-
/**
|
|
264
|
-
* Return the wallet's combined onchain and offchain balances.
|
|
265
|
-
*/
|
|
266
|
-
async getBalance() {
|
|
267
|
-
const [boardingUtxos, vtxos] = await Promise.all([
|
|
268
|
-
this.getBoardingUtxos(),
|
|
269
|
-
this.getVtxos(),
|
|
270
|
-
]);
|
|
271
|
-
// boarding
|
|
272
|
-
let confirmed = 0;
|
|
273
|
-
let unconfirmed = 0;
|
|
274
|
-
for (const utxo of boardingUtxos) {
|
|
275
|
-
if (utxo.status.confirmed) {
|
|
276
|
-
confirmed += utxo.value;
|
|
277
|
-
}
|
|
278
|
-
else {
|
|
279
|
-
unconfirmed += utxo.value;
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
// offchain
|
|
283
|
-
let settled = 0;
|
|
284
|
-
let preconfirmed = 0;
|
|
285
|
-
let recoverable = 0;
|
|
286
|
-
settled = vtxos
|
|
287
|
-
.filter((coin) => coin.virtualStatus.state === "settled")
|
|
288
|
-
.reduce((sum, coin) => sum + coin.value, 0);
|
|
289
|
-
preconfirmed = vtxos
|
|
290
|
-
.filter((coin) => coin.virtualStatus.state === "preconfirmed")
|
|
291
|
-
.reduce((sum, coin) => sum + coin.value, 0);
|
|
292
|
-
recoverable = vtxos
|
|
293
|
-
.filter((coin) => isSpendable(coin) && coin.virtualStatus.state === "swept")
|
|
294
|
-
.reduce((sum, coin) => sum + coin.value, 0);
|
|
295
|
-
const totalBoarding = confirmed + unconfirmed;
|
|
296
|
-
const totalOffchain = settled + preconfirmed + recoverable;
|
|
297
|
-
// aggregate asset balances from spendable virtual outputs
|
|
298
|
-
const assetBalances = new Map();
|
|
299
|
-
for (const vtxo of vtxos) {
|
|
300
|
-
if (!isSpendable(vtxo))
|
|
301
|
-
continue;
|
|
302
|
-
if (vtxo.assets) {
|
|
303
|
-
for (const a of vtxo.assets) {
|
|
304
|
-
const current = assetBalances.get(a.assetId) ?? 0n;
|
|
305
|
-
assetBalances.set(a.assetId, current + a.amount);
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
const assets = Array.from(assetBalances.entries()).map(([assetId, amount]) => ({
|
|
310
|
-
assetId,
|
|
311
|
-
amount,
|
|
312
|
-
}));
|
|
313
|
-
return {
|
|
314
|
-
boarding: {
|
|
315
|
-
confirmed,
|
|
316
|
-
unconfirmed,
|
|
317
|
-
total: totalBoarding,
|
|
318
|
-
},
|
|
319
|
-
settled,
|
|
320
|
-
preconfirmed,
|
|
321
|
-
available: settled + preconfirmed,
|
|
322
|
-
recoverable,
|
|
323
|
-
total: totalBoarding + totalOffchain,
|
|
324
|
-
assets,
|
|
325
|
-
};
|
|
326
|
-
}
|
|
327
|
-
/**
|
|
328
|
-
* Return virtual outputs tracked by the wallet.
|
|
329
|
-
*
|
|
330
|
-
* @param filter - Optional flags controlling whether recoverable or unrolled VTXOs are included
|
|
331
|
-
*/
|
|
332
|
-
async getVtxos(filter) {
|
|
333
|
-
const f = filter ?? { withRecoverable: true, withUnrolled: false };
|
|
334
|
-
const contractManager = await this.getContractManager();
|
|
335
|
-
const vtxos = await contractManager.getContractsWithVtxos();
|
|
336
|
-
return vtxos
|
|
337
|
-
.flatMap((_) => _.vtxos)
|
|
338
|
-
.filter((vtxo) => {
|
|
339
|
-
if (this._pendingSpendOutpoints.has(`${vtxo.txid}:${vtxo.vout}`)) {
|
|
340
|
-
return false;
|
|
341
|
-
}
|
|
342
|
-
if (isSpendable(vtxo)) {
|
|
343
|
-
if (!f.withRecoverable &&
|
|
344
|
-
(isRecoverable(vtxo) || isExpired(vtxo))) {
|
|
345
|
-
return false;
|
|
346
|
-
}
|
|
347
|
-
return true;
|
|
348
|
-
}
|
|
349
|
-
return !!(f.withUnrolled && vtxo.isUnrolled);
|
|
350
|
-
});
|
|
351
|
-
}
|
|
352
|
-
/**
|
|
353
|
-
* Return wallet transaction history derived from Arkade state and boarding transactions.
|
|
354
|
-
*/
|
|
355
|
-
async getTransactionHistory() {
|
|
356
|
-
const contractManager = await this.getContractManager();
|
|
357
|
-
const response = await contractManager.getContractsWithVtxos();
|
|
358
|
-
const allVtxos = response.flatMap((_) => _.vtxos);
|
|
359
|
-
const { boardingTxs, commitmentsToIgnore } = await this.getBoardingTxs();
|
|
360
|
-
const getTxCreatedAt = (txid) => this.indexerProvider
|
|
361
|
-
.getVtxos({ outpoints: [{ txid, vout: 0 }] })
|
|
362
|
-
.then((res) => res.vtxos[0]?.createdAt.getTime());
|
|
363
|
-
return buildTransactionHistory(allVtxos, boardingTxs, commitmentsToIgnore, getTxCreatedAt);
|
|
364
|
-
}
|
|
365
|
-
/**
|
|
366
|
-
* Clear the global VTXO sync cursor, forcing a full re-bootstrap on next sync.
|
|
367
|
-
* Useful for recovery after indexer reprocessing or debugging.
|
|
368
|
-
*/
|
|
369
|
-
async clearSyncCursor() {
|
|
370
|
-
await clearSyncCursor(this.walletRepository);
|
|
371
|
-
}
|
|
372
|
-
/**
|
|
373
|
-
* Build a transaction history view for the wallet's boarding address.
|
|
374
|
-
*/
|
|
375
|
-
async getBoardingTxs() {
|
|
376
|
-
const utxos = [];
|
|
377
|
-
const commitmentsToIgnore = new Set();
|
|
378
|
-
const boardingAddress = await this.getBoardingAddress();
|
|
379
|
-
const txs = await this.onchainProvider.getTransactions(boardingAddress);
|
|
380
|
-
const outspendCache = new Map();
|
|
381
|
-
for (const tx of txs) {
|
|
382
|
-
for (let i = 0; i < tx.vout.length; i++) {
|
|
383
|
-
const vout = tx.vout[i];
|
|
384
|
-
if (vout.scriptpubkey_address === boardingAddress) {
|
|
385
|
-
let spentStatuses = outspendCache.get(tx.txid);
|
|
386
|
-
if (!spentStatuses) {
|
|
387
|
-
spentStatuses =
|
|
388
|
-
await this.onchainProvider.getTxOutspends(tx.txid);
|
|
389
|
-
outspendCache.set(tx.txid, spentStatuses);
|
|
390
|
-
}
|
|
391
|
-
const spentStatus = spentStatuses[i];
|
|
392
|
-
if (spentStatus?.spent) {
|
|
393
|
-
commitmentsToIgnore.add(spentStatus.txid);
|
|
394
|
-
}
|
|
395
|
-
utxos.push({
|
|
396
|
-
txid: tx.txid,
|
|
397
|
-
vout: i,
|
|
398
|
-
value: Number(vout.value),
|
|
399
|
-
status: {
|
|
400
|
-
confirmed: tx.status.confirmed,
|
|
401
|
-
block_time: tx.status.block_time,
|
|
402
|
-
},
|
|
403
|
-
isUnrolled: true,
|
|
404
|
-
virtualStatus: {
|
|
405
|
-
state: spentStatus?.spent ? "spent" : "settled",
|
|
406
|
-
commitmentTxIds: spentStatus?.spent
|
|
407
|
-
? [spentStatus.txid]
|
|
408
|
-
: undefined,
|
|
409
|
-
},
|
|
410
|
-
createdAt: tx.status.confirmed
|
|
411
|
-
? new Date(tx.status.block_time * 1000)
|
|
412
|
-
: new Date(0),
|
|
413
|
-
script: hex.encode(this.boardingTapscript.pkScript),
|
|
414
|
-
});
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
const unconfirmedTxs = [];
|
|
419
|
-
const confirmedTxs = [];
|
|
420
|
-
for (const utxo of utxos) {
|
|
421
|
-
const tx = {
|
|
422
|
-
key: {
|
|
423
|
-
boardingTxid: utxo.txid,
|
|
424
|
-
commitmentTxid: "",
|
|
425
|
-
arkTxid: "",
|
|
426
|
-
},
|
|
427
|
-
amount: utxo.value,
|
|
428
|
-
type: TxType.TxReceived,
|
|
429
|
-
settled: utxo.virtualStatus.state === "spent",
|
|
430
|
-
createdAt: utxo.status.block_time
|
|
431
|
-
? new Date(utxo.status.block_time * 1000).getTime()
|
|
432
|
-
: 0,
|
|
433
|
-
};
|
|
434
|
-
if (!utxo.status.block_time) {
|
|
435
|
-
unconfirmedTxs.push(tx);
|
|
436
|
-
}
|
|
437
|
-
else {
|
|
438
|
-
confirmedTxs.push(tx);
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
return {
|
|
442
|
-
boardingTxs: [...unconfirmedTxs, ...confirmedTxs],
|
|
443
|
-
commitmentsToIgnore,
|
|
444
|
-
};
|
|
445
|
-
}
|
|
446
|
-
/**
|
|
447
|
-
* Fetch and cache onchain inputs (UTXOs) received at the boarding address.
|
|
448
|
-
*/
|
|
449
|
-
async getBoardingUtxos() {
|
|
450
|
-
const boardingAddress = await this.getBoardingAddress();
|
|
451
|
-
const boardingUtxos = await this.onchainProvider.getCoins(boardingAddress);
|
|
452
|
-
const utxos = boardingUtxos.map((utxo) => {
|
|
453
|
-
return extendCoin(this, utxo);
|
|
454
|
-
});
|
|
455
|
-
// Save boarding inputs using unified repository
|
|
456
|
-
await this.walletRepository.saveUtxos(boardingAddress, utxos);
|
|
457
|
-
return utxos;
|
|
458
|
-
}
|
|
459
|
-
/**
|
|
460
|
-
* Subscribe to onchain and offchain notifications for newly received funds.
|
|
461
|
-
*
|
|
462
|
-
* @param eventCallback - Callback invoked when matching funds are detected
|
|
463
|
-
* @returns A function that stops the subscriptions
|
|
464
|
-
*/
|
|
465
|
-
async notifyIncomingFunds(eventCallback) {
|
|
466
|
-
const arkAddress = await this.getAddress();
|
|
467
|
-
const boardingAddress = await this.getBoardingAddress();
|
|
468
|
-
let onchainStopFunc;
|
|
469
|
-
let indexerStopFunc;
|
|
470
|
-
if (this.onchainProvider && boardingAddress) {
|
|
471
|
-
const findVoutOnTx = (tx) => {
|
|
472
|
-
return tx.vout.findIndex((v) => v.scriptpubkey_address === boardingAddress);
|
|
473
|
-
};
|
|
474
|
-
onchainStopFunc = await this.onchainProvider.watchAddresses([boardingAddress], (txs) => {
|
|
475
|
-
// find all onchain outputs belonging to our boarding address
|
|
476
|
-
const coins = txs
|
|
477
|
-
// filter txs where address is in output
|
|
478
|
-
.filter((tx) => findVoutOnTx(tx) !== -1)
|
|
479
|
-
// return boarding input as Coin
|
|
480
|
-
.map((tx) => {
|
|
481
|
-
const { txid, status } = tx;
|
|
482
|
-
const vout = findVoutOnTx(tx);
|
|
483
|
-
const value = Number(tx.vout[vout].value);
|
|
484
|
-
return { txid, vout, value, status };
|
|
485
|
-
});
|
|
486
|
-
// and notify via callback
|
|
487
|
-
eventCallback({
|
|
488
|
-
type: "utxo",
|
|
489
|
-
coins,
|
|
490
|
-
});
|
|
491
|
-
});
|
|
492
|
-
}
|
|
493
|
-
if (this.indexerProvider && arkAddress) {
|
|
494
|
-
// Share the ContractWatcher's single subscription instead of
|
|
495
|
-
// opening a second SSE stream.
|
|
496
|
-
const cm = await this.getContractManager();
|
|
497
|
-
// Serialize annotation+notification: parallel `annotateVtxos`
|
|
498
|
-
// awaits could resolve out of order and deliver eventCallback
|
|
499
|
-
// calls in the wrong sequence (e.g. `vtxo_spent` before its
|
|
500
|
-
// matching `vtxo_received`).
|
|
501
|
-
let annotationQueue = Promise.resolve();
|
|
502
|
-
indexerStopFunc = cm.onContractEvent((event) => {
|
|
503
|
-
if (event.type !== "vtxo_received" &&
|
|
504
|
-
event.type !== "vtxo_spent") {
|
|
505
|
-
return;
|
|
506
|
-
}
|
|
507
|
-
if (event.contract.type !== "default" &&
|
|
508
|
-
event.contract.type !== "delegate") {
|
|
509
|
-
return;
|
|
510
|
-
}
|
|
511
|
-
// `event.vtxos` carries placeholder tapscript fields from
|
|
512
|
-
// the watcher; `annotateVtxos` fills them in.
|
|
513
|
-
annotationQueue = annotationQueue.then(async () => {
|
|
514
|
-
try {
|
|
515
|
-
const annotated = await cm.annotateVtxos(event.vtxos);
|
|
516
|
-
eventCallback({
|
|
517
|
-
type: "vtxo",
|
|
518
|
-
newVtxos: event.type === "vtxo_received" ? annotated : [],
|
|
519
|
-
spentVtxos: event.type === "vtxo_spent" ? annotated : [],
|
|
520
|
-
});
|
|
521
|
-
}
|
|
522
|
-
catch (error) {
|
|
523
|
-
console.warn("Dropping subscription update after annotation failed; next sync will reconcile:", error);
|
|
524
|
-
}
|
|
525
|
-
});
|
|
526
|
-
});
|
|
527
|
-
}
|
|
528
|
-
const stopFunc = () => {
|
|
529
|
-
onchainStopFunc?.();
|
|
530
|
-
indexerStopFunc?.();
|
|
531
|
-
};
|
|
532
|
-
return stopFunc;
|
|
533
|
-
}
|
|
534
|
-
/** Fetch Arkade transaction ids that are still pending final settlement. */
|
|
535
|
-
async fetchPendingTxs() {
|
|
536
|
-
// get non-swept virtual outputs, rely on the indexer only in case DB doesn't have the right state
|
|
537
|
-
const scripts = await this.getWalletScripts();
|
|
538
|
-
let { vtxos } = await this.indexerProvider.getVtxos({
|
|
539
|
-
scripts,
|
|
540
|
-
});
|
|
541
|
-
return vtxos
|
|
542
|
-
.filter((vtxo) => vtxo.virtualStatus.state !== "swept" &&
|
|
543
|
-
vtxo.virtualStatus.state !== "settled" &&
|
|
544
|
-
vtxo.arkTxId !== undefined)
|
|
545
|
-
.map((_) => _.arkTxId);
|
|
546
|
-
}
|
|
547
|
-
// ========================================================================
|
|
548
|
-
// Multi-script support (default + delegate addresses)
|
|
549
|
-
// ========================================================================
|
|
550
|
-
/**
|
|
551
|
-
* Get all pkScript hex strings for the wallet's own addresses
|
|
552
|
-
* (both delegate and non-delegate, current and historical).
|
|
553
|
-
*/
|
|
554
|
-
async getWalletScripts() {
|
|
555
|
-
const manager = await this.getContractManager();
|
|
556
|
-
const contracts = await manager.getContracts({
|
|
557
|
-
type: ["default", "delegate"],
|
|
558
|
-
});
|
|
559
|
-
return contracts.map((c) => c.script);
|
|
560
|
-
}
|
|
561
|
-
/**
|
|
562
|
-
* Build a map of scriptHex → VtxoScript for all wallet contracts,
|
|
563
|
-
* so virtual outputs can be extended with the correct tapscript per contract.
|
|
564
|
-
*/
|
|
565
|
-
async getScriptMap() {
|
|
566
|
-
const map = new Map();
|
|
567
|
-
const manager = await this.getContractManager();
|
|
568
|
-
const contracts = await manager.getContracts({
|
|
569
|
-
type: ["default", "delegate"],
|
|
570
|
-
});
|
|
571
|
-
for (const contract of contracts) {
|
|
572
|
-
if (map.has(contract.script))
|
|
573
|
-
continue;
|
|
574
|
-
const handler = contractHandlers.get(contract.type);
|
|
575
|
-
if (handler) {
|
|
576
|
-
const script = handler.createScript(contract.params);
|
|
577
|
-
map.set(contract.script, script);
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
return map;
|
|
581
|
-
}
|
|
582
|
-
// ========================================================================
|
|
583
|
-
// Contract Management
|
|
584
|
-
// ========================================================================
|
|
585
|
-
/**
|
|
586
|
-
* Get the ContractManager for managing contracts including the wallet's default address.
|
|
587
|
-
*
|
|
588
|
-
* The ContractManager handles:
|
|
589
|
-
* - The wallet's default receiving address (as a "default" contract)
|
|
590
|
-
* - External contracts (Boltz swaps, HTLCs, etc.)
|
|
591
|
-
* - Multi-contract watching with resilient connections
|
|
592
|
-
*
|
|
593
|
-
* @example
|
|
594
|
-
* ```typescript
|
|
595
|
-
* const manager = await wallet.getContractManager();
|
|
596
|
-
*
|
|
597
|
-
* // Create a contract for a Boltz swap
|
|
598
|
-
* const contract = await manager.createContract({
|
|
599
|
-
* label: "Boltz Swap",
|
|
600
|
-
* type: "vhtlc",
|
|
601
|
-
* params: { ... },
|
|
602
|
-
* script: swapScript,
|
|
603
|
-
* address: swapAddress,
|
|
604
|
-
* });
|
|
605
|
-
*
|
|
606
|
-
* // Start watching for events (includes wallet's default address)
|
|
607
|
-
* const stop = await manager.onContractEvent((event) => {
|
|
608
|
-
* console.log(`${event.type} on ${event.contractScript}`);
|
|
609
|
-
* });
|
|
610
|
-
* ```
|
|
611
|
-
*/
|
|
612
|
-
async getContractManager() {
|
|
613
|
-
// Return existing manager if already initialized
|
|
614
|
-
if (this._contractManager) {
|
|
615
|
-
return this._contractManager;
|
|
616
|
-
}
|
|
617
|
-
// If initialization is in progress, wait for it
|
|
618
|
-
if (this._contractManagerInitializing) {
|
|
619
|
-
return this._contractManagerInitializing;
|
|
620
|
-
}
|
|
621
|
-
// Start initialization and store the promise
|
|
622
|
-
this._contractManagerInitializing = this.initializeContractManager();
|
|
623
|
-
try {
|
|
624
|
-
const manager = await this._contractManagerInitializing;
|
|
625
|
-
this._contractManager = manager;
|
|
626
|
-
return manager;
|
|
627
|
-
}
|
|
628
|
-
catch (error) {
|
|
629
|
-
// Clear the initializing promise so subsequent calls can retry
|
|
630
|
-
this._contractManagerInitializing = undefined;
|
|
631
|
-
throw error;
|
|
632
|
-
}
|
|
633
|
-
finally {
|
|
634
|
-
// Clear the initializing promise after completion
|
|
635
|
-
this._contractManagerInitializing = undefined;
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
async initializeContractManager() {
|
|
639
|
-
const manager = await ContractManager.create({
|
|
640
|
-
indexerProvider: this.indexerProvider,
|
|
641
|
-
contractRepository: this.contractRepository,
|
|
642
|
-
walletRepository: this.walletRepository,
|
|
643
|
-
watcherConfig: this.watcherConfig,
|
|
644
|
-
});
|
|
645
|
-
for (const csvTimelock of this.walletContractTimelocks) {
|
|
646
|
-
const csvTimelockStr = timelockToSequence(csvTimelock).toString();
|
|
647
|
-
const defaultScript = new DefaultVtxo.Script({
|
|
648
|
-
pubKey: this.offchainTapscript.options.pubKey,
|
|
649
|
-
serverPubKey: this.offchainTapscript.options.serverPubKey,
|
|
650
|
-
csvTimelock,
|
|
651
|
-
});
|
|
652
|
-
await manager.createContract({
|
|
653
|
-
type: "default",
|
|
654
|
-
params: {
|
|
655
|
-
pubKey: hex.encode(defaultScript.options.pubKey),
|
|
656
|
-
serverPubKey: hex.encode(defaultScript.options.serverPubKey),
|
|
657
|
-
csvTimelock: csvTimelockStr,
|
|
658
|
-
},
|
|
659
|
-
script: hex.encode(defaultScript.pkScript),
|
|
660
|
-
address: defaultScript
|
|
661
|
-
.address(this.network.hrp, this.arkServerPublicKey)
|
|
662
|
-
.encode(),
|
|
663
|
-
state: "active",
|
|
664
|
-
});
|
|
665
|
-
if (this.offchainTapscript instanceof DelegateVtxo.Script) {
|
|
666
|
-
const delegateScript = new DelegateVtxo.Script({
|
|
667
|
-
pubKey: this.offchainTapscript.options.pubKey,
|
|
668
|
-
serverPubKey: this.offchainTapscript.options.serverPubKey,
|
|
669
|
-
delegatePubKey: this.offchainTapscript.options.delegatePubKey,
|
|
670
|
-
csvTimelock,
|
|
671
|
-
});
|
|
672
|
-
await manager.createContract({
|
|
673
|
-
type: "delegate",
|
|
674
|
-
params: {
|
|
675
|
-
pubKey: hex.encode(delegateScript.options.pubKey),
|
|
676
|
-
serverPubKey: hex.encode(delegateScript.options.serverPubKey),
|
|
677
|
-
delegatePubKey: hex.encode(delegateScript.options.delegatePubKey),
|
|
678
|
-
csvTimelock: csvTimelockStr,
|
|
679
|
-
},
|
|
680
|
-
script: hex.encode(delegateScript.pkScript),
|
|
681
|
-
address: delegateScript
|
|
682
|
-
.address(this.network.hrp, this.arkServerPublicKey)
|
|
683
|
-
.encode(),
|
|
684
|
-
state: "active",
|
|
685
|
-
});
|
|
686
|
-
}
|
|
687
|
-
}
|
|
688
|
-
return manager;
|
|
689
|
-
}
|
|
690
|
-
/** Dispose wallet-owned managers and release background resources. */
|
|
691
|
-
async dispose() {
|
|
692
|
-
const manager = this._contractManager ??
|
|
693
|
-
(this._contractManagerInitializing
|
|
694
|
-
? await this._contractManagerInitializing.catch(() => undefined)
|
|
695
|
-
: undefined);
|
|
696
|
-
manager?.dispose();
|
|
697
|
-
this._contractManager = undefined;
|
|
698
|
-
this._contractManagerInitializing = undefined;
|
|
699
|
-
}
|
|
700
|
-
/** Async-dispose hook that forwards to `dispose()`. */
|
|
701
|
-
async [Symbol.asyncDispose]() {
|
|
702
|
-
await this.dispose();
|
|
703
|
-
}
|
|
704
|
-
}
|
|
705
|
-
/**
|
|
706
|
-
* Main wallet implementation for Bitcoin transactions with Arkade protocol support.
|
|
707
|
-
* The wallet does not store any data locally and relies on Arkade and onchain
|
|
708
|
-
* providers to fetch onchain and virtual outputs.
|
|
709
|
-
*
|
|
710
|
-
* @example
|
|
711
|
-
* ```typescript
|
|
712
|
-
* // Create a wallet with URL configuration
|
|
713
|
-
* const wallet = await Wallet.create({
|
|
714
|
-
* identity: MnemonicIdentity.fromMnemonic('abandon abandon...'),
|
|
715
|
-
* arkServerUrl: 'https://arkade.computer',
|
|
716
|
-
* esploraUrl: 'https://mempool.space/api'
|
|
717
|
-
* });
|
|
718
|
-
*
|
|
719
|
-
* // Or with custom provider instances (e.g., for Expo/React Native)
|
|
720
|
-
* const wallet = await Wallet.create({
|
|
721
|
-
* identity: MnemonicIdentity.fromMnemonic('abandon abandon...'),
|
|
722
|
-
* arkProvider: new ExpoArkProvider('https://arkade.computer'),
|
|
723
|
-
* indexerProvider: new ExpoIndexerProvider('https://arkade.computer'),
|
|
724
|
-
* esploraUrl: 'https://mempool.space/api'
|
|
725
|
-
* });
|
|
726
|
-
*
|
|
727
|
-
* // Get addresses
|
|
728
|
-
* const arkAddress = await wallet.getAddress();
|
|
729
|
-
* const boardingAddress = await wallet.getBoardingAddress();
|
|
730
|
-
*
|
|
731
|
-
* // Send bitcoin
|
|
732
|
-
* const txid = await wallet.send({
|
|
733
|
-
* address: 'ark1q...',
|
|
734
|
-
* amount: 50000,
|
|
735
|
-
* });
|
|
736
|
-
* ```
|
|
737
|
-
*/
|
|
738
|
-
export class Wallet extends ReadonlyWallet {
|
|
739
|
-
_addPendingSpends(inputs) {
|
|
740
|
-
for (const input of inputs) {
|
|
741
|
-
if ("virtualStatus" in input) {
|
|
742
|
-
this._pendingSpendOutpoints.add(`${input.txid}:${input.vout}`);
|
|
743
|
-
}
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
|
-
_removePendingSpends(inputs) {
|
|
747
|
-
for (const input of inputs) {
|
|
748
|
-
if ("virtualStatus" in input) {
|
|
749
|
-
this._pendingSpendOutpoints.delete(`${input.txid}:${input.vout}`);
|
|
750
|
-
}
|
|
751
|
-
}
|
|
752
|
-
}
|
|
753
|
-
_withTxLock(fn) {
|
|
754
|
-
let release;
|
|
755
|
-
const lock = new Promise((r) => (release = r));
|
|
756
|
-
const prev = this._txLock;
|
|
757
|
-
this._txLock = lock;
|
|
758
|
-
return prev.then(async () => {
|
|
759
|
-
try {
|
|
760
|
-
return await fn();
|
|
761
|
-
}
|
|
762
|
-
finally {
|
|
763
|
-
release();
|
|
764
|
-
}
|
|
765
|
-
});
|
|
766
|
-
}
|
|
767
|
-
constructor(identity, network, onchainProvider, arkProvider, indexerProvider, arkServerPublicKey, offchainTapscript, boardingTapscript, serverUnrollScript, forfeitOutputScript, forfeitPubkey, dustAmount, walletRepository, contractRepository,
|
|
768
|
-
/** @deprecated Use settlementConfig */
|
|
769
|
-
renewalConfig, delegatorProvider, watcherConfig, settlementConfig, walletContractTimelocks) {
|
|
770
|
-
super(identity, network, onchainProvider, indexerProvider, arkServerPublicKey, offchainTapscript, boardingTapscript, dustAmount, walletRepository, contractRepository, delegatorProvider, watcherConfig, walletContractTimelocks);
|
|
771
|
-
this.arkProvider = arkProvider;
|
|
772
|
-
this.serverUnrollScript = serverUnrollScript;
|
|
773
|
-
this.forfeitOutputScript = forfeitOutputScript;
|
|
774
|
-
this.forfeitPubkey = forfeitPubkey;
|
|
775
|
-
/**
|
|
776
|
-
* Async mutex that serializes all operations submitting VTXOs to the Arkade
|
|
777
|
-
* server (`settle`, `send`, `sendBitcoin`). This prevents VtxoManager's
|
|
778
|
-
* background renewal from racing with user-initiated transactions for the
|
|
779
|
-
* same VTXO inputs.
|
|
780
|
-
*/
|
|
781
|
-
this._txLock = Promise.resolve();
|
|
782
|
-
this.identity = identity;
|
|
783
|
-
// Backwards-compatible: keep renewalConfig populated for any code reading it
|
|
784
|
-
this.renewalConfig = {
|
|
785
|
-
enabled: renewalConfig?.enabled ?? false,
|
|
786
|
-
...DEFAULT_RENEWAL_CONFIG,
|
|
787
|
-
...renewalConfig,
|
|
788
|
-
};
|
|
789
|
-
// Normalize: prefer settlementConfig, fall back to renewalConfig, default to enabled
|
|
790
|
-
if (settlementConfig !== undefined) {
|
|
791
|
-
this.settlementConfig = settlementConfig;
|
|
792
|
-
}
|
|
793
|
-
else if (renewalConfig && this.renewalConfig.enabled) {
|
|
794
|
-
this.settlementConfig = {
|
|
795
|
-
vtxoThreshold: renewalConfig.thresholdMs
|
|
796
|
-
? renewalConfig.thresholdMs / 1000
|
|
797
|
-
: undefined,
|
|
798
|
-
};
|
|
799
|
-
}
|
|
800
|
-
else if (renewalConfig) {
|
|
801
|
-
// renewalConfig provided but not enabled → disabled
|
|
802
|
-
this.settlementConfig = false;
|
|
803
|
-
}
|
|
804
|
-
else {
|
|
805
|
-
// No config at all → enabled by default
|
|
806
|
-
this.settlementConfig = { ...DEFAULT_SETTLEMENT_CONFIG };
|
|
807
|
-
}
|
|
808
|
-
this._delegatorManager = delegatorProvider
|
|
809
|
-
? new DelegatorManagerImpl(delegatorProvider, arkProvider, identity)
|
|
810
|
-
: undefined;
|
|
811
|
-
}
|
|
812
|
-
get assetManager() {
|
|
813
|
-
this._walletAssetManager ?? (this._walletAssetManager = new AssetManager(this));
|
|
814
|
-
return this._walletAssetManager;
|
|
815
|
-
}
|
|
816
|
-
async getVtxoManager() {
|
|
817
|
-
if (this._vtxoManager) {
|
|
818
|
-
return this._vtxoManager;
|
|
819
|
-
}
|
|
820
|
-
if (this._vtxoManagerInitializing) {
|
|
821
|
-
return this._vtxoManagerInitializing;
|
|
822
|
-
}
|
|
823
|
-
this._vtxoManagerInitializing = Promise.resolve(new VtxoManager(this, this.renewalConfig, this.settlementConfig));
|
|
824
|
-
try {
|
|
825
|
-
const manager = await this._vtxoManagerInitializing;
|
|
826
|
-
this._vtxoManager = manager;
|
|
827
|
-
return manager;
|
|
828
|
-
}
|
|
829
|
-
catch (error) {
|
|
830
|
-
this._vtxoManagerInitializing = undefined;
|
|
831
|
-
throw error;
|
|
832
|
-
}
|
|
833
|
-
finally {
|
|
834
|
-
this._vtxoManagerInitializing = undefined;
|
|
835
|
-
}
|
|
836
|
-
}
|
|
837
|
-
async dispose() {
|
|
838
|
-
const manager = this._vtxoManager ??
|
|
839
|
-
(this._vtxoManagerInitializing
|
|
840
|
-
? await this._vtxoManagerInitializing.catch(() => undefined)
|
|
841
|
-
: undefined);
|
|
842
|
-
try {
|
|
843
|
-
if (manager) {
|
|
844
|
-
await manager.dispose();
|
|
845
|
-
}
|
|
846
|
-
}
|
|
847
|
-
catch {
|
|
848
|
-
// best-effort teardown; ensure super.dispose() still runs
|
|
849
|
-
}
|
|
850
|
-
finally {
|
|
851
|
-
this._vtxoManager = undefined;
|
|
852
|
-
this._vtxoManagerInitializing = undefined;
|
|
853
|
-
await super.dispose();
|
|
854
|
-
}
|
|
855
|
-
}
|
|
856
|
-
/**
|
|
857
|
-
* Create a full wallet and initialize its background managers.
|
|
858
|
-
*
|
|
859
|
-
* @param config - Wallet configuration
|
|
860
|
-
* @returns A wallet ready to query balances and send transactions
|
|
861
|
-
* @example
|
|
862
|
-
* ```typescript
|
|
863
|
-
* const wallet = await Wallet.create({
|
|
864
|
-
* identity,
|
|
865
|
-
* arkServerUrl: 'https://arkade.computer',
|
|
866
|
-
* });
|
|
867
|
-
* ```
|
|
868
|
-
*/
|
|
869
|
-
static async create(config) {
|
|
870
|
-
const pubkey = await config.identity.xOnlyPublicKey();
|
|
871
|
-
if (!pubkey) {
|
|
872
|
-
throw new Error("Invalid configured public key");
|
|
873
|
-
}
|
|
874
|
-
const setup = await ReadonlyWallet.setupWalletConfig(config, pubkey);
|
|
875
|
-
// Compute Wallet-specific forfeit and unroll scripts
|
|
876
|
-
// the serverUnrollScript is the one used to create output scripts of the checkpoint transactions
|
|
877
|
-
let serverUnrollScript;
|
|
878
|
-
try {
|
|
879
|
-
const raw = hex.decode(setup.info.checkpointTapscript);
|
|
880
|
-
serverUnrollScript = CSVMultisigTapscript.decode(raw);
|
|
881
|
-
}
|
|
882
|
-
catch (e) {
|
|
883
|
-
throw new Error("Invalid checkpointTapscript from server");
|
|
884
|
-
}
|
|
885
|
-
// parse the server forfeit address
|
|
886
|
-
// server is expecting funds to be sent to this address
|
|
887
|
-
const forfeitPubkey = hex.decode(setup.info.forfeitPubkey).slice(1);
|
|
888
|
-
const forfeitAddress = Address(setup.network).decode(setup.info.forfeitAddress);
|
|
889
|
-
const forfeitOutputScript = OutScript.encode(forfeitAddress);
|
|
890
|
-
const wallet = new Wallet(config.identity, setup.network, 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, config.settlementConfig, setup.walletContractTimelocks);
|
|
891
|
-
await wallet.getVtxoManager();
|
|
892
|
-
return wallet;
|
|
893
|
-
}
|
|
894
|
-
/**
|
|
895
|
-
* Convert this wallet to a readonly wallet.
|
|
896
|
-
*
|
|
897
|
-
* @returns A readonly wallet with the same configuration but readonly identity
|
|
898
|
-
* @example
|
|
899
|
-
* ```typescript
|
|
900
|
-
* const wallet = await Wallet.create({ identity: MnemonicIdentity.fromMnemonic('abandon abandon...'), ... });
|
|
901
|
-
* const readonlyWallet = await wallet.toReadonly();
|
|
902
|
-
*
|
|
903
|
-
* // Can query balance and addresses
|
|
904
|
-
* const balance = await readonlyWallet.getBalance();
|
|
905
|
-
* const address = await readonlyWallet.getAddress();
|
|
906
|
-
*
|
|
907
|
-
* // But cannot send transactions (type error)
|
|
908
|
-
* // readonlyWallet.send(...); // TypeScript error
|
|
909
|
-
* ```
|
|
910
|
-
*/
|
|
911
|
-
async toReadonly() {
|
|
912
|
-
// Check if the identity has a toReadonly method using type guard
|
|
913
|
-
const readonlyIdentity = hasToReadonly(this.identity)
|
|
914
|
-
? await this.identity.toReadonly()
|
|
915
|
-
: this.identity; // Identity extends ReadonlyIdentity, so this is safe
|
|
916
|
-
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, this.walletContractTimelocks);
|
|
917
|
-
}
|
|
918
|
-
/** Returns the delegator manager when delegation support is configured. */
|
|
919
|
-
async getDelegatorManager() {
|
|
920
|
-
return this._delegatorManager;
|
|
921
|
-
}
|
|
922
|
-
/**
|
|
923
|
-
* Send bitcoin to an Arkade address.
|
|
924
|
-
*
|
|
925
|
-
* @deprecated Use `send`.
|
|
926
|
-
* @param params - Send parameters
|
|
927
|
-
*/
|
|
928
|
-
async sendBitcoin(params) {
|
|
929
|
-
if (params.amount <= 0) {
|
|
930
|
-
throw new Error("Amount must be positive");
|
|
931
|
-
}
|
|
932
|
-
if (!isValidArkAddress(params.address)) {
|
|
933
|
-
throw new Error("Invalid Arkade address " + params.address);
|
|
934
|
-
}
|
|
935
|
-
if (params.selectedVtxos && params.selectedVtxos.length > 0) {
|
|
936
|
-
return this._withTxLock(async () => {
|
|
937
|
-
const selectedVtxoSum = params
|
|
938
|
-
.selectedVtxos.map((v) => v.value)
|
|
939
|
-
.reduce((a, b) => a + b, 0);
|
|
940
|
-
if (selectedVtxoSum < params.amount) {
|
|
941
|
-
throw new Error("Selected VTXOs do not cover specified amount");
|
|
942
|
-
}
|
|
943
|
-
const changeAmount = selectedVtxoSum - params.amount;
|
|
944
|
-
const selected = {
|
|
945
|
-
inputs: params.selectedVtxos,
|
|
946
|
-
changeAmount: BigInt(changeAmount),
|
|
947
|
-
};
|
|
948
|
-
const outputAddress = ArkAddress.decode(params.address);
|
|
949
|
-
const outputScript = BigInt(params.amount) < this.dustAmount
|
|
950
|
-
? outputAddress.subdustPkScript
|
|
951
|
-
: outputAddress.pkScript;
|
|
952
|
-
const outputs = [
|
|
953
|
-
{
|
|
954
|
-
script: outputScript,
|
|
955
|
-
amount: BigInt(params.amount),
|
|
956
|
-
},
|
|
957
|
-
];
|
|
958
|
-
// add change output if needed
|
|
959
|
-
if (selected.changeAmount > 0n) {
|
|
960
|
-
const changeOutputScript = selected.changeAmount < this.dustAmount
|
|
961
|
-
? this.arkAddress.subdustPkScript
|
|
962
|
-
: this.arkAddress.pkScript;
|
|
963
|
-
outputs.push({
|
|
964
|
-
script: changeOutputScript,
|
|
965
|
-
amount: BigInt(selected.changeAmount),
|
|
966
|
-
});
|
|
967
|
-
}
|
|
968
|
-
this._addPendingSpends(selected.inputs);
|
|
969
|
-
try {
|
|
970
|
-
const { arkTxid, signedCheckpointTxs } = await this.buildAndSubmitOffchainTx(selected.inputs, outputs);
|
|
971
|
-
await this.updateDbAfterOffchainTx(selected.inputs, arkTxid, signedCheckpointTxs, params.amount, selected.changeAmount, selected.changeAmount > 0n ? outputs.length - 1 : 0);
|
|
972
|
-
return arkTxid;
|
|
973
|
-
}
|
|
974
|
-
finally {
|
|
975
|
-
this._removePendingSpends(selected.inputs);
|
|
976
|
-
}
|
|
977
|
-
});
|
|
978
|
-
}
|
|
979
|
-
return this.send({
|
|
980
|
-
address: params.address,
|
|
981
|
-
amount: params.amount,
|
|
982
|
-
});
|
|
983
|
-
}
|
|
984
|
-
/**
|
|
985
|
-
* Settle boarding inputs and/or virtual outputs into a finalized mainnet transaction.
|
|
986
|
-
*
|
|
987
|
-
* @param params - Optional settlement inputs and outputs. When omitted, the wallet settles all eligible funds.
|
|
988
|
-
* @param eventCallback - Optional callback invoked for settlement stream events.
|
|
989
|
-
* @returns The finalized Arkade transaction id
|
|
990
|
-
*/
|
|
991
|
-
async settle(params, eventCallback) {
|
|
992
|
-
return this._withTxLock(() => this._settleImpl(params, eventCallback));
|
|
993
|
-
}
|
|
994
|
-
async _settleImpl(params, eventCallback) {
|
|
995
|
-
if (params?.inputs) {
|
|
996
|
-
for (const input of params.inputs) {
|
|
997
|
-
// validate arknotes inputs
|
|
998
|
-
if (typeof input === "string") {
|
|
999
|
-
try {
|
|
1000
|
-
ArkNote.fromString(input);
|
|
1001
|
-
}
|
|
1002
|
-
catch (e) {
|
|
1003
|
-
throw new Error(`Invalid arknote "${input}"`);
|
|
1004
|
-
}
|
|
1005
|
-
}
|
|
1006
|
-
}
|
|
1007
|
-
}
|
|
1008
|
-
// if no params are provided, use all non-expired boarding inputs and offchain virtual outputs as inputs
|
|
1009
|
-
// and send all to the offchain address
|
|
1010
|
-
if (!params) {
|
|
1011
|
-
const { fees } = await this.arkProvider.getInfo();
|
|
1012
|
-
const estimator = new Estimator(fees.intentFee);
|
|
1013
|
-
let amount = 0;
|
|
1014
|
-
const exitScript = CSVMultisigTapscript.decode(hex.decode(this.boardingTapscript.exitScript));
|
|
1015
|
-
const boardingTimelock = exitScript.params.timelock;
|
|
1016
|
-
// For block-based timelocks, fetch the chain tip height
|
|
1017
|
-
let chainTipHeight;
|
|
1018
|
-
if (boardingTimelock.type === "blocks") {
|
|
1019
|
-
const tip = await this.onchainProvider.getChainTip();
|
|
1020
|
-
chainTipHeight = tip.height;
|
|
1021
|
-
}
|
|
1022
|
-
const boardingUtxos = (await this.getBoardingUtxos()).filter((utxo) => utxo.status.confirmed &&
|
|
1023
|
-
!hasBoardingTxExpired(utxo, boardingTimelock, chainTipHeight));
|
|
1024
|
-
const filteredBoardingUtxos = [];
|
|
1025
|
-
for (const utxo of boardingUtxos) {
|
|
1026
|
-
const inputFee = estimator.evalOnchainInput({
|
|
1027
|
-
amount: BigInt(utxo.value),
|
|
1028
|
-
});
|
|
1029
|
-
if (inputFee.value >= utxo.value) {
|
|
1030
|
-
// skip if fees are greater than the boarding input value
|
|
1031
|
-
continue;
|
|
1032
|
-
}
|
|
1033
|
-
filteredBoardingUtxos.push(utxo);
|
|
1034
|
-
amount += utxo.value - inputFee.satoshis;
|
|
1035
|
-
}
|
|
1036
|
-
const vtxos = await this.getVtxos({ withRecoverable: true });
|
|
1037
|
-
const filteredVtxos = [];
|
|
1038
|
-
for (const vtxo of vtxos) {
|
|
1039
|
-
const inputFee = estimator.evalOffchainInput({
|
|
1040
|
-
amount: BigInt(vtxo.value),
|
|
1041
|
-
type: vtxo.virtualStatus.state === "swept"
|
|
1042
|
-
? "recoverable"
|
|
1043
|
-
: "vtxo",
|
|
1044
|
-
weight: 0,
|
|
1045
|
-
birth: vtxo.createdAt,
|
|
1046
|
-
expiry: vtxo.virtualStatus.batchExpiry
|
|
1047
|
-
? new Date(vtxo.virtualStatus.batchExpiry)
|
|
1048
|
-
: undefined,
|
|
1049
|
-
});
|
|
1050
|
-
if (inputFee.satoshis >= vtxo.value) {
|
|
1051
|
-
// skip if fees are greater than the virtual output value
|
|
1052
|
-
continue;
|
|
1053
|
-
}
|
|
1054
|
-
filteredVtxos.push(vtxo);
|
|
1055
|
-
amount += vtxo.value - inputFee.satoshis;
|
|
1056
|
-
}
|
|
1057
|
-
const inputs = [...filteredBoardingUtxos, ...filteredVtxos];
|
|
1058
|
-
if (inputs.length === 0) {
|
|
1059
|
-
throw new Error("No inputs found");
|
|
1060
|
-
}
|
|
1061
|
-
const output = {
|
|
1062
|
-
address: await this.getAddress(),
|
|
1063
|
-
amount: BigInt(amount),
|
|
1064
|
-
};
|
|
1065
|
-
const outputFee = estimator.evalOffchainOutput({
|
|
1066
|
-
amount: output.amount,
|
|
1067
|
-
script: hex.encode(ArkAddress.decode(output.address).pkScript),
|
|
1068
|
-
});
|
|
1069
|
-
output.amount -= BigInt(outputFee.satoshis);
|
|
1070
|
-
if (output.amount <= this.dustAmount) {
|
|
1071
|
-
throw new Error("Output amount is below dust limit");
|
|
1072
|
-
}
|
|
1073
|
-
params = {
|
|
1074
|
-
inputs,
|
|
1075
|
-
outputs: [output],
|
|
1076
|
-
};
|
|
1077
|
-
}
|
|
1078
|
-
const onchainOutputIndexes = [];
|
|
1079
|
-
const outputs = [];
|
|
1080
|
-
let hasOffchainOutputs = false;
|
|
1081
|
-
for (const [index, output] of params.outputs.entries()) {
|
|
1082
|
-
let script;
|
|
1083
|
-
try {
|
|
1084
|
-
// offchain
|
|
1085
|
-
const addr = ArkAddress.decode(output.address);
|
|
1086
|
-
script = addr.pkScript;
|
|
1087
|
-
hasOffchainOutputs = true;
|
|
1088
|
-
}
|
|
1089
|
-
catch {
|
|
1090
|
-
// onchain
|
|
1091
|
-
const addr = Address(this.network).decode(output.address);
|
|
1092
|
-
script = OutScript.encode(addr);
|
|
1093
|
-
onchainOutputIndexes.push(index);
|
|
1094
|
-
}
|
|
1095
|
-
outputs.push({
|
|
1096
|
-
amount: output.amount,
|
|
1097
|
-
script,
|
|
1098
|
-
});
|
|
1099
|
-
}
|
|
1100
|
-
// if some of the inputs hold assets, build the asset packet and append as output
|
|
1101
|
-
// in the intent proof tx, there is a "fake" input at index 0
|
|
1102
|
-
// so the real coin indices are offset by +1
|
|
1103
|
-
const assetInputs = new Map();
|
|
1104
|
-
for (let i = 0; i < params.inputs.length; i++) {
|
|
1105
|
-
if ("assets" in params.inputs[i]) {
|
|
1106
|
-
const assets = params.inputs[i]
|
|
1107
|
-
.assets;
|
|
1108
|
-
if (assets && assets.length > 0) {
|
|
1109
|
-
assetInputs.set(i + 1, assets);
|
|
1110
|
-
}
|
|
1111
|
-
}
|
|
1112
|
-
}
|
|
1113
|
-
let outputAssets;
|
|
1114
|
-
const destinationScript = ArkAddress.decode(await this.getAddress()).pkScript;
|
|
1115
|
-
const assetOutputIndex = findDestinationOutputIndex(outputs, destinationScript);
|
|
1116
|
-
if (assetInputs.size > 0) {
|
|
1117
|
-
if (assetOutputIndex === -1) {
|
|
1118
|
-
throw new Error("Cannot assign assets: no output matches the destination address");
|
|
1119
|
-
}
|
|
1120
|
-
// collect all input assets and assign them to the destination output
|
|
1121
|
-
const allAssets = new Map();
|
|
1122
|
-
for (const [, assets] of assetInputs) {
|
|
1123
|
-
for (const asset of assets) {
|
|
1124
|
-
const existing = allAssets.get(asset.assetId) ?? 0n;
|
|
1125
|
-
allAssets.set(asset.assetId, existing + asset.amount);
|
|
1126
|
-
}
|
|
1127
|
-
}
|
|
1128
|
-
outputAssets = [];
|
|
1129
|
-
for (const [assetId, amount] of allAssets) {
|
|
1130
|
-
outputAssets.push({ assetId, amount });
|
|
1131
|
-
}
|
|
1132
|
-
}
|
|
1133
|
-
const recipients = params.outputs.map((output, i) => ({
|
|
1134
|
-
address: output.address,
|
|
1135
|
-
amount: Number(output.amount),
|
|
1136
|
-
assets: i === assetOutputIndex ? outputAssets : undefined,
|
|
1137
|
-
}));
|
|
1138
|
-
if (outputAssets && outputAssets.length > 0) {
|
|
1139
|
-
const assetPacket = createAssetPacket(assetInputs, recipients);
|
|
1140
|
-
outputs.push(Extension.create([assetPacket]).txOut());
|
|
1141
|
-
}
|
|
1142
|
-
// session holds the state of the musig2 signing process of the virtual output tree
|
|
1143
|
-
let session;
|
|
1144
|
-
const signingPublicKeys = [];
|
|
1145
|
-
if (hasOffchainOutputs) {
|
|
1146
|
-
session = this.identity.signerSession();
|
|
1147
|
-
signingPublicKeys.push(hex.encode(await session.getPublicKey()));
|
|
1148
|
-
}
|
|
1149
|
-
const [intent, deleteIntent] = await Promise.all([
|
|
1150
|
-
this.makeRegisterIntentSignature(params.inputs, outputs, onchainOutputIndexes, signingPublicKeys),
|
|
1151
|
-
this.makeDeleteIntentSignature(params.inputs),
|
|
1152
|
-
]);
|
|
1153
|
-
const topics = [
|
|
1154
|
-
...signingPublicKeys,
|
|
1155
|
-
...params.inputs.map((input) => `${input.txid}:${input.vout}`),
|
|
1156
|
-
];
|
|
1157
|
-
const abortController = new AbortController();
|
|
1158
|
-
let stream;
|
|
1159
|
-
// Optimistically hide these inputs from concurrent getVtxos() callers
|
|
1160
|
-
// while the settlement is in flight. Set before safeRegisterIntent so
|
|
1161
|
-
// there's no window between intent registration and coin-visibility.
|
|
1162
|
-
this._addPendingSpends(params.inputs);
|
|
1163
|
-
try {
|
|
1164
|
-
stream = this.arkProvider.getEventStream(abortController.signal, topics);
|
|
1165
|
-
// Prime the iterator so the provider opens the SSE subscription
|
|
1166
|
-
// before safeRegisterIntent can trigger server-side batch events.
|
|
1167
|
-
const firstNext = stream.next();
|
|
1168
|
-
// If settle exits before Batch.join consumes the primed result,
|
|
1169
|
-
// keep the orphaned promise from surfacing as an unhandled rejection.
|
|
1170
|
-
void firstNext.catch(() => { });
|
|
1171
|
-
const primedStream = (async function* () {
|
|
1172
|
-
const first = await firstNext;
|
|
1173
|
-
if (!first.done) {
|
|
1174
|
-
yield first.value;
|
|
1175
|
-
}
|
|
1176
|
-
yield* stream;
|
|
1177
|
-
})();
|
|
1178
|
-
const intentId = await this.safeRegisterIntent(intent, params.inputs);
|
|
1179
|
-
const handler = this.createBatchHandler(intentId, params.inputs, recipients, session);
|
|
1180
|
-
const commitmentTxid = await Batch.join(primedStream, handler, {
|
|
1181
|
-
abortController,
|
|
1182
|
-
skipVtxoTreeSigning: !hasOffchainOutputs,
|
|
1183
|
-
eventCallback: eventCallback
|
|
1184
|
-
? (event) => Promise.resolve(eventCallback(event))
|
|
1185
|
-
: undefined,
|
|
1186
|
-
});
|
|
1187
|
-
await this.updateDbAfterSettle(params.inputs, commitmentTxid);
|
|
1188
|
-
return commitmentTxid;
|
|
1189
|
-
}
|
|
1190
|
-
catch (error) {
|
|
1191
|
-
// delete the intent to not be stuck in the queue. If deletion fails
|
|
1192
|
-
// the intent stays on the server and the next settle will hit
|
|
1193
|
-
// "duplicated input" in safeRegisterIntent — surface the failure
|
|
1194
|
-
// rather than silently swallowing it.
|
|
1195
|
-
const inputIds = params.inputs
|
|
1196
|
-
.map((i) => `${i.txid}:${i.vout}`)
|
|
1197
|
-
.join(",");
|
|
1198
|
-
await this.arkProvider.deleteIntent(deleteIntent).catch((e) => {
|
|
1199
|
-
console.warn(`Failed to delete intent after settle failure for inputs [${inputIds}]; intent may linger on server and cause 'duplicated input' on next settle`, e);
|
|
1200
|
-
});
|
|
1201
|
-
throw error;
|
|
1202
|
-
}
|
|
1203
|
-
finally {
|
|
1204
|
-
// Clear state first so a synchronous handler firing from abort()
|
|
1205
|
-
// never observes a stale pending-spend set.
|
|
1206
|
-
this._removePendingSpends(params.inputs);
|
|
1207
|
-
// close the stream — abort() fires the in-body handler if the
|
|
1208
|
-
// generator has started iterating; return() also releases the
|
|
1209
|
-
// eager resource if the body is still suspended or never ran
|
|
1210
|
-
// (e.g. safeRegisterIntent threw before Batch.join was called).
|
|
1211
|
-
abortController.abort();
|
|
1212
|
-
await stream?.return?.().catch(() => { });
|
|
1213
|
-
}
|
|
1214
|
-
}
|
|
1215
|
-
async handleSettlementFinalizationEvent(event, inputs, forfeitOutputScript, connectorsGraph) {
|
|
1216
|
-
// the signed forfeits transactions to submit
|
|
1217
|
-
const signedForfeits = [];
|
|
1218
|
-
const isVtxo = (input) => "virtualStatus" in input;
|
|
1219
|
-
let settlementPsbt = Transaction.fromPSBT(base64.decode(event.commitmentTx));
|
|
1220
|
-
let hasBoardingUtxos = false;
|
|
1221
|
-
let connectorIndex = 0;
|
|
1222
|
-
const connectorsLeaves = connectorsGraph?.leaves() || [];
|
|
1223
|
-
for (const input of inputs) {
|
|
1224
|
-
// boarding input, we need to sign the settlement tx
|
|
1225
|
-
if (!isVtxo(input)) {
|
|
1226
|
-
for (let i = 0; i < settlementPsbt.inputsLength; i++) {
|
|
1227
|
-
const settlementInput = settlementPsbt.getInput(i);
|
|
1228
|
-
if (!settlementInput.txid ||
|
|
1229
|
-
settlementInput.index === undefined) {
|
|
1230
|
-
throw new Error("The server returned incomplete data. No settlement input found in the PSBT");
|
|
1231
|
-
}
|
|
1232
|
-
const inputTxId = hex.encode(settlementInput.txid);
|
|
1233
|
-
if (inputTxId !== input.txid)
|
|
1234
|
-
continue;
|
|
1235
|
-
if (settlementInput.index !== input.vout)
|
|
1236
|
-
continue;
|
|
1237
|
-
// input found in the settlement tx, sign it
|
|
1238
|
-
settlementPsbt.updateInput(i, {
|
|
1239
|
-
tapLeafScript: [input.forfeitTapLeafScript],
|
|
1240
|
-
});
|
|
1241
|
-
settlementPsbt = await this.identity.sign(settlementPsbt, [
|
|
1242
|
-
i,
|
|
1243
|
-
]);
|
|
1244
|
-
hasBoardingUtxos = true;
|
|
1245
|
-
break;
|
|
1246
|
-
}
|
|
1247
|
-
continue;
|
|
1248
|
-
}
|
|
1249
|
-
if (isRecoverable(input) || isSubdust(input, this.dustAmount)) {
|
|
1250
|
-
// recoverable or subdust coin, we don't need to create a forfeit tx
|
|
1251
|
-
continue;
|
|
1252
|
-
}
|
|
1253
|
-
if (connectorsLeaves.length === 0) {
|
|
1254
|
-
throw new Error("connectors not received");
|
|
1255
|
-
}
|
|
1256
|
-
if (connectorIndex >= connectorsLeaves.length) {
|
|
1257
|
-
throw new Error("not enough connectors received");
|
|
1258
|
-
}
|
|
1259
|
-
const connectorLeaf = connectorsLeaves[connectorIndex];
|
|
1260
|
-
const connectorTxId = connectorLeaf.id;
|
|
1261
|
-
const connectorOutput = connectorLeaf.getOutput(0);
|
|
1262
|
-
if (!connectorOutput) {
|
|
1263
|
-
throw new Error("connector output not found");
|
|
1264
|
-
}
|
|
1265
|
-
const connectorAmount = connectorOutput.amount;
|
|
1266
|
-
const connectorPkScript = connectorOutput.script;
|
|
1267
|
-
if (!connectorAmount || !connectorPkScript) {
|
|
1268
|
-
throw new Error("invalid connector output");
|
|
1269
|
-
}
|
|
1270
|
-
connectorIndex++;
|
|
1271
|
-
let forfeitTx = buildForfeitTx([
|
|
1272
|
-
{
|
|
1273
|
-
txid: input.txid,
|
|
1274
|
-
index: input.vout,
|
|
1275
|
-
witnessUtxo: {
|
|
1276
|
-
amount: BigInt(input.value),
|
|
1277
|
-
script: VtxoScript.decode(input.tapTree).pkScript,
|
|
1278
|
-
},
|
|
1279
|
-
sighashType: SigHash.DEFAULT,
|
|
1280
|
-
tapLeafScript: [input.forfeitTapLeafScript],
|
|
1281
|
-
},
|
|
1282
|
-
{
|
|
1283
|
-
txid: connectorTxId,
|
|
1284
|
-
index: 0,
|
|
1285
|
-
witnessUtxo: {
|
|
1286
|
-
amount: connectorAmount,
|
|
1287
|
-
script: connectorPkScript,
|
|
1288
|
-
},
|
|
1289
|
-
},
|
|
1290
|
-
], forfeitOutputScript);
|
|
1291
|
-
// do not sign the connector input
|
|
1292
|
-
forfeitTx = await this.identity.sign(forfeitTx, [0]);
|
|
1293
|
-
signedForfeits.push(base64.encode(forfeitTx.toPSBT()));
|
|
1294
|
-
}
|
|
1295
|
-
if (signedForfeits.length > 0 || hasBoardingUtxos) {
|
|
1296
|
-
await this.arkProvider.submitSignedForfeitTxs(signedForfeits, hasBoardingUtxos
|
|
1297
|
-
? base64.encode(settlementPsbt.toPSBT())
|
|
1298
|
-
: undefined);
|
|
1299
|
-
}
|
|
1300
|
-
}
|
|
1301
|
-
/**
|
|
1302
|
-
* Create a batch event handler for settlement flows.
|
|
1303
|
-
*
|
|
1304
|
-
* @param intentId - The intent ID.
|
|
1305
|
-
* @param inputs - Inputs used by the intent.
|
|
1306
|
-
* @param expectedRecipients - Expected recipients to validate in the virtual output tree.
|
|
1307
|
-
* @param session - Optional musig2 signing session. When omitted, signing steps are skipped.
|
|
1308
|
-
*/
|
|
1309
|
-
createBatchHandler(intentId, inputs, expectedRecipients, session) {
|
|
1310
|
-
let sweepTapTreeRoot;
|
|
1311
|
-
return {
|
|
1312
|
-
onBatchStarted: async (event) => {
|
|
1313
|
-
const utf8IntentId = new TextEncoder().encode(intentId);
|
|
1314
|
-
const intentIdHash = sha256(utf8IntentId);
|
|
1315
|
-
const intentIdHashStr = hex.encode(intentIdHash);
|
|
1316
|
-
let skip = true;
|
|
1317
|
-
// check if our intent ID hash matches any in the event
|
|
1318
|
-
for (const idHash of event.intentIdHashes) {
|
|
1319
|
-
if (idHash === intentIdHashStr) {
|
|
1320
|
-
if (!this.arkProvider) {
|
|
1321
|
-
throw new Error("Arkade provider not configured");
|
|
1322
|
-
}
|
|
1323
|
-
await this.arkProvider.confirmRegistration(intentId);
|
|
1324
|
-
skip = false;
|
|
1325
|
-
}
|
|
1326
|
-
}
|
|
1327
|
-
if (skip) {
|
|
1328
|
-
return { skip };
|
|
1329
|
-
}
|
|
1330
|
-
const sweepTapscript = CSVMultisigTapscript.encode({
|
|
1331
|
-
timelock: {
|
|
1332
|
-
value: event.batchExpiry,
|
|
1333
|
-
type: event.batchExpiry >= 512n ? "seconds" : "blocks",
|
|
1334
|
-
},
|
|
1335
|
-
pubkeys: [this.forfeitPubkey],
|
|
1336
|
-
}).script;
|
|
1337
|
-
sweepTapTreeRoot = tapLeafHash(sweepTapscript);
|
|
1338
|
-
return { skip: false };
|
|
1339
|
-
},
|
|
1340
|
-
onTreeSigningStarted: async (event, vtxoTree) => {
|
|
1341
|
-
if (!session) {
|
|
1342
|
-
return { skip: true };
|
|
1343
|
-
}
|
|
1344
|
-
if (!sweepTapTreeRoot) {
|
|
1345
|
-
throw new Error("Sweep tap tree root not set");
|
|
1346
|
-
}
|
|
1347
|
-
const xOnlyPublicKeys = event.cosignersPublicKeys.map((k) => k.slice(2));
|
|
1348
|
-
const signerPublicKey = await session.getPublicKey();
|
|
1349
|
-
const xonlySignerPublicKey = signerPublicKey.subarray(1);
|
|
1350
|
-
if (!xOnlyPublicKeys.includes(hex.encode(xonlySignerPublicKey))) {
|
|
1351
|
-
// not a cosigner, skip the signing
|
|
1352
|
-
return { skip: true };
|
|
1353
|
-
}
|
|
1354
|
-
// validate the unsigned virtual output tree
|
|
1355
|
-
const commitmentTx = Transaction.fromPSBT(base64.decode(event.unsignedCommitmentTx));
|
|
1356
|
-
validateVtxoTxGraph(vtxoTree, commitmentTx, sweepTapTreeRoot);
|
|
1357
|
-
// validate that all expected receivers are in the virtual output tree with correct amounts and assets
|
|
1358
|
-
if (expectedRecipients && expectedRecipients.length > 0) {
|
|
1359
|
-
validateBatchRecipients(commitmentTx, vtxoTree.leaves(), expectedRecipients, this.network);
|
|
1360
|
-
}
|
|
1361
|
-
const sharedOutput = commitmentTx.getOutput(0);
|
|
1362
|
-
if (!sharedOutput?.amount) {
|
|
1363
|
-
throw new Error("Shared output not found");
|
|
1364
|
-
}
|
|
1365
|
-
await session.init(vtxoTree, sweepTapTreeRoot, sharedOutput.amount);
|
|
1366
|
-
const pubkey = hex.encode(await session.getPublicKey());
|
|
1367
|
-
const nonces = await session.getNonces();
|
|
1368
|
-
await this.arkProvider.submitTreeNonces(event.id, pubkey, nonces);
|
|
1369
|
-
return { skip: false };
|
|
1370
|
-
},
|
|
1371
|
-
onTreeNonces: async (event) => {
|
|
1372
|
-
if (!session) {
|
|
1373
|
-
return { fullySigned: true }; // Signing complete (no signing needed)
|
|
1374
|
-
}
|
|
1375
|
-
const { hasAllNonces } = await session.aggregatedNonces(event.txid, event.nonces);
|
|
1376
|
-
// wait to receive and aggregate all nonces before sending signatures
|
|
1377
|
-
if (!hasAllNonces)
|
|
1378
|
-
return { fullySigned: false };
|
|
1379
|
-
const signatures = await session.sign();
|
|
1380
|
-
const pubkey = hex.encode(await session.getPublicKey());
|
|
1381
|
-
await this.arkProvider.submitTreeSignatures(event.id, pubkey, signatures);
|
|
1382
|
-
return { fullySigned: true };
|
|
1383
|
-
},
|
|
1384
|
-
onBatchFinalization: async (event, _, connectorTree) => {
|
|
1385
|
-
if (!this.forfeitOutputScript) {
|
|
1386
|
-
throw new Error("Forfeit output script not set");
|
|
1387
|
-
}
|
|
1388
|
-
if (connectorTree) {
|
|
1389
|
-
validateConnectorsTxGraph(event.commitmentTx, connectorTree);
|
|
1390
|
-
}
|
|
1391
|
-
await this.handleSettlementFinalizationEvent(event, inputs, this.forfeitOutputScript, connectorTree);
|
|
1392
|
-
},
|
|
1393
|
-
};
|
|
1394
|
-
}
|
|
1395
|
-
async safeRegisterIntent(intent, inputs) {
|
|
1396
|
-
try {
|
|
1397
|
-
return await this.arkProvider.registerIntent(intent);
|
|
1398
|
-
}
|
|
1399
|
-
catch (error) {
|
|
1400
|
-
// catch the "already registered by another intent" error
|
|
1401
|
-
if (error instanceof ArkError &&
|
|
1402
|
-
error.code === 0 &&
|
|
1403
|
-
error.message.includes("duplicated input")) {
|
|
1404
|
-
// Clear any queued intent spending these exact inputs. The
|
|
1405
|
-
// previous implementation signed a proof over getVtxos() only,
|
|
1406
|
-
// which misses boarding UTXOs — the most common trigger for
|
|
1407
|
-
// "duplicated input" on the auto-settle path. Signing the
|
|
1408
|
-
// caller's own inputs keeps the proof surgical and correct
|
|
1409
|
-
// regardless of whether the stuck input is a VTXO or boarding.
|
|
1410
|
-
const deleteIntent = await this.makeDeleteIntentSignature(inputs);
|
|
1411
|
-
await this.arkProvider.deleteIntent(deleteIntent);
|
|
1412
|
-
// try again
|
|
1413
|
-
return this.arkProvider.registerIntent(intent);
|
|
1414
|
-
}
|
|
1415
|
-
throw error;
|
|
1416
|
-
}
|
|
1417
|
-
}
|
|
1418
|
-
async makeRegisterIntentSignature(coins, outputs, onchainOutputsIndexes, cosignerPubKeys, validAt) {
|
|
1419
|
-
const message = {
|
|
1420
|
-
type: "register",
|
|
1421
|
-
onchain_output_indexes: onchainOutputsIndexes,
|
|
1422
|
-
valid_at: validAt ? Math.floor(validAt) : 0,
|
|
1423
|
-
expire_at: 0,
|
|
1424
|
-
cosigners_public_keys: cosignerPubKeys,
|
|
1425
|
-
};
|
|
1426
|
-
const proof = Intent.create(message, coins, outputs);
|
|
1427
|
-
const signedProof = await this.identity.sign(proof);
|
|
1428
|
-
return {
|
|
1429
|
-
proof: base64.encode(signedProof.toPSBT()),
|
|
1430
|
-
message,
|
|
1431
|
-
};
|
|
1432
|
-
}
|
|
1433
|
-
async makeDeleteIntentSignature(coins) {
|
|
1434
|
-
const message = {
|
|
1435
|
-
type: "delete",
|
|
1436
|
-
expire_at: 0,
|
|
1437
|
-
};
|
|
1438
|
-
const proof = Intent.create(message, coins, []);
|
|
1439
|
-
const signedProof = await this.identity.sign(proof);
|
|
1440
|
-
return {
|
|
1441
|
-
proof: base64.encode(signedProof.toPSBT()),
|
|
1442
|
-
message,
|
|
1443
|
-
};
|
|
1444
|
-
}
|
|
1445
|
-
async makeGetPendingTxIntentSignature(coins) {
|
|
1446
|
-
const message = {
|
|
1447
|
-
type: "get-pending-tx",
|
|
1448
|
-
expire_at: 0,
|
|
1449
|
-
};
|
|
1450
|
-
const proof = Intent.create(message, coins, []);
|
|
1451
|
-
const signedProof = await this.identity.sign(proof);
|
|
1452
|
-
return {
|
|
1453
|
-
proof: base64.encode(signedProof.toPSBT()),
|
|
1454
|
-
message,
|
|
1455
|
-
};
|
|
1456
|
-
}
|
|
1457
|
-
/**
|
|
1458
|
-
* Finalizes pending transactions by retrieving them from the server and finalizing each one.
|
|
1459
|
-
* Skips the server check entirely when no send was interrupted (no pending tx flag set).
|
|
1460
|
-
* @param vtxos - Optional list of virtual outputs to use instead of retrieving them from the server
|
|
1461
|
-
* @returns Array of transaction IDs that were finalized
|
|
1462
|
-
*/
|
|
1463
|
-
async finalizePendingTxs(vtxos) {
|
|
1464
|
-
const hasPending = await this.hasPendingTxFlag();
|
|
1465
|
-
if (!hasPending) {
|
|
1466
|
-
return { finalized: [], pending: [] };
|
|
1467
|
-
}
|
|
1468
|
-
const MAX_INPUTS_PER_INTENT = 20;
|
|
1469
|
-
if (!vtxos || vtxos.length === 0) {
|
|
1470
|
-
// Batch all scripts into a single indexer call
|
|
1471
|
-
const scriptMap = await this.getScriptMap();
|
|
1472
|
-
const allExtended = [];
|
|
1473
|
-
const allScripts = [...scriptMap.keys()];
|
|
1474
|
-
const { vtxos: fetchedVtxos } = await this.indexerProvider.getVtxos({
|
|
1475
|
-
scripts: allScripts,
|
|
1476
|
-
});
|
|
1477
|
-
for (const vtxo of fetchedVtxos) {
|
|
1478
|
-
const vtxoScript = scriptMap.get(vtxo.script);
|
|
1479
|
-
if (!vtxoScript)
|
|
1480
|
-
continue;
|
|
1481
|
-
if (vtxo.virtualStatus.state === "swept" ||
|
|
1482
|
-
vtxo.virtualStatus.state === "settled") {
|
|
1483
|
-
continue;
|
|
1484
|
-
}
|
|
1485
|
-
allExtended.push({
|
|
1486
|
-
...vtxo,
|
|
1487
|
-
forfeitTapLeafScript: vtxoScript.forfeit(),
|
|
1488
|
-
intentTapLeafScript: vtxoScript.forfeit(),
|
|
1489
|
-
tapTree: vtxoScript.encode(),
|
|
1490
|
-
});
|
|
1491
|
-
}
|
|
1492
|
-
if (allExtended.length === 0) {
|
|
1493
|
-
return { finalized: [], pending: [] };
|
|
1494
|
-
}
|
|
1495
|
-
vtxos = allExtended;
|
|
1496
|
-
}
|
|
1497
|
-
const batches = [];
|
|
1498
|
-
for (let i = 0; i < vtxos.length; i += MAX_INPUTS_PER_INTENT) {
|
|
1499
|
-
batches.push(vtxos.slice(i, i + MAX_INPUTS_PER_INTENT));
|
|
1500
|
-
}
|
|
1501
|
-
// Track seen arkTxids so parallel batches don't finalize the same tx twice
|
|
1502
|
-
const seen = new Set();
|
|
1503
|
-
const results = await Promise.all(batches.map(async (batch) => {
|
|
1504
|
-
const batchFinalized = [];
|
|
1505
|
-
const batchPending = [];
|
|
1506
|
-
const intent = await this.makeGetPendingTxIntentSignature(batch);
|
|
1507
|
-
const pendingTxs = await this.arkProvider.getPendingTxs(intent);
|
|
1508
|
-
for (const pendingTx of pendingTxs) {
|
|
1509
|
-
if (seen.has(pendingTx.arkTxid))
|
|
1510
|
-
continue;
|
|
1511
|
-
seen.add(pendingTx.arkTxid);
|
|
1512
|
-
batchPending.push(pendingTx.arkTxid);
|
|
1513
|
-
try {
|
|
1514
|
-
const finalCheckpoints = await Promise.all(pendingTx.signedCheckpointTxs.map(async (c) => {
|
|
1515
|
-
const tx = Transaction.fromPSBT(base64.decode(c));
|
|
1516
|
-
const signedCheckpoint = await this.identity.sign(tx);
|
|
1517
|
-
return base64.encode(signedCheckpoint.toPSBT());
|
|
1518
|
-
}));
|
|
1519
|
-
await this.arkProvider.finalizeTx(pendingTx.arkTxid, finalCheckpoints);
|
|
1520
|
-
batchFinalized.push(pendingTx.arkTxid);
|
|
1521
|
-
}
|
|
1522
|
-
catch (error) {
|
|
1523
|
-
console.error(`Failed to finalize transaction ${pendingTx.arkTxid}:`, error);
|
|
1524
|
-
}
|
|
1525
|
-
}
|
|
1526
|
-
return {
|
|
1527
|
-
finalized: batchFinalized,
|
|
1528
|
-
pending: batchPending,
|
|
1529
|
-
};
|
|
1530
|
-
}));
|
|
1531
|
-
const finalized = [];
|
|
1532
|
-
const pending = [];
|
|
1533
|
-
for (const result of results) {
|
|
1534
|
-
finalized.push(...result.finalized);
|
|
1535
|
-
pending.push(...result.pending);
|
|
1536
|
-
}
|
|
1537
|
-
// Only clear the flag if every discovered pending tx was finalized;
|
|
1538
|
-
// if any failed, keep it so recovery retries on next startup.
|
|
1539
|
-
if (finalized.length === pending.length) {
|
|
1540
|
-
await this.setPendingTxFlag(false);
|
|
1541
|
-
}
|
|
1542
|
-
return { finalized, pending };
|
|
1543
|
-
}
|
|
1544
|
-
async hasPendingTxFlag() {
|
|
1545
|
-
const state = await this.walletRepository.getWalletState();
|
|
1546
|
-
return state?.settings?.hasPendingTx === true;
|
|
1547
|
-
}
|
|
1548
|
-
async setPendingTxFlag(value) {
|
|
1549
|
-
await updateWalletState(this.walletRepository, (state) => ({
|
|
1550
|
-
...state,
|
|
1551
|
-
settings: { ...state.settings, hasPendingTx: value },
|
|
1552
|
-
}));
|
|
1553
|
-
}
|
|
1554
|
-
/**
|
|
1555
|
-
* Send BTC and/or assets to one or more recipients.
|
|
1556
|
-
*
|
|
1557
|
-
* @param args - Recipients with their addresses, BTC amounts, and assets
|
|
1558
|
-
* @returns Promise resolving to the Arkade transaction ID
|
|
1559
|
-
*
|
|
1560
|
-
* @example
|
|
1561
|
-
* ```typescript
|
|
1562
|
-
* const txid = await wallet.send({
|
|
1563
|
-
* address: 'ark1q...',
|
|
1564
|
-
* amount: 1000, // (optional, default to dust) btc amount to send to the output
|
|
1565
|
-
* assets: [{ assetId: 'abc123...', amount: 50n }] // (optional) list of assets to send
|
|
1566
|
-
* });
|
|
1567
|
-
* ```
|
|
1568
|
-
*/
|
|
1569
|
-
async send(...args) {
|
|
1570
|
-
return this._withTxLock(() => this._sendImpl(...args));
|
|
1571
|
-
}
|
|
1572
|
-
async _sendImpl(...args) {
|
|
1573
|
-
if (args.length === 0) {
|
|
1574
|
-
throw new Error("At least one receiver is required");
|
|
1575
|
-
}
|
|
1576
|
-
// validate recipients and populate undefined amount with dust amount
|
|
1577
|
-
const recipients = validateRecipients(args, Number(this.dustAmount));
|
|
1578
|
-
const address = await this.getAddress();
|
|
1579
|
-
const outputAddress = ArkAddress.decode(address);
|
|
1580
|
-
const virtualCoins = await this.getVtxos({
|
|
1581
|
-
withRecoverable: false,
|
|
1582
|
-
});
|
|
1583
|
-
// keep track of asset changes
|
|
1584
|
-
const assetChanges = new Map();
|
|
1585
|
-
let selectedCoins = [];
|
|
1586
|
-
let btcAmountToSelect = 0;
|
|
1587
|
-
for (const recipient of recipients) {
|
|
1588
|
-
btcAmountToSelect += Math.max(recipient.amount, Number(this.dustAmount));
|
|
1589
|
-
}
|
|
1590
|
-
// select assets
|
|
1591
|
-
for (const recipient of recipients) {
|
|
1592
|
-
if (!recipient.assets) {
|
|
1593
|
-
continue;
|
|
1594
|
-
}
|
|
1595
|
-
for (const receiverAsset of recipient.assets) {
|
|
1596
|
-
let amountToSelect = receiverAsset.amount;
|
|
1597
|
-
// check if existing change covers the needed amount
|
|
1598
|
-
const existingChange = assetChanges.get(receiverAsset.assetId) ?? 0n;
|
|
1599
|
-
if (existingChange >= amountToSelect) {
|
|
1600
|
-
assetChanges.set(receiverAsset.assetId, existingChange - amountToSelect);
|
|
1601
|
-
if (assetChanges.get(receiverAsset.assetId) === 0n) {
|
|
1602
|
-
assetChanges.delete(receiverAsset.assetId);
|
|
1603
|
-
}
|
|
1604
|
-
continue;
|
|
1605
|
-
}
|
|
1606
|
-
if (existingChange > 0n) {
|
|
1607
|
-
amountToSelect -= existingChange;
|
|
1608
|
-
assetChanges.delete(receiverAsset.assetId);
|
|
1609
|
-
}
|
|
1610
|
-
const availableCoins = virtualCoins.filter((c) => !selectedCoins.find((sc) => sc.txid === c.txid && sc.vout === c.vout));
|
|
1611
|
-
const { selected, totalAssetAmount } = selectCoinsWithAsset(availableCoins, receiverAsset.assetId, amountToSelect);
|
|
1612
|
-
for (const coin of selected) {
|
|
1613
|
-
selectedCoins.push(coin);
|
|
1614
|
-
// asset coins contain btc, subtract from total amount to select
|
|
1615
|
-
btcAmountToSelect -= coin.value;
|
|
1616
|
-
// coin may contain other assets, add them to asset changes
|
|
1617
|
-
if (coin.assets) {
|
|
1618
|
-
for (const a of coin.assets) {
|
|
1619
|
-
if (a.assetId === receiverAsset.assetId) {
|
|
1620
|
-
continue;
|
|
1621
|
-
}
|
|
1622
|
-
const existing = assetChanges.get(a.assetId) ?? 0n;
|
|
1623
|
-
assetChanges.set(a.assetId, existing + a.amount);
|
|
1624
|
-
}
|
|
1625
|
-
}
|
|
1626
|
-
}
|
|
1627
|
-
const assetChangeAmount = totalAssetAmount - amountToSelect;
|
|
1628
|
-
if (assetChangeAmount > 0n) {
|
|
1629
|
-
const existing = assetChanges.get(receiverAsset.assetId) ?? 0n;
|
|
1630
|
-
assetChanges.set(receiverAsset.assetId, existing + assetChangeAmount);
|
|
1631
|
-
}
|
|
1632
|
-
}
|
|
1633
|
-
}
|
|
1634
|
-
// select remaining btc
|
|
1635
|
-
if (btcAmountToSelect > 0) {
|
|
1636
|
-
const availableCoins = virtualCoins.filter((c) => !selectedCoins.find((sc) => sc.txid === c.txid && sc.vout === c.vout));
|
|
1637
|
-
const { inputs: btcCoins } = selectVirtualCoins(availableCoins, btcAmountToSelect);
|
|
1638
|
-
// some coins may contain assets, add them to asset changes
|
|
1639
|
-
for (const coin of btcCoins) {
|
|
1640
|
-
if (coin.assets) {
|
|
1641
|
-
for (const asset of coin.assets) {
|
|
1642
|
-
const existing = assetChanges.get(asset.assetId) ?? 0n;
|
|
1643
|
-
assetChanges.set(asset.assetId, existing + asset.amount);
|
|
1644
|
-
}
|
|
1645
|
-
}
|
|
1646
|
-
}
|
|
1647
|
-
selectedCoins = [...selectedCoins, ...btcCoins];
|
|
1648
|
-
}
|
|
1649
|
-
let totalBtcSelected = selectedCoins.reduce((sum, c) => sum + c.value, 0);
|
|
1650
|
-
// build tx outputs
|
|
1651
|
-
const outputs = recipients.map((recipient) => ({
|
|
1652
|
-
script: recipient.script,
|
|
1653
|
-
amount: BigInt(recipient.amount),
|
|
1654
|
-
}));
|
|
1655
|
-
const totalBtcOutput = outputs.reduce((sum, o) => sum + Number(o.amount), 0);
|
|
1656
|
-
let changeAmount = totalBtcSelected - totalBtcOutput;
|
|
1657
|
-
// enforce minimum change amount when there are asset changes
|
|
1658
|
-
if (assetChanges.size > 0 && changeAmount < Number(this.dustAmount)) {
|
|
1659
|
-
const availableCoins = virtualCoins.filter((c) => !selectedCoins.find((sc) => sc.txid === c.txid && sc.vout === c.vout));
|
|
1660
|
-
const { inputs: extraCoins } = selectVirtualCoins(availableCoins, Number(this.dustAmount) - changeAmount);
|
|
1661
|
-
for (const coin of extraCoins) {
|
|
1662
|
-
if (coin.assets) {
|
|
1663
|
-
for (const asset of coin.assets) {
|
|
1664
|
-
const existing = assetChanges.get(asset.assetId) ?? 0n;
|
|
1665
|
-
assetChanges.set(asset.assetId, existing + asset.amount);
|
|
1666
|
-
}
|
|
1667
|
-
}
|
|
1668
|
-
}
|
|
1669
|
-
selectedCoins = [...selectedCoins, ...extraCoins];
|
|
1670
|
-
totalBtcSelected += extraCoins.reduce((sum, c) => sum + c.value, 0);
|
|
1671
|
-
changeAmount = totalBtcSelected - totalBtcOutput;
|
|
1672
|
-
}
|
|
1673
|
-
// build change receiver with BTC change and all asset changes
|
|
1674
|
-
let changeReceiver;
|
|
1675
|
-
let changeIndex = 0;
|
|
1676
|
-
if (changeAmount > 0) {
|
|
1677
|
-
const changeAssets = [];
|
|
1678
|
-
for (const [assetId, amount] of assetChanges) {
|
|
1679
|
-
if (amount > 0n) {
|
|
1680
|
-
changeAssets.push({ assetId, amount });
|
|
1681
|
-
}
|
|
1682
|
-
}
|
|
1683
|
-
changeIndex = outputs.length;
|
|
1684
|
-
outputs.push({
|
|
1685
|
-
script: BigInt(changeAmount) < this.dustAmount
|
|
1686
|
-
? outputAddress.subdustPkScript
|
|
1687
|
-
: outputAddress.pkScript,
|
|
1688
|
-
amount: BigInt(changeAmount),
|
|
1689
|
-
});
|
|
1690
|
-
changeReceiver = {
|
|
1691
|
-
address: address,
|
|
1692
|
-
amount: changeAmount,
|
|
1693
|
-
assets: changeAssets.length > 0 ? changeAssets : undefined,
|
|
1694
|
-
};
|
|
1695
|
-
}
|
|
1696
|
-
// create asset packet only if there are assets involved
|
|
1697
|
-
const assetInputs = selectedCoinsToAssetInputs(selectedCoins);
|
|
1698
|
-
const hasAssets = assetInputs.size > 0 ||
|
|
1699
|
-
recipients.some((r) => r.assets && r.assets.length > 0);
|
|
1700
|
-
if (hasAssets) {
|
|
1701
|
-
const assetPacket = createAssetPacket(assetInputs, recipients, changeReceiver);
|
|
1702
|
-
outputs.push(Extension.create([assetPacket]).txOut());
|
|
1703
|
-
}
|
|
1704
|
-
const sentAmount = recipients.reduce((sum, r) => sum + r.amount, 0);
|
|
1705
|
-
// Optimistically hide selected coins from concurrent getVtxos() while
|
|
1706
|
-
// the offchain tx is in flight.
|
|
1707
|
-
this._addPendingSpends(selectedCoins);
|
|
1708
|
-
try {
|
|
1709
|
-
const { arkTxid, signedCheckpointTxs } = await this.buildAndSubmitOffchainTx(selectedCoins, outputs);
|
|
1710
|
-
await this.updateDbAfterOffchainTx(selectedCoins, arkTxid, signedCheckpointTxs, sentAmount, BigInt(changeAmount), changeReceiver ? changeIndex : 0, changeReceiver?.assets);
|
|
1711
|
-
return arkTxid;
|
|
1712
|
-
}
|
|
1713
|
-
finally {
|
|
1714
|
-
this._removePendingSpends(selectedCoins);
|
|
1715
|
-
}
|
|
1716
|
-
}
|
|
1717
|
-
/**
|
|
1718
|
-
* Build an offchain transaction from the given inputs and outputs,
|
|
1719
|
-
* sign it, submit to the Arkade provider, and finalize.
|
|
1720
|
-
* @returns The Arkade transaction id and server-signed checkpoint PSBTs (for bookkeeping)
|
|
1721
|
-
*/
|
|
1722
|
-
async buildAndSubmitOffchainTx(inputs, outputs) {
|
|
1723
|
-
const offchainTx = buildOffchainTx(inputs.map((input) => {
|
|
1724
|
-
return {
|
|
1725
|
-
...input,
|
|
1726
|
-
tapLeafScript: input.forfeitTapLeafScript,
|
|
1727
|
-
};
|
|
1728
|
-
}), outputs, this.serverUnrollScript);
|
|
1729
|
-
let signedVirtualTx;
|
|
1730
|
-
let userSignedCheckpoints;
|
|
1731
|
-
if (isBatchSignable(this.identity)) {
|
|
1732
|
-
// Batch-sign arkTx + all checkpoints in one wallet popup.
|
|
1733
|
-
// Clone so the provider can't mutate originals before submitTx.
|
|
1734
|
-
const requests = [
|
|
1735
|
-
{ tx: offchainTx.arkTx.clone() },
|
|
1736
|
-
...offchainTx.checkpoints.map((c) => ({ tx: c.clone() })),
|
|
1737
|
-
];
|
|
1738
|
-
const signed = await this.identity.signMultiple(requests);
|
|
1739
|
-
if (signed.length !== requests.length) {
|
|
1740
|
-
throw new Error(`signMultiple returned ${signed.length} transactions, expected ${requests.length}`);
|
|
1741
|
-
}
|
|
1742
|
-
const [firstSignedTx, ...signedCheckpoints] = signed;
|
|
1743
|
-
signedVirtualTx = firstSignedTx;
|
|
1744
|
-
userSignedCheckpoints = signedCheckpoints;
|
|
1745
|
-
}
|
|
1746
|
-
else {
|
|
1747
|
-
signedVirtualTx = await this.identity.sign(offchainTx.arkTx);
|
|
1748
|
-
}
|
|
1749
|
-
// Mark pending before submitting — if we crash between submit and
|
|
1750
|
-
// finalize, the next init will recover via finalizePendingTxs.
|
|
1751
|
-
await this.setPendingTxFlag(true);
|
|
1752
|
-
const { arkTxid, signedCheckpointTxs } = await this.arkProvider.submitTx(base64.encode(signedVirtualTx.toPSBT()), offchainTx.checkpoints.map((c) => base64.encode(c.toPSBT())));
|
|
1753
|
-
let finalCheckpoints;
|
|
1754
|
-
if (userSignedCheckpoints) {
|
|
1755
|
-
// Merge pre-signed user signatures onto server-signed checkpoints
|
|
1756
|
-
finalCheckpoints = signedCheckpointTxs.map((c, i) => {
|
|
1757
|
-
const serverSigned = Transaction.fromPSBT(base64.decode(c));
|
|
1758
|
-
combineTapscriptSigs(userSignedCheckpoints[i], serverSigned);
|
|
1759
|
-
return base64.encode(serverSigned.toPSBT());
|
|
1760
|
-
});
|
|
1761
|
-
}
|
|
1762
|
-
else {
|
|
1763
|
-
// Legacy: sign each checkpoint individually (N popups)
|
|
1764
|
-
finalCheckpoints = await Promise.all(signedCheckpointTxs.map(async (c) => {
|
|
1765
|
-
const tx = Transaction.fromPSBT(base64.decode(c));
|
|
1766
|
-
const signedCheckpoint = await this.identity.sign(tx);
|
|
1767
|
-
return base64.encode(signedCheckpoint.toPSBT());
|
|
1768
|
-
}));
|
|
1769
|
-
}
|
|
1770
|
-
await this.arkProvider.finalizeTx(arkTxid, finalCheckpoints);
|
|
1771
|
-
try {
|
|
1772
|
-
await this.setPendingTxFlag(false);
|
|
1773
|
-
}
|
|
1774
|
-
catch (error) {
|
|
1775
|
-
console.error("Failed to clear pending tx flag:", error);
|
|
1776
|
-
}
|
|
1777
|
-
return { arkTxid, signedCheckpointTxs };
|
|
1778
|
-
}
|
|
1779
|
-
// mark virtual outputs as spent, save change outputs if any
|
|
1780
|
-
async updateDbAfterOffchainTx(inputs, arkTxid, signedCheckpointTxs, sentAmount, changeAmount, changeVout, changeAssets) {
|
|
1781
|
-
try {
|
|
1782
|
-
const spentVtxos = [];
|
|
1783
|
-
const commitmentTxIds = new Set();
|
|
1784
|
-
let batchExpiry = Number.MAX_SAFE_INTEGER;
|
|
1785
|
-
if (inputs.length !== signedCheckpointTxs.length) {
|
|
1786
|
-
console.warn(`updateDbAfterOffchainTx: inputs length (${inputs.length}) differs from signedCheckpointTxs length (${signedCheckpointTxs.length})`);
|
|
1787
|
-
}
|
|
1788
|
-
const safeLength = Math.min(inputs.length, signedCheckpointTxs.length);
|
|
1789
|
-
const cm = await this.getContractManager();
|
|
1790
|
-
const annotatedInputs = await cm.annotateVtxos(inputs);
|
|
1791
|
-
for (const [inputIndex, vtxo] of annotatedInputs.entries()) {
|
|
1792
|
-
if (inputIndex < safeLength &&
|
|
1793
|
-
signedCheckpointTxs[inputIndex]) {
|
|
1794
|
-
const checkpoint = Transaction.fromPSBT(base64.decode(signedCheckpointTxs[inputIndex]));
|
|
1795
|
-
spentVtxos.push({
|
|
1796
|
-
...vtxo,
|
|
1797
|
-
virtualStatus: {
|
|
1798
|
-
...vtxo.virtualStatus,
|
|
1799
|
-
state: "spent",
|
|
1800
|
-
},
|
|
1801
|
-
spentBy: checkpoint.id,
|
|
1802
|
-
arkTxId: arkTxid,
|
|
1803
|
-
isSpent: true,
|
|
1804
|
-
});
|
|
1805
|
-
}
|
|
1806
|
-
else {
|
|
1807
|
-
spentVtxos.push({
|
|
1808
|
-
...vtxo,
|
|
1809
|
-
virtualStatus: {
|
|
1810
|
-
...vtxo.virtualStatus,
|
|
1811
|
-
state: "spent",
|
|
1812
|
-
},
|
|
1813
|
-
arkTxId: arkTxid,
|
|
1814
|
-
isSpent: true,
|
|
1815
|
-
});
|
|
1816
|
-
}
|
|
1817
|
-
if (vtxo.virtualStatus.commitmentTxIds) {
|
|
1818
|
-
for (const id of vtxo.virtualStatus.commitmentTxIds) {
|
|
1819
|
-
commitmentTxIds.add(id);
|
|
1820
|
-
}
|
|
1821
|
-
}
|
|
1822
|
-
if (vtxo.virtualStatus.batchExpiry) {
|
|
1823
|
-
batchExpiry = Math.min(batchExpiry, vtxo.virtualStatus.batchExpiry);
|
|
1824
|
-
}
|
|
1825
|
-
}
|
|
1826
|
-
const createdAt = Date.now();
|
|
1827
|
-
const primaryAddr = this.arkAddress.encode();
|
|
1828
|
-
// Only save a change virtual output for preconfirmed coins (those with a batchExpiry).
|
|
1829
|
-
// Inputs without a batchExpiry are already settled/unrolled and don't need tracking.
|
|
1830
|
-
let changeVtxo;
|
|
1831
|
-
if (changeAmount > 0n && batchExpiry !== Number.MAX_SAFE_INTEGER) {
|
|
1832
|
-
changeVtxo = {
|
|
1833
|
-
txid: arkTxid,
|
|
1834
|
-
vout: changeVout,
|
|
1835
|
-
createdAt: new Date(createdAt),
|
|
1836
|
-
forfeitTapLeafScript: this.offchainTapscript.forfeit(),
|
|
1837
|
-
intentTapLeafScript: this.offchainTapscript.forfeit(),
|
|
1838
|
-
isUnrolled: false,
|
|
1839
|
-
isSpent: false,
|
|
1840
|
-
tapTree: this.offchainTapscript.encode(),
|
|
1841
|
-
value: Number(changeAmount),
|
|
1842
|
-
virtualStatus: {
|
|
1843
|
-
state: "preconfirmed",
|
|
1844
|
-
commitmentTxIds: Array.from(commitmentTxIds),
|
|
1845
|
-
batchExpiry,
|
|
1846
|
-
},
|
|
1847
|
-
status: {
|
|
1848
|
-
confirmed: false,
|
|
1849
|
-
},
|
|
1850
|
-
assets: changeAssets,
|
|
1851
|
-
script: hex.encode(this.offchainTapscript.pkScript),
|
|
1852
|
-
};
|
|
1853
|
-
}
|
|
1854
|
-
// Route spent rows to their owning contract bucket. The wallet's
|
|
1855
|
-
// primary contract is registered with the manager at boot, so
|
|
1856
|
-
// `addrByScript` already includes it; in a multi-contract spend
|
|
1857
|
-
// each input may belong to a different contract.
|
|
1858
|
-
const contracts = await cm.getContracts();
|
|
1859
|
-
const addrByScript = new Map(contracts.map((c) => [c.script, c.address]));
|
|
1860
|
-
const spentByScript = new Map();
|
|
1861
|
-
for (const v of spentVtxos) {
|
|
1862
|
-
if (!v.script) {
|
|
1863
|
-
throw new Error(`Wallet.updateDbAfterOffchainTx: spent VTXO ${v.txid}:${v.vout} has no script`);
|
|
1864
|
-
}
|
|
1865
|
-
const arr = spentByScript.get(v.script) ?? [];
|
|
1866
|
-
arr.push(v);
|
|
1867
|
-
spentByScript.set(v.script, arr);
|
|
1868
|
-
}
|
|
1869
|
-
for (const [script, vtxos] of spentByScript) {
|
|
1870
|
-
// User-initiated send path: a wrong-script row here means the
|
|
1871
|
-
// wallet is about to record ownership against the wrong
|
|
1872
|
-
// contract — fail loudly rather than persist inconsistent state.
|
|
1873
|
-
validateVtxosForScript(vtxos, script, "Wallet.updateDbAfterOffchainTx");
|
|
1874
|
-
const targetAddr = addrByScript.get(script);
|
|
1875
|
-
if (!targetAddr) {
|
|
1876
|
-
throw new Error(`Wallet.updateDbAfterOffchainTx: no contract owns script ${script}`);
|
|
1877
|
-
}
|
|
1878
|
-
await saveVtxosForContract(this.walletRepository, { script, address: targetAddr }, vtxos);
|
|
1879
|
-
}
|
|
1880
|
-
// Change is always primary-script by construction.
|
|
1881
|
-
if (changeVtxo) {
|
|
1882
|
-
await saveVtxosForContract(this.walletRepository, { script: changeVtxo.script, address: primaryAddr }, [changeVtxo]);
|
|
1883
|
-
}
|
|
1884
|
-
await this.walletRepository.saveTransactions(primaryAddr, [
|
|
1885
|
-
{
|
|
1886
|
-
key: {
|
|
1887
|
-
boardingTxid: "",
|
|
1888
|
-
commitmentTxid: "",
|
|
1889
|
-
arkTxid: arkTxid,
|
|
1890
|
-
},
|
|
1891
|
-
amount: sentAmount,
|
|
1892
|
-
type: TxType.TxSent,
|
|
1893
|
-
settled: false,
|
|
1894
|
-
createdAt,
|
|
1895
|
-
},
|
|
1896
|
-
]);
|
|
1897
|
-
}
|
|
1898
|
-
catch (e) {
|
|
1899
|
-
console.warn("error saving offchain tx to repository", e);
|
|
1900
|
-
throw e;
|
|
1901
|
-
}
|
|
1902
|
-
}
|
|
1903
|
-
// mark virtual outputs as spent/settled, remove boarding inputs
|
|
1904
|
-
async updateDbAfterSettle(inputs, commitmentTxid) {
|
|
1905
|
-
try {
|
|
1906
|
-
const boardingAddress = await this.getBoardingAddress();
|
|
1907
|
-
const spentVtxos = [];
|
|
1908
|
-
const inputArkTxIds = new Set();
|
|
1909
|
-
const boardingUtxoToRemove = new Set();
|
|
1910
|
-
const isVtxo = (input) => "virtualStatus" in input;
|
|
1911
|
-
const vtxoInputs = inputs.filter(isVtxo);
|
|
1912
|
-
const cm = await this.getContractManager();
|
|
1913
|
-
const annotatedVtxos = await cm.annotateVtxos(vtxoInputs);
|
|
1914
|
-
const annotatedByKey = new Map(annotatedVtxos.map((v) => [`${v.txid}:${v.vout}`, v]));
|
|
1915
|
-
for (const input of inputs) {
|
|
1916
|
-
if (isVtxo(input)) {
|
|
1917
|
-
// virtual output = mark it settled
|
|
1918
|
-
const vtxo = annotatedByKey.get(`${input.txid}:${input.vout}`);
|
|
1919
|
-
if (vtxo.arkTxId) {
|
|
1920
|
-
inputArkTxIds.add(vtxo.arkTxId);
|
|
1921
|
-
}
|
|
1922
|
-
spentVtxos.push({
|
|
1923
|
-
...vtxo,
|
|
1924
|
-
virtualStatus: {
|
|
1925
|
-
...vtxo.virtualStatus,
|
|
1926
|
-
state: "settled",
|
|
1927
|
-
},
|
|
1928
|
-
settledBy: commitmentTxid,
|
|
1929
|
-
isSpent: true,
|
|
1930
|
-
});
|
|
1931
|
-
}
|
|
1932
|
-
else {
|
|
1933
|
-
// boarding input = remove it
|
|
1934
|
-
boardingUtxoToRemove.add(`${input.txid}:${input.vout}`);
|
|
1935
|
-
}
|
|
1936
|
-
}
|
|
1937
|
-
if (spentVtxos.length > 0) {
|
|
1938
|
-
// Route settled rows to their owning contract bucket. In a
|
|
1939
|
-
// multi-contract settle the inputs may belong to several
|
|
1940
|
-
// contracts; the wallet's primary contract is registered with
|
|
1941
|
-
// the manager at boot, so its address is in `addrByScript`
|
|
1942
|
-
// alongside the rest.
|
|
1943
|
-
const contracts = await cm.getContracts();
|
|
1944
|
-
const addrByScript = new Map(contracts.map((c) => [c.script, c.address]));
|
|
1945
|
-
const byScript = new Map();
|
|
1946
|
-
for (const v of spentVtxos) {
|
|
1947
|
-
if (!v.script) {
|
|
1948
|
-
throw new Error(`Wallet.updateDbAfterSettle: spent VTXO ${v.txid}:${v.vout} has no script`);
|
|
1949
|
-
}
|
|
1950
|
-
const arr = byScript.get(v.script) ?? [];
|
|
1951
|
-
arr.push(v);
|
|
1952
|
-
byScript.set(v.script, arr);
|
|
1953
|
-
}
|
|
1954
|
-
for (const [script, vtxos] of byScript) {
|
|
1955
|
-
// User-initiated settle path: refuse to record a settle
|
|
1956
|
-
// against the wrong script.
|
|
1957
|
-
validateVtxosForScript(vtxos, script, "Wallet.updateDbAfterSettle");
|
|
1958
|
-
const targetAddr = addrByScript.get(script);
|
|
1959
|
-
if (!targetAddr) {
|
|
1960
|
-
throw new Error(`Wallet.updateDbAfterSettle: no contract owns script ${script}`);
|
|
1961
|
-
}
|
|
1962
|
-
await saveVtxosForContract(this.walletRepository, { script, address: targetAddr }, vtxos);
|
|
1963
|
-
}
|
|
1964
|
-
}
|
|
1965
|
-
if (boardingUtxoToRemove.size > 0) {
|
|
1966
|
-
const currentUtxos = await this.walletRepository.getUtxos(boardingAddress);
|
|
1967
|
-
const filtered = currentUtxos.filter((u) => !boardingUtxoToRemove.has(`${u.txid}:${u.vout}`));
|
|
1968
|
-
// Clear and re-save the filtered list
|
|
1969
|
-
await this.walletRepository.deleteUtxos(boardingAddress);
|
|
1970
|
-
if (filtered.length > 0) {
|
|
1971
|
-
await this.walletRepository.saveUtxos(boardingAddress, filtered);
|
|
1972
|
-
}
|
|
1973
|
-
}
|
|
1974
|
-
}
|
|
1975
|
-
catch (e) {
|
|
1976
|
-
console.warn("error updating repository after settle", e);
|
|
1977
|
-
throw e;
|
|
1978
|
-
}
|
|
1979
|
-
}
|
|
1980
|
-
}
|
|
1981
|
-
Wallet.MIN_FEE_RATE = 1; // sats/vbyte
|
|
1982
|
-
/**
|
|
1983
|
-
* Select virtual outputs to reach a target amount, prioritizing those closer to expiry
|
|
1984
|
-
* @param coins List of virtual outputs to select from
|
|
1985
|
-
* @param targetAmount Target amount to reach in satoshis
|
|
1986
|
-
* @returns Selected virtual outputs and change amount
|
|
1987
|
-
*/
|
|
1988
|
-
export function selectVirtualCoins(coins, targetAmount) {
|
|
1989
|
-
// Sort virtual outputs by expiry (ascending) and amount (descending)
|
|
1990
|
-
const sortedCoins = [...coins].sort((a, b) => {
|
|
1991
|
-
// First sort by expiry if available
|
|
1992
|
-
const expiryA = a.virtualStatus.batchExpiry || Number.MAX_SAFE_INTEGER;
|
|
1993
|
-
const expiryB = b.virtualStatus.batchExpiry || Number.MAX_SAFE_INTEGER;
|
|
1994
|
-
if (expiryA !== expiryB) {
|
|
1995
|
-
return expiryA - expiryB; // Earlier expiry first
|
|
1996
|
-
}
|
|
1997
|
-
// Then sort by amount
|
|
1998
|
-
return b.value - a.value; // Larger amount first
|
|
1999
|
-
});
|
|
2000
|
-
const selectedCoins = [];
|
|
2001
|
-
let selectedAmount = 0;
|
|
2002
|
-
// Select coins until we have enough
|
|
2003
|
-
for (const coin of sortedCoins) {
|
|
2004
|
-
selectedCoins.push(coin);
|
|
2005
|
-
selectedAmount += coin.value;
|
|
2006
|
-
if (selectedAmount >= targetAmount) {
|
|
2007
|
-
break;
|
|
2008
|
-
}
|
|
2009
|
-
}
|
|
2010
|
-
if (selectedAmount === targetAmount) {
|
|
2011
|
-
return { inputs: selectedCoins, changeAmount: 0n };
|
|
2012
|
-
}
|
|
2013
|
-
// Check if we have enough
|
|
2014
|
-
if (selectedAmount < targetAmount) {
|
|
2015
|
-
throw new Error("Insufficient funds");
|
|
2016
|
-
}
|
|
2017
|
-
const changeAmount = BigInt(selectedAmount - targetAmount);
|
|
2018
|
-
return {
|
|
2019
|
-
inputs: selectedCoins,
|
|
2020
|
-
changeAmount,
|
|
2021
|
-
};
|
|
2022
|
-
}
|
|
2023
|
-
/**
|
|
2024
|
-
* Wait for incoming funds to the wallet
|
|
2025
|
-
* @param wallet - The wallet to wait for incoming funds
|
|
2026
|
-
* @returns A promise that resolves the next new coins received by the wallet's address
|
|
2027
|
-
*/
|
|
2028
|
-
export async function waitForIncomingFunds(wallet) {
|
|
2029
|
-
let stopFunc;
|
|
2030
|
-
return new Promise((resolve) => {
|
|
2031
|
-
wallet
|
|
2032
|
-
.notifyIncomingFunds((coins) => {
|
|
2033
|
-
resolve(coins);
|
|
2034
|
-
if (stopFunc)
|
|
2035
|
-
stopFunc();
|
|
2036
|
-
})
|
|
2037
|
-
.then((stop) => {
|
|
2038
|
-
stopFunc = stop;
|
|
2039
|
-
});
|
|
2040
|
-
});
|
|
2041
|
-
}
|