@towns-labs/relayer 3.3.1 → 3.4.0

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.
package/dist/index.js CHANGED
@@ -12198,7 +12198,7 @@ var cors = /* @__PURE__ */ __name((options) => {
12198
12198
  }, "cors2");
12199
12199
  }, "cors");
12200
12200
 
12201
- // ../deployments/dist/addresses.json
12201
+ // ../contracts/dist/addresses.json
12202
12202
  var addresses_default = {
12203
12203
  dev: {
12204
12204
  "84532": {
@@ -12292,7 +12292,7 @@ var addresses_default = {
12292
12292
  }
12293
12293
  };
12294
12294
 
12295
- // ../deployments/dist/index.js
12295
+ // ../contracts/dist/index.js
12296
12296
  var requiredAddressKeys = [
12297
12297
  "orchestrator",
12298
12298
  "simpleFunder",
@@ -21741,7 +21741,7 @@ function errorDetails(error48) {
21741
21741
  __name(errorDetails, "errorDetails");
21742
21742
  var logger = createLogger("eip7702-relayer");
21743
21743
 
21744
- // ../deployments/dist/abis/index.js
21744
+ // ../contracts/dist/abis/index.js
21745
21745
  var orchestratorAbi = [
21746
21746
  { type: "receive", stateMutability: "payable" },
21747
21747
  {
@@ -44780,6 +44780,76 @@ function isPendingTransactionIdUniqueConstraintError(errorMessage) {
44780
44780
  }
44781
44781
  __name(isPendingTransactionIdUniqueConstraintError, "isPendingTransactionIdUniqueConstraintError");
44782
44782
 
44783
+ // src/durable-objects/signer-replacement-policy.ts
44784
+ function ceilMultiplyByBps(value, bps) {
44785
+ if (bps < 0) {
44786
+ throw new Error("bumpBps must be non-negative");
44787
+ }
44788
+ const multiplier = BigInt(1e4 + bps);
44789
+ const numerator = value * multiplier;
44790
+ return (numerator + 9999n) / 10000n;
44791
+ }
44792
+ __name(ceilMultiplyByBps, "ceilMultiplyByBps");
44793
+ function mapStoredTxStatusToPublicStatus(storedStatus) {
44794
+ if (storedStatus === "confirmed") return "confirmed";
44795
+ if (storedStatus === "failed" || storedStatus === "stuck" || storedStatus === "abandoned") {
44796
+ return "failed";
44797
+ }
44798
+ return "pending";
44799
+ }
44800
+ __name(mapStoredTxStatusToPublicStatus, "mapStoredTxStatusToPublicStatus");
44801
+ function shouldTriggerReplacementByFee(input) {
44802
+ return input.requiredMaxFeePerGas > input.currentMaxFeePerGas + input.staleThresholdPerGas;
44803
+ }
44804
+ __name(shouldTriggerReplacementByFee, "shouldTriggerReplacementByFee");
44805
+ function computeReplacementFees(input) {
44806
+ const bumpedMaxFee = ceilMultiplyByBps(input.currentMaxFeePerGas, input.bumpBps);
44807
+ const bumpedMaxPriorityFee = ceilMultiplyByBps(input.currentMaxPriorityFeePerGas, input.bumpBps);
44808
+ const maxFeePerGas = bumpedMaxFee > input.requiredMaxFeePerGas ? bumpedMaxFee : input.requiredMaxFeePerGas;
44809
+ const maxPriorityFeePerGas = bumpedMaxPriorityFee > input.requiredMaxPriorityFeePerGas ? bumpedMaxPriorityFee : input.requiredMaxPriorityFeePerGas;
44810
+ if (input.maxFeePerGasCap !== void 0 && (maxFeePerGas > input.maxFeePerGasCap || maxPriorityFeePerGas > input.maxFeePerGasCap)) {
44811
+ return null;
44812
+ }
44813
+ return {
44814
+ maxFeePerGas,
44815
+ maxPriorityFeePerGas
44816
+ };
44817
+ }
44818
+ __name(computeReplacementFees, "computeReplacementFees");
44819
+ function shouldApplyFinalization(input) {
44820
+ if (!input.eventTxHash) return true;
44821
+ if (input.status === "confirmed") return true;
44822
+ return input.activeTxHash.toLowerCase() === input.eventTxHash.toLowerCase();
44823
+ }
44824
+ __name(shouldApplyFinalization, "shouldApplyFinalization");
44825
+ function shouldAttemptReplacementNow(input) {
44826
+ if (input.maxAttempts <= 0) return false;
44827
+ if (input.attempts >= input.maxAttempts) return false;
44828
+ if (input.attempts === 0) return true;
44829
+ const exponent = input.attempts - 1;
44830
+ const backoffMs = input.baseBackoffMs * Math.pow(2, exponent);
44831
+ return input.nowMs >= input.lastReplacementAtMs + backoffMs;
44832
+ }
44833
+ __name(shouldAttemptReplacementNow, "shouldAttemptReplacementNow");
44834
+ async function withCleanupOnError(operation, cleanup) {
44835
+ try {
44836
+ return await operation();
44837
+ } catch (error48) {
44838
+ await cleanup();
44839
+ throw error48;
44840
+ }
44841
+ }
44842
+ __name(withCleanupOnError, "withCleanupOnError");
44843
+ async function withRecoveryOnError(operation, recovery, fallback) {
44844
+ try {
44845
+ return await operation();
44846
+ } catch {
44847
+ await recovery();
44848
+ return fallback;
44849
+ }
44850
+ }
44851
+ __name(withRecoveryOnError, "withRecoveryOnError");
44852
+
44783
44853
  // src/durable-objects/signer.do.ts
44784
44854
  var DEFAULT_MAX_PENDING = 16;
44785
44855
  var BALANCE_CHECK_INTERVAL_MS = 3e5;
@@ -44787,12 +44857,14 @@ var STALE_TX_THRESHOLD_MS = 5 * 60 * 1e3;
44787
44857
  var DEFAULT_INTENT_EXPIRY_BUFFER_SECONDS = 30;
44788
44858
  var DUPLICATE_TX_WAIT_MS = 2e3;
44789
44859
  var DUPLICATE_TX_WAIT_POLL_MS = 100;
44790
- function mapStoredTxStatusToPublicStatus(storedStatus) {
44791
- if (storedStatus === "confirmed") return "confirmed";
44792
- if (storedStatus === "failed" || storedStatus === "stuck") return "failed";
44793
- return "pending";
44794
- }
44795
- __name(mapStoredTxStatusToPublicStatus, "mapStoredTxStatusToPublicStatus");
44860
+ var DEFAULT_REPLACEMENT_BUMP_BPS = 1250;
44861
+ var DEFAULT_REPLACEMENT_TRIGGER_WEI = 0n;
44862
+ var DEFAULT_REPLACEMENT_MAX_ATTEMPTS = 3;
44863
+ var DEFAULT_REPLACEMENT_BACKOFF_MS = 3e4;
44864
+ function mapStoredTxStatusToPublicStatus2(storedStatus) {
44865
+ return mapStoredTxStatusToPublicStatus(storedStatus);
44866
+ }
44867
+ __name(mapStoredTxStatusToPublicStatus2, "mapStoredTxStatusToPublicStatus");
44796
44868
  function isIntentExpired(expiryTimestamp, bufferSeconds = DEFAULT_INTENT_EXPIRY_BUFFER_SECONDS) {
44797
44869
  const expiry = typeof expiryTimestamp === "string" ? BigInt(expiryTimestamp) : expiryTimestamp;
44798
44870
  const currentTime = BigInt(Math.floor(Date.now() / 1e3));
@@ -44851,8 +44923,8 @@ var SignerDO = class extends DurableObject {
44851
44923
  if (request.method !== "POST") {
44852
44924
  return new Response("Method not allowed", { status: 405 });
44853
44925
  }
44854
- const { txId, status } = await request.json();
44855
- await this.handleFinalized(txId, status);
44926
+ const { txId, status, txHash } = await request.json();
44927
+ await this.handleFinalized(txId, status, txHash);
44856
44928
  return Response.json({ ok: true });
44857
44929
  }
44858
44930
  case "/maintenance": {
@@ -44934,6 +45006,20 @@ var SignerDO = class extends DurableObject {
44934
45006
  UPDATE schema_version SET version = 2 WHERE id = 1;
44935
45007
  `);
44936
45008
  }
45009
+ if (currentVersion < 3) {
45010
+ this.sql.exec(`
45011
+ ALTER TABLE pending_transactions ADD COLUMN tx_to TEXT;
45012
+ ALTER TABLE pending_transactions ADD COLUMN tx_data TEXT;
45013
+ ALTER TABLE pending_transactions ADD COLUMN tx_value TEXT;
45014
+ ALTER TABLE pending_transactions ADD COLUMN tx_authorization_list TEXT;
45015
+ ALTER TABLE pending_transactions ADD COLUMN max_fee_per_gas TEXT;
45016
+ ALTER TABLE pending_transactions ADD COLUMN max_priority_fee_per_gas TEXT;
45017
+ ALTER TABLE pending_transactions ADD COLUMN replacement_attempts INTEGER NOT NULL DEFAULT 0;
45018
+ ALTER TABLE pending_transactions ADD COLUMN last_replacement_at INTEGER NOT NULL DEFAULT 0;
45019
+ CREATE INDEX IF NOT EXISTS idx_pending_replacement ON pending_transactions(status, sent_at);
45020
+ UPDATE schema_version SET version = 3 WHERE id = 1;
45021
+ `);
45022
+ }
44937
45023
  }
44938
45024
  /**
44939
45025
  * Self-initialize on first request
@@ -45097,7 +45183,7 @@ var SignerDO = class extends DurableObject {
45097
45183
  this.sql.exec("DELETE FROM pending_transactions WHERE id = ?", txId);
45098
45184
  }
45099
45185
  const pendingMaxRows = this.sql.exec(
45100
- "SELECT MAX(nonce) as max_nonce FROM pending_transactions WHERE status = 'pending'"
45186
+ "SELECT MAX(nonce) as max_nonce FROM pending_transactions WHERE status IN ('pending', 'replacing')"
45101
45187
  ).toArray();
45102
45188
  const pendingMax = pendingMaxRows[0]?.max_nonce;
45103
45189
  const nextNonce = Math.max(onChainNonce, (pendingMax ?? -1) + 1);
@@ -45202,7 +45288,7 @@ var SignerDO = class extends DurableObject {
45202
45288
  try {
45203
45289
  const receipt = await this.getTransactionReceipt(txHash, chainId);
45204
45290
  if (receipt) {
45205
- if (status === "pending") {
45291
+ if (status === "pending" || status === "replacing") {
45206
45292
  const finalStatus = receipt.status === "0x1" ? "confirmed" : "failed";
45207
45293
  this.sql.exec(
45208
45294
  "UPDATE pending_transactions SET status = ? WHERE id = ?",
@@ -45227,7 +45313,7 @@ var SignerDO = class extends DurableObject {
45227
45313
  }
45228
45314
  } catch {
45229
45315
  }
45230
- const fallbackStatus = mapStoredTxStatusToPublicStatus(status);
45316
+ const fallbackStatus = mapStoredTxStatusToPublicStatus2(status);
45231
45317
  return {
45232
45318
  txId,
45233
45319
  txHash,
@@ -45333,7 +45419,9 @@ var SignerDO = class extends DurableObject {
45333
45419
  }
45334
45420
  const minBalance = this.parseBalance(this.env.MIN_SIGNER_BALANCE, 10000000000000000n);
45335
45421
  const isPaused = !!state.paused || balanceWei < minBalance;
45336
- const pendingCount = this.sql.exec("SELECT COUNT(*) as c FROM pending_transactions WHERE status = 'pending'").toArray()[0]?.c ?? 0;
45422
+ const pendingCount = this.sql.exec(
45423
+ "SELECT COUNT(*) as c FROM pending_transactions WHERE status IN ('pending', 'replacing')"
45424
+ ).toArray()[0]?.c ?? 0;
45337
45425
  const maxPending = parseInt(
45338
45426
  this.env.MAX_PENDING_PER_SIGNER ?? String(DEFAULT_MAX_PENDING),
45339
45427
  10
@@ -45372,7 +45460,7 @@ var SignerDO = class extends DurableObject {
45372
45460
  return { error: "Signer is paused", code: "PAUSED" };
45373
45461
  }
45374
45462
  const pending = this.sql.exec(
45375
- "SELECT COUNT(*) as c FROM pending_transactions WHERE status = 'pending'"
45463
+ "SELECT COUNT(*) as c FROM pending_transactions WHERE status IN ('pending', 'replacing')"
45376
45464
  ).toArray()[0]?.c ?? 0;
45377
45465
  if (pending >= maxPending) {
45378
45466
  return {
@@ -45415,13 +45503,34 @@ var SignerDO = class extends DurableObject {
45415
45503
  throw new SignerDOError(errorResult.error, errorResult.code ?? "BROADCAST_FAILED");
45416
45504
  }
45417
45505
  const successResult = result;
45506
+ const { txParams, initialFeeParams } = await withCleanupOnError(
45507
+ async () => {
45508
+ const preparedTxParams = await this.buildTxParams(
45509
+ tx,
45510
+ successResult.chainId,
45511
+ successResult.address
45512
+ );
45513
+ const recommendedFeeParams = await this.getRecommendedFeeParams(
45514
+ successResult.chainId
45515
+ );
45516
+ return {
45517
+ txParams: preparedTxParams,
45518
+ initialFeeParams: recommendedFeeParams
45519
+ };
45520
+ },
45521
+ async () => {
45522
+ await this.syncNonceFromChainAtomic(tx.id);
45523
+ }
45524
+ );
45418
45525
  let txHash;
45526
+ let usedFeeParams = initialFeeParams;
45527
+ let usedNonce = successResult.nonce;
45419
45528
  try {
45420
- txHash = await this.signAndBroadcast(
45421
- tx,
45529
+ txHash = await this.signAndBroadcastPrepared(
45530
+ txParams,
45422
45531
  successResult.nonce,
45423
- successResult.address,
45424
- successResult.chainId
45532
+ successResult.chainId,
45533
+ initialFeeParams
45425
45534
  );
45426
45535
  } catch (error48) {
45427
45536
  const errorMessage = getErrorMessage(error48);
@@ -45448,12 +45557,15 @@ var SignerDO = class extends DurableObject {
45448
45557
  `[SignerDO] Retrying with synced nonce: ${retryNonce} (synced base: ${syncedNonce})`
45449
45558
  );
45450
45559
  try {
45451
- txHash = await this.signAndBroadcast(
45452
- tx,
45560
+ const retryFeeParams = await this.getRecommendedFeeParams(successResult.chainId);
45561
+ txHash = await this.signAndBroadcastPrepared(
45562
+ txParams,
45453
45563
  retryNonce,
45454
- successResult.address,
45455
- successResult.chainId
45564
+ successResult.chainId,
45565
+ retryFeeParams
45456
45566
  );
45567
+ usedFeeParams = retryFeeParams;
45568
+ usedNonce = retryNonce;
45457
45569
  } catch (retryError) {
45458
45570
  await this.syncNonceFromChainAtomic(tx.id);
45459
45571
  throw retryError;
@@ -45463,7 +45575,22 @@ var SignerDO = class extends DurableObject {
45463
45575
  throw error48;
45464
45576
  }
45465
45577
  }
45466
- this.sql.exec("UPDATE pending_transactions SET tx_hash = ? WHERE id = ?", txHash, tx.id);
45578
+ this.sql.exec(
45579
+ `
45580
+ UPDATE pending_transactions
45581
+ SET tx_hash = ?, tx_to = ?, tx_data = ?, tx_value = ?, tx_authorization_list = ?,
45582
+ max_fee_per_gas = ?, max_priority_fee_per_gas = ?
45583
+ WHERE id = ?
45584
+ `,
45585
+ txHash,
45586
+ txParams.to,
45587
+ txParams.data,
45588
+ txParams.value.toString(),
45589
+ this.serializeAuthorizationList(txParams.authorizationList),
45590
+ usedFeeParams.maxFeePerGas.toString(),
45591
+ usedFeeParams.maxPriorityFeePerGas.toString(),
45592
+ tx.id
45593
+ );
45467
45594
  const signerName = this.getSignerName();
45468
45595
  if (!signerName) {
45469
45596
  throw new SignerDOError(
@@ -45471,18 +45598,10 @@ var SignerDO = class extends DurableObject {
45471
45598
  "NOT_INITIALIZED"
45472
45599
  );
45473
45600
  }
45474
- await this.env.MONITOR_QUEUE.send({
45475
- type: "monitor",
45476
- txId: tx.id,
45477
- txHash,
45478
- signerName,
45479
- chainId: successResult.chainId,
45480
- attempt: 0
45481
- });
45482
- this.sql.exec("UPDATE pending_transactions SET queued = 1 WHERE id = ?", tx.id);
45601
+ await this.enqueueMonitorJob(tx.id, txHash, signerName, successResult.chainId);
45483
45602
  return {
45484
45603
  txHash,
45485
- nonce: successResult.nonce,
45604
+ nonce: usedNonce,
45486
45605
  signer: successResult.address,
45487
45606
  signerName
45488
45607
  };
@@ -45550,16 +45669,76 @@ var SignerDO = class extends DurableObject {
45550
45669
  }
45551
45670
  return "";
45552
45671
  }
45553
- /**
45554
- * Sign and broadcast a transaction
45555
- */
45556
- async signAndBroadcast(tx, nonce, _signerAddress, chainId) {
45557
- const { walletClient, account } = this.ensureClients(chainId);
45672
+ async enqueueMonitorJob(txId, txHash, signerName, chainId) {
45673
+ try {
45674
+ await this.env.MONITOR_QUEUE.send({
45675
+ type: "monitor",
45676
+ txId,
45677
+ txHash,
45678
+ signerName,
45679
+ chainId,
45680
+ attempt: 0
45681
+ });
45682
+ this.sql.exec("UPDATE pending_transactions SET queued = 1 WHERE id = ?", txId);
45683
+ return true;
45684
+ } catch {
45685
+ this.sql.exec("UPDATE pending_transactions SET queued = 0 WHERE id = ?", txId);
45686
+ return false;
45687
+ }
45688
+ }
45689
+ parseOptionalBigInt(value) {
45690
+ if (!value) return void 0;
45691
+ const trimmed = value.trim();
45692
+ if (!trimmed) return void 0;
45693
+ return BigInt(trimmed);
45694
+ }
45695
+ serializeAuthorizationList(authorizationList) {
45696
+ if (!authorizationList || authorizationList.length === 0) return null;
45697
+ return JSON.stringify(
45698
+ authorizationList,
45699
+ (_key, value) => typeof value === "bigint" ? value.toString() : value
45700
+ );
45701
+ }
45702
+ deserializeAuthorizationList(value) {
45703
+ if (!value) return void 0;
45704
+ const parsed = JSON.parse(value);
45705
+ return parsed.map((item) => {
45706
+ const copy = { ...item };
45707
+ if (typeof copy.chainId === "string") copy.chainId = BigInt(copy.chainId);
45708
+ if (typeof copy.nonce === "string") copy.nonce = BigInt(copy.nonce);
45709
+ return copy;
45710
+ });
45711
+ }
45712
+ getRecommendedFeeParams(chainId) {
45713
+ const { publicClient } = this.ensureClients(chainId);
45714
+ return publicClient.estimateFeesPerGas({
45715
+ type: "eip1559",
45716
+ chain: publicClient.chain
45717
+ });
45718
+ }
45719
+ parseReplacementConfig() {
45720
+ return {
45721
+ bumpBps: parseInt(
45722
+ this.env.REPLACEMENT_BUMP_BPS ?? String(DEFAULT_REPLACEMENT_BUMP_BPS),
45723
+ 10
45724
+ ),
45725
+ triggerThresholdWei: this.parseOptionalBigInt(this.env.REPLACEMENT_TRIGGER_THRESHOLD_WEI) ?? DEFAULT_REPLACEMENT_TRIGGER_WEI,
45726
+ maxAttempts: parseInt(
45727
+ this.env.REPLACEMENT_MAX_ATTEMPTS ?? String(DEFAULT_REPLACEMENT_MAX_ATTEMPTS),
45728
+ 10
45729
+ ),
45730
+ baseBackoffMs: parseInt(
45731
+ this.env.REPLACEMENT_BACKOFF_BASE_MS ?? String(DEFAULT_REPLACEMENT_BACKOFF_MS),
45732
+ 10
45733
+ ),
45734
+ maxFeeCapWei: this.parseOptionalBigInt(this.env.REPLACEMENT_MAX_FEE_PER_GAS_WEI)
45735
+ };
45736
+ }
45737
+ async buildTxParams(tx, chainId, signerAddress) {
45558
45738
  const contracts = getContractAddresses(
45559
45739
  this.env,
45560
45740
  chainId
45561
45741
  );
45562
- let txParams;
45563
45742
  switch (tx.type) {
45564
45743
  case "create-account": {
45565
45744
  if (tx.preCall && tx.preCall.executionData !== "0x") {
@@ -45569,23 +45748,23 @@ var SignerDO = class extends DurableObject {
45569
45748
  nonce: BigInt(tx.preCall.nonce),
45570
45749
  signature: tx.preCall.signature
45571
45750
  };
45572
- txParams = {
45751
+ return {
45573
45752
  to: contracts.orchestrator,
45574
45753
  data: encodeFunctionData({
45575
45754
  abi: orchestratorAbi,
45576
45755
  functionName: "executePreCalls",
45577
45756
  args: [tx.accountAddress, [signedCall]]
45578
45757
  }),
45579
- authorizationList: [tx.authorization]
45580
- };
45581
- } else {
45582
- txParams = {
45583
- to: tx.accountAddress,
45584
- data: "0x",
45758
+ value: 0n,
45585
45759
  authorizationList: [tx.authorization]
45586
45760
  };
45587
45761
  }
45588
- break;
45762
+ return {
45763
+ to: tx.accountAddress,
45764
+ data: "0x",
45765
+ value: 0n,
45766
+ authorizationList: [tx.authorization]
45767
+ };
45589
45768
  }
45590
45769
  case "execute-intent": {
45591
45770
  const bufferSeconds = parseInt(
@@ -45600,18 +45779,18 @@ var SignerDO = class extends DurableObject {
45600
45779
  }
45601
45780
  const intentWithRecipient = {
45602
45781
  ...tx.intent,
45603
- paymentRecipient: getPaymentRecipient(this.env.FEE_RECIPIENT, account.address)
45782
+ paymentRecipient: getPaymentRecipient(this.env.FEE_RECIPIENT, signerAddress)
45604
45783
  };
45605
45784
  const encodedIntent = this.encodeIntentToBytes(intentWithRecipient);
45606
- txParams = {
45785
+ return {
45607
45786
  to: contracts.orchestrator,
45608
45787
  data: encodeFunctionData({
45609
45788
  abi: orchestratorAbi,
45610
45789
  functionName: "execute",
45611
45790
  args: [encodedIntent]
45612
- })
45791
+ }),
45792
+ value: 0n
45613
45793
  };
45614
- break;
45615
45794
  }
45616
45795
  case "batch-execute-intent": {
45617
45796
  const batchBufferSeconds = parseInt(
@@ -45628,35 +45807,34 @@ var SignerDO = class extends DurableObject {
45628
45807
  }
45629
45808
  const intentsWithRecipient = tx.intents.map((intent) => ({
45630
45809
  ...intent,
45631
- paymentRecipient: getPaymentRecipient(this.env.FEE_RECIPIENT, account.address)
45810
+ paymentRecipient: getPaymentRecipient(this.env.FEE_RECIPIENT, signerAddress)
45632
45811
  }));
45633
45812
  const encodedIntents = intentsWithRecipient.map(
45634
45813
  (intent) => this.encodeIntentToBytes(intent)
45635
45814
  );
45636
- txParams = {
45815
+ return {
45637
45816
  to: contracts.orchestrator,
45638
45817
  data: encodeFunctionData({
45639
45818
  abi: orchestratorAbi,
45640
45819
  functionName: "execute",
45641
45820
  args: [encodedIntents]
45642
- })
45821
+ }),
45822
+ value: 0n
45643
45823
  };
45644
- break;
45645
45824
  }
45646
- default:
45647
- throw new SignerDOError(
45648
- `Unknown transaction type: ${tx.type}`,
45649
- "BROADCAST_FAILED"
45650
- );
45651
45825
  }
45826
+ }
45827
+ async signAndBroadcastPrepared(txParams, nonce, chainId, feeParams) {
45828
+ const { walletClient, account } = this.ensureClients(chainId);
45652
45829
  try {
45653
- const txHash = await walletClient.sendTransaction({
45830
+ return await walletClient.sendTransaction({
45654
45831
  ...txParams,
45655
45832
  nonce,
45656
45833
  account,
45657
- chain: { id: chainId }
45834
+ chain: { id: chainId },
45835
+ maxFeePerGas: feeParams.maxFeePerGas,
45836
+ maxPriorityFeePerGas: feeParams.maxPriorityFeePerGas
45658
45837
  });
45659
- return txHash;
45660
45838
  } catch (error48) {
45661
45839
  const message = getErrorMessage(error48);
45662
45840
  throw new SignerDOError(
@@ -45745,9 +45923,187 @@ var SignerDO = class extends DurableObject {
45745
45923
  /**
45746
45924
  * Handle transaction finalization (called by queue consumer)
45747
45925
  */
45748
- async handleFinalized(txId, status) {
45926
+ async handleFinalized(txId, status, txHash) {
45927
+ if (txHash) {
45928
+ const rows = this.sql.exec("SELECT tx_hash FROM pending_transactions WHERE id = ?", txId).toArray();
45929
+ if (rows.length === 0) return;
45930
+ const activeTxHash = rows[0].tx_hash ?? "";
45931
+ if (!shouldApplyFinalization({ activeTxHash, eventTxHash: txHash, status })) {
45932
+ return;
45933
+ }
45934
+ }
45749
45935
  this.sql.exec("UPDATE pending_transactions SET status = ? WHERE id = ?", status, txId);
45750
45936
  }
45937
+ async tryReplaceStaleTransaction(txId, chainId, signerName) {
45938
+ const config2 = this.parseReplacementConfig();
45939
+ const nowMs = Date.now();
45940
+ const claimedRows = this.sql.exec(
45941
+ `
45942
+ UPDATE pending_transactions
45943
+ SET status = 'replacing'
45944
+ WHERE id = ? AND status = 'pending'
45945
+ RETURNING
45946
+ nonce,
45947
+ tx_to,
45948
+ tx_data,
45949
+ tx_value,
45950
+ tx_authorization_list,
45951
+ max_fee_per_gas,
45952
+ max_priority_fee_per_gas,
45953
+ replacement_attempts,
45954
+ last_replacement_at
45955
+ `,
45956
+ txId
45957
+ ).toArray();
45958
+ if (claimedRows.length === 0) return "skipped";
45959
+ return withRecoveryOnError(
45960
+ async () => {
45961
+ const claimed = claimedRows[0];
45962
+ const attempts = claimed.replacement_attempts ?? 0;
45963
+ const lastReplacementAtMs = claimed.last_replacement_at ?? 0;
45964
+ if (attempts >= config2.maxAttempts) {
45965
+ this.sql.exec(
45966
+ "UPDATE pending_transactions SET status = ? WHERE id = ?",
45967
+ "abandoned",
45968
+ txId
45969
+ );
45970
+ return "abandoned";
45971
+ }
45972
+ if (!shouldAttemptReplacementNow({
45973
+ nowMs,
45974
+ attempts,
45975
+ maxAttempts: config2.maxAttempts,
45976
+ lastReplacementAtMs,
45977
+ baseBackoffMs: config2.baseBackoffMs
45978
+ })) {
45979
+ this.sql.exec(
45980
+ "UPDATE pending_transactions SET status = ? WHERE id = ?",
45981
+ "pending",
45982
+ txId
45983
+ );
45984
+ return "skipped";
45985
+ }
45986
+ const currentMaxFee = this.parseOptionalBigInt(
45987
+ claimed.max_fee_per_gas
45988
+ );
45989
+ const currentMaxPriority = this.parseOptionalBigInt(
45990
+ claimed.max_priority_fee_per_gas
45991
+ );
45992
+ const txTo = claimed.tx_to ?? "";
45993
+ const txData = claimed.tx_data ?? "";
45994
+ const txValue = this.parseOptionalBigInt(claimed.tx_value);
45995
+ if (!currentMaxFee || !currentMaxPriority || !txTo || !txData || txValue === void 0) {
45996
+ this.sql.exec(
45997
+ "UPDATE pending_transactions SET status = ? WHERE id = ?",
45998
+ "stuck",
45999
+ txId
46000
+ );
46001
+ return "skipped";
46002
+ }
46003
+ const requiredFees = await this.getRecommendedFeeParams(chainId);
46004
+ const triggerReplacement = shouldTriggerReplacementByFee({
46005
+ requiredMaxFeePerGas: requiredFees.maxFeePerGas,
46006
+ currentMaxFeePerGas: currentMaxFee,
46007
+ staleThresholdPerGas: config2.triggerThresholdWei
46008
+ });
46009
+ if (!triggerReplacement) {
46010
+ this.sql.exec(
46011
+ "UPDATE pending_transactions SET status = ? WHERE id = ?",
46012
+ "pending",
46013
+ txId
46014
+ );
46015
+ return "skipped";
46016
+ }
46017
+ const nextFeeParams = computeReplacementFees({
46018
+ currentMaxFeePerGas: currentMaxFee,
46019
+ currentMaxPriorityFeePerGas: currentMaxPriority,
46020
+ requiredMaxFeePerGas: requiredFees.maxFeePerGas,
46021
+ requiredMaxPriorityFeePerGas: requiredFees.maxPriorityFeePerGas,
46022
+ bumpBps: config2.bumpBps,
46023
+ maxFeePerGasCap: config2.maxFeeCapWei
46024
+ });
46025
+ const nextAttempts = attempts + 1;
46026
+ if (!nextFeeParams) {
46027
+ this.sql.exec(
46028
+ `
46029
+ UPDATE pending_transactions
46030
+ SET status = 'abandoned',
46031
+ replacement_attempts = ?,
46032
+ last_replacement_at = ?
46033
+ WHERE id = ?
46034
+ `,
46035
+ nextAttempts,
46036
+ nowMs,
46037
+ txId
46038
+ );
46039
+ return "abandoned";
46040
+ }
46041
+ const txParams = {
46042
+ to: txTo,
46043
+ data: txData,
46044
+ value: txValue,
46045
+ authorizationList: this.deserializeAuthorizationList(
46046
+ claimed.tx_authorization_list
46047
+ )
46048
+ };
46049
+ try {
46050
+ const nonce = claimed.nonce;
46051
+ const replacementHash = await this.signAndBroadcastPrepared(
46052
+ txParams,
46053
+ nonce,
46054
+ chainId,
46055
+ nextFeeParams
46056
+ );
46057
+ this.sql.exec(
46058
+ `
46059
+ UPDATE pending_transactions
46060
+ SET tx_hash = ?,
46061
+ status = 'pending',
46062
+ queued = 0,
46063
+ max_fee_per_gas = ?,
46064
+ max_priority_fee_per_gas = ?,
46065
+ replacement_attempts = ?,
46066
+ last_replacement_at = ?,
46067
+ sent_at = ?
46068
+ WHERE id = ?
46069
+ `,
46070
+ replacementHash,
46071
+ nextFeeParams.maxFeePerGas.toString(),
46072
+ nextFeeParams.maxPriorityFeePerGas.toString(),
46073
+ nextAttempts,
46074
+ nowMs,
46075
+ nowMs,
46076
+ txId
46077
+ );
46078
+ await this.enqueueMonitorJob(txId, replacementHash, signerName, chainId);
46079
+ return "replaced";
46080
+ } catch {
46081
+ const terminal = nextAttempts >= config2.maxAttempts;
46082
+ this.sql.exec(
46083
+ `
46084
+ UPDATE pending_transactions
46085
+ SET status = ?,
46086
+ replacement_attempts = ?,
46087
+ last_replacement_at = ?
46088
+ WHERE id = ?
46089
+ `,
46090
+ terminal ? "abandoned" : "pending",
46091
+ nextAttempts,
46092
+ nowMs,
46093
+ txId
46094
+ );
46095
+ return terminal ? "abandoned" : "skipped";
46096
+ }
46097
+ },
46098
+ async () => {
46099
+ this.sql.exec(
46100
+ "UPDATE pending_transactions SET status = 'pending' WHERE id = ? AND status = 'replacing'",
46101
+ txId
46102
+ );
46103
+ },
46104
+ "skipped"
46105
+ );
46106
+ }
45751
46107
  /**
45752
46108
  * Maintenance: clean up stale transactions, check balance
45753
46109
  */
@@ -45783,11 +46139,25 @@ var SignerDO = class extends DurableObject {
45783
46139
  if (newStatus === "confirmed") confirmed++;
45784
46140
  else failed++;
45785
46141
  } else {
45786
- this.sql.exec(
45787
- "UPDATE pending_transactions SET status = 'stuck' WHERE id = ?",
45788
- tx.id
46142
+ const signerName2 = this.getSignerName();
46143
+ if (!signerName2) {
46144
+ this.sql.exec(
46145
+ "UPDATE pending_transactions SET status = 'stuck' WHERE id = ?",
46146
+ tx.id
46147
+ );
46148
+ stuck++;
46149
+ continue;
46150
+ }
46151
+ const replacementResult = await this.tryReplaceStaleTransaction(
46152
+ tx.id,
46153
+ chainId,
46154
+ signerName2
45789
46155
  );
45790
- stuck++;
46156
+ if (replacementResult === "abandoned") {
46157
+ failed++;
46158
+ } else if (replacementResult === "skipped") {
46159
+ stuck++;
46160
+ }
45791
46161
  }
45792
46162
  }
45793
46163
  let requeued = 0;
@@ -45800,18 +46170,14 @@ var SignerDO = class extends DurableObject {
45800
46170
  `
45801
46171
  ).toArray();
45802
46172
  for (const tx of orphaned) {
45803
- try {
45804
- await this.env.MONITOR_QUEUE.send({
45805
- type: "monitor",
45806
- txId: tx.id,
45807
- txHash: tx.tx_hash,
45808
- signerName,
45809
- chainId,
45810
- attempt: 0
45811
- });
45812
- this.sql.exec("UPDATE pending_transactions SET queued = 1 WHERE id = ?", tx.id);
46173
+ const queued = await this.enqueueMonitorJob(
46174
+ tx.id,
46175
+ tx.tx_hash,
46176
+ signerName,
46177
+ chainId
46178
+ );
46179
+ if (queued) {
45813
46180
  requeued++;
45814
- } catch {
45815
46181
  }
45816
46182
  }
45817
46183
  }
@@ -45861,7 +46227,9 @@ var SignerDO = class extends DurableObject {
45861
46227
  async getStatus() {
45862
46228
  await this.ensureInitialized();
45863
46229
  const stateRows = this.sql.exec("SELECT * FROM signer_state WHERE id = 1").toArray();
45864
- const pendingCount = this.sql.exec("SELECT COUNT(*) as c FROM pending_transactions WHERE status = 'pending'").toArray()[0]?.c ?? 0;
46230
+ const pendingCount = this.sql.exec(
46231
+ "SELECT COUNT(*) as c FROM pending_transactions WHERE status IN ('pending', 'replacing')"
46232
+ ).toArray()[0]?.c ?? 0;
45865
46233
  const recentTransactions = this.sql.exec("SELECT * FROM pending_transactions ORDER BY sent_at DESC LIMIT 10").toArray();
45866
46234
  return {
45867
46235
  state: stateRows.length > 0 ? stateRows[0] : null,
@@ -47229,6 +47597,7 @@ async function handleMonitorJob(msg, env) {
47229
47597
  headers: { "Content-Type": "application/json" },
47230
47598
  body: JSON.stringify({
47231
47599
  txId,
47600
+ txHash,
47232
47601
  status: receipt.status === "0x1" ? "confirmed" : "failed"
47233
47602
  })
47234
47603
  });