@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
|
@@ -10,12 +10,11 @@ import { RestArkProvider, } from "../providers/ark.js";
|
|
|
10
10
|
import { buildForfeitTx } from "../forfeit.js";
|
|
11
11
|
import { validateConnectorsTxGraph, validateVtxoTxGraph, } from "../tree/validation.js";
|
|
12
12
|
import { validateBatchRecipients } from "./validation.js";
|
|
13
|
-
import { isBatchSignable } from "../identity/index.js";
|
|
14
13
|
import { DEFAULT_ARKADE_SERVER_URL, isExpired, isRecoverable, isSpendable, isSubdust, TxType, } from "./index.js";
|
|
15
14
|
import { createAssetPacket, selectCoinsWithAsset, selectedCoinsToAssetInputs, } from "./asset.js";
|
|
16
15
|
import { VtxoScript } from "../script/base.js";
|
|
17
16
|
import { CSVMultisigTapscript } from "../script/tapscript.js";
|
|
18
|
-
import { buildOffchainTx,
|
|
17
|
+
import { buildOffchainTx, hasBoardingTxExpired, isValidArkAddress, } from "../utils/arkTransaction.js";
|
|
19
18
|
import { DEFAULT_RENEWAL_CONFIG, DEFAULT_SETTLEMENT_CONFIG, VtxoManager, } from "./vtxo-manager.js";
|
|
20
19
|
import { ArkNote } from "../arknote/index.js";
|
|
21
20
|
import { Intent } from "../intent/index.js";
|
|
@@ -35,7 +34,32 @@ import { contractHandlers } from "../contracts/handlers/index.js";
|
|
|
35
34
|
import { timelockToSequence } from "../utils/timelock.js";
|
|
36
35
|
import { clearSyncCursor, updateWalletState } from "../utils/syncCursors.js";
|
|
37
36
|
import { validateVtxosForScript, saveVtxosForContract, } from "../contracts/vtxoOwnership.js";
|
|
37
|
+
import { WalletReceiveRotator } from "./walletReceiveRotator.js";
|
|
38
|
+
import { InputSignerRouter } from "./inputSignerRouter.js";
|
|
39
|
+
import { DescriptorSigningProviderMissingError, MissingSigningDescriptorError, } from "./signingErrors.js";
|
|
38
40
|
export const getArkadeServerUrl = ({ arkServerUrl, }) => arkServerUrl || DEFAULT_ARKADE_SERVER_URL;
|
|
41
|
+
// Build per-input jobs for an intent proof. Index 0 of the proof is a
|
|
42
|
+
// synthetic BIP-322 toSpend reference whose witnessUtxo.script mirrors
|
|
43
|
+
// coin[0]'s pkScript, so we map it to the same source contract as
|
|
44
|
+
// coin[0]; coins 0..N-1 then map to proof inputs 1..N.
|
|
45
|
+
function intentProofJobs(coins) {
|
|
46
|
+
if (coins.length === 0)
|
|
47
|
+
return [];
|
|
48
|
+
const coinJobs = coins.map((coin, i) => ({
|
|
49
|
+
index: i + 1,
|
|
50
|
+
lookupScript: VtxoScript.decode(coin.tapTree).pkScript,
|
|
51
|
+
}));
|
|
52
|
+
return [{ index: 0, lookupScript: coinJobs[0].lookupScript }, ...coinJobs];
|
|
53
|
+
}
|
|
54
|
+
// Built-in ArkProvider implementations (Rest/Expo) expose `serverUrl`,
|
|
55
|
+
// but the interface itself does not declare a URL accessor — so this is a
|
|
56
|
+
// structural read that returns undefined for custom implementations.
|
|
57
|
+
function extractArkProviderUrl(provider) {
|
|
58
|
+
const serverUrl = provider.serverUrl;
|
|
59
|
+
return typeof serverUrl === "string" && serverUrl.length > 0
|
|
60
|
+
? serverUrl
|
|
61
|
+
: undefined;
|
|
62
|
+
}
|
|
39
63
|
// Historical unilateral exit delay for mainnet (~7 days in seconds).
|
|
40
64
|
// Kept so existing wallets can still discover and spend VTXOs sent to the
|
|
41
65
|
// legacy address after arkd starts advertising a different delay.
|
|
@@ -67,6 +91,7 @@ function hasToReadonly(identity) {
|
|
|
67
91
|
"toReadonly" in identity &&
|
|
68
92
|
typeof identity.toReadonly === "function");
|
|
69
93
|
}
|
|
94
|
+
export { DescriptorSigningProviderMissingError, MissingSigningDescriptorError };
|
|
70
95
|
export class ReadonlyWallet {
|
|
71
96
|
get assetManager() {
|
|
72
97
|
return this._assetManager;
|
|
@@ -77,7 +102,6 @@ export class ReadonlyWallet {
|
|
|
77
102
|
this.onchainProvider = onchainProvider;
|
|
78
103
|
this.indexerProvider = indexerProvider;
|
|
79
104
|
this.arkServerPublicKey = arkServerPublicKey;
|
|
80
|
-
this.offchainTapscript = offchainTapscript;
|
|
81
105
|
this.boardingTapscript = boardingTapscript;
|
|
82
106
|
this.dustAmount = dustAmount;
|
|
83
107
|
this.walletRepository = walletRepository;
|
|
@@ -102,6 +126,7 @@ export class ReadonlyWallet {
|
|
|
102
126
|
`Create identity with { isMainnet: ${serverIsMainnet} } to match.`);
|
|
103
127
|
}
|
|
104
128
|
}
|
|
129
|
+
this._offchainTapscript = offchainTapscript;
|
|
105
130
|
this.watcherConfig = watcherConfig;
|
|
106
131
|
this._assetManager = new ReadonlyAssetManager(this.indexerProvider);
|
|
107
132
|
// Defensive for direct-construction callers; setupWalletConfig already
|
|
@@ -114,25 +139,45 @@ export class ReadonlyWallet {
|
|
|
114
139
|
DefaultVtxo.Script.DEFAULT_TIMELOCK,
|
|
115
140
|
];
|
|
116
141
|
}
|
|
142
|
+
/**
|
|
143
|
+
* Currently-active receive tapscript. Read-only from the outside;
|
|
144
|
+
* mutated only via {@link Wallet.setOffchainTapscriptForRotation}
|
|
145
|
+
* by {@link WalletReceiveRotator.rotate}.
|
|
146
|
+
*/
|
|
147
|
+
get offchainTapscript() {
|
|
148
|
+
return this._offchainTapscript;
|
|
149
|
+
}
|
|
117
150
|
/**
|
|
118
151
|
* Protected helper to set up shared wallet configuration.
|
|
119
152
|
* Extracts common logic used by both ReadonlyWallet.create() and Wallet.create().
|
|
120
153
|
*/
|
|
121
154
|
static async setupWalletConfig(config, pubKey) {
|
|
155
|
+
const arkadeServerUrl = getArkadeServerUrl(config);
|
|
122
156
|
// Use provided arkProvider instance or create a new one from arkServerUrl
|
|
123
|
-
const arkProvider = config.arkProvider
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
//
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
157
|
+
const arkProvider = config.arkProvider ?? new RestArkProvider(arkadeServerUrl);
|
|
158
|
+
// Resolve the indexer provider. If a full instance is supplied, use it
|
|
159
|
+
// directly. Otherwise pick a URL with priority:
|
|
160
|
+
// 1. explicit config.indexerUrl
|
|
161
|
+
// 2. URL derived from the injected arkProvider (so a custom
|
|
162
|
+
// arkProvider does not silently pair with the public default)
|
|
163
|
+
// 3. arkadeServerUrl (only when no custom arkProvider was injected)
|
|
164
|
+
let indexerProvider = config.indexerProvider;
|
|
165
|
+
if (!indexerProvider) {
|
|
166
|
+
let indexerUrl = config.indexerUrl;
|
|
167
|
+
if (!indexerUrl) {
|
|
168
|
+
if (config.arkProvider) {
|
|
169
|
+
const derived = extractArkProviderUrl(config.arkProvider);
|
|
170
|
+
if (!derived) {
|
|
171
|
+
throw new Error("indexerUrl is required when arkProvider is provided without a discoverable serverUrl");
|
|
172
|
+
}
|
|
173
|
+
indexerUrl = derived;
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
indexerUrl = arkadeServerUrl;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
indexerProvider = new RestIndexerProvider(indexerUrl);
|
|
180
|
+
}
|
|
136
181
|
const info = await arkProvider.getInfo();
|
|
137
182
|
const network = getNetwork(info.network);
|
|
138
183
|
// Guard: detect identity/server network mismatch for seed-based identities.
|
|
@@ -642,13 +687,28 @@ export class ReadonlyWallet {
|
|
|
642
687
|
walletRepository: this.walletRepository,
|
|
643
688
|
watcherConfig: this.watcherConfig,
|
|
644
689
|
});
|
|
690
|
+
// Register the wallet's baseline always-active contracts: every
|
|
691
|
+
// `walletContractTimelocks` entry × {default, delegate-if-enabled}.
|
|
692
|
+
// This matrix is bound to INDEX 0 — the identity's x-only pubkey
|
|
693
|
+
// — by design: it's the permanent fallback set the wallet wants
|
|
694
|
+
// active forever, independent of any HD rotation. Rotated
|
|
695
|
+
// display contracts (registered separately by
|
|
696
|
+
// {@link WalletReceiveRotator.rotate}) are intentionally
|
|
697
|
+
// single-timelock-single-pubkey at the CURRENT arkd delay, and
|
|
698
|
+
// get the `metadata.source = WALLET_RECEIVE_SOURCE` tag so the
|
|
699
|
+
// next boot can find them. We deliberately do NOT re-register
|
|
700
|
+
// the matrix at a rotated pubkey: doing so would dilute the
|
|
701
|
+
// "index-0 baseline" guarantee and turn every rotation into a
|
|
702
|
+
// multi-timelock matrix expansion on every boot.
|
|
703
|
+
const baselinePubkey = await this.identity.xOnlyPublicKey();
|
|
645
704
|
for (const csvTimelock of this.walletContractTimelocks) {
|
|
646
705
|
const csvTimelockStr = timelockToSequence(csvTimelock).toString();
|
|
647
706
|
const defaultScript = new DefaultVtxo.Script({
|
|
648
|
-
pubKey:
|
|
707
|
+
pubKey: baselinePubkey,
|
|
649
708
|
serverPubKey: this.offchainTapscript.options.serverPubKey,
|
|
650
709
|
csvTimelock,
|
|
651
710
|
});
|
|
711
|
+
const defaultScriptHex = hex.encode(defaultScript.pkScript);
|
|
652
712
|
await manager.createContract({
|
|
653
713
|
type: "default",
|
|
654
714
|
params: {
|
|
@@ -656,7 +716,7 @@ export class ReadonlyWallet {
|
|
|
656
716
|
serverPubKey: hex.encode(defaultScript.options.serverPubKey),
|
|
657
717
|
csvTimelock: csvTimelockStr,
|
|
658
718
|
},
|
|
659
|
-
script:
|
|
719
|
+
script: defaultScriptHex,
|
|
660
720
|
address: defaultScript
|
|
661
721
|
.address(this.network.hrp, this.arkServerPublicKey)
|
|
662
722
|
.encode(),
|
|
@@ -664,11 +724,12 @@ export class ReadonlyWallet {
|
|
|
664
724
|
});
|
|
665
725
|
if (this.offchainTapscript instanceof DelegateVtxo.Script) {
|
|
666
726
|
const delegateScript = new DelegateVtxo.Script({
|
|
667
|
-
pubKey:
|
|
727
|
+
pubKey: baselinePubkey,
|
|
668
728
|
serverPubKey: this.offchainTapscript.options.serverPubKey,
|
|
669
729
|
delegatePubKey: this.offchainTapscript.options.delegatePubKey,
|
|
670
730
|
csvTimelock,
|
|
671
731
|
});
|
|
732
|
+
const delegateScriptHex = hex.encode(delegateScript.pkScript);
|
|
672
733
|
await manager.createContract({
|
|
673
734
|
type: "delegate",
|
|
674
735
|
params: {
|
|
@@ -677,7 +738,7 @@ export class ReadonlyWallet {
|
|
|
677
738
|
delegatePubKey: hex.encode(delegateScript.options.delegatePubKey),
|
|
678
739
|
csvTimelock: csvTimelockStr,
|
|
679
740
|
},
|
|
680
|
-
script:
|
|
741
|
+
script: delegateScriptHex,
|
|
681
742
|
address: delegateScript
|
|
682
743
|
.address(this.network.hrp, this.arkServerPublicKey)
|
|
683
744
|
.encode(),
|
|
@@ -736,6 +797,15 @@ export class ReadonlyWallet {
|
|
|
736
797
|
* ```
|
|
737
798
|
*/
|
|
738
799
|
export class Wallet extends ReadonlyWallet {
|
|
800
|
+
/**
|
|
801
|
+
* @internal Sole write path for `offchainTapscript` after construction.
|
|
802
|
+
* Called by {@link WalletReceiveRotator.rotate} once the rotated
|
|
803
|
+
* display contract has been persisted. External code must treat
|
|
804
|
+
* `offchainTapscript` as read-only.
|
|
805
|
+
*/
|
|
806
|
+
setOffchainTapscriptForRotation(tapscript) {
|
|
807
|
+
this._offchainTapscript = tapscript;
|
|
808
|
+
}
|
|
739
809
|
_addPendingSpends(inputs) {
|
|
740
810
|
for (const input of inputs) {
|
|
741
811
|
if ("virtualStatus" in input) {
|
|
@@ -766,12 +836,13 @@ export class Wallet extends ReadonlyWallet {
|
|
|
766
836
|
}
|
|
767
837
|
constructor(identity, network, onchainProvider, arkProvider, indexerProvider, arkServerPublicKey, offchainTapscript, boardingTapscript, serverUnrollScript, forfeitOutputScript, forfeitPubkey, dustAmount, walletRepository, contractRepository,
|
|
768
838
|
/** @deprecated Use settlementConfig */
|
|
769
|
-
renewalConfig, delegatorProvider, watcherConfig, settlementConfig, walletContractTimelocks) {
|
|
839
|
+
renewalConfig, delegatorProvider, watcherConfig, settlementConfig, walletContractTimelocks, receiveRotator, descriptorProvider) {
|
|
770
840
|
super(identity, network, onchainProvider, indexerProvider, arkServerPublicKey, offchainTapscript, boardingTapscript, dustAmount, walletRepository, contractRepository, delegatorProvider, watcherConfig, walletContractTimelocks);
|
|
771
841
|
this.arkProvider = arkProvider;
|
|
772
842
|
this.serverUnrollScript = serverUnrollScript;
|
|
773
843
|
this.forfeitOutputScript = forfeitOutputScript;
|
|
774
844
|
this.forfeitPubkey = forfeitPubkey;
|
|
845
|
+
this._receiveRotatorInstalled = false;
|
|
775
846
|
/**
|
|
776
847
|
* Async mutex that serializes all operations submitting VTXOs to the Arkade
|
|
777
848
|
* server (`settle`, `send`, `sendBitcoin`). This prevents VtxoManager's
|
|
@@ -808,6 +879,14 @@ export class Wallet extends ReadonlyWallet {
|
|
|
808
879
|
this._delegatorManager = delegatorProvider
|
|
809
880
|
? new DelegatorManagerImpl(delegatorProvider, arkProvider, identity)
|
|
810
881
|
: undefined;
|
|
882
|
+
this._receiveRotator = receiveRotator;
|
|
883
|
+
this._descriptorProvider = descriptorProvider;
|
|
884
|
+
this._signerRouter = new InputSignerRouter({
|
|
885
|
+
identity,
|
|
886
|
+
contractRepository,
|
|
887
|
+
descriptorProvider,
|
|
888
|
+
boardingPkScript: boardingTapscript.pkScript,
|
|
889
|
+
});
|
|
811
890
|
}
|
|
812
891
|
get assetManager() {
|
|
813
892
|
this._walletAssetManager ?? (this._walletAssetManager = new AssetManager(this));
|
|
@@ -823,18 +902,48 @@ export class Wallet extends ReadonlyWallet {
|
|
|
823
902
|
this._vtxoManagerInitializing = Promise.resolve(new VtxoManager(this, this.renewalConfig, this.settlementConfig));
|
|
824
903
|
try {
|
|
825
904
|
const manager = await this._vtxoManagerInitializing;
|
|
905
|
+
// First-time hookup of the HD rotator: subscribe to
|
|
906
|
+
// `vtxo_received` AFTER the contract manager (which is
|
|
907
|
+
// initialised inside the VtxoManager construction path) has
|
|
908
|
+
// registered the wallet's baseline contracts. The flag
|
|
909
|
+
// makes this idempotent across repeated `getVtxoManager`
|
|
910
|
+
// calls — install runs at most once per wallet instance.
|
|
911
|
+
// Cache the manager and flip the install flag only after
|
|
912
|
+
// `install()` resolves; otherwise a failing install would
|
|
913
|
+
// leave the manager cached and silently disable HD
|
|
914
|
+
// rotation for the lifetime of this wallet.
|
|
915
|
+
if (this._receiveRotator && !this._receiveRotatorInstalled) {
|
|
916
|
+
try {
|
|
917
|
+
await this._receiveRotator.install(this);
|
|
918
|
+
}
|
|
919
|
+
catch (installErr) {
|
|
920
|
+
await manager.dispose();
|
|
921
|
+
throw installErr;
|
|
922
|
+
}
|
|
923
|
+
this._receiveRotatorInstalled = true;
|
|
924
|
+
}
|
|
826
925
|
this._vtxoManager = manager;
|
|
827
926
|
return manager;
|
|
828
927
|
}
|
|
829
|
-
catch (error) {
|
|
830
|
-
this._vtxoManagerInitializing = undefined;
|
|
831
|
-
throw error;
|
|
832
|
-
}
|
|
833
928
|
finally {
|
|
834
929
|
this._vtxoManagerInitializing = undefined;
|
|
835
930
|
}
|
|
836
931
|
}
|
|
837
932
|
async dispose() {
|
|
933
|
+
// Tear down the rotation subscription + drain in-flight rotations
|
|
934
|
+
// first so no late `vtxo_received` event can queue work on a
|
|
935
|
+
// disposing wallet, and so any in-flight `createContract` call
|
|
936
|
+
// finishes before we dispose the contract manager underneath it.
|
|
937
|
+
// A rotator-disposal failure must not abort the rest of
|
|
938
|
+
// teardown — the contract manager / super still need to run on
|
|
939
|
+
// best-effort, so we capture and rethrow at the end.
|
|
940
|
+
let rotatorError;
|
|
941
|
+
try {
|
|
942
|
+
await this._receiveRotator?.dispose();
|
|
943
|
+
}
|
|
944
|
+
catch (error) {
|
|
945
|
+
rotatorError = error;
|
|
946
|
+
}
|
|
838
947
|
const manager = this._vtxoManager ??
|
|
839
948
|
(this._vtxoManagerInitializing
|
|
840
949
|
? await this._vtxoManagerInitializing.catch(() => undefined)
|
|
@@ -852,6 +961,9 @@ export class Wallet extends ReadonlyWallet {
|
|
|
852
961
|
this._vtxoManagerInitializing = undefined;
|
|
853
962
|
await super.dispose();
|
|
854
963
|
}
|
|
964
|
+
if (rotatorError) {
|
|
965
|
+
throw rotatorError;
|
|
966
|
+
}
|
|
855
967
|
}
|
|
856
968
|
/**
|
|
857
969
|
* Create a full wallet and initialize its background managers.
|
|
@@ -887,7 +999,14 @@ export class Wallet extends ReadonlyWallet {
|
|
|
887
999
|
const forfeitPubkey = hex.decode(setup.info.forfeitPubkey).slice(1);
|
|
888
1000
|
const forfeitAddress = Address(setup.network).decode(setup.info.forfeitAddress);
|
|
889
1001
|
const forfeitOutputScript = OutScript.encode(forfeitAddress);
|
|
890
|
-
|
|
1002
|
+
// HD wiring (boot path) — resolved via the descriptor provider.
|
|
1003
|
+
// The rotator (when present) is handed to the constructor as
|
|
1004
|
+
// the last positional arg and `getVtxoManager()` lazily
|
|
1005
|
+
// installs its `vtxo_received` subscription on first call,
|
|
1006
|
+
// after the contract manager has registered the wallet's
|
|
1007
|
+
// baseline contracts.
|
|
1008
|
+
const boot = await WalletReceiveRotator.resolveBoot(config, setup);
|
|
1009
|
+
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);
|
|
891
1010
|
await wallet.getVtxoManager();
|
|
892
1011
|
return wallet;
|
|
893
1012
|
}
|
|
@@ -934,6 +1053,14 @@ export class Wallet extends ReadonlyWallet {
|
|
|
934
1053
|
}
|
|
935
1054
|
if (params.selectedVtxos && params.selectedVtxos.length > 0) {
|
|
936
1055
|
return this._withTxLock(async () => {
|
|
1056
|
+
// Snapshot the active receive tapscript synchronously
|
|
1057
|
+
// before any `await` so the change output's pkScript and
|
|
1058
|
+
// the change-VTXO metadata written later by
|
|
1059
|
+
// `updateDbAfterOffchainTx` are bound to the same
|
|
1060
|
+
// tapscript even if `WalletReceiveRotator.rotate` fires
|
|
1061
|
+
// during the offchain round-trip.
|
|
1062
|
+
const offchainTapscript = this.offchainTapscript;
|
|
1063
|
+
const arkAddress = offchainTapscript.address(this.network.hrp, this.arkServerPublicKey);
|
|
937
1064
|
const selectedVtxoSum = params
|
|
938
1065
|
.selectedVtxos.map((v) => v.value)
|
|
939
1066
|
.reduce((a, b) => a + b, 0);
|
|
@@ -958,8 +1085,8 @@ export class Wallet extends ReadonlyWallet {
|
|
|
958
1085
|
// add change output if needed
|
|
959
1086
|
if (selected.changeAmount > 0n) {
|
|
960
1087
|
const changeOutputScript = selected.changeAmount < this.dustAmount
|
|
961
|
-
?
|
|
962
|
-
:
|
|
1088
|
+
? arkAddress.subdustPkScript
|
|
1089
|
+
: arkAddress.pkScript;
|
|
963
1090
|
outputs.push({
|
|
964
1091
|
script: changeOutputScript,
|
|
965
1092
|
amount: BigInt(selected.changeAmount),
|
|
@@ -968,7 +1095,7 @@ export class Wallet extends ReadonlyWallet {
|
|
|
968
1095
|
this._addPendingSpends(selected.inputs);
|
|
969
1096
|
try {
|
|
970
1097
|
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);
|
|
1098
|
+
await this.updateDbAfterOffchainTx(selected.inputs, arkTxid, signedCheckpointTxs, params.amount, selected.changeAmount, selected.changeAmount > 0n ? outputs.length - 1 : 0, offchainTapscript);
|
|
972
1099
|
return arkTxid;
|
|
973
1100
|
}
|
|
974
1101
|
finally {
|
|
@@ -1238,9 +1365,11 @@ export class Wallet extends ReadonlyWallet {
|
|
|
1238
1365
|
settlementPsbt.updateInput(i, {
|
|
1239
1366
|
tapLeafScript: [input.forfeitTapLeafScript],
|
|
1240
1367
|
});
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1368
|
+
const script = settlementPsbt.getInput(i).witnessUtxo?.script;
|
|
1369
|
+
if (!script) {
|
|
1370
|
+
throw new Error("The server returned incomplete data. Settlement input is missing witnessUtxo.script");
|
|
1371
|
+
}
|
|
1372
|
+
settlementPsbt = await this._signerRouter.sign(settlementPsbt, [{ index: i, lookupScript: script }]);
|
|
1244
1373
|
hasBoardingUtxos = true;
|
|
1245
1374
|
break;
|
|
1246
1375
|
}
|
|
@@ -1289,7 +1418,12 @@ export class Wallet extends ReadonlyWallet {
|
|
|
1289
1418
|
},
|
|
1290
1419
|
], forfeitOutputScript);
|
|
1291
1420
|
// do not sign the connector input
|
|
1292
|
-
forfeitTx = await this.
|
|
1421
|
+
forfeitTx = await this._signerRouter.sign(forfeitTx, [
|
|
1422
|
+
{
|
|
1423
|
+
index: 0,
|
|
1424
|
+
lookupScript: VtxoScript.decode(input.tapTree).pkScript,
|
|
1425
|
+
},
|
|
1426
|
+
]);
|
|
1293
1427
|
signedForfeits.push(base64.encode(forfeitTx.toPSBT()));
|
|
1294
1428
|
}
|
|
1295
1429
|
if (signedForfeits.length > 0 || hasBoardingUtxos) {
|
|
@@ -1392,6 +1526,22 @@ export class Wallet extends ReadonlyWallet {
|
|
|
1392
1526
|
},
|
|
1393
1527
|
};
|
|
1394
1528
|
}
|
|
1529
|
+
/**
|
|
1530
|
+
* Build {@link InputSigningJob}s for a tx whose signable inputs can be
|
|
1531
|
+
* resolved from their own `witnessUtxo.script`. Inputs without a
|
|
1532
|
+
* `witnessUtxo` are silently omitted, mirroring the wallet's
|
|
1533
|
+
* historical silent-skip behaviour for cosigner/connector inputs.
|
|
1534
|
+
*/
|
|
1535
|
+
inputSigningJobsFromWitnessUtxos(tx, indexes) {
|
|
1536
|
+
const candidateIndexes = indexes ?? Array.from({ length: tx.inputsLength }, (_, i) => i);
|
|
1537
|
+
const jobs = [];
|
|
1538
|
+
for (const index of candidateIndexes) {
|
|
1539
|
+
const script = tx.getInput(index).witnessUtxo?.script;
|
|
1540
|
+
if (script)
|
|
1541
|
+
jobs.push({ index, lookupScript: script });
|
|
1542
|
+
}
|
|
1543
|
+
return jobs;
|
|
1544
|
+
}
|
|
1395
1545
|
async safeRegisterIntent(intent, inputs) {
|
|
1396
1546
|
try {
|
|
1397
1547
|
return await this.arkProvider.registerIntent(intent);
|
|
@@ -1424,7 +1574,7 @@ export class Wallet extends ReadonlyWallet {
|
|
|
1424
1574
|
cosigners_public_keys: cosignerPubKeys,
|
|
1425
1575
|
};
|
|
1426
1576
|
const proof = Intent.create(message, coins, outputs);
|
|
1427
|
-
const signedProof = await this.
|
|
1577
|
+
const signedProof = await this._signerRouter.sign(proof, intentProofJobs(coins));
|
|
1428
1578
|
return {
|
|
1429
1579
|
proof: base64.encode(signedProof.toPSBT()),
|
|
1430
1580
|
message,
|
|
@@ -1436,7 +1586,7 @@ export class Wallet extends ReadonlyWallet {
|
|
|
1436
1586
|
expire_at: 0,
|
|
1437
1587
|
};
|
|
1438
1588
|
const proof = Intent.create(message, coins, []);
|
|
1439
|
-
const signedProof = await this.
|
|
1589
|
+
const signedProof = await this._signerRouter.sign(proof, intentProofJobs(coins));
|
|
1440
1590
|
return {
|
|
1441
1591
|
proof: base64.encode(signedProof.toPSBT()),
|
|
1442
1592
|
message,
|
|
@@ -1448,7 +1598,7 @@ export class Wallet extends ReadonlyWallet {
|
|
|
1448
1598
|
expire_at: 0,
|
|
1449
1599
|
};
|
|
1450
1600
|
const proof = Intent.create(message, coins, []);
|
|
1451
|
-
const signedProof = await this.
|
|
1601
|
+
const signedProof = await this._signerRouter.sign(proof, intentProofJobs(coins));
|
|
1452
1602
|
return {
|
|
1453
1603
|
proof: base64.encode(signedProof.toPSBT()),
|
|
1454
1604
|
message,
|
|
@@ -1513,7 +1663,7 @@ export class Wallet extends ReadonlyWallet {
|
|
|
1513
1663
|
try {
|
|
1514
1664
|
const finalCheckpoints = await Promise.all(pendingTx.signedCheckpointTxs.map(async (c) => {
|
|
1515
1665
|
const tx = Transaction.fromPSBT(base64.decode(c));
|
|
1516
|
-
const signedCheckpoint = await this.
|
|
1666
|
+
const signedCheckpoint = await this._signerRouter.sign(tx, this.inputSigningJobsFromWitnessUtxos(tx));
|
|
1517
1667
|
return base64.encode(signedCheckpoint.toPSBT());
|
|
1518
1668
|
}));
|
|
1519
1669
|
await this.arkProvider.finalizeTx(pendingTx.arkTxid, finalCheckpoints);
|
|
@@ -1573,10 +1723,19 @@ export class Wallet extends ReadonlyWallet {
|
|
|
1573
1723
|
if (args.length === 0) {
|
|
1574
1724
|
throw new Error("At least one receiver is required");
|
|
1575
1725
|
}
|
|
1726
|
+
// Snapshot the active receive tapscript synchronously before any
|
|
1727
|
+
// `await`. `WalletReceiveRotator.rotate` mutates
|
|
1728
|
+
// `this.offchainTapscript` without acquiring `_txLock`, so any
|
|
1729
|
+
// yield between here and `updateDbAfterOffchainTx` opens a window
|
|
1730
|
+
// where the change-output pkScript (built from `outputAddress`
|
|
1731
|
+
// below) and the change-VTXO metadata (built from the snapshot
|
|
1732
|
+
// inside `updateDbAfterOffchainTx`) could come from different
|
|
1733
|
+
// tapscripts. Threading the snapshot pins both reads.
|
|
1734
|
+
const offchainTapscript = this.offchainTapscript;
|
|
1735
|
+
const outputAddress = offchainTapscript.address(this.network.hrp, this.arkServerPublicKey);
|
|
1736
|
+
const address = outputAddress.encode();
|
|
1576
1737
|
// validate recipients and populate undefined amount with dust amount
|
|
1577
1738
|
const recipients = validateRecipients(args, Number(this.dustAmount));
|
|
1578
|
-
const address = await this.getAddress();
|
|
1579
|
-
const outputAddress = ArkAddress.decode(address);
|
|
1580
1739
|
const virtualCoins = await this.getVtxos({
|
|
1581
1740
|
withRecoverable: false,
|
|
1582
1741
|
});
|
|
@@ -1707,7 +1866,7 @@ export class Wallet extends ReadonlyWallet {
|
|
|
1707
1866
|
this._addPendingSpends(selectedCoins);
|
|
1708
1867
|
try {
|
|
1709
1868
|
const { arkTxid, signedCheckpointTxs } = await this.buildAndSubmitOffchainTx(selectedCoins, outputs);
|
|
1710
|
-
await this.updateDbAfterOffchainTx(selectedCoins, arkTxid, signedCheckpointTxs, sentAmount, BigInt(changeAmount), changeReceiver ? changeIndex : 0, changeReceiver?.assets);
|
|
1869
|
+
await this.updateDbAfterOffchainTx(selectedCoins, arkTxid, signedCheckpointTxs, sentAmount, BigInt(changeAmount), changeReceiver ? changeIndex : 0, offchainTapscript, changeReceiver?.assets);
|
|
1711
1870
|
return arkTxid;
|
|
1712
1871
|
}
|
|
1713
1872
|
finally {
|
|
@@ -1726,47 +1885,25 @@ export class Wallet extends ReadonlyWallet {
|
|
|
1726
1885
|
tapLeafScript: input.forfeitTapLeafScript,
|
|
1727
1886
|
};
|
|
1728
1887
|
}), outputs, this.serverUnrollScript);
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
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
|
-
}
|
|
1888
|
+
// arkTx inputs spend checkpoint outputs, so each input's
|
|
1889
|
+
// `witnessUtxo.script` is the checkpoint pkScript — not the
|
|
1890
|
+
// source VTXO contract's pkScript. Build the routing jobs from
|
|
1891
|
+
// the source VTXO scripts (positionally aligned to `inputs[i]`)
|
|
1892
|
+
// so the router can resolve each input's owning contract.
|
|
1893
|
+
const arkTxJobs = inputs.map((input, index) => ({
|
|
1894
|
+
index,
|
|
1895
|
+
lookupScript: VtxoScript.decode(input.tapTree).pkScript,
|
|
1896
|
+
}));
|
|
1897
|
+
const signedVirtualTx = await this._signerRouter.sign(offchainTx.arkTx, arkTxJobs);
|
|
1749
1898
|
// Mark pending before submitting — if we crash between submit and
|
|
1750
1899
|
// finalize, the next init will recover via finalizePendingTxs.
|
|
1751
1900
|
await this.setPendingTxFlag(true);
|
|
1752
1901
|
const { arkTxid, signedCheckpointTxs } = await this.arkProvider.submitTx(base64.encode(signedVirtualTx.toPSBT()), offchainTx.checkpoints.map((c) => base64.encode(c.toPSBT())));
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
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
|
-
}
|
|
1902
|
+
const finalCheckpoints = await Promise.all(signedCheckpointTxs.map(async (c) => {
|
|
1903
|
+
const tx = Transaction.fromPSBT(base64.decode(c));
|
|
1904
|
+
const signedCheckpoint = await this._signerRouter.sign(tx, this.inputSigningJobsFromWitnessUtxos(tx));
|
|
1905
|
+
return base64.encode(signedCheckpoint.toPSBT());
|
|
1906
|
+
}));
|
|
1770
1907
|
await this.arkProvider.finalizeTx(arkTxid, finalCheckpoints);
|
|
1771
1908
|
try {
|
|
1772
1909
|
await this.setPendingTxFlag(false);
|
|
@@ -1776,8 +1913,17 @@ export class Wallet extends ReadonlyWallet {
|
|
|
1776
1913
|
}
|
|
1777
1914
|
return { arkTxid, signedCheckpointTxs };
|
|
1778
1915
|
}
|
|
1779
|
-
// mark virtual outputs as spent, save change outputs if any
|
|
1780
|
-
|
|
1916
|
+
// mark virtual outputs as spent, save change outputs if any.
|
|
1917
|
+
// `offchainTapscript` is the snapshot the caller captured under
|
|
1918
|
+
// `_txLock` before any `await`; deriving both the change-VTXO
|
|
1919
|
+
// metadata and `primaryAddress` from it here guarantees the local
|
|
1920
|
+
// record matches the pkScript the server saw on the inbound
|
|
1921
|
+
// transaction, even if `WalletReceiveRotator.rotate` swaps
|
|
1922
|
+
// `this.offchainTapscript` mid-flight.
|
|
1923
|
+
async updateDbAfterOffchainTx(inputs, arkTxid, signedCheckpointTxs, sentAmount, changeAmount, changeVout, offchainTapscript, changeAssets) {
|
|
1924
|
+
const primaryAddress = offchainTapscript
|
|
1925
|
+
.address(this.network.hrp, this.arkServerPublicKey)
|
|
1926
|
+
.encode();
|
|
1781
1927
|
try {
|
|
1782
1928
|
const spentVtxos = [];
|
|
1783
1929
|
const commitmentTxIds = new Set();
|
|
@@ -1824,7 +1970,6 @@ export class Wallet extends ReadonlyWallet {
|
|
|
1824
1970
|
}
|
|
1825
1971
|
}
|
|
1826
1972
|
const createdAt = Date.now();
|
|
1827
|
-
const primaryAddr = this.arkAddress.encode();
|
|
1828
1973
|
// Only save a change virtual output for preconfirmed coins (those with a batchExpiry).
|
|
1829
1974
|
// Inputs without a batchExpiry are already settled/unrolled and don't need tracking.
|
|
1830
1975
|
let changeVtxo;
|
|
@@ -1833,11 +1978,11 @@ export class Wallet extends ReadonlyWallet {
|
|
|
1833
1978
|
txid: arkTxid,
|
|
1834
1979
|
vout: changeVout,
|
|
1835
1980
|
createdAt: new Date(createdAt),
|
|
1836
|
-
forfeitTapLeafScript:
|
|
1837
|
-
intentTapLeafScript:
|
|
1981
|
+
forfeitTapLeafScript: offchainTapscript.forfeit(),
|
|
1982
|
+
intentTapLeafScript: offchainTapscript.forfeit(),
|
|
1838
1983
|
isUnrolled: false,
|
|
1839
1984
|
isSpent: false,
|
|
1840
|
-
tapTree:
|
|
1985
|
+
tapTree: offchainTapscript.encode(),
|
|
1841
1986
|
value: Number(changeAmount),
|
|
1842
1987
|
virtualStatus: {
|
|
1843
1988
|
state: "preconfirmed",
|
|
@@ -1848,7 +1993,7 @@ export class Wallet extends ReadonlyWallet {
|
|
|
1848
1993
|
confirmed: false,
|
|
1849
1994
|
},
|
|
1850
1995
|
assets: changeAssets,
|
|
1851
|
-
script: hex.encode(
|
|
1996
|
+
script: hex.encode(offchainTapscript.pkScript),
|
|
1852
1997
|
};
|
|
1853
1998
|
}
|
|
1854
1999
|
// Route spent rows to their owning contract bucket. The wallet's
|
|
@@ -1879,9 +2024,9 @@ export class Wallet extends ReadonlyWallet {
|
|
|
1879
2024
|
}
|
|
1880
2025
|
// Change is always primary-script by construction.
|
|
1881
2026
|
if (changeVtxo) {
|
|
1882
|
-
await saveVtxosForContract(this.walletRepository, { script: changeVtxo.script, address:
|
|
2027
|
+
await saveVtxosForContract(this.walletRepository, { script: changeVtxo.script, address: primaryAddress }, [changeVtxo]);
|
|
1883
2028
|
}
|
|
1884
|
-
await this.walletRepository.saveTransactions(
|
|
2029
|
+
await this.walletRepository.saveTransactions(primaryAddress, [
|
|
1885
2030
|
{
|
|
1886
2031
|
key: {
|
|
1887
2032
|
boardingTxid: "",
|