@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
@@ -0,0 +1,691 @@
1
+ import { INetworkType } from '@sidhujag/sysweb3-network';
2
+ import * as syscoinjs from 'syscoinjs-lib';
3
+ // import { BIP_84, ONE_HUNDRED_MILLION, SYSCOIN_BASIC_FEE } from 'utils';
4
+ import { formatUnits } from '../ethers-v6';
5
+ import { KeyringAccountType, } from '../types';
6
+ import { excludedOutpointIds, filterExcludedUtxos, assertNoExcludedInputs, } from '../utils/excluded-utxos';
7
+ import { fetchOwnedBlockbookAccount } from '../utils/owned-blockbook-account';
8
+ import { PsbtUtils } from '../utils/psbt';
9
+ import { assertPsbtAccountScope, } from '../utils/psbt-account-scope';
10
+ export const createHardwareSignerUnavailableError = (hardware) => Object.assign(new Error(`${hardware} signer is unavailable in this host`), {
11
+ code: 'HARDWARE_SIGNER_UNAVAILABLE',
12
+ });
13
+ const sweepError = (code, message) => ({
14
+ error: true,
15
+ code,
16
+ message,
17
+ });
18
+ const sweepAtomicValue = (value) => {
19
+ const text = typeof value === 'number'
20
+ ? Number.isSafeInteger(value)
21
+ ? String(value)
22
+ : ''
23
+ : typeof value === 'string' ||
24
+ typeof value === 'bigint' ||
25
+ syscoinjs.utils.BN.isBN(value)
26
+ ? value.toString()
27
+ : '';
28
+ if (!/^\d+$/.test(text))
29
+ throw sweepError('INVALID_NATIVE_SWEEP_UTXOS', 'Invalid native sweep atomic value.');
30
+ return new syscoinjs.utils.BN(text);
31
+ };
32
+ function captureBlockbookRead(options, assertContext) {
33
+ if (!options)
34
+ throw new Error('Missing owned UTXO read');
35
+ const { fetch, signal, assertCurrent: assertHost } = options;
36
+ if (typeof fetch !== 'function' ||
37
+ typeof assertHost !== 'function' ||
38
+ typeof signal?.aborted !== 'boolean')
39
+ throw new Error('Invalid owned UTXO read');
40
+ const assertCurrent = () => {
41
+ if (signal.aborted)
42
+ throw new Error('Wallet UTXO preparation context changed');
43
+ const result = assertHost();
44
+ if (result !== undefined) {
45
+ if (result &&
46
+ (typeof result === 'object' || typeof result === 'function'))
47
+ void Promise.resolve(result).catch(() => undefined);
48
+ throw new Error('Owned UTXO read requires a synchronous guard');
49
+ }
50
+ assertContext();
51
+ if (signal.aborted)
52
+ throw new Error('Wallet UTXO preparation context changed');
53
+ };
54
+ assertCurrent();
55
+ const transport = Object.freeze({
56
+ signal,
57
+ async fetch(input, init) {
58
+ assertCurrent();
59
+ const response = await fetch(input, init);
60
+ assertCurrent();
61
+ return response;
62
+ },
63
+ });
64
+ return { assertCurrent, transport };
65
+ }
66
+ async function readOwnedFee(explorerUrl, { assertCurrent, transport }) {
67
+ assertCurrent();
68
+ const fee = await syscoinjs.utils.fetchEstimateFee(explorerUrl, 1, undefined, transport);
69
+ assertCurrent();
70
+ return fee / 1024;
71
+ }
72
+ /** Public network fee estimate. It has no wallet, account or signing access. */
73
+ export async function readPublicSyscoinFee(explorerUrl, owner) {
74
+ return readOwnedFee(explorerUrl, captureBlockbookRead(owner, () => undefined));
75
+ }
76
+ /** Bind both transaction preparation and address reads to the active wallet. */
77
+ export function captureOwnedSyscoinRead(options, getState, captureSigningContext) {
78
+ if (!options)
79
+ throw new Error('Missing owned UTXO read');
80
+ const assertSession = captureSigningContext(true);
81
+ const { activeNetwork, activeAccountId, activeAccountType, accounts } = getState();
82
+ if (activeNetwork.kind !== INetworkType.Syscoin)
83
+ throw new Error('Switch to UTXO chain');
84
+ const selected = accounts[activeAccountType]?.[activeAccountId];
85
+ if (!selected)
86
+ throw new Error('Active account not found');
87
+ const account = Object.freeze({
88
+ address: selected.address,
89
+ xpub: selected.xpub,
90
+ });
91
+ const network = Object.freeze({ ...activeNetwork });
92
+ const read = captureBlockbookRead(options, () => {
93
+ assertSession();
94
+ const current = getState();
95
+ const selected = current.accounts[current.activeAccountType]?.[current.activeAccountId];
96
+ if (current.activeAccountId !== activeAccountId ||
97
+ current.activeAccountType !== activeAccountType ||
98
+ selected?.address !== account.address ||
99
+ selected?.xpub !== account.xpub ||
100
+ current.activeNetwork.kind !== network.kind ||
101
+ current.activeNetwork.chainId !== network.chainId ||
102
+ current.activeNetwork.slip44 !== network.slip44 ||
103
+ current.activeNetwork.currency !== network.currency ||
104
+ current.activeNetwork.url !== network.url)
105
+ throw new Error('Wallet UTXO preparation context changed');
106
+ });
107
+ return {
108
+ account,
109
+ accountId: activeAccountId,
110
+ accountType: activeAccountType,
111
+ read: Object.freeze({ ...read, network, xpub: account.xpub }),
112
+ };
113
+ }
114
+ export class SyscoinTransactionsCore {
115
+ constructor(getSyscoinSigner, getReadOnlySigner, getState, hardwareSigners, captureSigningContext) {
116
+ this.getTransactionPSBT = async ({ txOptions, outputs, changeAddress, feeRateBN, xpub, fundingUtxos, }, main) => {
117
+ try {
118
+ // Use syscoinjs-lib directly for transaction creation
119
+ const result = await main.createTransaction(txOptions, changeAddress, outputs, feeRateBN, xpub, // sysFromXpubOrAddress
120
+ ...(fundingUtxos ? [fundingUtxos] : []));
121
+ if (result && result.psbt) {
122
+ return { psbt: result.psbt, fee: result.fee };
123
+ }
124
+ throw new Error('psbt not found');
125
+ }
126
+ catch (error) {
127
+ // Propagate structured error from syscoinjs-lib
128
+ if (error.error && error.code) {
129
+ throw error;
130
+ }
131
+ // Wrap non-structured errors
132
+ throw {
133
+ error: true,
134
+ code: 'TRANSACTION_CREATION_FAILED',
135
+ message: error.message || 'Failed to create transaction',
136
+ details: error,
137
+ };
138
+ }
139
+ };
140
+ this.decodeRawTransaction = (psbtOrHex, isRawHex = false) => {
141
+ const { main } = this.getReadOnlySigner();
142
+ if (isRawHex) {
143
+ // Handle raw transaction hex
144
+ const bitcoinTx = syscoinjs.utils.bitcoinjs.Transaction.fromHex(psbtOrHex);
145
+ return main.decodeRawTransaction(bitcoinTx);
146
+ }
147
+ else {
148
+ // Handle PSBT format (existing behavior)
149
+ const psbtObj = PsbtUtils.fromPali(psbtOrHex, this.getState().activeNetwork);
150
+ return main.decodeRawTransaction(psbtObj);
151
+ }
152
+ };
153
+ this.getRecommendedFee = async (explorerUrl, ownedRead) => {
154
+ const { read } = this.captureOwnedRead(ownedRead);
155
+ if (explorerUrl !== read.network.url)
156
+ throw new Error('Owned UTXO read endpoint changed');
157
+ return readOwnedFee(explorerUrl, read);
158
+ };
159
+ // Internal method for signing with the HD signer
160
+ this.signPSBTWithSigner = async ({ psbt, signer, }) => await signer.sign(psbt);
161
+ this.createNativeSweepPSBT = async ({ txOptions, changeAddress, receivingAddress, feeRateBN, source, excluded, }, main, read) => {
162
+ const raw = await syscoinjs.utils.fetchBackendUTXOS(main.blockbookURL, source, undefined, read.transport);
163
+ read.assertCurrent();
164
+ const rows = excluded.size
165
+ ? filterExcludedUtxos(raw, excluded).utxos
166
+ : Array.isArray(raw)
167
+ ? raw
168
+ : raw?.utxos;
169
+ if (!Array.isArray(rows))
170
+ throw sweepError('INVALID_NATIVE_SWEEP_UTXOS', 'Invalid native sweep UTXO response.');
171
+ const seen = new Set();
172
+ const expected = new Set();
173
+ let gross = new syscoinjs.utils.BN(0);
174
+ const utxos = [];
175
+ for (const row of rows) {
176
+ const txid = row?.txId ?? row?.txid;
177
+ if (typeof txid !== 'string' ||
178
+ !/^[0-9a-fA-F]{64}$/.test(txid) ||
179
+ !Number.isSafeInteger(row.vout) ||
180
+ row.vout < 0 ||
181
+ row.vout > 0xffffffff ||
182
+ (row.txId !== undefined &&
183
+ row.txid !== undefined &&
184
+ row.txId !== row.txid) ||
185
+ (row.assetInfo != null &&
186
+ (typeof row.assetInfo !== 'object' || Array.isArray(row.assetInfo))))
187
+ throw sweepError('INVALID_NATIVE_SWEEP_UTXOS', 'Invalid native sweep outpoint.');
188
+ const outpoint = `${txid.toLowerCase()}:${row.vout}`;
189
+ if (seen.has(outpoint))
190
+ throw sweepError('INVALID_NATIVE_SWEEP_UTXOS', 'Duplicate native sweep outpoint.');
191
+ seen.add(outpoint);
192
+ const value = sweepAtomicValue(row.value);
193
+ // Native MAX must leave asset carriers and their allocations untouched.
194
+ if (row.assetInfo != null)
195
+ continue;
196
+ expected.add(outpoint);
197
+ gross = gross.add(value);
198
+ utxos.push({ ...row, value });
199
+ }
200
+ if (gross.isZero())
201
+ throw sweepError('INSUFFICIENT_FUNDS', 'No native UTXOs are available to sweep.');
202
+ // The library sanitizes this snapshot once; its optional UTXO argument
203
+ // prevents a second funding lookup between resolving MAX and building it.
204
+ const result = await main.createTransaction(txOptions, changeAddress, [{ address: receivingAddress, value: gross, subtractFeeFrom: true }], feeRateBN, source, { utxos, assets: new Map() });
205
+ const inputs = result.psbt?.txInputs;
206
+ const outputs = result.psbt?.txOutputs;
207
+ const selected = Array.isArray(inputs)
208
+ ? inputs.map((input) => `${Buffer.from(input.hash).reverse().toString('hex')}:${input.index}`)
209
+ : [];
210
+ if (selected.length !== expected.size ||
211
+ new Set(selected).size !== expected.size ||
212
+ selected.some((outpoint) => !expected.has(outpoint)) ||
213
+ !Array.isArray(outputs) ||
214
+ outputs.length !== 1)
215
+ throw sweepError('INCOMPLETE_NATIVE_SWEEP', 'The builder did not preserve the complete native sweep.');
216
+ const script = syscoinjs.utils.bitcoinjs.address.toOutputScript(receivingAddress, main.network);
217
+ const recipient = sweepAtomicValue(outputs[0].value);
218
+ const fee = sweepAtomicValue(result.fee);
219
+ if (!Buffer.from(outputs[0].script).equals(Buffer.from(script)) ||
220
+ recipient.isZero() ||
221
+ !recipient.add(fee).eq(gross))
222
+ throw sweepError('INCOMPLETE_NATIVE_SWEEP', 'The native sweep amounts do not match its PSBT.');
223
+ return {
224
+ psbt: result.psbt,
225
+ fee: result.fee,
226
+ grossAmount: formatUnits(gross.toString(), 8).replace(/\.0$/, ''),
227
+ recipientAmount: formatUnits(recipient.toString(), 8).replace(/\.0$/, ''),
228
+ };
229
+ };
230
+ // Create unsigned PSBT for any transaction type
231
+ this.createUnsignedPSBT = async ({ txOptions = {}, isMax = false, nativeSweep = false, amount, receivingAddress, feeRateBN, token = null, }, owned) => {
232
+ // Ensure RBF is enabled by default if not explicitly set
233
+ const finalTxOptions = { rbf: true, ...txOptions };
234
+ const { account, accountType: activeAccountType, excluded, main, read, } = owned;
235
+ const xpub = account.xpub;
236
+ const isSingleAddressImported = activeAccountType === KeyringAccountType.Imported &&
237
+ xpub === account.address;
238
+ // For imported extended-key accounts we store BIP84 xpubs (zpub/vpub).
239
+ // Blockbook derives different address branches depending on version bytes, so preserve xpub as-is.
240
+ const sourceForUtxoLookup = isSingleAddressImported
241
+ ? account.address
242
+ : xpub;
243
+ // Convert amount to satoshis (1 SYS = 1e8 satoshis)
244
+ // Using BigNumber to prevent precision loss
245
+ const amountStr = amount.toString();
246
+ // Safe conversion without parseFloat to avoid precision loss
247
+ // Split the string to handle decimal values properly
248
+ const parts = amountStr.split('.');
249
+ const integerPart = parts[0] || '0';
250
+ const decimalPart = parts[1] || '';
251
+ // Pad or truncate decimal part to 8 places (satoshi precision)
252
+ const paddedDecimal = decimalPart.padEnd(8, '0').substring(0, 8);
253
+ // Combine to get satoshis (integer + decimal as one number)
254
+ const satoshiStr = integerPart + paddedDecimal;
255
+ // Remove leading zeros but keep at least one digit
256
+ const trimmedSatoshis = satoshiStr.replace(/^0+/, '') || '0';
257
+ const value = new syscoinjs.utils.BN(trimmedSatoshis);
258
+ // If this is a single-address imported account (WIF import), use the account.address for both from and change
259
+ const changeAddress = isSingleAddressImported
260
+ ? account.address
261
+ : await owned.getOwnedAddress(xpub, true);
262
+ read.assertCurrent();
263
+ try {
264
+ if (nativeSweep) {
265
+ return await this.createNativeSweepPSBT({
266
+ txOptions: finalTxOptions,
267
+ changeAddress,
268
+ receivingAddress,
269
+ feeRateBN,
270
+ source: sourceForUtxoLookup,
271
+ excluded,
272
+ }, main, read);
273
+ }
274
+ const fundingUtxos = excluded.size
275
+ ? filterExcludedUtxos(await syscoinjs.utils.fetchBackendUTXOS(main.blockbookURL, sourceForUtxoLookup, undefined, read.transport), excluded)
276
+ : undefined;
277
+ read.assertCurrent();
278
+ if (token && token.guid) {
279
+ // Token transaction: use assetAllocationSend
280
+ // Create a Map for the asset allocation
281
+ const assetMap = new Map();
282
+ assetMap.set(token.guid, {
283
+ changeAddress,
284
+ outputs: [
285
+ {
286
+ value: value,
287
+ address: receivingAddress,
288
+ },
289
+ ],
290
+ });
291
+ // Pass xpub to get back just the PSBT without signing and sending
292
+ // syscoinjs-lib will validate asset existence and sufficient balance
293
+ const result = await main.assetAllocationSend(finalTxOptions, assetMap, changeAddress, feeRateBN, sourceForUtxoLookup, // Pass source (address for WIF imports, zpub/vpub for HD)
294
+ ...(fundingUtxos ? [fundingUtxos] : []));
295
+ if (excluded.size)
296
+ assertNoExcludedInputs(result.psbt, excluded);
297
+ // Return PSBT and fee
298
+ return { psbt: result.psbt, fee: result.fee };
299
+ }
300
+ else {
301
+ // Native transaction: use getTransactionPSBT to create unsigned PSBT
302
+ const outputs = [
303
+ {
304
+ address: receivingAddress,
305
+ value: value, // Pass BN object directly
306
+ ...(isMax && { subtractFeeFrom: true }), // Only add subtractFeeFrom if isMax is true
307
+ },
308
+ ];
309
+ const result = await this.getTransactionPSBT({
310
+ txOptions: finalTxOptions,
311
+ outputs,
312
+ changeAddress,
313
+ feeRateBN,
314
+ xpub: sourceForUtxoLookup,
315
+ fundingUtxos,
316
+ }, main);
317
+ if (excluded.size)
318
+ assertNoExcludedInputs(result.psbt, excluded);
319
+ return result;
320
+ }
321
+ }
322
+ catch (error) {
323
+ // Re-throw structured errors from syscoinjs-lib
324
+ if (error.error && error.code) {
325
+ throw error;
326
+ }
327
+ // Wrap other errors in structured format
328
+ throw {
329
+ error: true,
330
+ code: 'TRANSACTION_CREATION_FAILED',
331
+ message: error.message || 'Failed to create unsigned PSBT',
332
+ details: error,
333
+ };
334
+ }
335
+ };
336
+ // Sign PSBT with appropriate method - separated for better error handling
337
+ this.signPSBTWithMethod = async (psbt, ownedRead) => {
338
+ const { assertCurrent, network: activeNetwork, transport } = ownedRead;
339
+ assertCurrent();
340
+ const { activeAccountId, activeAccountType, accounts } = this.getState();
341
+ const account = accounts[activeAccountType]?.[activeAccountId];
342
+ if (!account)
343
+ throw new Error('Active account not found');
344
+ // Device choice belongs to the selected account, never to PSBT/caller flags.
345
+ const isTrezor = activeAccountType === KeyringAccountType.Trezor ||
346
+ account.isTrezorWallet === true;
347
+ const isLedger = activeAccountType === KeyringAccountType.Ledger ||
348
+ account.isLedgerWallet === true;
349
+ const scope = Object.freeze({
350
+ account: Object.freeze({ address: account.address, xpub: account.xpub }),
351
+ accountId: activeAccountId,
352
+ accountType: activeAccountType,
353
+ network: activeNetwork,
354
+ });
355
+ const sign = isLedger
356
+ ? this.hardwareSigners.ledger
357
+ : isTrezor
358
+ ? this.hardwareSigners.trezor
359
+ : undefined;
360
+ if ((isLedger || isTrezor) && typeof sign !== 'function')
361
+ throw createHardwareSignerUnavailableError(isLedger ? 'Ledger' : 'Trezor');
362
+ if (isTrezor || isLedger)
363
+ assertPsbtAccountScope(psbt, scope);
364
+ if (isLedger || isTrezor) {
365
+ const assertHardwareCurrent = () => {
366
+ try {
367
+ assertCurrent();
368
+ }
369
+ catch {
370
+ // The vendor retry loop already treats AbortError as terminal.
371
+ throw Object.assign(new Error('Wallet signing context changed'), {
372
+ name: 'AbortError',
373
+ });
374
+ }
375
+ };
376
+ const signed = await sign({
377
+ psbt,
378
+ account: Object.freeze({ id: account.id, xpub: account.xpub }),
379
+ network: scope.network,
380
+ scope,
381
+ assertCurrent: assertHardwareCurrent,
382
+ signal: transport.signal,
383
+ ...(isLedger && {
384
+ getRawTransaction: (txid) => syscoinjs.utils.fetchBackendRawTx(activeNetwork.url, txid, transport),
385
+ }),
386
+ });
387
+ assertHardwareCurrent();
388
+ return signed;
389
+ }
390
+ else {
391
+ // HD signing (seed-based HDAccount or imported BIP84 zprv/vprv account).
392
+ // Some Blockbook deployments return UTXOs without per-input derivation `path`,
393
+ // which causes bitcoinjs-lib to throw "Need derivation path to sign with HD".
394
+ //
395
+ // We can recover by enriching PSBT inputs with a proprietary `path` field using
396
+ // Blockbook's xpub token list (address -> path mapping), which reliably includes paths.
397
+ const account = accounts[activeAccountType]?.[activeAccountId];
398
+ const accountXpub = account?.xpub;
399
+ const accountAddress = account?.address;
400
+ const isSingleAddressImported = activeAccountType === KeyringAccountType.Imported &&
401
+ Boolean(accountXpub) &&
402
+ Boolean(accountAddress) &&
403
+ accountXpub === accountAddress;
404
+ const isImportedExtended = activeAccountType === KeyringAccountType.Imported &&
405
+ Boolean(accountXpub) &&
406
+ !isSingleAddressImported;
407
+ // IMPORTANT: Imported extended keys (zprv/vprv) are typically *account-level* keys.
408
+ // syscoinjs-lib signs using the imported key as the BIP32 root, so PSBT `path` must be
409
+ // relative (change/index), not the full `m/84'/.../change/index`. If we keep full paths,
410
+ // bip32.derivePath can throw, leaving no derivations and causing "Need derivation path".
411
+ if (isImportedExtended) {
412
+ const inputs = psbt?.data?.inputs || [];
413
+ for (const input of inputs) {
414
+ const unknowns = input?.unknownKeyVals || [];
415
+ const pathKv = unknowns.find((kv) => kv?.key?.toString?.() === 'path');
416
+ if (!pathKv?.value)
417
+ continue;
418
+ const fullPath = String(Buffer.from(pathKv.value).toString());
419
+ const parts = fullPath.split('/').filter(Boolean);
420
+ // Expected: m/84'/slip44'/account'/change/index
421
+ if (parts.length >= 2) {
422
+ const change = parts[parts.length - 2];
423
+ const index = parts[parts.length - 1];
424
+ const rel = `${change}/${index}`;
425
+ pathKv.value = Buffer.from(rel);
426
+ }
427
+ }
428
+ }
429
+ const needsAnyPath = psbt?.data?.inputs?.some((input) => {
430
+ const unknowns = input?.unknownKeyVals || [];
431
+ return (!input.bip32Derivation?.length &&
432
+ !input.tapBip32Derivation?.length &&
433
+ !unknowns.some((kv) => kv?.key?.toString?.() === 'path'));
434
+ });
435
+ if (needsAnyPath &&
436
+ accountXpub &&
437
+ !isSingleAddressImported &&
438
+ typeof psbt.addUnknownKeyValToInput === 'function') {
439
+ // Fetch address->path mapping from Blockbook xpub endpoint
440
+ const blockbookUrl = activeNetwork.url;
441
+ const res = await fetchOwnedBlockbookAccount(blockbookUrl, accountXpub, transport);
442
+ assertCurrent();
443
+ const tokens = Array.isArray(res?.tokens) ? res.tokens : [];
444
+ const addrToPath = new Map();
445
+ for (const t of tokens) {
446
+ if (t?.name && t?.path) {
447
+ addrToPath.set(String(t.name), String(t.path));
448
+ }
449
+ }
450
+ const inputs = psbt?.data?.inputs || [];
451
+ for (let i = 0; i < inputs.length; i++) {
452
+ const input = inputs[i];
453
+ const unknowns = input?.unknownKeyVals || [];
454
+ const hasPath = unknowns.some((kv) => kv?.key?.toString?.() === 'path');
455
+ if (hasPath)
456
+ continue;
457
+ const addrKv = unknowns.find((kv) => kv?.key?.toString?.() === 'address');
458
+ const addr = addrKv?.value
459
+ ? String(Buffer.from(addrKv.value).toString())
460
+ : null;
461
+ if (!addr)
462
+ continue;
463
+ const fullPath = addrToPath.get(addr);
464
+ if (!fullPath)
465
+ continue;
466
+ // Imported extended keys are account-level (BIP84 account node), so we must provide a
467
+ // path relative to that node (change/index), not the full m/84'/... path.
468
+ const pathForSigner = isImportedExtended
469
+ ? (() => {
470
+ const parts = String(fullPath).split('/').filter(Boolean);
471
+ // Expected: m/84'/slip44'/account'/change/index
472
+ if (parts.length >= 2) {
473
+ const change = parts[parts.length - 2];
474
+ const index = parts[parts.length - 1];
475
+ return `${change}/${index}`;
476
+ }
477
+ return fullPath;
478
+ })()
479
+ : fullPath;
480
+ psbt.addUnknownKeyValToInput(i, {
481
+ key: Buffer.from('path'),
482
+ value: Buffer.from(pathForSigner),
483
+ });
484
+ }
485
+ }
486
+ assertCurrent();
487
+ assertPsbtAccountScope(psbt, scope);
488
+ const { hd } = this.getSigner();
489
+ const signedPsbt = await this.signPSBTWithSigner({
490
+ psbt,
491
+ signer: hd,
492
+ });
493
+ assertCurrent();
494
+ return signedPsbt;
495
+ }
496
+ };
497
+ // Create unsigned PSBT and estimate fee - NO SIGNING
498
+ this.getEstimateSysTransactionFee = async ({ txOptions = {}, isMax = false, nativeSweep = false, amount, receivingAddress, feeRate, token = null, excludedOutpoints = [], }, ownedRead) => {
499
+ try {
500
+ if (nativeSweep !== false &&
501
+ (nativeSweep !== true || isMax !== true || token !== null))
502
+ throw sweepError('INVALID_NATIVE_SWEEP', 'Native sweep requires explicit MAX and a native transaction.');
503
+ // Ensure RBF is enabled by default if not explicitly set
504
+ const finalTxOptions = { rbf: true, ...txOptions };
505
+ const preparation = this.captureOwnedRead(ownedRead);
506
+ const { main, getOwnedAddress } = this.getReadOnlySigner(preparation.read);
507
+ if (typeof getOwnedAddress !== 'function' ||
508
+ main.blockbookURL !== preparation.read.network.url)
509
+ throw new Error('Owned UTXO read adapter is unavailable');
510
+ const owned = {
511
+ ...preparation,
512
+ main,
513
+ getOwnedAddress,
514
+ excluded: excludedOutpointIds(excludedOutpoints),
515
+ };
516
+ const selectedToken = token ? { ...token } : token;
517
+ const { assetWhiteList } = finalTxOptions;
518
+ if (assetWhiteList instanceof Map)
519
+ finalTxOptions.assetWhiteList = new Map(assetWhiteList);
520
+ else if (assetWhiteList instanceof Set)
521
+ finalTxOptions.assetWhiteList = new Set(assetWhiteList);
522
+ for (const key of ['memo', 'memoHeader', 'blobHash', 'blobData']) {
523
+ const bytes = finalTxOptions[key];
524
+ if (bytes instanceof Uint8Array)
525
+ finalTxOptions[key] = Buffer.from(bytes);
526
+ }
527
+ const actualFeeRate = feeRate === undefined
528
+ ? await readOwnedFee(main.blockbookURL, owned.read)
529
+ : feeRate;
530
+ owned.read.assertCurrent();
531
+ // Convert fee rate to satoshis/byte and ensure minimum relay of 1 sat/vB
532
+ // Blockbook returns coins per kB; after division by 1024, some testnets
533
+ // can yield < 1 sat/vB (e.g., 0.9765625). Round up and clamp to 1 to
534
+ // avoid zero-fee transactions due to truncation when converting to BN.
535
+ const satPerByte = Math.max(1, Math.ceil(actualFeeRate * 1e8));
536
+ const feeRateBN = new syscoinjs.utils.BN(satPerByte);
537
+ // Step 2: Create unsigned PSBT
538
+ const result = await this.createUnsignedPSBT({
539
+ txOptions: finalTxOptions,
540
+ isMax,
541
+ nativeSweep,
542
+ amount,
543
+ receivingAddress,
544
+ feeRateBN,
545
+ token: selectedToken,
546
+ }, owned);
547
+ owned.read.assertCurrent();
548
+ const quote = {
549
+ fee: result.fee / 1e8,
550
+ psbt: PsbtUtils.toPali(result.psbt), // Return UNSIGNED PSBT as JSON
551
+ ...(nativeSweep
552
+ ? {
553
+ grossAmount: result.grossAmount,
554
+ recipientAmount: result.recipientAmount,
555
+ }
556
+ : {}),
557
+ };
558
+ owned.read.assertCurrent();
559
+ return quote;
560
+ }
561
+ catch (error) {
562
+ // Pass through structured errors from syscoinjs-lib
563
+ if (error.error && error.code) {
564
+ // Convert fee from satoshis to SYS if available
565
+ if (error.fee !== undefined) {
566
+ // If fee is a BN object, convert to number first
567
+ if (typeof error.fee === 'object' && error.fee.toNumber) {
568
+ error.fee = error.fee.toNumber() / 1e8;
569
+ }
570
+ else {
571
+ error.fee = error.fee / 1e8;
572
+ }
573
+ }
574
+ if (error.remainingFee !== undefined) {
575
+ // If remainingFee is a BN object, convert to number first
576
+ if (typeof error.remainingFee === 'object' &&
577
+ error.remainingFee.toNumber) {
578
+ error.remainingFee = error.remainingFee.toNumber() / 1e8;
579
+ }
580
+ else {
581
+ error.remainingFee = error.remainingFee / 1e8;
582
+ }
583
+ }
584
+ if (error.shortfall !== undefined) {
585
+ // If shortfall is a BN object, convert to number first
586
+ if (typeof error.shortfall === 'object' && error.shortfall.toNumber) {
587
+ error.shortfall = error.shortfall.toNumber() / 1e8;
588
+ }
589
+ else {
590
+ error.shortfall = error.shortfall / 1e8;
591
+ }
592
+ }
593
+ throw error;
594
+ }
595
+ // Wrap other errors
596
+ throw {
597
+ error: true,
598
+ code: 'TRANSACTION_CREATION_FAILED',
599
+ message: error.message || 'Failed to estimate transaction fee',
600
+ details: error,
601
+ };
602
+ }
603
+ };
604
+ // Removed createAndSignSysTransaction - functionality merged into getEstimateSysTransactionFee
605
+ this.signPSBT = async ({ psbt }, ownedRead) => {
606
+ const { read } = this.captureOwnedRead(ownedRead);
607
+ const psbtObj = PsbtUtils.fromPali(psbt, read.network);
608
+ const unsigned = Buffer.from(psbtObj.data.globalMap.unsignedTx.toBuffer());
609
+ // Scope validation temporarily removes other signers' hints. Preserve the
610
+ // public metadata for the next cosigner, without exposing it to our signer.
611
+ const cosignerMetadata = psbtObj.data.inputs.map((input, index) => ({
612
+ hash: Buffer.from(psbtObj.txInputs[index].hash),
613
+ index: psbtObj.txInputs[index].index,
614
+ bip32Derivation: input.bip32Derivation?.map((value) => ({ ...value })),
615
+ tapBip32Derivation: input.tapBip32Derivation?.map((value) => ({
616
+ ...value,
617
+ })),
618
+ path: input.unknownKeyVals
619
+ ?.filter((value) => Buffer.from(value.key).toString() === 'path')
620
+ .map((value) => ({
621
+ key: Buffer.from(value.key),
622
+ value: Buffer.from(value.value),
623
+ }))[0],
624
+ }));
625
+ const signedPsbt = await this.signPSBTWithMethod(psbtObj, read);
626
+ read.assertCurrent();
627
+ if (!unsigned.equals(signedPsbt.data.globalMap.unsignedTx.toBuffer()))
628
+ throw new Error('Signed PSBT differs from the prepared transaction');
629
+ signedPsbt.data.inputs.forEach((input, index) => {
630
+ if (input.finalScriptSig || input.finalScriptWitness)
631
+ return;
632
+ const metadata = cosignerMetadata[index];
633
+ if (!metadata)
634
+ return;
635
+ const signed = signedPsbt.txInputs[index];
636
+ if (!signed ||
637
+ metadata.index !== signed.index ||
638
+ !metadata.hash.equals(Buffer.from(signed.hash)))
639
+ throw new Error('Wallet signing context changed');
640
+ if (metadata.bip32Derivation)
641
+ input.bip32Derivation = metadata.bip32Derivation;
642
+ if (metadata.tapBip32Derivation)
643
+ input.tapBip32Derivation = metadata.tapBip32Derivation;
644
+ if (metadata.path) {
645
+ input.unknownKeyVals = (input.unknownKeyVals || []).filter((value) => Buffer.from(value.key).toString() !== 'path');
646
+ input.unknownKeyVals.push(metadata.path);
647
+ }
648
+ });
649
+ const result = PsbtUtils.toPali(signedPsbt);
650
+ read.assertCurrent();
651
+ return result;
652
+ };
653
+ this.sendTransaction = async (rawTransaction, authority) => {
654
+ let read;
655
+ let fetch;
656
+ let signal;
657
+ let prepare;
658
+ try {
659
+ if (!authority)
660
+ throw new Error('Missing owned UTXO submission context');
661
+ const { assertCurrent } = authority;
662
+ ({ fetch, signal, prepare } = authority);
663
+ if (typeof prepare !== 'function')
664
+ throw new Error('Missing UTXO submission admission');
665
+ ({ read } = this.captureOwnedRead({ assertCurrent, fetch, signal }));
666
+ }
667
+ catch {
668
+ throw Object.assign(new Error('UTXO submission was not dispatched'), {
669
+ code: 'UTXO_SUBMISSION_NOT_DISPATCHED',
670
+ transactionNotBroadcast: true,
671
+ });
672
+ }
673
+ // SyscoinJS owns the admission, final synchronous guard and exact ACK.
674
+ // The POST uses the captured transport directly so a matching ACK can
675
+ // still be observed after a host context change without a signal abort.
676
+ const { result: txid } = await syscoinjs.utils.sendRawTransaction(read.network.url, rawTransaction, undefined, { assertCurrent: read.assertCurrent, fetch, prepare, signal });
677
+ return { txid };
678
+ };
679
+ if (typeof captureSigningContext !== 'function')
680
+ throw new Error('Missing UTXO signing context');
681
+ this.getSigner = getSyscoinSigner;
682
+ this.getReadOnlySigner = getReadOnlySigner;
683
+ this.getState = getState;
684
+ this.hardwareSigners = hardwareSigners;
685
+ this.captureSigningContext = captureSigningContext;
686
+ }
687
+ captureOwnedRead(options) {
688
+ return captureOwnedSyscoinRead(options, this.getState, this.captureSigningContext);
689
+ }
690
+ }
691
+ //# sourceMappingURL=syscoin-core.js.map