@sidhujag/sysweb3-keyring 2.1.3 → 2.1.4
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/CHANGELOG.md +6 -0
- package/README.md +47 -47
- package/cjs/hardware-transaction-authority.js +36 -0
- package/cjs/hardware-transaction-authority.js.map +1 -0
- package/cjs/index.js +5 -5
- package/cjs/index.js.map +1 -1
- package/cjs/keyring-core.js +171 -220
- package/cjs/keyring-core.js.map +1 -1
- package/cjs/keyring-host.js +113 -0
- package/cjs/keyring-host.js.map +1 -0
- package/cjs/keyring-manager.js +15 -151
- package/cjs/keyring-manager.js.map +1 -1
- package/cjs/ledger/host-types.js.map +1 -1
- package/cjs/ledger/index.js +44 -19
- package/cjs/ledger/index.js.map +1 -1
- package/cjs/providers.js +3 -1
- package/cjs/providers.js.map +1 -1
- package/cjs/software.js +10 -56
- package/cjs/software.js.map +1 -1
- package/cjs/transactions/ethereum.js +306 -229
- package/cjs/transactions/ethereum.js.map +1 -1
- package/cjs/transactions/evm-local-signer.js +15 -0
- package/cjs/transactions/evm-local-signer.js.map +1 -1
- package/cjs/transactions/evm-submission.js +61 -65
- package/cjs/transactions/evm-submission.js.map +1 -1
- package/cjs/transactions/hardware-psbt.js +11 -8
- package/cjs/transactions/hardware-psbt.js.map +1 -1
- package/cjs/transactions/index.js +10 -2
- package/cjs/transactions/index.js.map +1 -1
- package/cjs/transactions/prepared-evm.js +10 -7
- package/cjs/transactions/prepared-evm.js.map +1 -1
- package/cjs/transactions/syscoin-core.js +282 -202
- package/cjs/transactions/syscoin-core.js.map +1 -1
- package/cjs/trezor/connect.js +19 -2
- package/cjs/trezor/connect.js.map +1 -1
- package/cjs/trezor/index.js +38 -16
- package/cjs/trezor/index.js.map +1 -1
- package/cjs/types.js.map +1 -1
- package/cjs/utils/owned-blockbook-account.js +45 -0
- package/cjs/utils/owned-blockbook-account.js.map +1 -0
- package/esm/current-vault-refusal.js +3 -0
- package/esm/current-vault-refusal.js.map +1 -0
- package/esm/evm-fees.js +242 -0
- package/esm/evm-fees.js.map +1 -0
- package/esm/hardware-transaction-authority.js +33 -0
- package/esm/hardware-transaction-authority.js.map +1 -0
- package/esm/initial-state.js +102 -0
- package/esm/initial-state.js.map +1 -0
- package/esm/keyring-core.js +2781 -0
- package/esm/keyring-core.js.map +1 -0
- package/esm/keyring-host.js +108 -0
- package/esm/keyring-host.js.map +1 -0
- package/esm/ledger/dmk-session.js +1 -0
- package/esm/ledger/dmk-session.js.map +1 -1
- package/esm/ledger/dmk-utxo.js +9 -3
- package/esm/ledger/dmk-utxo.js.map +1 -1
- package/esm/ledger/host-types.js.map +1 -1
- package/esm/ledger/index.js +44 -19
- package/esm/ledger/index.js.map +1 -1
- package/esm/network-utils.js +70 -0
- package/esm/network-utils.js.map +1 -0
- package/esm/providers.js +3 -1
- package/esm/providers.js.map +1 -1
- package/esm/software-hd-account.js +20 -0
- package/esm/software-hd-account.js.map +1 -0
- package/esm/storage-core.js +258 -0
- package/esm/storage-core.js.map +1 -0
- package/esm/transactions/ethereum.js +2753 -0
- package/esm/transactions/ethereum.js.map +1 -0
- package/esm/transactions/evm-cancellation.js +40 -0
- package/esm/transactions/evm-cancellation.js.map +1 -0
- package/esm/transactions/evm-local-signer.js +202 -0
- package/esm/transactions/evm-local-signer.js.map +1 -0
- package/esm/transactions/evm-submission.js +187 -0
- package/esm/transactions/evm-submission.js.map +1 -0
- package/esm/transactions/prepared-evm.js +194 -0
- package/esm/transactions/prepared-evm.js.map +1 -0
- package/esm/transactions/syscoin-core.js +691 -0
- package/esm/transactions/syscoin-core.js.map +1 -0
- package/esm/trezor/connect.js +19 -2
- package/esm/trezor/connect.js.map +1 -1
- package/esm/trezor/index.js +38 -16
- package/esm/trezor/index.js.map +1 -1
- package/esm/types.js.map +1 -1
- package/esm/utils/excluded-utxos.js +53 -0
- package/esm/utils/excluded-utxos.js.map +1 -0
- package/esm/utils/owned-blockbook-account.js +9 -0
- package/esm/utils/owned-blockbook-account.js.map +1 -0
- package/esm/utils/psbt.js +32 -0
- package/esm/utils/psbt.js.map +1 -0
- package/esm/utils/watch-descriptor.js +138 -0
- package/esm/utils/watch-descriptor.js.map +1 -0
- package/package.json +5 -11
- package/types/hardware-transaction-authority.d.ts +5 -0
- package/types/index.d.ts +3 -3
- package/types/keyring-core.d.ts +22 -51
- package/types/keyring-host.d.ts +46 -0
- package/types/keyring-manager.d.ts +13 -18
- package/types/ledger/host-types.d.ts +4 -1
- package/types/ledger/index.d.ts +1 -1
- package/types/ledger/types.d.ts +3 -2
- package/types/providers.d.ts +1 -1
- package/types/software.d.ts +6 -19
- package/types/transactions/ethereum.d.ts +14 -11
- package/types/transactions/evm-local-signer.d.ts +2 -2
- package/types/transactions/evm-submission.d.ts +11 -4
- package/types/transactions/hardware-psbt.d.ts +1 -1
- package/types/transactions/index.d.ts +3 -2
- package/types/transactions/prepared-evm.d.ts +4 -1
- package/types/transactions/syscoin-core.d.ts +48 -21
- package/types/trezor/connect.d.ts +6 -3
- package/types/trezor/index.d.ts +4 -3
- package/types/types.d.ts +95 -63
- package/types/utils/owned-blockbook-account.d.ts +3 -0
- package/cjs/hardware-wallet-manager-singleton.js +0 -34
- package/cjs/hardware-wallet-manager-singleton.js.map +0 -1
- package/cjs/storage.js +0 -20
- package/cjs/storage.js.map +0 -1
- package/cjs/transactions/syscoin.js +0 -16
- package/cjs/transactions/syscoin.js.map +0 -1
- package/cjs/utils/blockbook-cache.js +0 -76
- package/cjs/utils/blockbook-cache.js.map +0 -1
- package/types/hardware-wallet-manager-singleton.d.ts +0 -19
- package/types/storage.d.ts +0 -5
- package/types/transactions/syscoin.d.ts +0 -9
- package/types/utils/blockbook-cache.d.ts +0 -6
|
@@ -34,22 +34,21 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.SyscoinTransactionsCore = exports.createHardwareSignerUnavailableError = void 0;
|
|
37
|
-
|
|
37
|
+
exports.readPublicSyscoinFee = readPublicSyscoinFee;
|
|
38
|
+
exports.captureOwnedSyscoinRead = captureOwnedSyscoinRead;
|
|
39
|
+
const sysweb3_network_1 = require("@sidhujag/sysweb3-network");
|
|
38
40
|
const syscoinjs = __importStar(require("syscoinjs-lib"));
|
|
39
41
|
// import { BIP_84, ONE_HUNDRED_MILLION, SYSCOIN_BASIC_FEE } from 'utils';
|
|
40
42
|
const ethers_v6_1 = require("../ethers-v6");
|
|
41
43
|
const types_1 = require("../types");
|
|
42
|
-
const blockbook_cache_1 = require("../utils/blockbook-cache");
|
|
43
44
|
const excluded_utxos_1 = require("../utils/excluded-utxos");
|
|
45
|
+
const owned_blockbook_account_1 = require("../utils/owned-blockbook-account");
|
|
44
46
|
const psbt_1 = require("../utils/psbt");
|
|
45
47
|
const psbt_account_scope_1 = require("../utils/psbt-account-scope");
|
|
46
48
|
const createHardwareSignerUnavailableError = (hardware) => Object.assign(new Error(`${hardware} signer is unavailable in this host`), {
|
|
47
49
|
code: 'HARDWARE_SIGNER_UNAVAILABLE',
|
|
48
50
|
});
|
|
49
51
|
exports.createHardwareSignerUnavailableError = createHardwareSignerUnavailableError;
|
|
50
|
-
// Recommended fee changes slowly; cache per explorer to avoid hammering
|
|
51
|
-
// Blockbook while the user edits amounts on the send screen
|
|
52
|
-
const FEE_CACHE_TTL_MS = 60000;
|
|
53
52
|
const sweepError = (code, message) => ({
|
|
54
53
|
error: true,
|
|
55
54
|
code,
|
|
@@ -69,8 +68,90 @@ const sweepAtomicValue = (value) => {
|
|
|
69
68
|
throw sweepError('INVALID_NATIVE_SWEEP_UTXOS', 'Invalid native sweep atomic value.');
|
|
70
69
|
return new syscoinjs.utils.BN(text);
|
|
71
70
|
};
|
|
71
|
+
function captureBlockbookRead(options, assertContext) {
|
|
72
|
+
if (!options)
|
|
73
|
+
throw new Error('Missing owned UTXO read');
|
|
74
|
+
const { fetch, signal, assertCurrent: assertHost } = options;
|
|
75
|
+
if (typeof fetch !== 'function' ||
|
|
76
|
+
typeof assertHost !== 'function' ||
|
|
77
|
+
typeof signal?.aborted !== 'boolean')
|
|
78
|
+
throw new Error('Invalid owned UTXO read');
|
|
79
|
+
const assertCurrent = () => {
|
|
80
|
+
if (signal.aborted)
|
|
81
|
+
throw new Error('Wallet UTXO preparation context changed');
|
|
82
|
+
const result = assertHost();
|
|
83
|
+
if (result !== undefined) {
|
|
84
|
+
if (result &&
|
|
85
|
+
(typeof result === 'object' || typeof result === 'function'))
|
|
86
|
+
void Promise.resolve(result).catch(() => undefined);
|
|
87
|
+
throw new Error('Owned UTXO read requires a synchronous guard');
|
|
88
|
+
}
|
|
89
|
+
assertContext();
|
|
90
|
+
if (signal.aborted)
|
|
91
|
+
throw new Error('Wallet UTXO preparation context changed');
|
|
92
|
+
};
|
|
93
|
+
assertCurrent();
|
|
94
|
+
const transport = Object.freeze({
|
|
95
|
+
signal,
|
|
96
|
+
async fetch(input, init) {
|
|
97
|
+
assertCurrent();
|
|
98
|
+
const response = await fetch(input, init);
|
|
99
|
+
assertCurrent();
|
|
100
|
+
return response;
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
return { assertCurrent, transport };
|
|
104
|
+
}
|
|
105
|
+
async function readOwnedFee(explorerUrl, { assertCurrent, transport }) {
|
|
106
|
+
assertCurrent();
|
|
107
|
+
const fee = await syscoinjs.utils.fetchEstimateFee(explorerUrl, 1, undefined, transport);
|
|
108
|
+
assertCurrent();
|
|
109
|
+
return fee / 1024;
|
|
110
|
+
}
|
|
111
|
+
/** Public network fee estimate. It has no wallet, account or signing access. */
|
|
112
|
+
async function readPublicSyscoinFee(explorerUrl, owner) {
|
|
113
|
+
return readOwnedFee(explorerUrl, captureBlockbookRead(owner, () => undefined));
|
|
114
|
+
}
|
|
115
|
+
/** Bind both transaction preparation and address reads to the active wallet. */
|
|
116
|
+
function captureOwnedSyscoinRead(options, getState, captureSigningContext) {
|
|
117
|
+
if (!options)
|
|
118
|
+
throw new Error('Missing owned UTXO read');
|
|
119
|
+
const assertSession = captureSigningContext(true);
|
|
120
|
+
const { activeNetwork, activeAccountId, activeAccountType, accounts } = getState();
|
|
121
|
+
if (activeNetwork.kind !== sysweb3_network_1.INetworkType.Syscoin)
|
|
122
|
+
throw new Error('Switch to UTXO chain');
|
|
123
|
+
const selected = accounts[activeAccountType]?.[activeAccountId];
|
|
124
|
+
if (!selected)
|
|
125
|
+
throw new Error('Active account not found');
|
|
126
|
+
const account = Object.freeze({
|
|
127
|
+
address: selected.address,
|
|
128
|
+
xpub: selected.xpub,
|
|
129
|
+
});
|
|
130
|
+
const network = Object.freeze({ ...activeNetwork });
|
|
131
|
+
const read = captureBlockbookRead(options, () => {
|
|
132
|
+
assertSession();
|
|
133
|
+
const current = getState();
|
|
134
|
+
const selected = current.accounts[current.activeAccountType]?.[current.activeAccountId];
|
|
135
|
+
if (current.activeAccountId !== activeAccountId ||
|
|
136
|
+
current.activeAccountType !== activeAccountType ||
|
|
137
|
+
selected?.address !== account.address ||
|
|
138
|
+
selected?.xpub !== account.xpub ||
|
|
139
|
+
current.activeNetwork.kind !== network.kind ||
|
|
140
|
+
current.activeNetwork.chainId !== network.chainId ||
|
|
141
|
+
current.activeNetwork.slip44 !== network.slip44 ||
|
|
142
|
+
current.activeNetwork.currency !== network.currency ||
|
|
143
|
+
current.activeNetwork.url !== network.url)
|
|
144
|
+
throw new Error('Wallet UTXO preparation context changed');
|
|
145
|
+
});
|
|
146
|
+
return {
|
|
147
|
+
account,
|
|
148
|
+
accountId: activeAccountId,
|
|
149
|
+
accountType: activeAccountType,
|
|
150
|
+
read: Object.freeze({ ...read, network, xpub: account.xpub }),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
72
153
|
class SyscoinTransactionsCore {
|
|
73
|
-
constructor(getSyscoinSigner, getReadOnlySigner, getState,
|
|
154
|
+
constructor(getSyscoinSigner, getReadOnlySigner, getState, hardwareSigners, captureSigningContext) {
|
|
74
155
|
this.getTransactionPSBT = async ({ txOptions, outputs, changeAddress, feeRateBN, xpub, fundingUtxos, }, main) => {
|
|
75
156
|
try {
|
|
76
157
|
// Use syscoinjs-lib directly for transaction creation
|
|
@@ -108,26 +189,17 @@ class SyscoinTransactionsCore {
|
|
|
108
189
|
return main.decodeRawTransaction(psbtObj);
|
|
109
190
|
}
|
|
110
191
|
};
|
|
111
|
-
this.
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}
|
|
117
|
-
const fee = (await syscoinjs.utils.fetchEstimateFee(explorerUrl, 1)) / 1024;
|
|
118
|
-
this.feeCache.set(explorerUrl, { fee, timestamp: Date.now() });
|
|
119
|
-
return fee;
|
|
120
|
-
};
|
|
121
|
-
this.txUtilsFunctions = () => {
|
|
122
|
-
const { getRawTransaction } = (0, sysweb3_utils_1.txUtils)();
|
|
123
|
-
return {
|
|
124
|
-
getRawTransaction,
|
|
125
|
-
};
|
|
192
|
+
this.getRecommendedFee = async (explorerUrl, ownedRead) => {
|
|
193
|
+
const { read } = this.captureOwnedRead(ownedRead);
|
|
194
|
+
if (explorerUrl !== read.network.url)
|
|
195
|
+
throw new Error('Owned UTXO read endpoint changed');
|
|
196
|
+
return readOwnedFee(explorerUrl, read);
|
|
126
197
|
};
|
|
127
198
|
// Internal method for signing with the HD signer
|
|
128
199
|
this.signPSBTWithSigner = async ({ psbt, signer, }) => await signer.sign(psbt);
|
|
129
|
-
this.createNativeSweepPSBT = async ({ txOptions, changeAddress, receivingAddress, feeRateBN, source, excluded, }, main) => {
|
|
130
|
-
const raw = await syscoinjs.utils.fetchBackendUTXOS(main.blockbookURL, source);
|
|
200
|
+
this.createNativeSweepPSBT = async ({ txOptions, changeAddress, receivingAddress, feeRateBN, source, excluded, }, main, read) => {
|
|
201
|
+
const raw = await syscoinjs.utils.fetchBackendUTXOS(main.blockbookURL, source, undefined, read.transport);
|
|
202
|
+
read.assertCurrent();
|
|
131
203
|
const rows = excluded.size
|
|
132
204
|
? (0, excluded_utxos_1.filterExcludedUtxos)(raw, excluded).utxos
|
|
133
205
|
: Array.isArray(raw)
|
|
@@ -195,16 +267,10 @@ class SyscoinTransactionsCore {
|
|
|
195
267
|
};
|
|
196
268
|
};
|
|
197
269
|
// Create unsigned PSBT for any transaction type
|
|
198
|
-
this.createUnsignedPSBT = async ({ txOptions = {}, isMax = false, nativeSweep = false, amount, receivingAddress, feeRateBN, token = null,
|
|
270
|
+
this.createUnsignedPSBT = async ({ txOptions = {}, isMax = false, nativeSweep = false, amount, receivingAddress, feeRateBN, token = null, }, owned) => {
|
|
199
271
|
// Ensure RBF is enabled by default if not explicitly set
|
|
200
272
|
const finalTxOptions = { rbf: true, ...txOptions };
|
|
201
|
-
const {
|
|
202
|
-
// Use read-only signer since we're just creating an unsigned PSBT
|
|
203
|
-
const { main } = this.getReadOnlySigner();
|
|
204
|
-
const account = accounts[activeAccountType]?.[activeAccountId];
|
|
205
|
-
if (!account) {
|
|
206
|
-
throw new Error('Active account not found');
|
|
207
|
-
}
|
|
273
|
+
const { account, accountType: activeAccountType, excluded, main, read, } = owned;
|
|
208
274
|
const xpub = account.xpub;
|
|
209
275
|
const isSingleAddressImported = activeAccountType === types_1.KeyringAccountType.Imported &&
|
|
210
276
|
xpub === account.address;
|
|
@@ -231,9 +297,9 @@ class SyscoinTransactionsCore {
|
|
|
231
297
|
// If this is a single-address imported account (WIF import), use the account.address for both from and change
|
|
232
298
|
const changeAddress = isSingleAddressImported
|
|
233
299
|
? account.address
|
|
234
|
-
: await
|
|
300
|
+
: await owned.getOwnedAddress(xpub, true);
|
|
301
|
+
read.assertCurrent();
|
|
235
302
|
try {
|
|
236
|
-
const excluded = (0, excluded_utxos_1.excludedOutpointIds)(excludedOutpoints);
|
|
237
303
|
if (nativeSweep) {
|
|
238
304
|
return await this.createNativeSweepPSBT({
|
|
239
305
|
txOptions: finalTxOptions,
|
|
@@ -242,11 +308,12 @@ class SyscoinTransactionsCore {
|
|
|
242
308
|
feeRateBN,
|
|
243
309
|
source: sourceForUtxoLookup,
|
|
244
310
|
excluded,
|
|
245
|
-
}, main);
|
|
311
|
+
}, main, read);
|
|
246
312
|
}
|
|
247
313
|
const fundingUtxos = excluded.size
|
|
248
|
-
? (0, excluded_utxos_1.filterExcludedUtxos)(await syscoinjs.utils.fetchBackendUTXOS(main.blockbookURL, sourceForUtxoLookup), excluded)
|
|
314
|
+
? (0, excluded_utxos_1.filterExcludedUtxos)(await syscoinjs.utils.fetchBackendUTXOS(main.blockbookURL, sourceForUtxoLookup, undefined, read.transport), excluded)
|
|
249
315
|
: undefined;
|
|
316
|
+
read.assertCurrent();
|
|
250
317
|
if (token && token.guid) {
|
|
251
318
|
// Token transaction: use assetAllocationSend
|
|
252
319
|
// Create a Map for the asset allocation
|
|
@@ -306,43 +373,24 @@ class SyscoinTransactionsCore {
|
|
|
306
373
|
}
|
|
307
374
|
};
|
|
308
375
|
// Sign PSBT with appropriate method - separated for better error handling
|
|
309
|
-
this.signPSBTWithMethod = async (psbt,
|
|
310
|
-
const {
|
|
376
|
+
this.signPSBTWithMethod = async (psbt, ownedRead) => {
|
|
377
|
+
const { assertCurrent, network: activeNetwork, transport } = ownedRead;
|
|
378
|
+
assertCurrent();
|
|
379
|
+
const { activeAccountId, activeAccountType, accounts } = this.getState();
|
|
311
380
|
const account = accounts[activeAccountType]?.[activeAccountId];
|
|
312
381
|
if (!account)
|
|
313
382
|
throw new Error('Active account not found');
|
|
314
383
|
// Device choice belongs to the selected account, never to PSBT/caller flags.
|
|
315
|
-
isTrezor =
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
activeAccountType === types_1.KeyringAccountType.Ledger ||
|
|
320
|
-
account.isLedgerWallet === true;
|
|
321
|
-
const assertSession = this.captureSigningContext?.() || (() => undefined);
|
|
322
|
-
// The adapter gets a detached, immutable public context. It must not be
|
|
323
|
-
// able to mutate the wallet state or the baseline used by assertCurrent.
|
|
384
|
+
const isTrezor = activeAccountType === types_1.KeyringAccountType.Trezor ||
|
|
385
|
+
account.isTrezorWallet === true;
|
|
386
|
+
const isLedger = activeAccountType === types_1.KeyringAccountType.Ledger ||
|
|
387
|
+
account.isLedgerWallet === true;
|
|
324
388
|
const scope = Object.freeze({
|
|
325
389
|
account: Object.freeze({ address: account.address, xpub: account.xpub }),
|
|
326
390
|
accountId: activeAccountId,
|
|
327
391
|
accountType: activeAccountType,
|
|
328
|
-
network:
|
|
392
|
+
network: activeNetwork,
|
|
329
393
|
});
|
|
330
|
-
const assertCurrent = () => {
|
|
331
|
-
assertSession();
|
|
332
|
-
const current = this.getState();
|
|
333
|
-
const selected = current.accounts[current.activeAccountType]?.[current.activeAccountId];
|
|
334
|
-
if (current.activeAccountId !== activeAccountId ||
|
|
335
|
-
current.activeAccountType !== activeAccountType ||
|
|
336
|
-
selected?.address !== scope.account.address ||
|
|
337
|
-
selected?.xpub !== scope.account.xpub ||
|
|
338
|
-
current.activeNetwork.kind !== scope.network.kind ||
|
|
339
|
-
current.activeNetwork.chainId !== scope.network.chainId ||
|
|
340
|
-
current.activeNetwork.slip44 !== scope.network.slip44 ||
|
|
341
|
-
current.activeNetwork.url !== scope.network.url) {
|
|
342
|
-
throw new Error('Wallet signing context changed');
|
|
343
|
-
}
|
|
344
|
-
};
|
|
345
|
-
assertCurrent();
|
|
346
394
|
const sign = isLedger
|
|
347
395
|
? this.hardwareSigners.ledger
|
|
348
396
|
: isTrezor
|
|
@@ -353,17 +401,29 @@ class SyscoinTransactionsCore {
|
|
|
353
401
|
if (isTrezor || isLedger)
|
|
354
402
|
(0, psbt_account_scope_1.assertPsbtAccountScope)(psbt, scope);
|
|
355
403
|
if (isLedger || isTrezor) {
|
|
404
|
+
const assertHardwareCurrent = () => {
|
|
405
|
+
try {
|
|
406
|
+
assertCurrent();
|
|
407
|
+
}
|
|
408
|
+
catch {
|
|
409
|
+
// The vendor retry loop already treats AbortError as terminal.
|
|
410
|
+
throw Object.assign(new Error('Wallet signing context changed'), {
|
|
411
|
+
name: 'AbortError',
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
};
|
|
356
415
|
const signed = await sign({
|
|
357
416
|
psbt,
|
|
358
417
|
account: Object.freeze({ id: account.id, xpub: account.xpub }),
|
|
359
418
|
network: scope.network,
|
|
360
419
|
scope,
|
|
361
|
-
assertCurrent,
|
|
420
|
+
assertCurrent: assertHardwareCurrent,
|
|
421
|
+
signal: transport.signal,
|
|
362
422
|
...(isLedger && {
|
|
363
|
-
getRawTransaction:
|
|
423
|
+
getRawTransaction: (txid) => syscoinjs.utils.fetchBackendRawTx(activeNetwork.url, txid, transport),
|
|
364
424
|
}),
|
|
365
425
|
});
|
|
366
|
-
|
|
426
|
+
assertHardwareCurrent();
|
|
367
427
|
return signed;
|
|
368
428
|
}
|
|
369
429
|
else {
|
|
@@ -373,99 +433,94 @@ class SyscoinTransactionsCore {
|
|
|
373
433
|
//
|
|
374
434
|
// We can recover by enriching PSBT inputs with a proprietary `path` field using
|
|
375
435
|
// Blockbook's xpub token list (address -> path mapping), which reliably includes paths.
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
for (const input of inputs) {
|
|
394
|
-
const unknowns = input?.unknownKeyVals || [];
|
|
395
|
-
const pathKv = unknowns.find((kv) => kv?.key?.toString?.() === 'path');
|
|
396
|
-
if (!pathKv?.value)
|
|
397
|
-
continue;
|
|
398
|
-
const fullPath = String(Buffer.from(pathKv.value).toString());
|
|
399
|
-
const parts = fullPath.split('/').filter(Boolean);
|
|
400
|
-
// Expected: m/84'/slip44'/account'/change/index
|
|
401
|
-
if (parts.length >= 2) {
|
|
402
|
-
const change = parts[parts.length - 2];
|
|
403
|
-
const index = parts[parts.length - 1];
|
|
404
|
-
const rel = `${change}/${index}`;
|
|
405
|
-
pathKv.value = Buffer.from(rel);
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
const needsAnyPath = psbt?.data?.inputs?.some((input) => {
|
|
436
|
+
const account = accounts[activeAccountType]?.[activeAccountId];
|
|
437
|
+
const accountXpub = account?.xpub;
|
|
438
|
+
const accountAddress = account?.address;
|
|
439
|
+
const isSingleAddressImported = activeAccountType === types_1.KeyringAccountType.Imported &&
|
|
440
|
+
Boolean(accountXpub) &&
|
|
441
|
+
Boolean(accountAddress) &&
|
|
442
|
+
accountXpub === accountAddress;
|
|
443
|
+
const isImportedExtended = activeAccountType === types_1.KeyringAccountType.Imported &&
|
|
444
|
+
Boolean(accountXpub) &&
|
|
445
|
+
!isSingleAddressImported;
|
|
446
|
+
// IMPORTANT: Imported extended keys (zprv/vprv) are typically *account-level* keys.
|
|
447
|
+
// syscoinjs-lib signs using the imported key as the BIP32 root, so PSBT `path` must be
|
|
448
|
+
// relative (change/index), not the full `m/84'/.../change/index`. If we keep full paths,
|
|
449
|
+
// bip32.derivePath can throw, leaving no derivations and causing "Need derivation path".
|
|
450
|
+
if (isImportedExtended) {
|
|
451
|
+
const inputs = psbt?.data?.inputs || [];
|
|
452
|
+
for (const input of inputs) {
|
|
410
453
|
const unknowns = input?.unknownKeyVals || [];
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
const res = await (0, blockbook_cache_1.fetchBackendAccountCached)(blockbookUrl, accountXpub, 'tokens=used&details=tokens');
|
|
423
|
-
const tokens = Array.isArray(res?.tokens) ? res.tokens : [];
|
|
424
|
-
const addrToPath = new Map();
|
|
425
|
-
for (const t of tokens) {
|
|
426
|
-
if (t?.name && t?.path) {
|
|
427
|
-
addrToPath.set(String(t.name), String(t.path));
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
const inputs = psbt?.data?.inputs || [];
|
|
431
|
-
for (let i = 0; i < inputs.length; i++) {
|
|
432
|
-
const input = inputs[i];
|
|
433
|
-
const unknowns = input?.unknownKeyVals || [];
|
|
434
|
-
const hasPath = unknowns.some((kv) => kv?.key?.toString?.() === 'path');
|
|
435
|
-
if (hasPath)
|
|
436
|
-
continue;
|
|
437
|
-
const addrKv = unknowns.find((kv) => kv?.key?.toString?.() === 'address');
|
|
438
|
-
const addr = addrKv?.value
|
|
439
|
-
? String(Buffer.from(addrKv.value).toString())
|
|
440
|
-
: null;
|
|
441
|
-
if (!addr)
|
|
442
|
-
continue;
|
|
443
|
-
const fullPath = addrToPath.get(addr);
|
|
444
|
-
if (!fullPath)
|
|
445
|
-
continue;
|
|
446
|
-
// Imported extended keys are account-level (BIP84 account node), so we must provide a
|
|
447
|
-
// path relative to that node (change/index), not the full m/84'/... path.
|
|
448
|
-
const pathForSigner = isImportedExtended
|
|
449
|
-
? (() => {
|
|
450
|
-
const parts = String(fullPath).split('/').filter(Boolean);
|
|
451
|
-
// Expected: m/84'/slip44'/account'/change/index
|
|
452
|
-
if (parts.length >= 2) {
|
|
453
|
-
const change = parts[parts.length - 2];
|
|
454
|
-
const index = parts[parts.length - 1];
|
|
455
|
-
return `${change}/${index}`;
|
|
456
|
-
}
|
|
457
|
-
return fullPath;
|
|
458
|
-
})()
|
|
459
|
-
: fullPath;
|
|
460
|
-
psbt.addUnknownKeyValToInput(i, {
|
|
461
|
-
key: Buffer.from('path'),
|
|
462
|
-
value: Buffer.from(pathForSigner),
|
|
463
|
-
});
|
|
454
|
+
const pathKv = unknowns.find((kv) => kv?.key?.toString?.() === 'path');
|
|
455
|
+
if (!pathKv?.value)
|
|
456
|
+
continue;
|
|
457
|
+
const fullPath = String(Buffer.from(pathKv.value).toString());
|
|
458
|
+
const parts = fullPath.split('/').filter(Boolean);
|
|
459
|
+
// Expected: m/84'/slip44'/account'/change/index
|
|
460
|
+
if (parts.length >= 2) {
|
|
461
|
+
const change = parts[parts.length - 2];
|
|
462
|
+
const index = parts[parts.length - 1];
|
|
463
|
+
const rel = `${change}/${index}`;
|
|
464
|
+
pathKv.value = Buffer.from(rel);
|
|
464
465
|
}
|
|
465
466
|
}
|
|
466
467
|
}
|
|
467
|
-
|
|
468
|
-
|
|
468
|
+
const needsAnyPath = psbt?.data?.inputs?.some((input) => {
|
|
469
|
+
const unknowns = input?.unknownKeyVals || [];
|
|
470
|
+
return (!input.bip32Derivation?.length &&
|
|
471
|
+
!input.tapBip32Derivation?.length &&
|
|
472
|
+
!unknowns.some((kv) => kv?.key?.toString?.() === 'path'));
|
|
473
|
+
});
|
|
474
|
+
if (needsAnyPath &&
|
|
475
|
+
accountXpub &&
|
|
476
|
+
!isSingleAddressImported &&
|
|
477
|
+
typeof psbt.addUnknownKeyValToInput === 'function') {
|
|
478
|
+
// Fetch address->path mapping from Blockbook xpub endpoint
|
|
479
|
+
const blockbookUrl = activeNetwork.url;
|
|
480
|
+
const res = await (0, owned_blockbook_account_1.fetchOwnedBlockbookAccount)(blockbookUrl, accountXpub, transport);
|
|
481
|
+
assertCurrent();
|
|
482
|
+
const tokens = Array.isArray(res?.tokens) ? res.tokens : [];
|
|
483
|
+
const addrToPath = new Map();
|
|
484
|
+
for (const t of tokens) {
|
|
485
|
+
if (t?.name && t?.path) {
|
|
486
|
+
addrToPath.set(String(t.name), String(t.path));
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
const inputs = psbt?.data?.inputs || [];
|
|
490
|
+
for (let i = 0; i < inputs.length; i++) {
|
|
491
|
+
const input = inputs[i];
|
|
492
|
+
const unknowns = input?.unknownKeyVals || [];
|
|
493
|
+
const hasPath = unknowns.some((kv) => kv?.key?.toString?.() === 'path');
|
|
494
|
+
if (hasPath)
|
|
495
|
+
continue;
|
|
496
|
+
const addrKv = unknowns.find((kv) => kv?.key?.toString?.() === 'address');
|
|
497
|
+
const addr = addrKv?.value
|
|
498
|
+
? String(Buffer.from(addrKv.value).toString())
|
|
499
|
+
: null;
|
|
500
|
+
if (!addr)
|
|
501
|
+
continue;
|
|
502
|
+
const fullPath = addrToPath.get(addr);
|
|
503
|
+
if (!fullPath)
|
|
504
|
+
continue;
|
|
505
|
+
// Imported extended keys are account-level (BIP84 account node), so we must provide a
|
|
506
|
+
// path relative to that node (change/index), not the full m/84'/... path.
|
|
507
|
+
const pathForSigner = isImportedExtended
|
|
508
|
+
? (() => {
|
|
509
|
+
const parts = String(fullPath).split('/').filter(Boolean);
|
|
510
|
+
// Expected: m/84'/slip44'/account'/change/index
|
|
511
|
+
if (parts.length >= 2) {
|
|
512
|
+
const change = parts[parts.length - 2];
|
|
513
|
+
const index = parts[parts.length - 1];
|
|
514
|
+
return `${change}/${index}`;
|
|
515
|
+
}
|
|
516
|
+
return fullPath;
|
|
517
|
+
})()
|
|
518
|
+
: fullPath;
|
|
519
|
+
psbt.addUnknownKeyValToInput(i, {
|
|
520
|
+
key: Buffer.from('path'),
|
|
521
|
+
value: Buffer.from(pathForSigner),
|
|
522
|
+
});
|
|
523
|
+
}
|
|
469
524
|
}
|
|
470
525
|
assertCurrent();
|
|
471
526
|
(0, psbt_account_scope_1.assertPsbtAccountScope)(psbt, scope);
|
|
@@ -479,23 +534,39 @@ class SyscoinTransactionsCore {
|
|
|
479
534
|
}
|
|
480
535
|
};
|
|
481
536
|
// Create unsigned PSBT and estimate fee - NO SIGNING
|
|
482
|
-
this.getEstimateSysTransactionFee = async ({ txOptions = {}, isMax = false, nativeSweep = false, amount, receivingAddress, feeRate, token = null, excludedOutpoints = [], }) => {
|
|
537
|
+
this.getEstimateSysTransactionFee = async ({ txOptions = {}, isMax = false, nativeSweep = false, amount, receivingAddress, feeRate, token = null, excludedOutpoints = [], }, ownedRead) => {
|
|
483
538
|
try {
|
|
484
539
|
if (nativeSweep !== false &&
|
|
485
540
|
(nativeSweep !== true || isMax !== true || token !== null))
|
|
486
541
|
throw sweepError('INVALID_NATIVE_SWEEP', 'Native sweep requires explicit MAX and a native transaction.');
|
|
487
542
|
// Ensure RBF is enabled by default if not explicitly set
|
|
488
543
|
const finalTxOptions = { rbf: true, ...txOptions };
|
|
489
|
-
|
|
490
|
-
const { main } = this.getReadOnlySigner();
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
544
|
+
const preparation = this.captureOwnedRead(ownedRead);
|
|
545
|
+
const { main, getOwnedAddress } = this.getReadOnlySigner(preparation.read);
|
|
546
|
+
if (typeof getOwnedAddress !== 'function' ||
|
|
547
|
+
main.blockbookURL !== preparation.read.network.url)
|
|
548
|
+
throw new Error('Owned UTXO read adapter is unavailable');
|
|
549
|
+
const owned = {
|
|
550
|
+
...preparation,
|
|
551
|
+
main,
|
|
552
|
+
getOwnedAddress,
|
|
553
|
+
excluded: (0, excluded_utxos_1.excludedOutpointIds)(excludedOutpoints),
|
|
554
|
+
};
|
|
555
|
+
const selectedToken = token ? { ...token } : token;
|
|
556
|
+
const { assetWhiteList } = finalTxOptions;
|
|
557
|
+
if (assetWhiteList instanceof Map)
|
|
558
|
+
finalTxOptions.assetWhiteList = new Map(assetWhiteList);
|
|
559
|
+
else if (assetWhiteList instanceof Set)
|
|
560
|
+
finalTxOptions.assetWhiteList = new Set(assetWhiteList);
|
|
561
|
+
for (const key of ['memo', 'memoHeader', 'blobHash', 'blobData']) {
|
|
562
|
+
const bytes = finalTxOptions[key];
|
|
563
|
+
if (bytes instanceof Uint8Array)
|
|
564
|
+
finalTxOptions[key] = Buffer.from(bytes);
|
|
498
565
|
}
|
|
566
|
+
const actualFeeRate = feeRate === undefined
|
|
567
|
+
? await readOwnedFee(main.blockbookURL, owned.read)
|
|
568
|
+
: feeRate;
|
|
569
|
+
owned.read.assertCurrent();
|
|
499
570
|
// Convert fee rate to satoshis/byte and ensure minimum relay of 1 sat/vB
|
|
500
571
|
// Blockbook returns coins per kB; after division by 1024, some testnets
|
|
501
572
|
// can yield < 1 sat/vB (e.g., 0.9765625). Round up and clamp to 1 to
|
|
@@ -510,10 +581,10 @@ class SyscoinTransactionsCore {
|
|
|
510
581
|
amount,
|
|
511
582
|
receivingAddress,
|
|
512
583
|
feeRateBN,
|
|
513
|
-
token,
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
584
|
+
token: selectedToken,
|
|
585
|
+
}, owned);
|
|
586
|
+
owned.read.assertCurrent();
|
|
587
|
+
const quote = {
|
|
517
588
|
fee: result.fee / 1e8,
|
|
518
589
|
psbt: psbt_1.PsbtUtils.toPali(result.psbt), // Return UNSIGNED PSBT as JSON
|
|
519
590
|
...(nativeSweep
|
|
@@ -523,6 +594,8 @@ class SyscoinTransactionsCore {
|
|
|
523
594
|
}
|
|
524
595
|
: {}),
|
|
525
596
|
};
|
|
597
|
+
owned.read.assertCurrent();
|
|
598
|
+
return quote;
|
|
526
599
|
}
|
|
527
600
|
catch (error) {
|
|
528
601
|
// Pass through structured errors from syscoinjs-lib
|
|
@@ -568,32 +641,10 @@ class SyscoinTransactionsCore {
|
|
|
568
641
|
}
|
|
569
642
|
};
|
|
570
643
|
// Removed createAndSignSysTransaction - functionality merged into getEstimateSysTransactionFee
|
|
571
|
-
this.
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
// Send the transaction
|
|
576
|
-
const result = await main.send(psbt);
|
|
577
|
-
// Extract the transaction ID
|
|
578
|
-
const txid = result.extractTransaction().getId();
|
|
579
|
-
return { txid };
|
|
580
|
-
}
|
|
581
|
-
catch (error) {
|
|
582
|
-
// Pass through structured errors
|
|
583
|
-
if (error.error && error.code) {
|
|
584
|
-
throw error;
|
|
585
|
-
}
|
|
586
|
-
// Wrap other errors
|
|
587
|
-
throw {
|
|
588
|
-
error: true,
|
|
589
|
-
code: 'TRANSACTION_SEND_FAILED',
|
|
590
|
-
message: error.message || 'Failed to send transaction',
|
|
591
|
-
details: error,
|
|
592
|
-
};
|
|
593
|
-
}
|
|
594
|
-
};
|
|
595
|
-
this.signPSBT = async ({ psbt, isTrezor = false, isLedger = false, }) => {
|
|
596
|
-
const psbtObj = psbt_1.PsbtUtils.fromPali(psbt, this.getState().activeNetwork);
|
|
644
|
+
this.signPSBT = async ({ psbt }, ownedRead) => {
|
|
645
|
+
const { read } = this.captureOwnedRead(ownedRead);
|
|
646
|
+
const psbtObj = psbt_1.PsbtUtils.fromPali(psbt, read.network);
|
|
647
|
+
const unsigned = Buffer.from(psbtObj.data.globalMap.unsignedTx.toBuffer());
|
|
597
648
|
// Scope validation temporarily removes other signers' hints. Preserve the
|
|
598
649
|
// public metadata for the next cosigner, without exposing it to our signer.
|
|
599
650
|
const cosignerMetadata = psbtObj.data.inputs.map((input, index) => ({
|
|
@@ -610,7 +661,10 @@ class SyscoinTransactionsCore {
|
|
|
610
661
|
value: Buffer.from(value.value),
|
|
611
662
|
}))[0],
|
|
612
663
|
}));
|
|
613
|
-
const signedPsbt = await this.signPSBTWithMethod(psbtObj,
|
|
664
|
+
const signedPsbt = await this.signPSBTWithMethod(psbtObj, read);
|
|
665
|
+
read.assertCurrent();
|
|
666
|
+
if (!unsigned.equals(signedPsbt.data.globalMap.unsignedTx.toBuffer()))
|
|
667
|
+
throw new Error('Signed PSBT differs from the prepared transaction');
|
|
614
668
|
signedPsbt.data.inputs.forEach((input, index) => {
|
|
615
669
|
if (input.finalScriptSig || input.finalScriptWitness)
|
|
616
670
|
return;
|
|
@@ -631,21 +685,47 @@ class SyscoinTransactionsCore {
|
|
|
631
685
|
input.unknownKeyVals.push(metadata.path);
|
|
632
686
|
}
|
|
633
687
|
});
|
|
634
|
-
|
|
688
|
+
const result = psbt_1.PsbtUtils.toPali(signedPsbt);
|
|
689
|
+
read.assertCurrent();
|
|
690
|
+
return result;
|
|
635
691
|
};
|
|
636
|
-
this.sendTransaction = async (
|
|
637
|
-
|
|
638
|
-
|
|
692
|
+
this.sendTransaction = async (rawTransaction, authority) => {
|
|
693
|
+
let read;
|
|
694
|
+
let fetch;
|
|
695
|
+
let signal;
|
|
696
|
+
let prepare;
|
|
697
|
+
try {
|
|
698
|
+
if (!authority)
|
|
699
|
+
throw new Error('Missing owned UTXO submission context');
|
|
700
|
+
const { assertCurrent } = authority;
|
|
701
|
+
({ fetch, signal, prepare } = authority);
|
|
702
|
+
if (typeof prepare !== 'function')
|
|
703
|
+
throw new Error('Missing UTXO submission admission');
|
|
704
|
+
({ read } = this.captureOwnedRead({ assertCurrent, fetch, signal }));
|
|
639
705
|
}
|
|
640
|
-
|
|
706
|
+
catch {
|
|
707
|
+
throw Object.assign(new Error('UTXO submission was not dispatched'), {
|
|
708
|
+
code: 'UTXO_SUBMISSION_NOT_DISPATCHED',
|
|
709
|
+
transactionNotBroadcast: true,
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
// SyscoinJS owns the admission, final synchronous guard and exact ACK.
|
|
713
|
+
// The POST uses the captured transport directly so a matching ACK can
|
|
714
|
+
// still be observed after a host context change without a signal abort.
|
|
715
|
+
const { result: txid } = await syscoinjs.utils.sendRawTransaction(read.network.url, rawTransaction, undefined, { assertCurrent: read.assertCurrent, fetch, prepare, signal });
|
|
716
|
+
return { txid };
|
|
641
717
|
};
|
|
718
|
+
if (typeof captureSigningContext !== 'function')
|
|
719
|
+
throw new Error('Missing UTXO signing context');
|
|
642
720
|
this.getSigner = getSyscoinSigner;
|
|
643
721
|
this.getReadOnlySigner = getReadOnlySigner;
|
|
644
722
|
this.getState = getState;
|
|
645
|
-
this.getAddress = getAddress;
|
|
646
723
|
this.hardwareSigners = hardwareSigners;
|
|
647
724
|
this.captureSigningContext = captureSigningContext;
|
|
648
725
|
}
|
|
726
|
+
captureOwnedRead(options) {
|
|
727
|
+
return captureOwnedSyscoinRead(options, this.getState, this.captureSigningContext);
|
|
728
|
+
}
|
|
649
729
|
}
|
|
650
730
|
exports.SyscoinTransactionsCore = SyscoinTransactionsCore;
|
|
651
731
|
//# sourceMappingURL=syscoin-core.js.map
|