@sidhujag/sysweb3-keyring 1.0.613 → 1.0.615

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.
@@ -7,6 +7,7 @@ exports.EthereumTransactions = void 0;
7
7
  const eth_sig_util_1 = require("@metamask/eth-sig-util");
8
8
  const sysweb3_network_1 = require("@sidhujag/sysweb3-network");
9
9
  const sysweb3_utils_1 = require("@sidhujag/sysweb3-utils");
10
+ const transaction_1 = require("ethers/transaction");
10
11
  const omit_1 = __importDefault(require("lodash/omit"));
11
12
  const ethers_v6_1 = require("../ethers-v6");
12
13
  const providers_1 = require("../providers");
@@ -427,18 +428,18 @@ class EthereumTransactions {
427
428
  throw error;
428
429
  }
429
430
  };
430
- this.getFeeDataWithDynamicMaxPriorityFeePerGas = async () => {
431
+ this.getFeeDataWithDynamicMaxPriorityFeePerGas = async (provider = this.web3Provider) => {
431
432
  let maxFeePerGas = this.toBigNumber(0);
432
433
  let maxPriorityFeePerGas = this.toBigNumber(0);
433
434
  try {
434
435
  // First, try to get the current gas price as a baseline
435
- const currentGasPrice = await this.web3Provider.getGasPrice();
436
- const block = await this.web3Provider.getBlock('latest');
436
+ const currentGasPrice = await provider.getGasPrice();
437
+ const block = await provider.getBlock('latest');
437
438
  if (block && block.baseFeePerGas != null) {
438
439
  const baseFeePerGas = ethers_v6_1.BigNumber.from(block.baseFeePerGas);
439
440
  try {
440
441
  // Some networks don't support this RPC method
441
- const ethMaxPriorityFee = await this.web3Provider.send('eth_maxPriorityFeePerGas', []);
442
+ const ethMaxPriorityFee = await provider.send('eth_maxPriorityFeePerGas', []);
442
443
  maxPriorityFeePerGas = ethers_v6_1.BigNumber.from(ethMaxPriorityFee);
443
444
  // Apply minimum priority fee override if set
444
445
  if (this.gasOverrides.minPriorityFee &&
@@ -466,7 +467,7 @@ class EthereumTransactions {
466
467
  // Use a more aggressive fallback strategy with higher priority fees
467
468
  // Check if we can get fee history for better estimation
468
469
  try {
469
- const feeHistory = await this.web3Provider.send('eth_feeHistory', [
470
+ const feeHistory = await provider.send('eth_feeHistory', [
470
471
  '0x5', // Last 5 blocks
471
472
  'latest',
472
473
  [25, 50, 75], // Percentiles for priority fees
@@ -499,7 +500,7 @@ class EthereumTransactions {
499
500
  // If fee history fails, use current gas price as reference
500
501
  console.warn('Fee history not available, using gas price based estimation');
501
502
  // Use 10% of current gas price as priority fee
502
- const currentGasPrice = await this.web3Provider.getGasPrice();
503
+ const currentGasPrice = await provider.getGasPrice();
503
504
  maxPriorityFeePerGas = currentGasPrice.mul(10).div(100);
504
505
  // Ensure minimum of 1 gwei
505
506
  const minPriority = ethers_v6_1.BigNumber.from('1000000000');
@@ -508,7 +509,7 @@ class EthereumTransactions {
508
509
  }
509
510
  }
510
511
  // Calculate maxFeePerGas based on current network conditions
511
- const currentGasPrice = await this.web3Provider.getGasPrice();
512
+ const currentGasPrice = await provider.getGasPrice();
512
513
  const baselineMaxFee = currentGasPrice.mul(120).div(100);
513
514
  const calculatedMaxFee = baseFeePerGas
514
515
  .mul(250)
@@ -1060,13 +1061,101 @@ class EthereumTransactions {
1060
1061
  return await sendEVMTransaction();
1061
1062
  }
1062
1063
  };
1063
- this.sendTransactionWithEditedFee = async (txHash, isLegacy) => {
1064
- let tx = (await this.web3Provider.getTransaction(txHash));
1064
+ this.sendTransactionWithEditedFee = async (txHash, isLegacy = false, options = {}) => {
1065
+ const { assertCurrentContext, validateOriginal, beforeBroadcast, approvedMaximumFee, } = options;
1066
+ const previewOnly = options.previewOnly === true;
1067
+ if (!previewOnly &&
1068
+ (typeof approvedMaximumFee !== 'string' ||
1069
+ !/^\d{1,78}$/.test(approvedMaximumFee))) {
1070
+ throw new Error('Replacement fee approval required');
1071
+ }
1072
+ const assertCurrent = () => assertCurrentContext?.();
1073
+ const assertBeforeBroadcast = () => {
1074
+ assertCurrent();
1075
+ beforeBroadcast?.();
1076
+ assertCurrent();
1077
+ };
1078
+ const assertSignedSender = (signedTx) => {
1079
+ if (transaction_1.Transaction.from(signedTx).from?.toLowerCase() !== expectedSender) {
1080
+ throw new Error('Replacement signature does not match account');
1081
+ }
1082
+ };
1083
+ assertCurrent();
1084
+ const state = this.getState();
1085
+ const activeAccountType = options.targetAccount?.type ?? state.activeAccountType;
1086
+ const activeAccountId = options.targetAccount?.id ?? state.activeAccountId;
1087
+ const targetAccount = options.targetAccount
1088
+ ? { type: activeAccountType, id: activeAccountId }
1089
+ : undefined;
1090
+ const activeAccount = {
1091
+ ...state.accounts?.[activeAccountType]?.[activeAccountId],
1092
+ };
1093
+ const activeNetwork = { ...state.activeNetwork };
1094
+ if (!activeAccount.address ||
1095
+ (targetAccount && activeAccountType === types_1.KeyringAccountType.SmartAccount)) {
1096
+ throw new Error('Replacement signing account is unavailable');
1097
+ }
1098
+ const expectedSender = activeAccount.address.toLowerCase();
1099
+ const requestedHash = txHash.toLowerCase();
1100
+ const provider = this.web3Provider;
1101
+ assertCurrent();
1102
+ const validateLookup = (transaction) => {
1103
+ assertCurrent();
1104
+ if (!transaction)
1105
+ return null;
1106
+ const fields = (0, ethers_v6_1.normalizeTransactionRequest)(transaction);
1107
+ if (fields.hash != null)
1108
+ fields.hash = fields.hash.toLowerCase();
1109
+ const type = fields.type ??
1110
+ (fields.maxFeePerGas != null ? 2 : fields.accessList != null ? 1 : 0);
1111
+ if (![0, 1, 2].includes(type)) {
1112
+ throw new Error('Unsupported replacement transaction type');
1113
+ }
1114
+ fields.signature = fields.signature ?? {
1115
+ r: fields.r,
1116
+ s: fields.s,
1117
+ v: fields.v ?? fields.yParity,
1118
+ };
1119
+ fields.type = type;
1120
+ fields.data = fields.data ?? fields.input ?? '0x';
1121
+ if (type === 2)
1122
+ delete fields.gasPrice; // Effective RPC gas price is not signed in type 2.
1123
+ const original = transaction_1.Transaction.from(fields);
1124
+ if (!original.signature ||
1125
+ original.hash !== requestedHash ||
1126
+ original.from?.toLowerCase() !== expectedSender ||
1127
+ original.chainId !== BigInt(activeNetwork.chainId)) {
1128
+ throw new Error('Original transaction is not authenticated');
1129
+ }
1130
+ if (type !== 1)
1131
+ isLegacy = type === 0;
1132
+ if (isLegacy &&
1133
+ type === 1 &&
1134
+ activeAccountType === types_1.KeyringAccountType.Trezor) {
1135
+ throw new Error('Trezor does not support type 1 replacement signing');
1136
+ }
1137
+ // Read only detached, authenticated signed fields after asynchronous work.
1138
+ const authenticated = (0, providers_1.wrapTransactionResponse)({
1139
+ ...original.toJSON(),
1140
+ hash: original.hash,
1141
+ from: original.from,
1142
+ gasPrice: original.gasPrice ?? 0,
1143
+ maxFeePerGas: original.maxFeePerGas ?? 0,
1144
+ maxPriorityFeePerGas: original.maxPriorityFeePerGas ?? 0,
1145
+ signature: original.signature,
1146
+ blockNumber: transaction.blockNumber,
1147
+ });
1148
+ validateOriginal?.(authenticated);
1149
+ assertCurrent();
1150
+ return authenticated;
1151
+ };
1152
+ let tx = validateLookup(await provider.getTransaction(txHash));
1065
1153
  if (!tx) {
1066
1154
  // Retry a couple of times in case the node hasn't indexed the pending tx yet
1067
1155
  for (let attempt = 0; attempt < 2 && !tx; attempt++) {
1068
1156
  await new Promise((resolve) => setTimeout(resolve, 500 * (attempt + 1)));
1069
- tx = (await this.web3Provider.getTransaction(txHash));
1157
+ assertCurrent();
1158
+ tx = validateLookup(await provider.getTransaction(txHash));
1070
1159
  }
1071
1160
  if (!tx) {
1072
1161
  return {
@@ -1077,17 +1166,17 @@ class EthereumTransactions {
1077
1166
  };
1078
1167
  }
1079
1168
  }
1080
- const { activeAccountType, activeAccountId, accounts, activeNetwork } = this.getState();
1081
- const activeAccount = accounts[activeAccountType][activeAccountId];
1082
1169
  // Check if this might be a max send transaction by comparing total cost to balance
1083
- const currentBalance = await this.web3Provider.getBalance(activeAccount.address);
1170
+ const currentBalance = await provider.getBalance(activeAccount.address);
1171
+ assertCurrent();
1084
1172
  // Ensure all transaction values are resolved from promises
1085
1173
  const gasLimit = await Promise.resolve(tx.gasLimit);
1086
1174
  const gasPrice = await Promise.resolve(tx.gasPrice || 0);
1087
- const maxFeePerGas = await Promise.resolve(tx.maxFeePerGas || 0);
1088
- const maxPriorityFeePerGas = await Promise.resolve(tx.maxPriorityFeePerGas || 0);
1175
+ const maxFeePerGas = await Promise.resolve(tx.type === 1 ? tx.gasPrice : tx.maxFeePerGas);
1176
+ const maxPriorityFeePerGas = await Promise.resolve(tx.type === 1 ? tx.gasPrice : tx.maxPriorityFeePerGas);
1089
1177
  const txValue = await Promise.resolve(tx.value);
1090
1178
  const txData = await Promise.resolve(tx.data || '0x');
1179
+ assertCurrent();
1091
1180
  // Check if this is a contract call (has data)
1092
1181
  const isContractCall = txData && txData !== '0x' && txData.length > 2;
1093
1182
  const originalGasCost = isLegacy
@@ -1097,113 +1186,73 @@ class EthereumTransactions {
1097
1186
  // If original transaction used >95% of balance, it's likely a max send
1098
1187
  const balanceThreshold = currentBalance.mul(95).div(100);
1099
1188
  const isLikelyMaxSend = originalTotalCost.gt(balanceThreshold);
1100
- let txWithEditedFee;
1101
- // If the original tx has 0 or very low gas price, fetch current network gas prices
1102
1189
  const oldTxsGasValues = {
1103
1190
  maxFeePerGas: maxFeePerGas,
1104
1191
  maxPriorityFeePerGas: maxPriorityFeePerGas,
1105
1192
  gasPrice: gasPrice,
1106
1193
  gasLimit: gasLimit,
1107
1194
  };
1108
- // Only fetch current gas prices if the original tx has exactly 0 or undefined gas
1109
- // This avoids unnecessary backend calls for legitimate low-gas networks (L2s, testnets)
1110
- if (isLegacy) {
1111
- if (!oldTxsGasValues.gasPrice || oldTxsGasValues.gasPrice.isZero()) {
1112
- // Fetch current network gas price for replacement
1113
- const currentGasPrice = await this.web3Provider.getGasPrice();
1114
- oldTxsGasValues.gasPrice = currentGasPrice;
1115
- }
1116
- }
1117
- else {
1118
- // For EIP-1559 transactions
1119
- if (!oldTxsGasValues.maxFeePerGas ||
1120
- oldTxsGasValues.maxFeePerGas.isZero()) {
1121
- // Fetch current network fee data for replacement
1122
- const feeData = await this.getFeeDataWithDynamicMaxPriorityFeePerGas();
1123
- oldTxsGasValues.maxFeePerGas = ethers_v6_1.BigNumber.from(feeData.maxFeePerGas || 0);
1124
- oldTxsGasValues.maxPriorityFeePerGas = ethers_v6_1.BigNumber.from(feeData.maxPriorityFeePerGas || 0);
1195
+ // A zero-fee original may need a current estimate; the preview shows it
1196
+ // and the final candidate must stay within the user's approved ceiling.
1197
+ if (isLegacy && oldTxsGasValues.gasPrice.isZero()) {
1198
+ oldTxsGasValues.gasPrice = await provider.getGasPrice();
1199
+ assertCurrent();
1200
+ }
1201
+ else if (!isLegacy && oldTxsGasValues.maxFeePerGas.isZero()) {
1202
+ const feeData = await this.getFeeDataWithDynamicMaxPriorityFeePerGas(provider);
1203
+ assertCurrent();
1204
+ oldTxsGasValues.maxFeePerGas = ethers_v6_1.BigNumber.from(feeData.maxFeePerGas || 0);
1205
+ oldTxsGasValues.maxPriorityFeePerGas = ethers_v6_1.BigNumber.from(feeData.maxPriorityFeePerGas || 0);
1206
+ }
1207
+ const newGasValues = this.calculateNewGasValues(oldTxsGasValues, false, isLegacy);
1208
+ if (!newGasValues.gasLimit.gt(0))
1209
+ throw new Error('Invalid replacement gas limit');
1210
+ if (!isLegacy &&
1211
+ newGasValues.maxPriorityFeePerGas.gt(newGasValues.maxFeePerGas))
1212
+ throw new Error('Invalid replacement fee');
1213
+ const maximumFee = newGasValues.gasLimit.mul(isLegacy ? newGasValues.gasPrice : newGasValues.maxFeePerGas);
1214
+ let adjustedValue = txValue;
1215
+ if (isLikelyMaxSend && txValue.add(maximumFee).gt(currentBalance)) {
1216
+ if (isContractCall) {
1217
+ console.error('[SpeedUp] Cannot adjust value for contract call - rejecting speedup');
1218
+ return {
1219
+ isSpeedUp: false,
1220
+ error: true,
1221
+ code: 'CONTRACT_CALL_MAX_SEND',
1222
+ message: 'Cannot speed up a likely max-send contract call; value cannot be adjusted to fit new gas',
1223
+ };
1125
1224
  }
1225
+ adjustedValue = currentBalance.gt(maximumFee)
1226
+ ? currentBalance.sub(maximumFee)
1227
+ : ethers_v6_1.Zero;
1126
1228
  }
1127
- if (!isLegacy) {
1128
- const newGasValues = this.calculateNewGasValues(oldTxsGasValues, false, false);
1129
- let adjustedValue = txValue;
1130
- // For likely max sends, check if we need to adjust value
1131
- if (isLikelyMaxSend &&
1132
- newGasValues.gasLimit &&
1133
- newGasValues.maxFeePerGas) {
1134
- const newGasCost = newGasValues.gasLimit.mul(newGasValues.maxFeePerGas);
1135
- const newTotalCost = txValue.add(newGasCost);
1136
- if (newTotalCost.gt(currentBalance)) {
1137
- // If this is a contract call, we cannot adjust the value
1138
- if (isContractCall) {
1139
- console.error('[SpeedUp] Cannot adjust value for contract call - rejecting speedup');
1140
- return {
1141
- isSpeedUp: false,
1142
- error: true,
1143
- code: 'CONTRACT_CALL_MAX_SEND',
1144
- message: 'Cannot speed up a likely max-send contract call; value cannot be adjusted to fit new gas',
1145
- };
1146
- }
1147
- // For non-contract calls, reduce value to fit within balance (clamp at zero)
1148
- adjustedValue = currentBalance.gt(newGasCost)
1149
- ? currentBalance.sub(newGasCost)
1150
- : ethers_v6_1.Zero;
1151
- }
1152
- }
1153
- txWithEditedFee = {
1154
- from: tx.from,
1155
- to: tx.to,
1156
- nonce: tx.nonce,
1157
- value: adjustedValue,
1158
- data: txData,
1159
- maxFeePerGas: newGasValues.maxFeePerGas,
1160
- maxPriorityFeePerGas: newGasValues.maxPriorityFeePerGas,
1161
- gasLimit: newGasValues.gasLimit,
1162
- // Don't set type - let ethers auto-detect for EIP-1559
1163
- };
1164
- }
1165
- else {
1166
- const newGasValues = this.calculateNewGasValues(oldTxsGasValues, false, true);
1167
- let adjustedValue = txValue;
1168
- // For likely max sends, check if we need to adjust value
1169
- if (isLikelyMaxSend && newGasValues.gasLimit && newGasValues.gasPrice) {
1170
- const newGasCost = newGasValues.gasLimit.mul(newGasValues.gasPrice);
1171
- const newTotalCost = txValue.add(newGasCost);
1172
- if (newTotalCost.gt(currentBalance)) {
1173
- // If this is a contract call, we cannot adjust the value
1174
- if (isContractCall) {
1175
- console.error('[SpeedUp] Cannot adjust value for contract call - rejecting speedup');
1176
- return {
1177
- isSpeedUp: false,
1178
- error: true,
1179
- code: 'CONTRACT_CALL_MAX_SEND',
1180
- message: 'Cannot speed up a likely max-send contract call; value cannot be adjusted to fit new gas',
1181
- };
1182
- }
1183
- // For non-contract calls, reduce value to fit within balance (clamp at zero)
1184
- adjustedValue = currentBalance.gt(newGasCost)
1185
- ? currentBalance.sub(newGasCost)
1186
- : ethers_v6_1.Zero;
1187
- }
1188
- }
1189
- txWithEditedFee = {
1190
- from: tx.from,
1191
- to: tx.to,
1192
- nonce: tx.nonce,
1193
- value: adjustedValue,
1194
- data: txData,
1195
- gasLimit: newGasValues.gasLimit,
1196
- gasPrice: newGasValues.gasPrice,
1197
- type: 0, // Force Type 0 for legacy speedup
1198
- };
1199
- }
1229
+ const txWithEditedFee = {
1230
+ chainId: activeNetwork.chainId,
1231
+ from: tx.from,
1232
+ to: tx.to,
1233
+ nonce: tx.nonce,
1234
+ value: adjustedValue,
1235
+ data: txData,
1236
+ gasLimit: newGasValues.gasLimit,
1237
+ ...(tx.accessList != null ? { accessList: tx.accessList } : {}),
1238
+ ...(isLegacy
1239
+ ? { gasPrice: newGasValues.gasPrice, type: tx.type === 1 ? 1 : 0 }
1240
+ : {
1241
+ maxFeePerGas: newGasValues.maxFeePerGas,
1242
+ maxPriorityFeePerGas: newGasValues.maxPriorityFeePerGas,
1243
+ }),
1244
+ };
1245
+ if (previewOnly)
1246
+ return { isSpeedUp: false, maximumFee: maximumFee.toString() };
1247
+ if (maximumFee.gt(approvedMaximumFee))
1248
+ throw new Error('Replacement fee exceeds approval');
1200
1249
  // Ledger speedup handler
1201
1250
  const speedUpWithLedger = async () => {
1202
1251
  try {
1203
1252
  const resolvedParams = await (0, ethers_v6_1.resolveProperties)((0, omit_1.default)(txWithEditedFee, 'from'));
1204
1253
  const formatParams = {
1205
1254
  ...resolvedParams,
1206
- nonce: resolvedParams.nonce
1255
+ nonce: resolvedParams.nonce != null
1207
1256
  ? Number(resolvedParams.nonce.toString())
1208
1257
  : undefined,
1209
1258
  };
@@ -1211,7 +1260,7 @@ class EthereumTransactions {
1211
1260
  ? {
1212
1261
  ...formatParams,
1213
1262
  chainId: activeNetwork.chainId,
1214
- type: 0, // Need explicit type for hardware wallet serialization
1263
+ type: txWithEditedFee.type,
1215
1264
  }
1216
1265
  : {
1217
1266
  ...formatParams,
@@ -1219,6 +1268,7 @@ class EthereumTransactions {
1219
1268
  type: 2, // Need explicit type for hardware wallet serialization
1220
1269
  };
1221
1270
  const rawTx = (0, ethers_v6_1.serializeTransaction)(txFormattedForEthers);
1271
+ assertCurrent();
1222
1272
  const signature = await this.ledgerSigner.evm.signEVMTransaction({
1223
1273
  rawTx: rawTx.replace('0x', ''),
1224
1274
  accountIndex: activeAccountId,
@@ -1230,7 +1280,9 @@ class EthereumTransactions {
1230
1280
  };
1231
1281
  if (signature) {
1232
1282
  const signedTx = (0, ethers_v6_1.serializeTransaction)(txFormattedForEthers, formattedSignature);
1233
- const transactionResponse = await this.web3Provider.sendTransaction(signedTx);
1283
+ assertSignedSender(signedTx);
1284
+ assertBeforeBroadcast();
1285
+ const transactionResponse = await provider.sendTransaction(signedTx);
1234
1286
  return {
1235
1287
  isSpeedUp: true,
1236
1288
  transaction: transactionResponse,
@@ -1276,7 +1328,7 @@ class EthereumTransactions {
1276
1328
  : this.toHex0x(formattedTx.value, 'value'),
1277
1329
  nonce: this.toHex0x(formattedTx.nonce, 'nonce'),
1278
1330
  chainId: activeNetwork.chainId,
1279
- type: isLegacy ? 0 : 2, // Need explicit type for hardware wallet serialization
1331
+ type: isLegacy ? txWithEditedFee.type : 2,
1280
1332
  };
1281
1333
  if (formattedTx.data && formattedTx.data !== '0x') {
1282
1334
  txFormattedForTrezor.data = formattedTx.data;
@@ -1297,6 +1349,7 @@ class EthereumTransactions {
1297
1349
  ? formattedTx.maxPriorityFeePerGas
1298
1350
  : this.toHex0x(formattedTx.maxPriorityFeePerGas, 'maxPriorityFeePerGas');
1299
1351
  }
1352
+ assertCurrent();
1300
1353
  const signature = await this.trezorSigner.signEthTransaction({
1301
1354
  coin: trezorCoin,
1302
1355
  tx: txFormattedForTrezor,
@@ -1305,7 +1358,9 @@ class EthereumTransactions {
1305
1358
  });
1306
1359
  if (signature.success) {
1307
1360
  const signedTx = (0, ethers_v6_1.serializeTransaction)(txFormattedForTrezor, signature.payload);
1308
- const transactionResponse = await this.web3Provider.sendTransaction(signedTx);
1361
+ assertSignedSender(signedTx);
1362
+ assertBeforeBroadcast();
1363
+ const transactionResponse = await provider.sendTransaction(signedTx);
1309
1364
  return {
1310
1365
  isSpeedUp: true,
1311
1366
  transaction: transactionResponse,
@@ -1339,8 +1394,13 @@ class EthereumTransactions {
1339
1394
  // Regular wallet speedup handler
1340
1395
  const speedUpWithPrivateKey = async () => {
1341
1396
  try {
1342
- const { decryptedPrivateKey } = this.getDecryptedPrivateKey();
1343
- const transactionResponse = await (0, evm_local_signer_1.sendLocalEvmTransaction)(this.web3Provider, decryptedPrivateKey, txWithEditedFee);
1397
+ assertCurrent();
1398
+ const { address, decryptedPrivateKey } = this.getDecryptedPrivateKey(targetAccount);
1399
+ assertCurrent();
1400
+ if (address.toLowerCase() !== expectedSender) {
1401
+ throw new Error('Replacement signing account changed');
1402
+ }
1403
+ const transactionResponse = await (0, evm_local_signer_1.sendLocalEvmTransaction)(provider, decryptedPrivateKey, txWithEditedFee, assertBeforeBroadcast, assertCurrent);
1344
1404
  if (transactionResponse) {
1345
1405
  return {
1346
1406
  isSpeedUp: true,