@sidhujag/sysweb3-keyring 1.0.626 → 1.0.628

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 (40) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/cjs/keyring-core.js +2512 -0
  3. package/cjs/keyring-core.js.map +1 -0
  4. package/cjs/keyring-manager.js +23 -2437
  5. package/cjs/keyring-manager.js.map +1 -1
  6. package/cjs/software.js +58 -0
  7. package/cjs/software.js.map +1 -0
  8. package/cjs/storage-core.js +227 -0
  9. package/cjs/storage-core.js.map +1 -0
  10. package/cjs/storage.js +15 -210
  11. package/cjs/storage.js.map +1 -1
  12. package/cjs/transactions/ethereum.js +121 -29
  13. package/cjs/transactions/ethereum.js.map +1 -1
  14. package/cjs/transactions/evm-local-signer.js +11 -1
  15. package/cjs/transactions/evm-local-signer.js.map +1 -1
  16. package/cjs/transactions/hardware-psbt.js +167 -0
  17. package/cjs/transactions/hardware-psbt.js.map +1 -0
  18. package/cjs/transactions/syscoin-core.js +651 -0
  19. package/cjs/transactions/syscoin-core.js.map +1 -0
  20. package/cjs/transactions/syscoin.js +8 -768
  21. package/cjs/transactions/syscoin.js.map +1 -1
  22. package/cjs/types.js.map +1 -1
  23. package/package.json +38 -4
  24. package/types/keyring-core.d.ts +257 -0
  25. package/types/keyring-manager.d.ts +5 -223
  26. package/types/software.d.ts +23 -0
  27. package/types/storage-core.d.ts +22 -0
  28. package/types/storage.d.ts +1 -4
  29. package/types/transactions/ethereum.d.ts +16 -6
  30. package/types/transactions/evm-local-signer.d.ts +3 -1
  31. package/types/transactions/hardware-psbt.d.ts +7 -0
  32. package/types/transactions/syscoin-core.d.ts +92 -0
  33. package/types/transactions/syscoin.d.ts +8 -67
  34. package/types/types.d.ts +10 -0
  35. package/cjs/transactions/__tests__/integration.test.js +0 -237
  36. package/cjs/transactions/__tests__/integration.test.js.map +0 -1
  37. package/cjs/transactions/__tests__/syscoin.test.js +0 -361
  38. package/cjs/transactions/__tests__/syscoin.test.js.map +0 -1
  39. package/types/transactions/__tests__/integration.test.d.ts +0 -1
  40. package/types/transactions/__tests__/syscoin.test.d.ts +0 -1
@@ -1,775 +1,15 @@
1
1
  "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
2
  Object.defineProperty(exports, "__esModule", { value: true });
36
3
  exports.SyscoinTransactions = void 0;
37
- const sysweb3_network_1 = require("@sidhujag/sysweb3-network");
38
- const sysweb3_utils_1 = require("@sidhujag/sysweb3-utils");
39
- const syscoinjs = __importStar(require("syscoinjs-lib"));
40
- // import { BIP_84, ONE_HUNDRED_MILLION, SYSCOIN_BASIC_FEE } from 'utils';
41
- const ethers_v6_1 = require("../ethers-v6");
42
- const bitcoin_client_1 = require("../ledger/bitcoin_client");
43
- const psbtv2_1 = require("../ledger/bitcoin_client/lib/psbtv2");
44
- const consts_1 = require("../ledger/consts");
45
- const types_1 = require("../types");
46
- const blockbook_cache_1 = require("../utils/blockbook-cache");
47
- const derivation_paths_1 = require("../utils/derivation-paths");
48
- const excluded_utxos_1 = require("../utils/excluded-utxos");
49
- const psbt_1 = require("../utils/psbt");
50
- const psbt_account_scope_1 = require("../utils/psbt-account-scope");
51
- // Recommended fee changes slowly; cache per explorer to avoid hammering
52
- // Blockbook while the user edits amounts on the send screen
53
- const FEE_CACHE_TTL_MS = 60000;
54
- const sweepError = (code, message) => ({
55
- error: true,
56
- code,
57
- message,
58
- });
59
- const sweepAtomicValue = (value) => {
60
- const text = typeof value === 'number'
61
- ? Number.isSafeInteger(value)
62
- ? String(value)
63
- : ''
64
- : typeof value === 'string' ||
65
- typeof value === 'bigint' ||
66
- syscoinjs.utils.BN.isBN(value)
67
- ? value.toString()
68
- : '';
69
- if (!/^\d+$/.test(text))
70
- throw sweepError('INVALID_NATIVE_SWEEP_UTXOS', 'Invalid native sweep atomic value.');
71
- return new syscoinjs.utils.BN(text);
72
- };
73
- class SyscoinTransactions {
4
+ const hardware_psbt_1 = require("./hardware-psbt");
5
+ const syscoin_core_1 = require("./syscoin-core");
6
+ /** Legacy constructor: preserve device arguments while composing shared logic. */
7
+ class SyscoinTransactions extends syscoin_core_1.SyscoinTransactionsCore {
74
8
  constructor(getSyscoinSigner, getReadOnlySigner, getState, getAddress, ledgerSigner, trezorSigner, captureSigningContext) {
75
- this.getTransactionPSBT = async ({ txOptions, outputs, changeAddress, feeRateBN, xpub, fundingUtxos, }, main) => {
76
- try {
77
- // Use syscoinjs-lib directly for transaction creation
78
- const result = await main.createTransaction(txOptions, changeAddress, outputs, feeRateBN, xpub, // sysFromXpubOrAddress
79
- ...(fundingUtxos ? [fundingUtxos] : []));
80
- if (result && result.psbt) {
81
- return { psbt: result.psbt, fee: result.fee };
82
- }
83
- throw new Error('psbt not found');
84
- }
85
- catch (error) {
86
- // Propagate structured error from syscoinjs-lib
87
- if (error.error && error.code) {
88
- throw error;
89
- }
90
- // Wrap non-structured errors
91
- throw {
92
- error: true,
93
- code: 'TRANSACTION_CREATION_FAILED',
94
- message: error.message || 'Failed to create transaction',
95
- details: error,
96
- };
97
- }
98
- };
99
- this.decodeRawTransaction = (psbtOrHex, isRawHex = false) => {
100
- const { main } = this.getReadOnlySigner();
101
- if (isRawHex) {
102
- // Handle raw transaction hex
103
- const bitcoinTx = syscoinjs.utils.bitcoinjs.Transaction.fromHex(psbtOrHex);
104
- return main.decodeRawTransaction(bitcoinTx);
105
- }
106
- else {
107
- // Handle PSBT format (existing behavior)
108
- const psbtObj = psbt_1.PsbtUtils.fromPali(psbtOrHex, this.getState().activeNetwork);
109
- return main.decodeRawTransaction(psbtObj);
110
- }
111
- };
112
- this.feeCache = new Map();
113
- this.getRecommendedFee = async (explorerUrl) => {
114
- const cached = this.feeCache.get(explorerUrl);
115
- if (cached && Date.now() - cached.timestamp < FEE_CACHE_TTL_MS) {
116
- return cached.fee;
117
- }
118
- const fee = (await syscoinjs.utils.fetchEstimateFee(explorerUrl, 1)) / 1024;
119
- this.feeCache.set(explorerUrl, { fee, timestamp: Date.now() });
120
- return fee;
121
- };
122
- this.txUtilsFunctions = () => {
123
- const { getRawTransaction } = (0, sysweb3_utils_1.txUtils)();
124
- return {
125
- getRawTransaction,
126
- };
127
- };
128
- // Internal method for signing with the HD signer
129
- this.signPSBTWithSigner = async ({ psbt, signer, }) => await signer.sign(psbt);
130
- this.createNativeSweepPSBT = async ({ txOptions, changeAddress, receivingAddress, feeRateBN, source, excluded, }, main) => {
131
- const raw = await syscoinjs.utils.fetchBackendUTXOS(main.blockbookURL, source);
132
- const rows = excluded.size
133
- ? (0, excluded_utxos_1.filterExcludedUtxos)(raw, excluded).utxos
134
- : Array.isArray(raw)
135
- ? raw
136
- : raw?.utxos;
137
- if (!Array.isArray(rows))
138
- throw sweepError('INVALID_NATIVE_SWEEP_UTXOS', 'Invalid native sweep UTXO response.');
139
- const seen = new Set();
140
- const expected = new Set();
141
- let gross = new syscoinjs.utils.BN(0);
142
- const utxos = [];
143
- for (const row of rows) {
144
- const txid = row?.txId ?? row?.txid;
145
- if (typeof txid !== 'string' ||
146
- !/^[0-9a-fA-F]{64}$/.test(txid) ||
147
- !Number.isSafeInteger(row.vout) ||
148
- row.vout < 0 ||
149
- row.vout > 0xffffffff ||
150
- (row.txId !== undefined &&
151
- row.txid !== undefined &&
152
- row.txId !== row.txid) ||
153
- (row.assetInfo != null &&
154
- (typeof row.assetInfo !== 'object' || Array.isArray(row.assetInfo))))
155
- throw sweepError('INVALID_NATIVE_SWEEP_UTXOS', 'Invalid native sweep outpoint.');
156
- const outpoint = `${txid.toLowerCase()}:${row.vout}`;
157
- if (seen.has(outpoint))
158
- throw sweepError('INVALID_NATIVE_SWEEP_UTXOS', 'Duplicate native sweep outpoint.');
159
- seen.add(outpoint);
160
- const value = sweepAtomicValue(row.value);
161
- // Native MAX must leave asset carriers and their allocations untouched.
162
- if (row.assetInfo != null)
163
- continue;
164
- expected.add(outpoint);
165
- gross = gross.add(value);
166
- utxos.push({ ...row, value });
167
- }
168
- if (gross.isZero())
169
- throw sweepError('INSUFFICIENT_FUNDS', 'No native UTXOs are available to sweep.');
170
- // The library sanitizes this snapshot once; its optional UTXO argument
171
- // prevents a second funding lookup between resolving MAX and building it.
172
- const result = await main.createTransaction(txOptions, changeAddress, [{ address: receivingAddress, value: gross, subtractFeeFrom: true }], feeRateBN, source, { utxos, assets: new Map() });
173
- const inputs = result.psbt?.txInputs;
174
- const outputs = result.psbt?.txOutputs;
175
- const selected = Array.isArray(inputs)
176
- ? inputs.map((input) => `${Buffer.from(input.hash).reverse().toString('hex')}:${input.index}`)
177
- : [];
178
- if (selected.length !== expected.size ||
179
- new Set(selected).size !== expected.size ||
180
- selected.some((outpoint) => !expected.has(outpoint)) ||
181
- !Array.isArray(outputs) ||
182
- outputs.length !== 1)
183
- throw sweepError('INCOMPLETE_NATIVE_SWEEP', 'The builder did not preserve the complete native sweep.');
184
- const script = syscoinjs.utils.bitcoinjs.address.toOutputScript(receivingAddress, main.network);
185
- const recipient = sweepAtomicValue(outputs[0].value);
186
- const fee = sweepAtomicValue(result.fee);
187
- if (!Buffer.from(outputs[0].script).equals(Buffer.from(script)) ||
188
- recipient.isZero() ||
189
- !recipient.add(fee).eq(gross))
190
- throw sweepError('INCOMPLETE_NATIVE_SWEEP', 'The native sweep amounts do not match its PSBT.');
191
- return {
192
- psbt: result.psbt,
193
- fee: result.fee,
194
- grossAmount: (0, ethers_v6_1.formatUnits)(gross.toString(), 8).replace(/\.0$/, ''),
195
- recipientAmount: (0, ethers_v6_1.formatUnits)(recipient.toString(), 8).replace(/\.0$/, ''),
196
- };
197
- };
198
- // Create unsigned PSBT for any transaction type
199
- this.createUnsignedPSBT = async ({ txOptions = {}, isMax = false, nativeSweep = false, amount, receivingAddress, feeRateBN, token = null, excludedOutpoints = [], }) => {
200
- // Ensure RBF is enabled by default if not explicitly set
201
- const finalTxOptions = { rbf: true, ...txOptions };
202
- const { activeAccountId, accounts, activeAccountType } = this.getState();
203
- // Use read-only signer since we're just creating an unsigned PSBT
204
- const { main } = this.getReadOnlySigner();
205
- const account = accounts[activeAccountType]?.[activeAccountId];
206
- if (!account) {
207
- throw new Error('Active account not found');
208
- }
209
- const xpub = account.xpub;
210
- const isSingleAddressImported = activeAccountType === types_1.KeyringAccountType.Imported &&
211
- xpub === account.address;
212
- // For imported extended-key accounts we store BIP84 xpubs (zpub/vpub).
213
- // Blockbook derives different address branches depending on version bytes, so preserve xpub as-is.
214
- const sourceForUtxoLookup = isSingleAddressImported
215
- ? account.address
216
- : xpub;
217
- // Convert amount to satoshis (1 SYS = 1e8 satoshis)
218
- // Using BigNumber to prevent precision loss
219
- const amountStr = amount.toString();
220
- // Safe conversion without parseFloat to avoid precision loss
221
- // Split the string to handle decimal values properly
222
- const parts = amountStr.split('.');
223
- const integerPart = parts[0] || '0';
224
- const decimalPart = parts[1] || '';
225
- // Pad or truncate decimal part to 8 places (satoshi precision)
226
- const paddedDecimal = decimalPart.padEnd(8, '0').substring(0, 8);
227
- // Combine to get satoshis (integer + decimal as one number)
228
- const satoshiStr = integerPart + paddedDecimal;
229
- // Remove leading zeros but keep at least one digit
230
- const trimmedSatoshis = satoshiStr.replace(/^0+/, '') || '0';
231
- const value = new syscoinjs.utils.BN(trimmedSatoshis);
232
- // If this is a single-address imported account (WIF import), use the account.address for both from and change
233
- const changeAddress = isSingleAddressImported
234
- ? account.address
235
- : await this.getAddress(xpub, true);
236
- try {
237
- const excluded = (0, excluded_utxos_1.excludedOutpointIds)(excludedOutpoints);
238
- if (nativeSweep) {
239
- return await this.createNativeSweepPSBT({
240
- txOptions: finalTxOptions,
241
- changeAddress,
242
- receivingAddress,
243
- feeRateBN,
244
- source: sourceForUtxoLookup,
245
- excluded,
246
- }, main);
247
- }
248
- const fundingUtxos = excluded.size
249
- ? (0, excluded_utxos_1.filterExcludedUtxos)(await syscoinjs.utils.fetchBackendUTXOS(main.blockbookURL, sourceForUtxoLookup), excluded)
250
- : undefined;
251
- if (token && token.guid) {
252
- // Token transaction: use assetAllocationSend
253
- // Create a Map for the asset allocation
254
- const assetMap = new Map();
255
- assetMap.set(token.guid, {
256
- changeAddress,
257
- outputs: [
258
- {
259
- value: value,
260
- address: receivingAddress,
261
- },
262
- ],
263
- });
264
- // Pass xpub to get back just the PSBT without signing and sending
265
- // syscoinjs-lib will validate asset existence and sufficient balance
266
- const result = await main.assetAllocationSend(finalTxOptions, assetMap, changeAddress, feeRateBN, sourceForUtxoLookup, // Pass source (address for WIF imports, zpub/vpub for HD)
267
- ...(fundingUtxos ? [fundingUtxos] : []));
268
- if (excluded.size)
269
- (0, excluded_utxos_1.assertNoExcludedInputs)(result.psbt, excluded);
270
- // Return PSBT and fee
271
- return { psbt: result.psbt, fee: result.fee };
272
- }
273
- else {
274
- // Native transaction: use getTransactionPSBT to create unsigned PSBT
275
- const outputs = [
276
- {
277
- address: receivingAddress,
278
- value: value, // Pass BN object directly
279
- ...(isMax && { subtractFeeFrom: true }), // Only add subtractFeeFrom if isMax is true
280
- },
281
- ];
282
- const result = await this.getTransactionPSBT({
283
- txOptions: finalTxOptions,
284
- outputs,
285
- changeAddress,
286
- feeRateBN,
287
- xpub: sourceForUtxoLookup,
288
- fundingUtxos,
289
- }, main);
290
- if (excluded.size)
291
- (0, excluded_utxos_1.assertNoExcludedInputs)(result.psbt, excluded);
292
- return result;
293
- }
294
- }
295
- catch (error) {
296
- // Re-throw structured errors from syscoinjs-lib
297
- if (error.error && error.code) {
298
- throw error;
299
- }
300
- // Wrap other errors in structured format
301
- throw {
302
- error: true,
303
- code: 'TRANSACTION_CREATION_FAILED',
304
- message: error.message || 'Failed to create unsigned PSBT',
305
- details: error,
306
- };
307
- }
308
- };
309
- // Sign PSBT with appropriate method - separated for better error handling
310
- this.signPSBTWithMethod = async (psbt, isTrezor, isLedger = false) => {
311
- const { activeNetwork, activeAccountId, activeAccountType, accounts } = this.getState();
312
- const account = accounts[activeAccountType]?.[activeAccountId];
313
- if (!account)
314
- throw new Error('Active account not found');
315
- // Device choice belongs to the selected account, never to PSBT/caller flags.
316
- isTrezor =
317
- activeAccountType === types_1.KeyringAccountType.Trezor ||
318
- account.isTrezorWallet === true;
319
- isLedger =
320
- activeAccountType === types_1.KeyringAccountType.Ledger ||
321
- account.isLedgerWallet === true;
322
- const assertSession = this.captureSigningContext?.() || (() => undefined);
323
- const scope = {
324
- account: { address: account.address, xpub: account.xpub },
325
- accountId: activeAccountId,
326
- accountType: activeAccountType,
327
- network: { ...activeNetwork },
328
- };
329
- const assertCurrent = () => {
330
- assertSession();
331
- const current = this.getState();
332
- const selected = current.accounts[current.activeAccountType]?.[current.activeAccountId];
333
- if (current.activeAccountId !== activeAccountId ||
334
- current.activeAccountType !== activeAccountType ||
335
- selected?.address !== scope.account.address ||
336
- selected?.xpub !== scope.account.xpub ||
337
- current.activeNetwork.kind !== scope.network.kind ||
338
- current.activeNetwork.chainId !== scope.network.chainId ||
339
- current.activeNetwork.slip44 !== scope.network.slip44 ||
340
- current.activeNetwork.url !== scope.network.url) {
341
- throw new Error('Wallet signing context changed');
342
- }
343
- };
344
- assertCurrent();
345
- if (isTrezor || isLedger)
346
- (0, psbt_account_scope_1.assertPsbtAccountScope)(psbt, scope);
347
- if (isLedger) {
348
- // CRITICAL: Enhance PSBT with required Ledger fields
349
- const account = accounts[activeAccountType]?.[activeAccountId];
350
- if (!account) {
351
- throw new Error('Active account not found');
352
- }
353
- const accountXpub = account.xpub;
354
- const accountId = account.id;
355
- // Enrich PSBT with nonWitnessUtxo for Ledger compatibility
356
- const { getRawTransaction } = this.txUtilsFunctions();
357
- const blockbookUrl = activeNetwork.url;
358
- // Some unit tests mock PSBTs without bitcoinjs-lib internals (data/txInputs).
359
- // Only attempt enrichment when the PSBT looks like a real bitcoinjs-lib Psbt.
360
- const psbtAny = psbt;
361
- const inputMetas = psbtAny?.data?.inputs;
362
- const txInputs = psbtAny?.txInputs;
363
- if (Array.isArray(inputMetas) &&
364
- Array.isArray(txInputs) &&
365
- txInputs.length > 0) {
366
- // Fetch and add nonWitnessUtxo where Ledger still needs it.
367
- const txFetchPromises = txInputs.map(async (_txInput, index) => {
368
- try {
369
- // Keep existing previous transactions, and finalized witness
370
- // inputs that already carry the prevout data needed by Ledger.
371
- const input = inputMetas[index];
372
- if (input?.nonWitnessUtxo ||
373
- (input?.witnessUtxo &&
374
- (input.finalScriptSig || input.finalScriptWitness)))
375
- return { index, success: true };
376
- const tx = txInputs[index];
377
- const prevTxId = Buffer.from(tx.hash).reverse().toString('hex');
378
- // Fetch the raw transaction
379
- const rawTxResponse = await getRawTransaction(blockbookUrl, prevTxId);
380
- // Handle different response formats (string, object with 'hex', or object with 'result')
381
- let rawTxHex;
382
- if (typeof rawTxResponse === 'string') {
383
- rawTxHex = rawTxResponse;
384
- }
385
- else if (rawTxResponse &&
386
- typeof rawTxResponse === 'object' &&
387
- 'hex' in rawTxResponse) {
388
- rawTxHex = rawTxResponse.hex;
389
- }
390
- else if (rawTxResponse &&
391
- typeof rawTxResponse === 'object' &&
392
- 'result' in rawTxResponse) {
393
- rawTxHex = rawTxResponse.result;
394
- }
395
- else {
396
- throw new Error(`Unexpected response format: ${JSON.stringify(rawTxResponse)}`);
397
- }
398
- if (!rawTxHex || typeof rawTxHex !== 'string') {
399
- throw new Error(`Invalid raw transaction hex received`);
400
- }
401
- // Convert hex to Buffer and add to PSBT
402
- const nonWitnessUtxo = Buffer.from(rawTxHex, 'hex');
403
- if (nonWitnessUtxo.length === 0) {
404
- throw new Error('Converted buffer is empty');
405
- }
406
- psbt.updateInput(index, { nonWitnessUtxo });
407
- return { index, success: true };
408
- }
409
- catch (error) {
410
- console.error(`[Ledger] Failed to enrich input ${index}:`, error);
411
- return { index, success: false, error };
412
- }
413
- });
414
- // Wait for all inputs to be enriched
415
- const results = await Promise.all(txFetchPromises);
416
- const failedInputs = results.filter((r) => !r.success);
417
- if (failedInputs.length > 0) {
418
- throw new Error(`Failed to enrich ${failedInputs.length} of ${results.length} inputs with nonWitnessUtxo. ` +
419
- `Ledger devices require this data for signing.`);
420
- }
421
- }
422
- assertCurrent();
423
- const enhancedPsbt = await this.ledger.convertToLedgerFormat(psbt, accountXpub, accountId, activeNetwork.currency, activeNetwork.slip44);
424
- assertCurrent();
425
- (0, psbt_account_scope_1.assertPsbtAccountScope)(enhancedPsbt, scope);
426
- // Get wallet policy for Ledger
427
- const fingerprint = await this.ledger.ledgerUtxoClient.getMasterFingerprint();
428
- const hdPath = (0, derivation_paths_1.getAccountDerivationPath)(activeNetwork.currency, activeNetwork.slip44, accountId);
429
- // For policy key preimages, match what the currently-open Ledger app expects.
430
- // Prefer the device-reported extended pubkey for this path (silent), and only fall back
431
- // to converting the stored xpub/vpub/zpub if needed.
432
- let deviceXpub = null;
433
- try {
434
- deviceXpub = await this.ledger.ledgerUtxoClient.getExtendedPubkey(hdPath, false);
435
- }
436
- catch (_e) {
437
- // ignore; we'll fall back
438
- }
439
- if (!deviceXpub) {
440
- const devicePubMagicHex = activeNetwork.slip44 === 1 ? '043587cf' : '0488b21e';
441
- deviceXpub = (0, derivation_paths_1.convertExtendedKeyVersion)(accountXpub, devicePubMagicHex);
442
- }
443
- const xpubWithDescriptor = `[${hdPath}]${deviceXpub}`.replace('m', fingerprint);
444
- const walletPolicy = new bitcoin_client_1.WalletPolicy(activeNetwork.currency.toLowerCase(), consts_1.DESCRIPTOR, [xpubWithDescriptor]);
445
- // Register lazily and retrieve HMAC for silent operations thereafter
446
- let hmac = null;
447
- if (typeof this.ledger.getOrRegisterHmac === 'function') {
448
- hmac = await this.ledger.getOrRegisterHmac(walletPolicy, fingerprint);
449
- }
450
- // Convert to PsbtV2 for direct signing without intermediate base64 encode/decode
451
- const psbtV2 = new psbtv2_1.PsbtV2().fromBitcoinJS(enhancedPsbt);
452
- assertCurrent();
453
- (0, psbt_account_scope_1.assertPsbtAccountScope)(enhancedPsbt, scope);
454
- const signatureEntries = await this.ledger.ledgerUtxoClient.signPsbt(psbtV2, walletPolicy, hmac);
455
- assertCurrent();
456
- signatureEntries.forEach(([inputIndex, partialSig]) => {
457
- const input = enhancedPsbt.data.inputs[inputIndex];
458
- if (!input ||
459
- input.finalScriptSig ||
460
- input.finalScriptWitness ||
461
- !input.bip32Derivation?.some((derivation) => Buffer.from(derivation.pubkey).equals(Buffer.from(partialSig.pubkey)))) {
462
- throw new Error('PSBT input is outside the approved account');
463
- }
464
- enhancedPsbt.updateInput(inputIndex, {
465
- partialSig: [partialSig],
466
- });
467
- });
468
- // Preserve external inputs that were finalized before device signing.
469
- enhancedPsbt.data.inputs.forEach((input, index) => {
470
- if (!input.finalScriptSig && !input.finalScriptWitness)
471
- enhancedPsbt.finalizeInput(index);
472
- });
473
- return enhancedPsbt;
474
- }
475
- else if (isTrezor) {
476
- // Handle Trezor signing for UTXO
477
- // Get network configuration for Trezor
478
- const networkConfig = (0, sysweb3_network_1.getNetworkConfig)(activeNetwork.slip44, activeNetwork.currency);
479
- const isTestnet = activeNetwork.slip44 === 1;
480
- const bitcoinjsNetwork = isTestnet
481
- ? networkConfig?.networks?.testnet
482
- : networkConfig?.networks?.mainnet;
483
- const trezorTx = this.trezor.convertToTrezorFormat({
484
- psbt,
485
- coin: activeNetwork.currency.toLowerCase(),
486
- network: bitcoinjsNetwork || undefined, // Pass network config for isScriptHash check
487
- });
488
- assertCurrent();
489
- const signedPsbt = await this.trezor.signUtxoTransaction(trezorTx, psbt);
490
- assertCurrent();
491
- return signedPsbt;
492
- }
493
- else {
494
- // HD signing (seed-based HDAccount or imported BIP84 zprv/vprv account).
495
- // Some Blockbook deployments return UTXOs without per-input derivation `path`,
496
- // which causes bitcoinjs-lib to throw "Need derivation path to sign with HD".
497
- //
498
- // We can recover by enriching PSBT inputs with a proprietary `path` field using
499
- // Blockbook's xpub token list (address -> path mapping), which reliably includes paths.
500
- try {
501
- const account = accounts[activeAccountType]?.[activeAccountId];
502
- const accountXpub = account?.xpub;
503
- const accountAddress = account?.address;
504
- const isSingleAddressImported = activeAccountType === types_1.KeyringAccountType.Imported &&
505
- Boolean(accountXpub) &&
506
- Boolean(accountAddress) &&
507
- accountXpub === accountAddress;
508
- const isImportedExtended = activeAccountType === types_1.KeyringAccountType.Imported &&
509
- Boolean(accountXpub) &&
510
- !isSingleAddressImported;
511
- // IMPORTANT: Imported extended keys (zprv/vprv) are typically *account-level* keys.
512
- // syscoinjs-lib signs using the imported key as the BIP32 root, so PSBT `path` must be
513
- // relative (change/index), not the full `m/84'/.../change/index`. If we keep full paths,
514
- // bip32.derivePath can throw, leaving no derivations and causing "Need derivation path".
515
- if (isImportedExtended) {
516
- const inputs = psbt?.data?.inputs || [];
517
- for (const input of inputs) {
518
- const unknowns = input?.unknownKeyVals || [];
519
- const pathKv = unknowns.find((kv) => kv?.key?.toString?.() === 'path');
520
- if (!pathKv?.value)
521
- continue;
522
- const fullPath = String(Buffer.from(pathKv.value).toString());
523
- const parts = fullPath.split('/').filter(Boolean);
524
- // Expected: m/84'/slip44'/account'/change/index
525
- if (parts.length >= 2) {
526
- const change = parts[parts.length - 2];
527
- const index = parts[parts.length - 1];
528
- const rel = `${change}/${index}`;
529
- pathKv.value = Buffer.from(rel);
530
- }
531
- }
532
- }
533
- const needsAnyPath = psbt?.data?.inputs?.some((input) => {
534
- const unknowns = input?.unknownKeyVals || [];
535
- return (!input.bip32Derivation?.length &&
536
- !input.tapBip32Derivation?.length &&
537
- !unknowns.some((kv) => kv?.key?.toString?.() === 'path'));
538
- });
539
- if (needsAnyPath &&
540
- accountXpub &&
541
- !isSingleAddressImported &&
542
- typeof psbt.addUnknownKeyValToInput === 'function') {
543
- // Fetch address->path mapping from Blockbook xpub endpoint
544
- // (in-flight deduplicated with the keyring's getAddress lookups)
545
- const blockbookUrl = activeNetwork.url;
546
- const res = await (0, blockbook_cache_1.fetchBackendAccountCached)(blockbookUrl, accountXpub, 'tokens=used&details=tokens');
547
- const tokens = Array.isArray(res?.tokens) ? res.tokens : [];
548
- const addrToPath = new Map();
549
- for (const t of tokens) {
550
- if (t?.name && t?.path) {
551
- addrToPath.set(String(t.name), String(t.path));
552
- }
553
- }
554
- const inputs = psbt?.data?.inputs || [];
555
- for (let i = 0; i < inputs.length; i++) {
556
- const input = inputs[i];
557
- const unknowns = input?.unknownKeyVals || [];
558
- const hasPath = unknowns.some((kv) => kv?.key?.toString?.() === 'path');
559
- if (hasPath)
560
- continue;
561
- const addrKv = unknowns.find((kv) => kv?.key?.toString?.() === 'address');
562
- const addr = addrKv?.value
563
- ? String(Buffer.from(addrKv.value).toString())
564
- : null;
565
- if (!addr)
566
- continue;
567
- const fullPath = addrToPath.get(addr);
568
- if (!fullPath)
569
- continue;
570
- // Imported extended keys are account-level (BIP84 account node), so we must provide a
571
- // path relative to that node (change/index), not the full m/84'/... path.
572
- const pathForSigner = isImportedExtended
573
- ? (() => {
574
- const parts = String(fullPath).split('/').filter(Boolean);
575
- // Expected: m/84'/slip44'/account'/change/index
576
- if (parts.length >= 2) {
577
- const change = parts[parts.length - 2];
578
- const index = parts[parts.length - 1];
579
- return `${change}/${index}`;
580
- }
581
- return fullPath;
582
- })()
583
- : fullPath;
584
- psbt.addUnknownKeyValToInput(i, {
585
- key: Buffer.from('path'),
586
- value: Buffer.from(pathForSigner),
587
- });
588
- }
589
- }
590
- }
591
- catch (_e) {
592
- // Best-effort enrichment; if it fails, fall back to default signing error path.
593
- }
594
- assertCurrent();
595
- (0, psbt_account_scope_1.assertPsbtAccountScope)(psbt, scope);
596
- const { hd } = this.getSigner();
597
- const signedPsbt = await this.signPSBTWithSigner({
598
- psbt,
599
- signer: hd,
600
- });
601
- assertCurrent();
602
- return signedPsbt;
603
- }
604
- };
605
- // Create unsigned PSBT and estimate fee - NO SIGNING
606
- this.getEstimateSysTransactionFee = async ({ txOptions = {}, isMax = false, nativeSweep = false, amount, receivingAddress, feeRate, token = null, excludedOutpoints = [], }) => {
607
- try {
608
- if (nativeSweep !== false &&
609
- (nativeSweep !== true || isMax !== true || token !== null))
610
- throw sweepError('INVALID_NATIVE_SWEEP', 'Native sweep requires explicit MAX and a native transaction.');
611
- // Ensure RBF is enabled by default if not explicitly set
612
- const finalTxOptions = { rbf: true, ...txOptions };
613
- // Use read-only signer since we're just estimating fees and creating unsigned PSBT
614
- const { main } = this.getReadOnlySigner();
615
- // Step 1: Determine fee rate
616
- let actualFeeRate;
617
- if (feeRate !== undefined) {
618
- actualFeeRate = feeRate;
619
- }
620
- else {
621
- actualFeeRate = await this.getRecommendedFee(main.blockbookURL);
622
- }
623
- // Convert fee rate to satoshis/byte and ensure minimum relay of 1 sat/vB
624
- // Blockbook returns coins per kB; after division by 1024, some testnets
625
- // can yield < 1 sat/vB (e.g., 0.9765625). Round up and clamp to 1 to
626
- // avoid zero-fee transactions due to truncation when converting to BN.
627
- const satPerByte = Math.max(1, Math.ceil(actualFeeRate * 1e8));
628
- const feeRateBN = new syscoinjs.utils.BN(satPerByte);
629
- // Step 2: Create unsigned PSBT
630
- const result = await this.createUnsignedPSBT({
631
- txOptions: finalTxOptions,
632
- isMax,
633
- nativeSweep,
634
- amount,
635
- receivingAddress,
636
- feeRateBN,
637
- token,
638
- excludedOutpoints,
639
- });
640
- return {
641
- fee: result.fee / 1e8,
642
- psbt: psbt_1.PsbtUtils.toPali(result.psbt), // Return UNSIGNED PSBT as JSON
643
- ...(nativeSweep
644
- ? {
645
- grossAmount: result.grossAmount,
646
- recipientAmount: result.recipientAmount,
647
- }
648
- : {}),
649
- };
650
- }
651
- catch (error) {
652
- // Pass through structured errors from syscoinjs-lib
653
- if (error.error && error.code) {
654
- // Convert fee from satoshis to SYS if available
655
- if (error.fee !== undefined) {
656
- // If fee is a BN object, convert to number first
657
- if (typeof error.fee === 'object' && error.fee.toNumber) {
658
- error.fee = error.fee.toNumber() / 1e8;
659
- }
660
- else {
661
- error.fee = error.fee / 1e8;
662
- }
663
- }
664
- if (error.remainingFee !== undefined) {
665
- // If remainingFee is a BN object, convert to number first
666
- if (typeof error.remainingFee === 'object' &&
667
- error.remainingFee.toNumber) {
668
- error.remainingFee = error.remainingFee.toNumber() / 1e8;
669
- }
670
- else {
671
- error.remainingFee = error.remainingFee / 1e8;
672
- }
673
- }
674
- if (error.shortfall !== undefined) {
675
- // If shortfall is a BN object, convert to number first
676
- if (typeof error.shortfall === 'object' && error.shortfall.toNumber) {
677
- error.shortfall = error.shortfall.toNumber() / 1e8;
678
- }
679
- else {
680
- error.shortfall = error.shortfall / 1e8;
681
- }
682
- }
683
- throw error;
684
- }
685
- // Wrap other errors
686
- throw {
687
- error: true,
688
- code: 'TRANSACTION_CREATION_FAILED',
689
- message: error.message || 'Failed to estimate transaction fee',
690
- details: error,
691
- };
692
- }
693
- };
694
- // Removed createAndSignSysTransaction - functionality merged into getEstimateSysTransactionFee
695
- this.sendSignedTransaction = async (psbt) => {
696
- try {
697
- // Use read-only signer since we're just broadcasting an already-signed transaction
698
- const { main } = this.getReadOnlySigner();
699
- // Send the transaction
700
- const result = await main.send(psbt);
701
- // Extract the transaction ID
702
- const txid = result.extractTransaction().getId();
703
- return { txid };
704
- }
705
- catch (error) {
706
- // Pass through structured errors
707
- if (error.error && error.code) {
708
- throw error;
709
- }
710
- // Wrap other errors
711
- throw {
712
- error: true,
713
- code: 'TRANSACTION_SEND_FAILED',
714
- message: error.message || 'Failed to send transaction',
715
- details: error,
716
- };
717
- }
718
- };
719
- this.signPSBT = async ({ psbt, isTrezor = false, isLedger = false, }) => {
720
- const psbtObj = psbt_1.PsbtUtils.fromPali(psbt, this.getState().activeNetwork);
721
- // Scope validation temporarily removes other signers' hints. Preserve the
722
- // public metadata for the next cosigner, without exposing it to our signer.
723
- const cosignerMetadata = psbtObj.data.inputs.map((input, index) => ({
724
- hash: Buffer.from(psbtObj.txInputs[index].hash),
725
- index: psbtObj.txInputs[index].index,
726
- bip32Derivation: input.bip32Derivation?.map((value) => ({ ...value })),
727
- tapBip32Derivation: input.tapBip32Derivation?.map((value) => ({
728
- ...value,
729
- })),
730
- path: input.unknownKeyVals
731
- ?.filter((value) => Buffer.from(value.key).toString() === 'path')
732
- .map((value) => ({
733
- key: Buffer.from(value.key),
734
- value: Buffer.from(value.value),
735
- }))[0],
736
- }));
737
- const signedPsbt = await this.signPSBTWithMethod(psbtObj, isTrezor, isLedger);
738
- signedPsbt.data.inputs.forEach((input, index) => {
739
- if (input.finalScriptSig || input.finalScriptWitness)
740
- return;
741
- const metadata = cosignerMetadata[index];
742
- if (!metadata)
743
- return;
744
- const signed = signedPsbt.txInputs[index];
745
- if (!signed ||
746
- metadata.index !== signed.index ||
747
- !metadata.hash.equals(Buffer.from(signed.hash)))
748
- throw new Error('Wallet signing context changed');
749
- if (metadata.bip32Derivation)
750
- input.bip32Derivation = metadata.bip32Derivation;
751
- if (metadata.tapBip32Derivation)
752
- input.tapBip32Derivation = metadata.tapBip32Derivation;
753
- if (metadata.path) {
754
- input.unknownKeyVals = (input.unknownKeyVals || []).filter((value) => Buffer.from(value.key).toString() !== 'path');
755
- input.unknownKeyVals.push(metadata.path);
756
- }
757
- });
758
- return psbt_1.PsbtUtils.toPali(signedPsbt);
759
- };
760
- this.sendTransaction = async (psbt) => {
761
- if (!psbt) {
762
- throw new Error('Signed PSBT is required for broadcasting.');
763
- }
764
- return await this.sendSignedTransaction(psbt_1.PsbtUtils.fromPali(psbt, this.getState().activeNetwork));
765
- };
766
- this.getSigner = getSyscoinSigner;
767
- this.getReadOnlySigner = getReadOnlySigner;
768
- this.getState = getState;
769
- this.getAddress = getAddress;
770
- this.trezor = trezorSigner;
771
- this.captureSigningContext = captureSigningContext;
772
- this.ledger = ledgerSigner;
9
+ super(getSyscoinSigner, getReadOnlySigner, getState, getAddress, {
10
+ ledger: ledgerSigner ? (0, hardware_psbt_1.createLedgerPsbtSigner)(ledgerSigner) : undefined,
11
+ trezor: trezorSigner ? (0, hardware_psbt_1.createTrezorPsbtSigner)(trezorSigner) : undefined,
12
+ }, captureSigningContext);
773
13
  }
774
14
  }
775
15
  exports.SyscoinTransactions = SyscoinTransactions;