@sidhujag/sysweb3-keyring 2.1.2 → 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.
Files changed (148) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +55 -47
  3. package/cjs/current-vault-refusal.js +6 -0
  4. package/cjs/current-vault-refusal.js.map +1 -0
  5. package/cjs/evm-fees.js +29 -1
  6. package/cjs/evm-fees.js.map +1 -1
  7. package/cjs/hardware-transaction-authority.js +36 -0
  8. package/cjs/hardware-transaction-authority.js.map +1 -0
  9. package/cjs/index.js +5 -5
  10. package/cjs/index.js.map +1 -1
  11. package/cjs/keyring-core.js +466 -250
  12. package/cjs/keyring-core.js.map +1 -1
  13. package/cjs/keyring-host.js +113 -0
  14. package/cjs/keyring-host.js.map +1 -0
  15. package/cjs/keyring-manager.js +15 -55
  16. package/cjs/keyring-manager.js.map +1 -1
  17. package/cjs/ledger/host-types.js.map +1 -1
  18. package/cjs/ledger/index.js +44 -19
  19. package/cjs/ledger/index.js.map +1 -1
  20. package/cjs/providers.js +3 -1
  21. package/cjs/providers.js.map +1 -1
  22. package/cjs/software.js +10 -43
  23. package/cjs/software.js.map +1 -1
  24. package/cjs/storage-core.js +44 -4
  25. package/cjs/storage-core.js.map +1 -1
  26. package/cjs/transactions/ethereum.js +358 -294
  27. package/cjs/transactions/ethereum.js.map +1 -1
  28. package/cjs/transactions/evm-cancellation.js +43 -0
  29. package/cjs/transactions/evm-cancellation.js.map +1 -0
  30. package/cjs/transactions/evm-local-signer.js +15 -0
  31. package/cjs/transactions/evm-local-signer.js.map +1 -1
  32. package/cjs/transactions/evm-submission.js +61 -65
  33. package/cjs/transactions/evm-submission.js.map +1 -1
  34. package/cjs/transactions/hardware-psbt.js +11 -8
  35. package/cjs/transactions/hardware-psbt.js.map +1 -1
  36. package/cjs/transactions/index.js +10 -2
  37. package/cjs/transactions/index.js.map +1 -1
  38. package/cjs/transactions/prepared-evm.js +10 -7
  39. package/cjs/transactions/prepared-evm.js.map +1 -1
  40. package/cjs/transactions/syscoin-core.js +282 -202
  41. package/cjs/transactions/syscoin-core.js.map +1 -1
  42. package/cjs/trezor/connect.js +19 -2
  43. package/cjs/trezor/connect.js.map +1 -1
  44. package/cjs/trezor/index.js +38 -16
  45. package/cjs/trezor/index.js.map +1 -1
  46. package/cjs/types.js.map +1 -1
  47. package/cjs/utils/owned-blockbook-account.js +45 -0
  48. package/cjs/utils/owned-blockbook-account.js.map +1 -0
  49. package/esm/current-vault-refusal.js +3 -0
  50. package/esm/current-vault-refusal.js.map +1 -0
  51. package/esm/evm-fees.js +242 -0
  52. package/esm/evm-fees.js.map +1 -0
  53. package/esm/hardware-transaction-authority.js +33 -0
  54. package/esm/hardware-transaction-authority.js.map +1 -0
  55. package/esm/initial-state.js +102 -0
  56. package/esm/initial-state.js.map +1 -0
  57. package/esm/keyring-core.js +2781 -0
  58. package/esm/keyring-core.js.map +1 -0
  59. package/esm/keyring-host.js +108 -0
  60. package/esm/keyring-host.js.map +1 -0
  61. package/esm/ledger/dmk-host.js +31 -0
  62. package/esm/ledger/dmk-host.js.map +1 -0
  63. package/esm/ledger/dmk-selected-host.js +6 -0
  64. package/esm/ledger/dmk-selected-host.js.map +1 -0
  65. package/esm/ledger/dmk-session.js +154 -34
  66. package/esm/ledger/dmk-session.js.map +1 -1
  67. package/esm/ledger/dmk-utxo.js +9 -3
  68. package/esm/ledger/dmk-utxo.js.map +1 -1
  69. package/esm/ledger/host-types.js.map +1 -1
  70. package/esm/ledger/index.js +44 -19
  71. package/esm/ledger/index.js.map +1 -1
  72. package/esm/ledger-host.js +3 -27
  73. package/esm/ledger-host.js.map +1 -1
  74. package/esm/network-utils.js +70 -0
  75. package/esm/network-utils.js.map +1 -0
  76. package/esm/providers.js +3 -1
  77. package/esm/providers.js.map +1 -1
  78. package/esm/software-hd-account.js +20 -0
  79. package/esm/software-hd-account.js.map +1 -0
  80. package/esm/storage-core.js +258 -0
  81. package/esm/storage-core.js.map +1 -0
  82. package/esm/transactions/ethereum.js +2753 -0
  83. package/esm/transactions/ethereum.js.map +1 -0
  84. package/esm/transactions/evm-cancellation.js +40 -0
  85. package/esm/transactions/evm-cancellation.js.map +1 -0
  86. package/esm/transactions/evm-local-signer.js +202 -0
  87. package/esm/transactions/evm-local-signer.js.map +1 -0
  88. package/esm/transactions/evm-submission.js +187 -0
  89. package/esm/transactions/evm-submission.js.map +1 -0
  90. package/esm/transactions/prepared-evm.js +194 -0
  91. package/esm/transactions/prepared-evm.js.map +1 -0
  92. package/esm/transactions/syscoin-core.js +691 -0
  93. package/esm/transactions/syscoin-core.js.map +1 -0
  94. package/esm/trezor/connect.js +19 -2
  95. package/esm/trezor/connect.js.map +1 -1
  96. package/esm/trezor/index.js +38 -16
  97. package/esm/trezor/index.js.map +1 -1
  98. package/esm/types.js.map +1 -1
  99. package/esm/utils/excluded-utxos.js +53 -0
  100. package/esm/utils/excluded-utxos.js.map +1 -0
  101. package/esm/utils/owned-blockbook-account.js +9 -0
  102. package/esm/utils/owned-blockbook-account.js.map +1 -0
  103. package/esm/utils/psbt.js +32 -0
  104. package/esm/utils/psbt.js.map +1 -0
  105. package/esm/utils/watch-descriptor.js +138 -0
  106. package/esm/utils/watch-descriptor.js.map +1 -0
  107. package/package.json +17 -11
  108. package/types/current-vault-refusal.d.ts +2 -0
  109. package/types/evm-fees.d.ts +25 -0
  110. package/types/hardware-transaction-authority.d.ts +5 -0
  111. package/types/index.d.ts +3 -3
  112. package/types/keyring-core.d.ts +48 -44
  113. package/types/keyring-host.d.ts +46 -0
  114. package/types/keyring-manager.d.ts +14 -7
  115. package/types/ledger/dmk-host.d.ts +7 -0
  116. package/types/ledger/dmk-selected-host.d.ts +7 -0
  117. package/types/ledger/dmk-session.d.ts +10 -2
  118. package/types/ledger/host-types.d.ts +4 -1
  119. package/types/ledger/index.d.ts +1 -1
  120. package/types/ledger/types.d.ts +3 -2
  121. package/types/ledger-host.d.ts +6 -2
  122. package/types/providers.d.ts +1 -1
  123. package/types/software.d.ts +7 -19
  124. package/types/storage-core.d.ts +3 -1
  125. package/types/transactions/ethereum.d.ts +14 -11
  126. package/types/transactions/evm-cancellation.d.ts +10 -0
  127. package/types/transactions/evm-local-signer.d.ts +2 -2
  128. package/types/transactions/evm-submission.d.ts +11 -4
  129. package/types/transactions/hardware-psbt.d.ts +1 -1
  130. package/types/transactions/index.d.ts +3 -2
  131. package/types/transactions/prepared-evm.d.ts +4 -1
  132. package/types/transactions/syscoin-core.d.ts +48 -21
  133. package/types/trezor/connect.d.ts +6 -3
  134. package/types/trezor/index.d.ts +4 -3
  135. package/types/types.d.ts +95 -63
  136. package/types/utils/owned-blockbook-account.d.ts +3 -0
  137. package/cjs/hardware-wallet-manager-singleton.js +0 -34
  138. package/cjs/hardware-wallet-manager-singleton.js.map +0 -1
  139. package/cjs/storage.js +0 -20
  140. package/cjs/storage.js.map +0 -1
  141. package/cjs/transactions/syscoin.js +0 -16
  142. package/cjs/transactions/syscoin.js.map +0 -1
  143. package/cjs/utils/blockbook-cache.js +0 -76
  144. package/cjs/utils/blockbook-cache.js.map +0 -1
  145. package/types/hardware-wallet-manager-singleton.d.ts +0 -19
  146. package/types/storage.d.ts +0 -5
  147. package/types/transactions/syscoin.d.ts +0 -9
  148. 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,26 +189,17 @@ 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();
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, excludedOutpoints = [], }) => {
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 { 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
- }
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 this.getAddress(xpub, true);
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, isTrezor, isLedger = false) => {
310
- const { activeNetwork, activeAccountId, activeAccountType, accounts } = this.getState();
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
- 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.
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: Object.freeze({ ...activeNetwork }),
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: this.txUtilsFunctions().getRawTransaction,
423
+ getRawTransaction: (txid) => syscoinjs.utils.fetchBackendRawTx(activeNetwork.url, txid, transport),
364
424
  }),
365
425
  });
366
- assertCurrent();
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
- 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) => {
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
- 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
- });
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
- catch (_e) {
468
- // Best-effort enrichment; if it fails, fall back to default signing error path.
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
- // 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);
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
- excludedOutpoints,
515
- });
516
- return {
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.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);
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, isTrezor, isLedger);
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
- return psbt_1.PsbtUtils.toPali(signedPsbt);
688
+ const result = psbt_1.PsbtUtils.toPali(signedPsbt);
689
+ read.assertCurrent();
690
+ return result;
635
691
  };
636
- this.sendTransaction = async (psbt) => {
637
- if (!psbt) {
638
- throw new Error('Signed PSBT is required for broadcasting.');
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
- return await this.sendSignedTransaction(psbt_1.PsbtUtils.fromPali(psbt, this.getState().activeNetwork));
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