@sidhujag/sysweb3-keyring 2.1.3 → 2.1.5

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.
Files changed (129) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +47 -47
  3. package/cjs/hardware-transaction-authority.js +36 -0
  4. package/cjs/hardware-transaction-authority.js.map +1 -0
  5. package/cjs/index.js +5 -5
  6. package/cjs/index.js.map +1 -1
  7. package/cjs/keyring-core.js +171 -220
  8. package/cjs/keyring-core.js.map +1 -1
  9. package/cjs/keyring-host.js +113 -0
  10. package/cjs/keyring-host.js.map +1 -0
  11. package/cjs/keyring-manager.js +15 -151
  12. package/cjs/keyring-manager.js.map +1 -1
  13. package/cjs/ledger/host-types.js.map +1 -1
  14. package/cjs/ledger/index.js +44 -19
  15. package/cjs/ledger/index.js.map +1 -1
  16. package/cjs/providers.js +3 -1
  17. package/cjs/providers.js.map +1 -1
  18. package/cjs/software.js +10 -56
  19. package/cjs/software.js.map +1 -1
  20. package/cjs/transactions/ethereum.js +306 -229
  21. package/cjs/transactions/ethereum.js.map +1 -1
  22. package/cjs/transactions/evm-local-signer.js +15 -0
  23. package/cjs/transactions/evm-local-signer.js.map +1 -1
  24. package/cjs/transactions/evm-submission.js +61 -65
  25. package/cjs/transactions/evm-submission.js.map +1 -1
  26. package/cjs/transactions/hardware-psbt.js +11 -8
  27. package/cjs/transactions/hardware-psbt.js.map +1 -1
  28. package/cjs/transactions/index.js +10 -2
  29. package/cjs/transactions/index.js.map +1 -1
  30. package/cjs/transactions/prepared-evm.js +10 -7
  31. package/cjs/transactions/prepared-evm.js.map +1 -1
  32. package/cjs/transactions/syscoin-core.js +308 -205
  33. package/cjs/transactions/syscoin-core.js.map +1 -1
  34. package/cjs/trezor/connect.js +19 -2
  35. package/cjs/trezor/connect.js.map +1 -1
  36. package/cjs/trezor/index.js +38 -16
  37. package/cjs/trezor/index.js.map +1 -1
  38. package/cjs/types.js.map +1 -1
  39. package/cjs/utils/excluded-utxos.js +6 -2
  40. package/cjs/utils/excluded-utxos.js.map +1 -1
  41. package/cjs/utils/owned-blockbook-account.js +45 -0
  42. package/cjs/utils/owned-blockbook-account.js.map +1 -0
  43. package/esm/current-vault-refusal.js +3 -0
  44. package/esm/current-vault-refusal.js.map +1 -0
  45. package/esm/evm-fees.js +242 -0
  46. package/esm/evm-fees.js.map +1 -0
  47. package/esm/hardware-transaction-authority.js +33 -0
  48. package/esm/hardware-transaction-authority.js.map +1 -0
  49. package/esm/initial-state.js +102 -0
  50. package/esm/initial-state.js.map +1 -0
  51. package/esm/keyring-core.js +2781 -0
  52. package/esm/keyring-core.js.map +1 -0
  53. package/esm/keyring-host.js +108 -0
  54. package/esm/keyring-host.js.map +1 -0
  55. package/esm/ledger/dmk-session.js +1 -0
  56. package/esm/ledger/dmk-session.js.map +1 -1
  57. package/esm/ledger/dmk-utxo.js +9 -3
  58. package/esm/ledger/dmk-utxo.js.map +1 -1
  59. package/esm/ledger/host-types.js.map +1 -1
  60. package/esm/ledger/index.js +44 -19
  61. package/esm/ledger/index.js.map +1 -1
  62. package/esm/network-utils.js +70 -0
  63. package/esm/network-utils.js.map +1 -0
  64. package/esm/providers.js +3 -1
  65. package/esm/providers.js.map +1 -1
  66. package/esm/software-hd-account.js +20 -0
  67. package/esm/software-hd-account.js.map +1 -0
  68. package/esm/storage-core.js +258 -0
  69. package/esm/storage-core.js.map +1 -0
  70. package/esm/transactions/ethereum.js +2753 -0
  71. package/esm/transactions/ethereum.js.map +1 -0
  72. package/esm/transactions/evm-cancellation.js +40 -0
  73. package/esm/transactions/evm-cancellation.js.map +1 -0
  74. package/esm/transactions/evm-local-signer.js +202 -0
  75. package/esm/transactions/evm-local-signer.js.map +1 -0
  76. package/esm/transactions/evm-submission.js +187 -0
  77. package/esm/transactions/evm-submission.js.map +1 -0
  78. package/esm/transactions/prepared-evm.js +194 -0
  79. package/esm/transactions/prepared-evm.js.map +1 -0
  80. package/esm/transactions/syscoin-core.js +714 -0
  81. package/esm/transactions/syscoin-core.js.map +1 -0
  82. package/esm/trezor/connect.js +19 -2
  83. package/esm/trezor/connect.js.map +1 -1
  84. package/esm/trezor/index.js +38 -16
  85. package/esm/trezor/index.js.map +1 -1
  86. package/esm/types.js.map +1 -1
  87. package/esm/utils/excluded-utxos.js +57 -0
  88. package/esm/utils/excluded-utxos.js.map +1 -0
  89. package/esm/utils/owned-blockbook-account.js +9 -0
  90. package/esm/utils/owned-blockbook-account.js.map +1 -0
  91. package/esm/utils/psbt.js +32 -0
  92. package/esm/utils/psbt.js.map +1 -0
  93. package/esm/utils/watch-descriptor.js +138 -0
  94. package/esm/utils/watch-descriptor.js.map +1 -0
  95. package/package.json +5 -11
  96. package/types/hardware-transaction-authority.d.ts +5 -0
  97. package/types/index.d.ts +3 -3
  98. package/types/keyring-core.d.ts +22 -51
  99. package/types/keyring-host.d.ts +46 -0
  100. package/types/keyring-manager.d.ts +13 -18
  101. package/types/ledger/host-types.d.ts +4 -1
  102. package/types/ledger/index.d.ts +1 -1
  103. package/types/ledger/types.d.ts +3 -2
  104. package/types/providers.d.ts +1 -1
  105. package/types/software.d.ts +6 -19
  106. package/types/transactions/ethereum.d.ts +14 -11
  107. package/types/transactions/evm-local-signer.d.ts +2 -2
  108. package/types/transactions/evm-submission.d.ts +11 -4
  109. package/types/transactions/hardware-psbt.d.ts +1 -1
  110. package/types/transactions/index.d.ts +3 -2
  111. package/types/transactions/prepared-evm.d.ts +4 -1
  112. package/types/transactions/syscoin-core.d.ts +48 -21
  113. package/types/trezor/connect.d.ts +6 -3
  114. package/types/trezor/index.d.ts +4 -3
  115. package/types/types.d.ts +95 -63
  116. package/types/utils/excluded-utxos.d.ts +1 -1
  117. package/types/utils/owned-blockbook-account.d.ts +3 -0
  118. package/cjs/hardware-wallet-manager-singleton.js +0 -34
  119. package/cjs/hardware-wallet-manager-singleton.js.map +0 -1
  120. package/cjs/storage.js +0 -20
  121. package/cjs/storage.js.map +0 -1
  122. package/cjs/transactions/syscoin.js +0 -16
  123. package/cjs/transactions/syscoin.js.map +0 -1
  124. package/cjs/utils/blockbook-cache.js +0 -76
  125. package/cjs/utils/blockbook-cache.js.map +0 -1
  126. package/types/hardware-wallet-manager-singleton.d.ts +0 -19
  127. package/types/storage.d.ts +0 -5
  128. package/types/transactions/syscoin.d.ts +0 -9
  129. 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
- const sysweb3_utils_1 = require("@sidhujag/sysweb3-utils");
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, getAddress, hardwareSigners = {}, captureSigningContext) {
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,28 +189,27 @@ class SyscoinTransactionsCore {
108
189
  return main.decodeRawTransaction(psbtObj);
109
190
  }
110
191
  };
111
- this.feeCache = new Map();
112
- this.getRecommendedFee = async (explorerUrl) => {
113
- const cached = this.feeCache.get(explorerUrl);
114
- if (cached && Date.now() - cached.timestamp < FEE_CACHE_TTL_MS) {
115
- return cached.fee;
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();
203
+ const excludedNativeValues = new Map();
131
204
  const rows = excluded.size
132
- ? (0, excluded_utxos_1.filterExcludedUtxos)(raw, excluded).utxos
205
+ ? (0, excluded_utxos_1.filterExcludedUtxos)(raw, excluded, (row, outpoint) => {
206
+ const unambiguous = !excludedNativeValues.has(outpoint) &&
207
+ row.assetInfo == null &&
208
+ (row.txId === undefined ||
209
+ row.txid === undefined ||
210
+ row.txId === row.txid);
211
+ excludedNativeValues.set(outpoint, unambiguous ? row.value : undefined);
212
+ }).utxos
133
213
  : Array.isArray(raw)
134
214
  ? raw
135
215
  : raw?.utxos;
@@ -164,8 +244,23 @@ class SyscoinTransactionsCore {
164
244
  gross = gross.add(value);
165
245
  utxos.push({ ...row, value });
166
246
  }
167
- if (gross.isZero())
168
- throw sweepError('INSUFFICIENT_FUNDS', 'No native UTXOs are available to sweep.');
247
+ if (gross.isZero()) {
248
+ const error = sweepError('INSUFFICIENT_FUNDS', 'No native UTXOs are available to sweep.');
249
+ const excludedNativeOutpoints = [...excludedNativeValues].flatMap(([outpoint, value]) => {
250
+ if (typeof value === 'number')
251
+ return Number.isSafeInteger(value) && value > 0 ? [outpoint] : [];
252
+ if (typeof value === 'string' &&
253
+ value.length <= 20 &&
254
+ !/[^0-9]/.test(value) &&
255
+ /[1-9]/.test(value) &&
256
+ (value.length < 20 || value <= '18446744073709551615'))
257
+ return [outpoint];
258
+ return [];
259
+ });
260
+ if (excludedNativeOutpoints.length)
261
+ throw { ...error, excludedNativeOutpoints };
262
+ throw error;
263
+ }
169
264
  // The library sanitizes this snapshot once; its optional UTXO argument
170
265
  // prevents a second funding lookup between resolving MAX and building it.
171
266
  const result = await main.createTransaction(txOptions, changeAddress, [{ address: receivingAddress, value: gross, subtractFeeFrom: true }], feeRateBN, source, { utxos, assets: new Map() });
@@ -195,16 +290,10 @@ class SyscoinTransactionsCore {
195
290
  };
196
291
  };
197
292
  // Create unsigned PSBT for any transaction type
198
- this.createUnsignedPSBT = async ({ txOptions = {}, isMax = false, nativeSweep = false, amount, receivingAddress, feeRateBN, token = null, excludedOutpoints = [], }) => {
293
+ this.createUnsignedPSBT = async ({ txOptions = {}, isMax = false, nativeSweep = false, amount, receivingAddress, feeRateBN, token = null, }, owned) => {
199
294
  // Ensure RBF is enabled by default if not explicitly set
200
295
  const finalTxOptions = { rbf: true, ...txOptions };
201
- const { activeAccountId, accounts, activeAccountType } = this.getState();
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
- }
296
+ const { account, accountType: activeAccountType, excluded, main, read, } = owned;
208
297
  const xpub = account.xpub;
209
298
  const isSingleAddressImported = activeAccountType === types_1.KeyringAccountType.Imported &&
210
299
  xpub === account.address;
@@ -231,9 +320,9 @@ class SyscoinTransactionsCore {
231
320
  // If this is a single-address imported account (WIF import), use the account.address for both from and change
232
321
  const changeAddress = isSingleAddressImported
233
322
  ? account.address
234
- : await this.getAddress(xpub, true);
323
+ : await owned.getOwnedAddress(xpub, true);
324
+ read.assertCurrent();
235
325
  try {
236
- const excluded = (0, excluded_utxos_1.excludedOutpointIds)(excludedOutpoints);
237
326
  if (nativeSweep) {
238
327
  return await this.createNativeSweepPSBT({
239
328
  txOptions: finalTxOptions,
@@ -242,11 +331,12 @@ class SyscoinTransactionsCore {
242
331
  feeRateBN,
243
332
  source: sourceForUtxoLookup,
244
333
  excluded,
245
- }, main);
334
+ }, main, read);
246
335
  }
247
336
  const fundingUtxos = excluded.size
248
- ? (0, excluded_utxos_1.filterExcludedUtxos)(await syscoinjs.utils.fetchBackendUTXOS(main.blockbookURL, sourceForUtxoLookup), excluded)
337
+ ? (0, excluded_utxos_1.filterExcludedUtxos)(await syscoinjs.utils.fetchBackendUTXOS(main.blockbookURL, sourceForUtxoLookup, undefined, read.transport), excluded)
249
338
  : undefined;
339
+ read.assertCurrent();
250
340
  if (token && token.guid) {
251
341
  // Token transaction: use assetAllocationSend
252
342
  // Create a Map for the asset allocation
@@ -306,43 +396,24 @@ class SyscoinTransactionsCore {
306
396
  }
307
397
  };
308
398
  // Sign PSBT with appropriate method - separated for better error handling
309
- this.signPSBTWithMethod = async (psbt, isTrezor, isLedger = false) => {
310
- const { activeNetwork, activeAccountId, activeAccountType, accounts } = this.getState();
399
+ this.signPSBTWithMethod = async (psbt, ownedRead) => {
400
+ const { assertCurrent, network: activeNetwork, transport } = ownedRead;
401
+ assertCurrent();
402
+ const { activeAccountId, activeAccountType, accounts } = this.getState();
311
403
  const account = accounts[activeAccountType]?.[activeAccountId];
312
404
  if (!account)
313
405
  throw new Error('Active account not found');
314
406
  // Device choice belongs to the selected account, never to PSBT/caller flags.
315
- isTrezor =
316
- activeAccountType === types_1.KeyringAccountType.Trezor ||
317
- account.isTrezorWallet === true;
318
- isLedger =
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.
407
+ const isTrezor = activeAccountType === types_1.KeyringAccountType.Trezor ||
408
+ account.isTrezorWallet === true;
409
+ const isLedger = activeAccountType === types_1.KeyringAccountType.Ledger ||
410
+ account.isLedgerWallet === true;
324
411
  const scope = Object.freeze({
325
412
  account: Object.freeze({ address: account.address, xpub: account.xpub }),
326
413
  accountId: activeAccountId,
327
414
  accountType: activeAccountType,
328
- network: Object.freeze({ ...activeNetwork }),
415
+ network: activeNetwork,
329
416
  });
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
417
  const sign = isLedger
347
418
  ? this.hardwareSigners.ledger
348
419
  : isTrezor
@@ -353,17 +424,29 @@ class SyscoinTransactionsCore {
353
424
  if (isTrezor || isLedger)
354
425
  (0, psbt_account_scope_1.assertPsbtAccountScope)(psbt, scope);
355
426
  if (isLedger || isTrezor) {
427
+ const assertHardwareCurrent = () => {
428
+ try {
429
+ assertCurrent();
430
+ }
431
+ catch {
432
+ // The vendor retry loop already treats AbortError as terminal.
433
+ throw Object.assign(new Error('Wallet signing context changed'), {
434
+ name: 'AbortError',
435
+ });
436
+ }
437
+ };
356
438
  const signed = await sign({
357
439
  psbt,
358
440
  account: Object.freeze({ id: account.id, xpub: account.xpub }),
359
441
  network: scope.network,
360
442
  scope,
361
- assertCurrent,
443
+ assertCurrent: assertHardwareCurrent,
444
+ signal: transport.signal,
362
445
  ...(isLedger && {
363
- getRawTransaction: this.txUtilsFunctions().getRawTransaction,
446
+ getRawTransaction: (txid) => syscoinjs.utils.fetchBackendRawTx(activeNetwork.url, txid, transport),
364
447
  }),
365
448
  });
366
- assertCurrent();
449
+ assertHardwareCurrent();
367
450
  return signed;
368
451
  }
369
452
  else {
@@ -373,99 +456,94 @@ class SyscoinTransactionsCore {
373
456
  //
374
457
  // We can recover by enriching PSBT inputs with a proprietary `path` field using
375
458
  // Blockbook's xpub token list (address -> path mapping), which reliably includes paths.
376
- try {
377
- const account = accounts[activeAccountType]?.[activeAccountId];
378
- const accountXpub = account?.xpub;
379
- const accountAddress = account?.address;
380
- const isSingleAddressImported = activeAccountType === types_1.KeyringAccountType.Imported &&
381
- Boolean(accountXpub) &&
382
- Boolean(accountAddress) &&
383
- accountXpub === accountAddress;
384
- const isImportedExtended = activeAccountType === types_1.KeyringAccountType.Imported &&
385
- Boolean(accountXpub) &&
386
- !isSingleAddressImported;
387
- // IMPORTANT: Imported extended keys (zprv/vprv) are typically *account-level* keys.
388
- // syscoinjs-lib signs using the imported key as the BIP32 root, so PSBT `path` must be
389
- // relative (change/index), not the full `m/84'/.../change/index`. If we keep full paths,
390
- // bip32.derivePath can throw, leaving no derivations and causing "Need derivation path".
391
- if (isImportedExtended) {
392
- const inputs = psbt?.data?.inputs || [];
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) => {
459
+ const account = accounts[activeAccountType]?.[activeAccountId];
460
+ const accountXpub = account?.xpub;
461
+ const accountAddress = account?.address;
462
+ const isSingleAddressImported = activeAccountType === types_1.KeyringAccountType.Imported &&
463
+ Boolean(accountXpub) &&
464
+ Boolean(accountAddress) &&
465
+ accountXpub === accountAddress;
466
+ const isImportedExtended = activeAccountType === types_1.KeyringAccountType.Imported &&
467
+ Boolean(accountXpub) &&
468
+ !isSingleAddressImported;
469
+ // IMPORTANT: Imported extended keys (zprv/vprv) are typically *account-level* keys.
470
+ // syscoinjs-lib signs using the imported key as the BIP32 root, so PSBT `path` must be
471
+ // relative (change/index), not the full `m/84'/.../change/index`. If we keep full paths,
472
+ // bip32.derivePath can throw, leaving no derivations and causing "Need derivation path".
473
+ if (isImportedExtended) {
474
+ const inputs = psbt?.data?.inputs || [];
475
+ for (const input of inputs) {
410
476
  const unknowns = input?.unknownKeyVals || [];
411
- return (!input.bip32Derivation?.length &&
412
- !input.tapBip32Derivation?.length &&
413
- !unknowns.some((kv) => kv?.key?.toString?.() === 'path'));
414
- });
415
- if (needsAnyPath &&
416
- accountXpub &&
417
- !isSingleAddressImported &&
418
- typeof psbt.addUnknownKeyValToInput === 'function') {
419
- // Fetch address->path mapping from Blockbook xpub endpoint
420
- // (in-flight deduplicated with the keyring's getAddress lookups)
421
- const blockbookUrl = activeNetwork.url;
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
- });
477
+ const pathKv = unknowns.find((kv) => kv?.key?.toString?.() === 'path');
478
+ if (!pathKv?.value)
479
+ continue;
480
+ const fullPath = String(Buffer.from(pathKv.value).toString());
481
+ const parts = fullPath.split('/').filter(Boolean);
482
+ // Expected: m/84'/slip44'/account'/change/index
483
+ if (parts.length >= 2) {
484
+ const change = parts[parts.length - 2];
485
+ const index = parts[parts.length - 1];
486
+ const rel = `${change}/${index}`;
487
+ pathKv.value = Buffer.from(rel);
464
488
  }
465
489
  }
466
490
  }
467
- catch (_e) {
468
- // Best-effort enrichment; if it fails, fall back to default signing error path.
491
+ const needsAnyPath = psbt?.data?.inputs?.some((input) => {
492
+ const unknowns = input?.unknownKeyVals || [];
493
+ return (!input.bip32Derivation?.length &&
494
+ !input.tapBip32Derivation?.length &&
495
+ !unknowns.some((kv) => kv?.key?.toString?.() === 'path'));
496
+ });
497
+ if (needsAnyPath &&
498
+ accountXpub &&
499
+ !isSingleAddressImported &&
500
+ typeof psbt.addUnknownKeyValToInput === 'function') {
501
+ // Fetch address->path mapping from Blockbook xpub endpoint
502
+ const blockbookUrl = activeNetwork.url;
503
+ const res = await (0, owned_blockbook_account_1.fetchOwnedBlockbookAccount)(blockbookUrl, accountXpub, transport);
504
+ assertCurrent();
505
+ const tokens = Array.isArray(res?.tokens) ? res.tokens : [];
506
+ const addrToPath = new Map();
507
+ for (const t of tokens) {
508
+ if (t?.name && t?.path) {
509
+ addrToPath.set(String(t.name), String(t.path));
510
+ }
511
+ }
512
+ const inputs = psbt?.data?.inputs || [];
513
+ for (let i = 0; i < inputs.length; i++) {
514
+ const input = inputs[i];
515
+ const unknowns = input?.unknownKeyVals || [];
516
+ const hasPath = unknowns.some((kv) => kv?.key?.toString?.() === 'path');
517
+ if (hasPath)
518
+ continue;
519
+ const addrKv = unknowns.find((kv) => kv?.key?.toString?.() === 'address');
520
+ const addr = addrKv?.value
521
+ ? String(Buffer.from(addrKv.value).toString())
522
+ : null;
523
+ if (!addr)
524
+ continue;
525
+ const fullPath = addrToPath.get(addr);
526
+ if (!fullPath)
527
+ continue;
528
+ // Imported extended keys are account-level (BIP84 account node), so we must provide a
529
+ // path relative to that node (change/index), not the full m/84'/... path.
530
+ const pathForSigner = isImportedExtended
531
+ ? (() => {
532
+ const parts = String(fullPath).split('/').filter(Boolean);
533
+ // Expected: m/84'/slip44'/account'/change/index
534
+ if (parts.length >= 2) {
535
+ const change = parts[parts.length - 2];
536
+ const index = parts[parts.length - 1];
537
+ return `${change}/${index}`;
538
+ }
539
+ return fullPath;
540
+ })()
541
+ : fullPath;
542
+ psbt.addUnknownKeyValToInput(i, {
543
+ key: Buffer.from('path'),
544
+ value: Buffer.from(pathForSigner),
545
+ });
546
+ }
469
547
  }
470
548
  assertCurrent();
471
549
  (0, psbt_account_scope_1.assertPsbtAccountScope)(psbt, scope);
@@ -479,23 +557,39 @@ class SyscoinTransactionsCore {
479
557
  }
480
558
  };
481
559
  // Create unsigned PSBT and estimate fee - NO SIGNING
482
- this.getEstimateSysTransactionFee = async ({ txOptions = {}, isMax = false, nativeSweep = false, amount, receivingAddress, feeRate, token = null, excludedOutpoints = [], }) => {
560
+ this.getEstimateSysTransactionFee = async ({ txOptions = {}, isMax = false, nativeSweep = false, amount, receivingAddress, feeRate, token = null, excludedOutpoints = [], }, ownedRead) => {
483
561
  try {
484
562
  if (nativeSweep !== false &&
485
563
  (nativeSweep !== true || isMax !== true || token !== null))
486
564
  throw sweepError('INVALID_NATIVE_SWEEP', 'Native sweep requires explicit MAX and a native transaction.');
487
565
  // Ensure RBF is enabled by default if not explicitly set
488
566
  const finalTxOptions = { rbf: true, ...txOptions };
489
- // Use read-only signer since we're just estimating fees and creating unsigned PSBT
490
- const { main } = this.getReadOnlySigner();
491
- // Step 1: Determine fee rate
492
- let actualFeeRate;
493
- if (feeRate !== undefined) {
494
- actualFeeRate = feeRate;
495
- }
496
- else {
497
- actualFeeRate = await this.getRecommendedFee(main.blockbookURL);
567
+ const preparation = this.captureOwnedRead(ownedRead);
568
+ const { main, getOwnedAddress } = this.getReadOnlySigner(preparation.read);
569
+ if (typeof getOwnedAddress !== 'function' ||
570
+ main.blockbookURL !== preparation.read.network.url)
571
+ throw new Error('Owned UTXO read adapter is unavailable');
572
+ const owned = {
573
+ ...preparation,
574
+ main,
575
+ getOwnedAddress,
576
+ excluded: (0, excluded_utxos_1.excludedOutpointIds)(excludedOutpoints),
577
+ };
578
+ const selectedToken = token ? { ...token } : token;
579
+ const { assetWhiteList } = finalTxOptions;
580
+ if (assetWhiteList instanceof Map)
581
+ finalTxOptions.assetWhiteList = new Map(assetWhiteList);
582
+ else if (assetWhiteList instanceof Set)
583
+ finalTxOptions.assetWhiteList = new Set(assetWhiteList);
584
+ for (const key of ['memo', 'memoHeader', 'blobHash', 'blobData']) {
585
+ const bytes = finalTxOptions[key];
586
+ if (bytes instanceof Uint8Array)
587
+ finalTxOptions[key] = Buffer.from(bytes);
498
588
  }
589
+ const actualFeeRate = feeRate === undefined
590
+ ? await readOwnedFee(main.blockbookURL, owned.read)
591
+ : feeRate;
592
+ owned.read.assertCurrent();
499
593
  // Convert fee rate to satoshis/byte and ensure minimum relay of 1 sat/vB
500
594
  // Blockbook returns coins per kB; after division by 1024, some testnets
501
595
  // can yield < 1 sat/vB (e.g., 0.9765625). Round up and clamp to 1 to
@@ -510,10 +604,10 @@ class SyscoinTransactionsCore {
510
604
  amount,
511
605
  receivingAddress,
512
606
  feeRateBN,
513
- token,
514
- excludedOutpoints,
515
- });
516
- return {
607
+ token: selectedToken,
608
+ }, owned);
609
+ owned.read.assertCurrent();
610
+ const quote = {
517
611
  fee: result.fee / 1e8,
518
612
  psbt: psbt_1.PsbtUtils.toPali(result.psbt), // Return UNSIGNED PSBT as JSON
519
613
  ...(nativeSweep
@@ -523,6 +617,8 @@ class SyscoinTransactionsCore {
523
617
  }
524
618
  : {}),
525
619
  };
620
+ owned.read.assertCurrent();
621
+ return quote;
526
622
  }
527
623
  catch (error) {
528
624
  // Pass through structured errors from syscoinjs-lib
@@ -568,32 +664,10 @@ class SyscoinTransactionsCore {
568
664
  }
569
665
  };
570
666
  // Removed createAndSignSysTransaction - functionality merged into getEstimateSysTransactionFee
571
- this.sendSignedTransaction = async (psbt) => {
572
- try {
573
- // Use read-only signer since we're just broadcasting an already-signed transaction
574
- const { main } = this.getReadOnlySigner();
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);
667
+ this.signPSBT = async ({ psbt }, ownedRead) => {
668
+ const { read } = this.captureOwnedRead(ownedRead);
669
+ const psbtObj = psbt_1.PsbtUtils.fromPali(psbt, read.network);
670
+ const unsigned = Buffer.from(psbtObj.data.globalMap.unsignedTx.toBuffer());
597
671
  // Scope validation temporarily removes other signers' hints. Preserve the
598
672
  // public metadata for the next cosigner, without exposing it to our signer.
599
673
  const cosignerMetadata = psbtObj.data.inputs.map((input, index) => ({
@@ -610,7 +684,10 @@ class SyscoinTransactionsCore {
610
684
  value: Buffer.from(value.value),
611
685
  }))[0],
612
686
  }));
613
- const signedPsbt = await this.signPSBTWithMethod(psbtObj, isTrezor, isLedger);
687
+ const signedPsbt = await this.signPSBTWithMethod(psbtObj, read);
688
+ read.assertCurrent();
689
+ if (!unsigned.equals(Buffer.from(signedPsbt.data.globalMap.unsignedTx.toBuffer())))
690
+ throw new Error('Signed PSBT differs from the prepared transaction');
614
691
  signedPsbt.data.inputs.forEach((input, index) => {
615
692
  if (input.finalScriptSig || input.finalScriptWitness)
616
693
  return;
@@ -631,21 +708,47 @@ class SyscoinTransactionsCore {
631
708
  input.unknownKeyVals.push(metadata.path);
632
709
  }
633
710
  });
634
- return psbt_1.PsbtUtils.toPali(signedPsbt);
711
+ const result = psbt_1.PsbtUtils.toPali(signedPsbt);
712
+ read.assertCurrent();
713
+ return result;
635
714
  };
636
- this.sendTransaction = async (psbt) => {
637
- if (!psbt) {
638
- throw new Error('Signed PSBT is required for broadcasting.');
715
+ this.sendTransaction = async (rawTransaction, authority) => {
716
+ let read;
717
+ let fetch;
718
+ let signal;
719
+ let prepare;
720
+ try {
721
+ if (!authority)
722
+ throw new Error('Missing owned UTXO submission context');
723
+ const { assertCurrent } = authority;
724
+ ({ fetch, signal, prepare } = authority);
725
+ if (typeof prepare !== 'function')
726
+ throw new Error('Missing UTXO submission admission');
727
+ ({ read } = this.captureOwnedRead({ assertCurrent, fetch, signal }));
639
728
  }
640
- return await this.sendSignedTransaction(psbt_1.PsbtUtils.fromPali(psbt, this.getState().activeNetwork));
729
+ catch {
730
+ throw Object.assign(new Error('UTXO submission was not dispatched'), {
731
+ code: 'UTXO_SUBMISSION_NOT_DISPATCHED',
732
+ transactionNotBroadcast: true,
733
+ });
734
+ }
735
+ // SyscoinJS owns the admission, final synchronous guard and exact ACK.
736
+ // The POST uses the captured transport directly so a matching ACK can
737
+ // still be observed after a host context change without a signal abort.
738
+ const { result: txid } = await syscoinjs.utils.sendRawTransaction(read.network.url, rawTransaction, undefined, { assertCurrent: read.assertCurrent, fetch, prepare, signal });
739
+ return { txid };
641
740
  };
741
+ if (typeof captureSigningContext !== 'function')
742
+ throw new Error('Missing UTXO signing context');
642
743
  this.getSigner = getSyscoinSigner;
643
744
  this.getReadOnlySigner = getReadOnlySigner;
644
745
  this.getState = getState;
645
- this.getAddress = getAddress;
646
746
  this.hardwareSigners = hardwareSigners;
647
747
  this.captureSigningContext = captureSigningContext;
648
748
  }
749
+ captureOwnedRead(options) {
750
+ return captureOwnedSyscoinRead(options, this.getState, this.captureSigningContext);
751
+ }
649
752
  }
650
753
  exports.SyscoinTransactionsCore = SyscoinTransactionsCore;
651
754
  //# sourceMappingURL=syscoin-core.js.map