@arkade-os/sdk 0.4.26 → 0.4.27
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 +5 -25
- package/dist/cjs/contracts/contractManager.js +31 -11
- package/dist/cjs/contracts/contractWatcher.js +2 -2
- package/dist/cjs/identity/hdCapableIdentity.js +18 -0
- package/dist/cjs/identity/index.js +3 -1
- package/dist/cjs/identity/seedIdentity.js +16 -0
- package/dist/cjs/index.js +4 -2
- package/dist/cjs/wallet/delegator.js +10 -4
- package/dist/cjs/wallet/hdDescriptorProvider.js +29 -0
- package/dist/cjs/wallet/inputSignerRouter.js +98 -0
- package/dist/cjs/wallet/serviceWorker/wallet.js +1 -0
- package/dist/cjs/wallet/signingErrors.js +32 -0
- package/dist/cjs/wallet/unroll.js +5 -1
- package/dist/cjs/wallet/wallet.js +232 -86
- package/dist/cjs/wallet/walletReceiveRotator.js +547 -0
- package/dist/cjs/worker/messageBus.js +1 -0
- package/dist/esm/contracts/contractManager.js +31 -11
- package/dist/esm/contracts/contractWatcher.js +2 -2
- package/dist/esm/identity/hdCapableIdentity.js +17 -1
- package/dist/esm/identity/index.js +1 -0
- package/dist/esm/identity/seedIdentity.js +16 -0
- package/dist/esm/index.js +2 -2
- package/dist/esm/wallet/delegator.js +10 -4
- package/dist/esm/wallet/hdDescriptorProvider.js +29 -0
- package/dist/esm/wallet/inputSignerRouter.js +94 -0
- package/dist/esm/wallet/serviceWorker/wallet.js +1 -0
- package/dist/esm/wallet/signingErrors.js +27 -0
- package/dist/esm/wallet/unroll.js +5 -1
- package/dist/esm/wallet/wallet.js +231 -86
- package/dist/esm/wallet/walletReceiveRotator.js +540 -0
- package/dist/esm/worker/messageBus.js +1 -0
- package/dist/types/contracts/contractManager.d.ts +33 -3
- package/dist/types/contracts/types.d.ts +19 -2
- package/dist/types/identity/descriptorProvider.d.ts +7 -0
- package/dist/types/identity/hdCapableIdentity.d.ts +30 -3
- package/dist/types/identity/index.d.ts +1 -0
- package/dist/types/identity/seedIdentity.d.ts +16 -0
- package/dist/types/index.d.ts +6 -6
- package/dist/types/wallet/hdDescriptorProvider.d.ts +22 -1
- package/dist/types/wallet/index.d.ts +34 -0
- package/dist/types/wallet/inputSignerRouter.d.ts +35 -0
- package/dist/types/wallet/serviceWorker/wallet.d.ts +10 -0
- package/dist/types/wallet/signingErrors.d.ts +19 -0
- package/dist/types/wallet/wallet.d.ts +51 -2
- package/dist/types/wallet/walletReceiveRotator.d.ts +306 -0
- package/dist/types/worker/messageBus.d.ts +1 -0
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.Wallet = exports.ReadonlyWallet = exports.getArkadeServerUrl = void 0;
|
|
3
|
+
exports.Wallet = exports.ReadonlyWallet = exports.MissingSigningDescriptorError = exports.DescriptorSigningProviderMissingError = exports.getArkadeServerUrl = void 0;
|
|
4
4
|
exports.selectVirtualCoins = selectVirtualCoins;
|
|
5
5
|
exports.waitForIncomingFunds = waitForIncomingFunds;
|
|
6
6
|
const base_1 = require("@scure/base");
|
|
@@ -15,7 +15,6 @@ const ark_1 = require("../providers/ark");
|
|
|
15
15
|
const forfeit_1 = require("../forfeit");
|
|
16
16
|
const validation_1 = require("../tree/validation");
|
|
17
17
|
const validation_2 = require("./validation");
|
|
18
|
-
const identity_1 = require("../identity");
|
|
19
18
|
const _1 = require(".");
|
|
20
19
|
const asset_1 = require("./asset");
|
|
21
20
|
const base_2 = require("../script/base");
|
|
@@ -40,8 +39,35 @@ const handlers_1 = require("../contracts/handlers");
|
|
|
40
39
|
const timelock_1 = require("../utils/timelock");
|
|
41
40
|
const syncCursors_1 = require("../utils/syncCursors");
|
|
42
41
|
const vtxoOwnership_1 = require("../contracts/vtxoOwnership");
|
|
42
|
+
const walletReceiveRotator_1 = require("./walletReceiveRotator");
|
|
43
|
+
const inputSignerRouter_1 = require("./inputSignerRouter");
|
|
44
|
+
const signingErrors_1 = require("./signingErrors");
|
|
45
|
+
Object.defineProperty(exports, "DescriptorSigningProviderMissingError", { enumerable: true, get: function () { return signingErrors_1.DescriptorSigningProviderMissingError; } });
|
|
46
|
+
Object.defineProperty(exports, "MissingSigningDescriptorError", { enumerable: true, get: function () { return signingErrors_1.MissingSigningDescriptorError; } });
|
|
43
47
|
const getArkadeServerUrl = ({ arkServerUrl, }) => arkServerUrl || _1.DEFAULT_ARKADE_SERVER_URL;
|
|
44
48
|
exports.getArkadeServerUrl = getArkadeServerUrl;
|
|
49
|
+
// Build per-input jobs for an intent proof. Index 0 of the proof is a
|
|
50
|
+
// synthetic BIP-322 toSpend reference whose witnessUtxo.script mirrors
|
|
51
|
+
// coin[0]'s pkScript, so we map it to the same source contract as
|
|
52
|
+
// coin[0]; coins 0..N-1 then map to proof inputs 1..N.
|
|
53
|
+
function intentProofJobs(coins) {
|
|
54
|
+
if (coins.length === 0)
|
|
55
|
+
return [];
|
|
56
|
+
const coinJobs = coins.map((coin, i) => ({
|
|
57
|
+
index: i + 1,
|
|
58
|
+
lookupScript: base_2.VtxoScript.decode(coin.tapTree).pkScript,
|
|
59
|
+
}));
|
|
60
|
+
return [{ index: 0, lookupScript: coinJobs[0].lookupScript }, ...coinJobs];
|
|
61
|
+
}
|
|
62
|
+
// Built-in ArkProvider implementations (Rest/Expo) expose `serverUrl`,
|
|
63
|
+
// but the interface itself does not declare a URL accessor — so this is a
|
|
64
|
+
// structural read that returns undefined for custom implementations.
|
|
65
|
+
function extractArkProviderUrl(provider) {
|
|
66
|
+
const serverUrl = provider.serverUrl;
|
|
67
|
+
return typeof serverUrl === "string" && serverUrl.length > 0
|
|
68
|
+
? serverUrl
|
|
69
|
+
: undefined;
|
|
70
|
+
}
|
|
45
71
|
// Historical unilateral exit delay for mainnet (~7 days in seconds).
|
|
46
72
|
// Kept so existing wallets can still discover and spend VTXOs sent to the
|
|
47
73
|
// legacy address after arkd starts advertising a different delay.
|
|
@@ -83,7 +109,6 @@ class ReadonlyWallet {
|
|
|
83
109
|
this.onchainProvider = onchainProvider;
|
|
84
110
|
this.indexerProvider = indexerProvider;
|
|
85
111
|
this.arkServerPublicKey = arkServerPublicKey;
|
|
86
|
-
this.offchainTapscript = offchainTapscript;
|
|
87
112
|
this.boardingTapscript = boardingTapscript;
|
|
88
113
|
this.dustAmount = dustAmount;
|
|
89
114
|
this.walletRepository = walletRepository;
|
|
@@ -108,6 +133,7 @@ class ReadonlyWallet {
|
|
|
108
133
|
`Create identity with { isMainnet: ${serverIsMainnet} } to match.`);
|
|
109
134
|
}
|
|
110
135
|
}
|
|
136
|
+
this._offchainTapscript = offchainTapscript;
|
|
111
137
|
this.watcherConfig = watcherConfig;
|
|
112
138
|
this._assetManager = new asset_manager_1.ReadonlyAssetManager(this.indexerProvider);
|
|
113
139
|
// Defensive for direct-construction callers; setupWalletConfig already
|
|
@@ -120,25 +146,45 @@ class ReadonlyWallet {
|
|
|
120
146
|
default_1.DefaultVtxo.Script.DEFAULT_TIMELOCK,
|
|
121
147
|
];
|
|
122
148
|
}
|
|
149
|
+
/**
|
|
150
|
+
* Currently-active receive tapscript. Read-only from the outside;
|
|
151
|
+
* mutated only via {@link Wallet.setOffchainTapscriptForRotation}
|
|
152
|
+
* by {@link WalletReceiveRotator.rotate}.
|
|
153
|
+
*/
|
|
154
|
+
get offchainTapscript() {
|
|
155
|
+
return this._offchainTapscript;
|
|
156
|
+
}
|
|
123
157
|
/**
|
|
124
158
|
* Protected helper to set up shared wallet configuration.
|
|
125
159
|
* Extracts common logic used by both ReadonlyWallet.create() and Wallet.create().
|
|
126
160
|
*/
|
|
127
161
|
static async setupWalletConfig(config, pubKey) {
|
|
162
|
+
const arkadeServerUrl = (0, exports.getArkadeServerUrl)(config);
|
|
128
163
|
// Use provided arkProvider instance or create a new one from arkServerUrl
|
|
129
|
-
const arkProvider = config.arkProvider
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
//
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
164
|
+
const arkProvider = config.arkProvider ?? new ark_1.RestArkProvider(arkadeServerUrl);
|
|
165
|
+
// Resolve the indexer provider. If a full instance is supplied, use it
|
|
166
|
+
// directly. Otherwise pick a URL with priority:
|
|
167
|
+
// 1. explicit config.indexerUrl
|
|
168
|
+
// 2. URL derived from the injected arkProvider (so a custom
|
|
169
|
+
// arkProvider does not silently pair with the public default)
|
|
170
|
+
// 3. arkadeServerUrl (only when no custom arkProvider was injected)
|
|
171
|
+
let indexerProvider = config.indexerProvider;
|
|
172
|
+
if (!indexerProvider) {
|
|
173
|
+
let indexerUrl = config.indexerUrl;
|
|
174
|
+
if (!indexerUrl) {
|
|
175
|
+
if (config.arkProvider) {
|
|
176
|
+
const derived = extractArkProviderUrl(config.arkProvider);
|
|
177
|
+
if (!derived) {
|
|
178
|
+
throw new Error("indexerUrl is required when arkProvider is provided without a discoverable serverUrl");
|
|
179
|
+
}
|
|
180
|
+
indexerUrl = derived;
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
indexerUrl = arkadeServerUrl;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
indexerProvider = new indexer_1.RestIndexerProvider(indexerUrl);
|
|
187
|
+
}
|
|
142
188
|
const info = await arkProvider.getInfo();
|
|
143
189
|
const network = (0, networks_1.getNetwork)(info.network);
|
|
144
190
|
// Guard: detect identity/server network mismatch for seed-based identities.
|
|
@@ -648,13 +694,28 @@ class ReadonlyWallet {
|
|
|
648
694
|
walletRepository: this.walletRepository,
|
|
649
695
|
watcherConfig: this.watcherConfig,
|
|
650
696
|
});
|
|
697
|
+
// Register the wallet's baseline always-active contracts: every
|
|
698
|
+
// `walletContractTimelocks` entry × {default, delegate-if-enabled}.
|
|
699
|
+
// This matrix is bound to INDEX 0 — the identity's x-only pubkey
|
|
700
|
+
// — by design: it's the permanent fallback set the wallet wants
|
|
701
|
+
// active forever, independent of any HD rotation. Rotated
|
|
702
|
+
// display contracts (registered separately by
|
|
703
|
+
// {@link WalletReceiveRotator.rotate}) are intentionally
|
|
704
|
+
// single-timelock-single-pubkey at the CURRENT arkd delay, and
|
|
705
|
+
// get the `metadata.source = WALLET_RECEIVE_SOURCE` tag so the
|
|
706
|
+
// next boot can find them. We deliberately do NOT re-register
|
|
707
|
+
// the matrix at a rotated pubkey: doing so would dilute the
|
|
708
|
+
// "index-0 baseline" guarantee and turn every rotation into a
|
|
709
|
+
// multi-timelock matrix expansion on every boot.
|
|
710
|
+
const baselinePubkey = await this.identity.xOnlyPublicKey();
|
|
651
711
|
for (const csvTimelock of this.walletContractTimelocks) {
|
|
652
712
|
const csvTimelockStr = (0, timelock_1.timelockToSequence)(csvTimelock).toString();
|
|
653
713
|
const defaultScript = new default_1.DefaultVtxo.Script({
|
|
654
|
-
pubKey:
|
|
714
|
+
pubKey: baselinePubkey,
|
|
655
715
|
serverPubKey: this.offchainTapscript.options.serverPubKey,
|
|
656
716
|
csvTimelock,
|
|
657
717
|
});
|
|
718
|
+
const defaultScriptHex = base_1.hex.encode(defaultScript.pkScript);
|
|
658
719
|
await manager.createContract({
|
|
659
720
|
type: "default",
|
|
660
721
|
params: {
|
|
@@ -662,7 +723,7 @@ class ReadonlyWallet {
|
|
|
662
723
|
serverPubKey: base_1.hex.encode(defaultScript.options.serverPubKey),
|
|
663
724
|
csvTimelock: csvTimelockStr,
|
|
664
725
|
},
|
|
665
|
-
script:
|
|
726
|
+
script: defaultScriptHex,
|
|
666
727
|
address: defaultScript
|
|
667
728
|
.address(this.network.hrp, this.arkServerPublicKey)
|
|
668
729
|
.encode(),
|
|
@@ -670,11 +731,12 @@ class ReadonlyWallet {
|
|
|
670
731
|
});
|
|
671
732
|
if (this.offchainTapscript instanceof delegate_1.DelegateVtxo.Script) {
|
|
672
733
|
const delegateScript = new delegate_1.DelegateVtxo.Script({
|
|
673
|
-
pubKey:
|
|
734
|
+
pubKey: baselinePubkey,
|
|
674
735
|
serverPubKey: this.offchainTapscript.options.serverPubKey,
|
|
675
736
|
delegatePubKey: this.offchainTapscript.options.delegatePubKey,
|
|
676
737
|
csvTimelock,
|
|
677
738
|
});
|
|
739
|
+
const delegateScriptHex = base_1.hex.encode(delegateScript.pkScript);
|
|
678
740
|
await manager.createContract({
|
|
679
741
|
type: "delegate",
|
|
680
742
|
params: {
|
|
@@ -683,7 +745,7 @@ class ReadonlyWallet {
|
|
|
683
745
|
delegatePubKey: base_1.hex.encode(delegateScript.options.delegatePubKey),
|
|
684
746
|
csvTimelock: csvTimelockStr,
|
|
685
747
|
},
|
|
686
|
-
script:
|
|
748
|
+
script: delegateScriptHex,
|
|
687
749
|
address: delegateScript
|
|
688
750
|
.address(this.network.hrp, this.arkServerPublicKey)
|
|
689
751
|
.encode(),
|
|
@@ -743,6 +805,15 @@ exports.ReadonlyWallet = ReadonlyWallet;
|
|
|
743
805
|
* ```
|
|
744
806
|
*/
|
|
745
807
|
class Wallet extends ReadonlyWallet {
|
|
808
|
+
/**
|
|
809
|
+
* @internal Sole write path for `offchainTapscript` after construction.
|
|
810
|
+
* Called by {@link WalletReceiveRotator.rotate} once the rotated
|
|
811
|
+
* display contract has been persisted. External code must treat
|
|
812
|
+
* `offchainTapscript` as read-only.
|
|
813
|
+
*/
|
|
814
|
+
setOffchainTapscriptForRotation(tapscript) {
|
|
815
|
+
this._offchainTapscript = tapscript;
|
|
816
|
+
}
|
|
746
817
|
_addPendingSpends(inputs) {
|
|
747
818
|
for (const input of inputs) {
|
|
748
819
|
if ("virtualStatus" in input) {
|
|
@@ -773,12 +844,13 @@ class Wallet extends ReadonlyWallet {
|
|
|
773
844
|
}
|
|
774
845
|
constructor(identity, network, onchainProvider, arkProvider, indexerProvider, arkServerPublicKey, offchainTapscript, boardingTapscript, serverUnrollScript, forfeitOutputScript, forfeitPubkey, dustAmount, walletRepository, contractRepository,
|
|
775
846
|
/** @deprecated Use settlementConfig */
|
|
776
|
-
renewalConfig, delegatorProvider, watcherConfig, settlementConfig, walletContractTimelocks) {
|
|
847
|
+
renewalConfig, delegatorProvider, watcherConfig, settlementConfig, walletContractTimelocks, receiveRotator, descriptorProvider) {
|
|
777
848
|
super(identity, network, onchainProvider, indexerProvider, arkServerPublicKey, offchainTapscript, boardingTapscript, dustAmount, walletRepository, contractRepository, delegatorProvider, watcherConfig, walletContractTimelocks);
|
|
778
849
|
this.arkProvider = arkProvider;
|
|
779
850
|
this.serverUnrollScript = serverUnrollScript;
|
|
780
851
|
this.forfeitOutputScript = forfeitOutputScript;
|
|
781
852
|
this.forfeitPubkey = forfeitPubkey;
|
|
853
|
+
this._receiveRotatorInstalled = false;
|
|
782
854
|
/**
|
|
783
855
|
* Async mutex that serializes all operations submitting VTXOs to the Arkade
|
|
784
856
|
* server (`settle`, `send`, `sendBitcoin`). This prevents VtxoManager's
|
|
@@ -815,6 +887,14 @@ class Wallet extends ReadonlyWallet {
|
|
|
815
887
|
this._delegatorManager = delegatorProvider
|
|
816
888
|
? new delegator_1.DelegatorManagerImpl(delegatorProvider, arkProvider, identity)
|
|
817
889
|
: undefined;
|
|
890
|
+
this._receiveRotator = receiveRotator;
|
|
891
|
+
this._descriptorProvider = descriptorProvider;
|
|
892
|
+
this._signerRouter = new inputSignerRouter_1.InputSignerRouter({
|
|
893
|
+
identity,
|
|
894
|
+
contractRepository,
|
|
895
|
+
descriptorProvider,
|
|
896
|
+
boardingPkScript: boardingTapscript.pkScript,
|
|
897
|
+
});
|
|
818
898
|
}
|
|
819
899
|
get assetManager() {
|
|
820
900
|
this._walletAssetManager ?? (this._walletAssetManager = new asset_manager_1.AssetManager(this));
|
|
@@ -830,18 +910,48 @@ class Wallet extends ReadonlyWallet {
|
|
|
830
910
|
this._vtxoManagerInitializing = Promise.resolve(new vtxo_manager_1.VtxoManager(this, this.renewalConfig, this.settlementConfig));
|
|
831
911
|
try {
|
|
832
912
|
const manager = await this._vtxoManagerInitializing;
|
|
913
|
+
// First-time hookup of the HD rotator: subscribe to
|
|
914
|
+
// `vtxo_received` AFTER the contract manager (which is
|
|
915
|
+
// initialised inside the VtxoManager construction path) has
|
|
916
|
+
// registered the wallet's baseline contracts. The flag
|
|
917
|
+
// makes this idempotent across repeated `getVtxoManager`
|
|
918
|
+
// calls — install runs at most once per wallet instance.
|
|
919
|
+
// Cache the manager and flip the install flag only after
|
|
920
|
+
// `install()` resolves; otherwise a failing install would
|
|
921
|
+
// leave the manager cached and silently disable HD
|
|
922
|
+
// rotation for the lifetime of this wallet.
|
|
923
|
+
if (this._receiveRotator && !this._receiveRotatorInstalled) {
|
|
924
|
+
try {
|
|
925
|
+
await this._receiveRotator.install(this);
|
|
926
|
+
}
|
|
927
|
+
catch (installErr) {
|
|
928
|
+
await manager.dispose();
|
|
929
|
+
throw installErr;
|
|
930
|
+
}
|
|
931
|
+
this._receiveRotatorInstalled = true;
|
|
932
|
+
}
|
|
833
933
|
this._vtxoManager = manager;
|
|
834
934
|
return manager;
|
|
835
935
|
}
|
|
836
|
-
catch (error) {
|
|
837
|
-
this._vtxoManagerInitializing = undefined;
|
|
838
|
-
throw error;
|
|
839
|
-
}
|
|
840
936
|
finally {
|
|
841
937
|
this._vtxoManagerInitializing = undefined;
|
|
842
938
|
}
|
|
843
939
|
}
|
|
844
940
|
async dispose() {
|
|
941
|
+
// Tear down the rotation subscription + drain in-flight rotations
|
|
942
|
+
// first so no late `vtxo_received` event can queue work on a
|
|
943
|
+
// disposing wallet, and so any in-flight `createContract` call
|
|
944
|
+
// finishes before we dispose the contract manager underneath it.
|
|
945
|
+
// A rotator-disposal failure must not abort the rest of
|
|
946
|
+
// teardown — the contract manager / super still need to run on
|
|
947
|
+
// best-effort, so we capture and rethrow at the end.
|
|
948
|
+
let rotatorError;
|
|
949
|
+
try {
|
|
950
|
+
await this._receiveRotator?.dispose();
|
|
951
|
+
}
|
|
952
|
+
catch (error) {
|
|
953
|
+
rotatorError = error;
|
|
954
|
+
}
|
|
845
955
|
const manager = this._vtxoManager ??
|
|
846
956
|
(this._vtxoManagerInitializing
|
|
847
957
|
? await this._vtxoManagerInitializing.catch(() => undefined)
|
|
@@ -859,6 +969,9 @@ class Wallet extends ReadonlyWallet {
|
|
|
859
969
|
this._vtxoManagerInitializing = undefined;
|
|
860
970
|
await super.dispose();
|
|
861
971
|
}
|
|
972
|
+
if (rotatorError) {
|
|
973
|
+
throw rotatorError;
|
|
974
|
+
}
|
|
862
975
|
}
|
|
863
976
|
/**
|
|
864
977
|
* Create a full wallet and initialize its background managers.
|
|
@@ -894,7 +1007,14 @@ class Wallet extends ReadonlyWallet {
|
|
|
894
1007
|
const forfeitPubkey = base_1.hex.decode(setup.info.forfeitPubkey).slice(1);
|
|
895
1008
|
const forfeitAddress = (0, btc_signer_1.Address)(setup.network).decode(setup.info.forfeitAddress);
|
|
896
1009
|
const forfeitOutputScript = btc_signer_1.OutScript.encode(forfeitAddress);
|
|
897
|
-
|
|
1010
|
+
// HD wiring (boot path) — resolved via the descriptor provider.
|
|
1011
|
+
// The rotator (when present) is handed to the constructor as
|
|
1012
|
+
// the last positional arg and `getVtxoManager()` lazily
|
|
1013
|
+
// installs its `vtxo_received` subscription on first call,
|
|
1014
|
+
// after the contract manager has registered the wallet's
|
|
1015
|
+
// baseline contracts.
|
|
1016
|
+
const boot = await walletReceiveRotator_1.WalletReceiveRotator.resolveBoot(config, setup);
|
|
1017
|
+
const wallet = new Wallet(config.identity, setup.network, setup.onchainProvider, setup.arkProvider, setup.indexerProvider, setup.serverPubKey, boot?.offchainTapscript ?? setup.offchainTapscript, setup.boardingTapscript, serverUnrollScript, forfeitOutputScript, forfeitPubkey, setup.dustAmount, setup.walletRepository, setup.contractRepository, config.renewalConfig, config.delegatorProvider, config.watcherConfig, config.settlementConfig, setup.walletContractTimelocks, boot?.rotator, boot?.provider);
|
|
898
1018
|
await wallet.getVtxoManager();
|
|
899
1019
|
return wallet;
|
|
900
1020
|
}
|
|
@@ -941,6 +1061,14 @@ class Wallet extends ReadonlyWallet {
|
|
|
941
1061
|
}
|
|
942
1062
|
if (params.selectedVtxos && params.selectedVtxos.length > 0) {
|
|
943
1063
|
return this._withTxLock(async () => {
|
|
1064
|
+
// Snapshot the active receive tapscript synchronously
|
|
1065
|
+
// before any `await` so the change output's pkScript and
|
|
1066
|
+
// the change-VTXO metadata written later by
|
|
1067
|
+
// `updateDbAfterOffchainTx` are bound to the same
|
|
1068
|
+
// tapscript even if `WalletReceiveRotator.rotate` fires
|
|
1069
|
+
// during the offchain round-trip.
|
|
1070
|
+
const offchainTapscript = this.offchainTapscript;
|
|
1071
|
+
const arkAddress = offchainTapscript.address(this.network.hrp, this.arkServerPublicKey);
|
|
944
1072
|
const selectedVtxoSum = params
|
|
945
1073
|
.selectedVtxos.map((v) => v.value)
|
|
946
1074
|
.reduce((a, b) => a + b, 0);
|
|
@@ -965,8 +1093,8 @@ class Wallet extends ReadonlyWallet {
|
|
|
965
1093
|
// add change output if needed
|
|
966
1094
|
if (selected.changeAmount > 0n) {
|
|
967
1095
|
const changeOutputScript = selected.changeAmount < this.dustAmount
|
|
968
|
-
?
|
|
969
|
-
:
|
|
1096
|
+
? arkAddress.subdustPkScript
|
|
1097
|
+
: arkAddress.pkScript;
|
|
970
1098
|
outputs.push({
|
|
971
1099
|
script: changeOutputScript,
|
|
972
1100
|
amount: BigInt(selected.changeAmount),
|
|
@@ -975,7 +1103,7 @@ class Wallet extends ReadonlyWallet {
|
|
|
975
1103
|
this._addPendingSpends(selected.inputs);
|
|
976
1104
|
try {
|
|
977
1105
|
const { arkTxid, signedCheckpointTxs } = await this.buildAndSubmitOffchainTx(selected.inputs, outputs);
|
|
978
|
-
await this.updateDbAfterOffchainTx(selected.inputs, arkTxid, signedCheckpointTxs, params.amount, selected.changeAmount, selected.changeAmount > 0n ? outputs.length - 1 : 0);
|
|
1106
|
+
await this.updateDbAfterOffchainTx(selected.inputs, arkTxid, signedCheckpointTxs, params.amount, selected.changeAmount, selected.changeAmount > 0n ? outputs.length - 1 : 0, offchainTapscript);
|
|
979
1107
|
return arkTxid;
|
|
980
1108
|
}
|
|
981
1109
|
finally {
|
|
@@ -1245,9 +1373,11 @@ class Wallet extends ReadonlyWallet {
|
|
|
1245
1373
|
settlementPsbt.updateInput(i, {
|
|
1246
1374
|
tapLeafScript: [input.forfeitTapLeafScript],
|
|
1247
1375
|
});
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1376
|
+
const script = settlementPsbt.getInput(i).witnessUtxo?.script;
|
|
1377
|
+
if (!script) {
|
|
1378
|
+
throw new Error("The server returned incomplete data. Settlement input is missing witnessUtxo.script");
|
|
1379
|
+
}
|
|
1380
|
+
settlementPsbt = await this._signerRouter.sign(settlementPsbt, [{ index: i, lookupScript: script }]);
|
|
1251
1381
|
hasBoardingUtxos = true;
|
|
1252
1382
|
break;
|
|
1253
1383
|
}
|
|
@@ -1296,7 +1426,12 @@ class Wallet extends ReadonlyWallet {
|
|
|
1296
1426
|
},
|
|
1297
1427
|
], forfeitOutputScript);
|
|
1298
1428
|
// do not sign the connector input
|
|
1299
|
-
forfeitTx = await this.
|
|
1429
|
+
forfeitTx = await this._signerRouter.sign(forfeitTx, [
|
|
1430
|
+
{
|
|
1431
|
+
index: 0,
|
|
1432
|
+
lookupScript: base_2.VtxoScript.decode(input.tapTree).pkScript,
|
|
1433
|
+
},
|
|
1434
|
+
]);
|
|
1300
1435
|
signedForfeits.push(base_1.base64.encode(forfeitTx.toPSBT()));
|
|
1301
1436
|
}
|
|
1302
1437
|
if (signedForfeits.length > 0 || hasBoardingUtxos) {
|
|
@@ -1399,6 +1534,22 @@ class Wallet extends ReadonlyWallet {
|
|
|
1399
1534
|
},
|
|
1400
1535
|
};
|
|
1401
1536
|
}
|
|
1537
|
+
/**
|
|
1538
|
+
* Build {@link InputSigningJob}s for a tx whose signable inputs can be
|
|
1539
|
+
* resolved from their own `witnessUtxo.script`. Inputs without a
|
|
1540
|
+
* `witnessUtxo` are silently omitted, mirroring the wallet's
|
|
1541
|
+
* historical silent-skip behaviour for cosigner/connector inputs.
|
|
1542
|
+
*/
|
|
1543
|
+
inputSigningJobsFromWitnessUtxos(tx, indexes) {
|
|
1544
|
+
const candidateIndexes = indexes ?? Array.from({ length: tx.inputsLength }, (_, i) => i);
|
|
1545
|
+
const jobs = [];
|
|
1546
|
+
for (const index of candidateIndexes) {
|
|
1547
|
+
const script = tx.getInput(index).witnessUtxo?.script;
|
|
1548
|
+
if (script)
|
|
1549
|
+
jobs.push({ index, lookupScript: script });
|
|
1550
|
+
}
|
|
1551
|
+
return jobs;
|
|
1552
|
+
}
|
|
1402
1553
|
async safeRegisterIntent(intent, inputs) {
|
|
1403
1554
|
try {
|
|
1404
1555
|
return await this.arkProvider.registerIntent(intent);
|
|
@@ -1431,7 +1582,7 @@ class Wallet extends ReadonlyWallet {
|
|
|
1431
1582
|
cosigners_public_keys: cosignerPubKeys,
|
|
1432
1583
|
};
|
|
1433
1584
|
const proof = intent_1.Intent.create(message, coins, outputs);
|
|
1434
|
-
const signedProof = await this.
|
|
1585
|
+
const signedProof = await this._signerRouter.sign(proof, intentProofJobs(coins));
|
|
1435
1586
|
return {
|
|
1436
1587
|
proof: base_1.base64.encode(signedProof.toPSBT()),
|
|
1437
1588
|
message,
|
|
@@ -1443,7 +1594,7 @@ class Wallet extends ReadonlyWallet {
|
|
|
1443
1594
|
expire_at: 0,
|
|
1444
1595
|
};
|
|
1445
1596
|
const proof = intent_1.Intent.create(message, coins, []);
|
|
1446
|
-
const signedProof = await this.
|
|
1597
|
+
const signedProof = await this._signerRouter.sign(proof, intentProofJobs(coins));
|
|
1447
1598
|
return {
|
|
1448
1599
|
proof: base_1.base64.encode(signedProof.toPSBT()),
|
|
1449
1600
|
message,
|
|
@@ -1455,7 +1606,7 @@ class Wallet extends ReadonlyWallet {
|
|
|
1455
1606
|
expire_at: 0,
|
|
1456
1607
|
};
|
|
1457
1608
|
const proof = intent_1.Intent.create(message, coins, []);
|
|
1458
|
-
const signedProof = await this.
|
|
1609
|
+
const signedProof = await this._signerRouter.sign(proof, intentProofJobs(coins));
|
|
1459
1610
|
return {
|
|
1460
1611
|
proof: base_1.base64.encode(signedProof.toPSBT()),
|
|
1461
1612
|
message,
|
|
@@ -1520,7 +1671,7 @@ class Wallet extends ReadonlyWallet {
|
|
|
1520
1671
|
try {
|
|
1521
1672
|
const finalCheckpoints = await Promise.all(pendingTx.signedCheckpointTxs.map(async (c) => {
|
|
1522
1673
|
const tx = btc_signer_1.Transaction.fromPSBT(base_1.base64.decode(c));
|
|
1523
|
-
const signedCheckpoint = await this.
|
|
1674
|
+
const signedCheckpoint = await this._signerRouter.sign(tx, this.inputSigningJobsFromWitnessUtxos(tx));
|
|
1524
1675
|
return base_1.base64.encode(signedCheckpoint.toPSBT());
|
|
1525
1676
|
}));
|
|
1526
1677
|
await this.arkProvider.finalizeTx(pendingTx.arkTxid, finalCheckpoints);
|
|
@@ -1580,10 +1731,19 @@ class Wallet extends ReadonlyWallet {
|
|
|
1580
1731
|
if (args.length === 0) {
|
|
1581
1732
|
throw new Error("At least one receiver is required");
|
|
1582
1733
|
}
|
|
1734
|
+
// Snapshot the active receive tapscript synchronously before any
|
|
1735
|
+
// `await`. `WalletReceiveRotator.rotate` mutates
|
|
1736
|
+
// `this.offchainTapscript` without acquiring `_txLock`, so any
|
|
1737
|
+
// yield between here and `updateDbAfterOffchainTx` opens a window
|
|
1738
|
+
// where the change-output pkScript (built from `outputAddress`
|
|
1739
|
+
// below) and the change-VTXO metadata (built from the snapshot
|
|
1740
|
+
// inside `updateDbAfterOffchainTx`) could come from different
|
|
1741
|
+
// tapscripts. Threading the snapshot pins both reads.
|
|
1742
|
+
const offchainTapscript = this.offchainTapscript;
|
|
1743
|
+
const outputAddress = offchainTapscript.address(this.network.hrp, this.arkServerPublicKey);
|
|
1744
|
+
const address = outputAddress.encode();
|
|
1583
1745
|
// validate recipients and populate undefined amount with dust amount
|
|
1584
1746
|
const recipients = (0, utils_1.validateRecipients)(args, Number(this.dustAmount));
|
|
1585
|
-
const address = await this.getAddress();
|
|
1586
|
-
const outputAddress = address_1.ArkAddress.decode(address);
|
|
1587
1747
|
const virtualCoins = await this.getVtxos({
|
|
1588
1748
|
withRecoverable: false,
|
|
1589
1749
|
});
|
|
@@ -1714,7 +1874,7 @@ class Wallet extends ReadonlyWallet {
|
|
|
1714
1874
|
this._addPendingSpends(selectedCoins);
|
|
1715
1875
|
try {
|
|
1716
1876
|
const { arkTxid, signedCheckpointTxs } = await this.buildAndSubmitOffchainTx(selectedCoins, outputs);
|
|
1717
|
-
await this.updateDbAfterOffchainTx(selectedCoins, arkTxid, signedCheckpointTxs, sentAmount, BigInt(changeAmount), changeReceiver ? changeIndex : 0, changeReceiver?.assets);
|
|
1877
|
+
await this.updateDbAfterOffchainTx(selectedCoins, arkTxid, signedCheckpointTxs, sentAmount, BigInt(changeAmount), changeReceiver ? changeIndex : 0, offchainTapscript, changeReceiver?.assets);
|
|
1718
1878
|
return arkTxid;
|
|
1719
1879
|
}
|
|
1720
1880
|
finally {
|
|
@@ -1733,47 +1893,25 @@ class Wallet extends ReadonlyWallet {
|
|
|
1733
1893
|
tapLeafScript: input.forfeitTapLeafScript,
|
|
1734
1894
|
};
|
|
1735
1895
|
}), outputs, this.serverUnrollScript);
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
if (signed.length !== requests.length) {
|
|
1747
|
-
throw new Error(`signMultiple returned ${signed.length} transactions, expected ${requests.length}`);
|
|
1748
|
-
}
|
|
1749
|
-
const [firstSignedTx, ...signedCheckpoints] = signed;
|
|
1750
|
-
signedVirtualTx = firstSignedTx;
|
|
1751
|
-
userSignedCheckpoints = signedCheckpoints;
|
|
1752
|
-
}
|
|
1753
|
-
else {
|
|
1754
|
-
signedVirtualTx = await this.identity.sign(offchainTx.arkTx);
|
|
1755
|
-
}
|
|
1896
|
+
// arkTx inputs spend checkpoint outputs, so each input's
|
|
1897
|
+
// `witnessUtxo.script` is the checkpoint pkScript — not the
|
|
1898
|
+
// source VTXO contract's pkScript. Build the routing jobs from
|
|
1899
|
+
// the source VTXO scripts (positionally aligned to `inputs[i]`)
|
|
1900
|
+
// so the router can resolve each input's owning contract.
|
|
1901
|
+
const arkTxJobs = inputs.map((input, index) => ({
|
|
1902
|
+
index,
|
|
1903
|
+
lookupScript: base_2.VtxoScript.decode(input.tapTree).pkScript,
|
|
1904
|
+
}));
|
|
1905
|
+
const signedVirtualTx = await this._signerRouter.sign(offchainTx.arkTx, arkTxJobs);
|
|
1756
1906
|
// Mark pending before submitting — if we crash between submit and
|
|
1757
1907
|
// finalize, the next init will recover via finalizePendingTxs.
|
|
1758
1908
|
await this.setPendingTxFlag(true);
|
|
1759
1909
|
const { arkTxid, signedCheckpointTxs } = await this.arkProvider.submitTx(base_1.base64.encode(signedVirtualTx.toPSBT()), offchainTx.checkpoints.map((c) => base_1.base64.encode(c.toPSBT())));
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
(0, arkTransaction_1.combineTapscriptSigs)(userSignedCheckpoints[i], serverSigned);
|
|
1766
|
-
return base_1.base64.encode(serverSigned.toPSBT());
|
|
1767
|
-
});
|
|
1768
|
-
}
|
|
1769
|
-
else {
|
|
1770
|
-
// Legacy: sign each checkpoint individually (N popups)
|
|
1771
|
-
finalCheckpoints = await Promise.all(signedCheckpointTxs.map(async (c) => {
|
|
1772
|
-
const tx = btc_signer_1.Transaction.fromPSBT(base_1.base64.decode(c));
|
|
1773
|
-
const signedCheckpoint = await this.identity.sign(tx);
|
|
1774
|
-
return base_1.base64.encode(signedCheckpoint.toPSBT());
|
|
1775
|
-
}));
|
|
1776
|
-
}
|
|
1910
|
+
const finalCheckpoints = await Promise.all(signedCheckpointTxs.map(async (c) => {
|
|
1911
|
+
const tx = btc_signer_1.Transaction.fromPSBT(base_1.base64.decode(c));
|
|
1912
|
+
const signedCheckpoint = await this._signerRouter.sign(tx, this.inputSigningJobsFromWitnessUtxos(tx));
|
|
1913
|
+
return base_1.base64.encode(signedCheckpoint.toPSBT());
|
|
1914
|
+
}));
|
|
1777
1915
|
await this.arkProvider.finalizeTx(arkTxid, finalCheckpoints);
|
|
1778
1916
|
try {
|
|
1779
1917
|
await this.setPendingTxFlag(false);
|
|
@@ -1783,8 +1921,17 @@ class Wallet extends ReadonlyWallet {
|
|
|
1783
1921
|
}
|
|
1784
1922
|
return { arkTxid, signedCheckpointTxs };
|
|
1785
1923
|
}
|
|
1786
|
-
// mark virtual outputs as spent, save change outputs if any
|
|
1787
|
-
|
|
1924
|
+
// mark virtual outputs as spent, save change outputs if any.
|
|
1925
|
+
// `offchainTapscript` is the snapshot the caller captured under
|
|
1926
|
+
// `_txLock` before any `await`; deriving both the change-VTXO
|
|
1927
|
+
// metadata and `primaryAddress` from it here guarantees the local
|
|
1928
|
+
// record matches the pkScript the server saw on the inbound
|
|
1929
|
+
// transaction, even if `WalletReceiveRotator.rotate` swaps
|
|
1930
|
+
// `this.offchainTapscript` mid-flight.
|
|
1931
|
+
async updateDbAfterOffchainTx(inputs, arkTxid, signedCheckpointTxs, sentAmount, changeAmount, changeVout, offchainTapscript, changeAssets) {
|
|
1932
|
+
const primaryAddress = offchainTapscript
|
|
1933
|
+
.address(this.network.hrp, this.arkServerPublicKey)
|
|
1934
|
+
.encode();
|
|
1788
1935
|
try {
|
|
1789
1936
|
const spentVtxos = [];
|
|
1790
1937
|
const commitmentTxIds = new Set();
|
|
@@ -1831,7 +1978,6 @@ class Wallet extends ReadonlyWallet {
|
|
|
1831
1978
|
}
|
|
1832
1979
|
}
|
|
1833
1980
|
const createdAt = Date.now();
|
|
1834
|
-
const primaryAddr = this.arkAddress.encode();
|
|
1835
1981
|
// Only save a change virtual output for preconfirmed coins (those with a batchExpiry).
|
|
1836
1982
|
// Inputs without a batchExpiry are already settled/unrolled and don't need tracking.
|
|
1837
1983
|
let changeVtxo;
|
|
@@ -1840,11 +1986,11 @@ class Wallet extends ReadonlyWallet {
|
|
|
1840
1986
|
txid: arkTxid,
|
|
1841
1987
|
vout: changeVout,
|
|
1842
1988
|
createdAt: new Date(createdAt),
|
|
1843
|
-
forfeitTapLeafScript:
|
|
1844
|
-
intentTapLeafScript:
|
|
1989
|
+
forfeitTapLeafScript: offchainTapscript.forfeit(),
|
|
1990
|
+
intentTapLeafScript: offchainTapscript.forfeit(),
|
|
1845
1991
|
isUnrolled: false,
|
|
1846
1992
|
isSpent: false,
|
|
1847
|
-
tapTree:
|
|
1993
|
+
tapTree: offchainTapscript.encode(),
|
|
1848
1994
|
value: Number(changeAmount),
|
|
1849
1995
|
virtualStatus: {
|
|
1850
1996
|
state: "preconfirmed",
|
|
@@ -1855,7 +2001,7 @@ class Wallet extends ReadonlyWallet {
|
|
|
1855
2001
|
confirmed: false,
|
|
1856
2002
|
},
|
|
1857
2003
|
assets: changeAssets,
|
|
1858
|
-
script: base_1.hex.encode(
|
|
2004
|
+
script: base_1.hex.encode(offchainTapscript.pkScript),
|
|
1859
2005
|
};
|
|
1860
2006
|
}
|
|
1861
2007
|
// Route spent rows to their owning contract bucket. The wallet's
|
|
@@ -1886,9 +2032,9 @@ class Wallet extends ReadonlyWallet {
|
|
|
1886
2032
|
}
|
|
1887
2033
|
// Change is always primary-script by construction.
|
|
1888
2034
|
if (changeVtxo) {
|
|
1889
|
-
await (0, vtxoOwnership_1.saveVtxosForContract)(this.walletRepository, { script: changeVtxo.script, address:
|
|
2035
|
+
await (0, vtxoOwnership_1.saveVtxosForContract)(this.walletRepository, { script: changeVtxo.script, address: primaryAddress }, [changeVtxo]);
|
|
1890
2036
|
}
|
|
1891
|
-
await this.walletRepository.saveTransactions(
|
|
2037
|
+
await this.walletRepository.saveTransactions(primaryAddress, [
|
|
1892
2038
|
{
|
|
1893
2039
|
key: {
|
|
1894
2040
|
boardingTxid: "",
|