@towns-labs/relayer 3.4.1 → 4.1.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
@@ -24114,6 +24114,13 @@ function getSeqKeyForDraftMark(intentNonce, seqKeyFromContext) {
24114
24114
  }
24115
24115
  }
24116
24116
  __name(getSeqKeyForDraftMark, "getSeqKeyForDraftMark");
24117
+ function buildBundleTrackingUnavailableError() {
24118
+ return new RpcError2(
24119
+ SERVICE_UNAVAILABLE,
24120
+ "Intent submitted but bundle tracking unavailable; retry status lookup later"
24121
+ );
24122
+ }
24123
+ __name(buildBundleTrackingUnavailableError, "buildBundleTrackingUnavailableError");
24117
24124
  async function handleSendPreparedCalls(params, ctx) {
24118
24125
  const env = ctx.env;
24119
24126
  const typedParams = unwrapParams(params);
@@ -24201,7 +24208,7 @@ async function handleSendPreparedCalls(params, ctx) {
24201
24208
  try {
24202
24209
  const bundleStatusId = env.BUNDLE_STATUS_DO.idFromName(`bundle-status-${chainId}`);
24203
24210
  const bundleStatus = env.BUNDLE_STATUS_DO.get(bundleStatusId);
24204
- await bundleStatus.fetch("http://do/add_bundle_tx", {
24211
+ const addBundleResponse = await bundleStatus.fetch("http://do/add_bundle_tx", {
24205
24212
  method: "POST",
24206
24213
  headers: { "Content-Type": "application/json" },
24207
24214
  body: JSON.stringify({
@@ -24210,30 +24217,87 @@ async function handleSendPreparedCalls(params, ctx) {
24210
24217
  signerName: result.signerName
24211
24218
  })
24212
24219
  });
24220
+ if (!addBundleResponse.ok) {
24221
+ logger.error(
24222
+ {
24223
+ category: "bundle_tracking_persist_failed",
24224
+ bundleId,
24225
+ chainId,
24226
+ txId: tx.id,
24227
+ signerName: result.signerName,
24228
+ status: addBundleResponse.status,
24229
+ statusText: addBundleResponse.statusText
24230
+ },
24231
+ "bundle tracking persistence failed"
24232
+ );
24233
+ throw buildBundleTrackingUnavailableError();
24234
+ }
24213
24235
  if ("quote" in context && context.quote?.quotes?.length) {
24214
24236
  const quote = context.quote.quotes[0];
24215
24237
  const telemetry = quote?.telemetry;
24216
24238
  if (telemetry?.combinedGas || telemetry?.simulationGas || telemetry?.txGas) {
24217
- await bundleStatus.fetch("http://do/upsert_bundle_telemetry", {
24218
- method: "POST",
24219
- headers: { "Content-Type": "application/json" },
24220
- body: JSON.stringify({
24221
- bundleId,
24222
- chainId,
24223
- eoa: intent.eoa,
24224
- paymentEnabled: telemetry.paymentEnabled ?? false,
24225
- simulationGas: telemetry.simulationGas,
24226
- combinedGas: telemetry.combinedGas,
24227
- txGas: telemetry.txGas
24228
- })
24229
- });
24239
+ try {
24240
+ const telemetryResponse = await bundleStatus.fetch(
24241
+ "http://do/upsert_bundle_telemetry",
24242
+ {
24243
+ method: "POST",
24244
+ headers: { "Content-Type": "application/json" },
24245
+ body: JSON.stringify({
24246
+ bundleId,
24247
+ chainId,
24248
+ eoa: intent.eoa,
24249
+ paymentEnabled: telemetry.paymentEnabled ?? false,
24250
+ simulationGas: telemetry.simulationGas,
24251
+ combinedGas: telemetry.combinedGas,
24252
+ txGas: telemetry.txGas
24253
+ })
24254
+ }
24255
+ );
24256
+ if (!telemetryResponse.ok) {
24257
+ logger.warn(
24258
+ {
24259
+ category: "bundle_telemetry_persist_failed",
24260
+ bundleId,
24261
+ chainId,
24262
+ txId: tx.id,
24263
+ signerName: result.signerName,
24264
+ status: telemetryResponse.status,
24265
+ statusText: telemetryResponse.statusText
24266
+ },
24267
+ "bundle telemetry persistence failed"
24268
+ );
24269
+ }
24270
+ } catch (error48) {
24271
+ logger.warn(
24272
+ {
24273
+ category: "bundle_telemetry_persist_failed",
24274
+ error: getErrorMessage(error48),
24275
+ bundleId,
24276
+ chainId,
24277
+ txId: tx.id,
24278
+ signerName: result.signerName
24279
+ },
24280
+ "failed to persist bundle telemetry"
24281
+ );
24282
+ }
24230
24283
  }
24231
24284
  }
24232
24285
  } catch (error48) {
24286
+ if (error48 instanceof RpcError2) {
24287
+ throw error48;
24288
+ }
24233
24289
  logger.warn(
24234
- { error: getErrorMessage(error48), bundleId },
24235
- "Failed to store bundle status"
24290
+ {
24291
+ category: "bundle_tracking_persist_failed",
24292
+ error: getErrorMessage(error48),
24293
+ bundleId,
24294
+ chainId,
24295
+ txId: tx.id,
24296
+ signerName: result.signerName
24297
+ },
24298
+ "failed to persist bundle tracking"
24236
24299
  );
24300
+ throw buildBundleTrackingUnavailableError();
24237
24301
  }
24238
24302
  }
24239
24303
  return { id: bundleId };
@@ -24369,12 +24433,13 @@ async function handleBatchSendPreparedCalls(requests, ctx) {
24369
24433
  );
24370
24434
  }
24371
24435
  }
24436
+ const bundleTrackingErrors = /* @__PURE__ */ new Map();
24372
24437
  if (env.BUNDLE_STATUS_DO) {
24373
24438
  const bundleStatusId = env.BUNDLE_STATUS_DO.idFromName(`bundle-status-${batchChainId}`);
24374
24439
  const bundleStatus = env.BUNDLE_STATUS_DO.get(bundleStatusId);
24375
24440
  for (const req of parsedRequests) {
24376
24441
  try {
24377
- await bundleStatus.fetch("http://do/add_bundle_tx", {
24442
+ const addBundleResponse = await bundleStatus.fetch("http://do/add_bundle_tx", {
24378
24443
  method: "POST",
24379
24444
  headers: { "Content-Type": "application/json" },
24380
24445
  body: JSON.stringify({
@@ -24383,14 +24448,34 @@ async function handleBatchSendPreparedCalls(requests, ctx) {
24383
24448
  signerName: result.signerName
24384
24449
  })
24385
24450
  });
24451
+ if (!addBundleResponse.ok) {
24452
+ logger.error(
24453
+ {
24454
+ category: "bundle_tracking_persist_failed",
24455
+ bundleId: req.bundleId,
24456
+ chainId: batchChainId,
24457
+ txId: batchTx.id,
24458
+ signerName: result.signerName,
24459
+ status: addBundleResponse.status,
24460
+ statusText: addBundleResponse.statusText
24461
+ },
24462
+ "bundle tracking persistence failed for batch request"
24463
+ );
24464
+ bundleTrackingErrors.set(req.id, buildBundleTrackingUnavailableError());
24465
+ }
24386
24466
  } catch (error48) {
24387
24467
  logger.warn(
24388
24468
  {
24469
+ category: "bundle_tracking_persist_failed",
24389
24470
  error: getErrorMessage(error48),
24390
- bundleId: req.bundleId
24471
+ bundleId: req.bundleId,
24472
+ chainId: batchChainId,
24473
+ txId: batchTx.id,
24474
+ signerName: result.signerName
24391
24475
  },
24392
- "Failed to store bundle status for batch request"
24476
+ "failed to persist bundle tracking for batch request"
24393
24477
  );
24478
+ bundleTrackingErrors.set(req.id, buildBundleTrackingUnavailableError());
24394
24479
  }
24395
24480
  }
24396
24481
  }
@@ -24399,6 +24484,10 @@ async function handleBatchSendPreparedCalls(requests, ctx) {
24399
24484
  if (validationError) {
24400
24485
  return { id: req.id, error: validationError };
24401
24486
  }
24487
+ const bundleTrackingError = bundleTrackingErrors.get(req.id);
24488
+ if (bundleTrackingError) {
24489
+ return { id: req.id, error: bundleTrackingError };
24490
+ }
24402
24491
  const successResult = parsedRequests.find((r) => r.id === req.id);
24403
24492
  if (successResult) {
24404
24493
  return { id: req.id, result: { id: successResult.bundleId } };
@@ -24613,7 +24702,7 @@ __name(checkRpc, "checkRpc");
24613
24702
  async function checkSignerPool(env, chainId) {
24614
24703
  const poolId = env.SIGNER_POOL.idFromName(`pool-${chainId}`);
24615
24704
  const pool = env.SIGNER_POOL.get(poolId);
24616
- const response = await pool.fetch("http://do/status");
24705
+ const response = await pool.fetch(`http://do/status?poolName=pool-${chainId}`);
24617
24706
  if (!response.ok) {
24618
24707
  throw new Error(`SignerPool status check failed: ${response.status} ${response.statusText}`);
24619
24708
  }
@@ -44831,6 +44920,14 @@ function shouldAttemptReplacementNow(input) {
44831
44920
  return input.nowMs >= input.lastReplacementAtMs + backoffMs;
44832
44921
  }
44833
44922
  __name(shouldAttemptReplacementNow, "shouldAttemptReplacementNow");
44923
+ function resolveNonTriggeredReplacement(attempts, maxAttempts) {
44924
+ const nextAttempts = attempts + 1;
44925
+ return {
44926
+ nextAttempts,
44927
+ status: nextAttempts >= maxAttempts ? "stuck" : "pending"
44928
+ };
44929
+ }
44930
+ __name(resolveNonTriggeredReplacement, "resolveNonTriggeredReplacement");
44834
44931
  async function withCleanupOnError(operation, cleanup) {
44835
44932
  try {
44836
44933
  return await operation();
@@ -44872,6 +44969,25 @@ function isIntentExpired(expiryTimestamp, bufferSeconds = DEFAULT_INTENT_EXPIRY_
44872
44969
  return currentTime + buffer2 >= expiry;
44873
44970
  }
44874
44971
  __name(isIntentExpired, "isIntentExpired");
44972
+ function isFillTransactionUnsupportedError(message) {
44973
+ const lower = message.toLowerCase();
44974
+ const mentionsMethod = lower.includes("eth_filltransaction");
44975
+ if (!mentionsMethod) return false;
44976
+ return lower.includes("not available") || lower.includes("does not exist") || lower.includes("method not found");
44977
+ }
44978
+ __name(isFillTransactionUnsupportedError, "isFillTransactionUnsupportedError");
44979
+ function buildRawFallbackBroadcastRequest(input) {
44980
+ return {
44981
+ ...input.txParams,
44982
+ nonce: input.nonce,
44983
+ account: input.account,
44984
+ chain: { id: input.chainId },
44985
+ gas: input.gas,
44986
+ maxFeePerGas: input.feeParams.maxFeePerGas,
44987
+ maxPriorityFeePerGas: input.feeParams.maxPriorityFeePerGas
44988
+ };
44989
+ }
44990
+ __name(buildRawFallbackBroadcastRequest, "buildRawFallbackBroadcastRequest");
44875
44991
  var SignerDO = class extends DurableObject {
44876
44992
  static {
44877
44993
  __name(this, "SignerDO");
@@ -45825,23 +45941,92 @@ var SignerDO = class extends DurableObject {
45825
45941
  }
45826
45942
  }
45827
45943
  async signAndBroadcastPrepared(txParams, nonce, chainId, feeParams) {
45828
- const { walletClient, account } = this.ensureClients(chainId);
45944
+ const { publicClient, walletClient, account } = this.ensureClients(chainId);
45829
45945
  try {
45830
- return await walletClient.sendTransaction({
45831
- ...txParams,
45946
+ return await this.sendWithPrimaryPath(
45947
+ txParams,
45832
45948
  nonce,
45833
- account,
45834
- chain: { id: chainId },
45835
- maxFeePerGas: feeParams.maxFeePerGas,
45836
- maxPriorityFeePerGas: feeParams.maxPriorityFeePerGas
45837
- });
45949
+ chainId,
45950
+ feeParams,
45951
+ walletClient,
45952
+ account
45953
+ );
45838
45954
  } catch (error48) {
45839
- const message = getErrorMessage(error48);
45955
+ const primaryMessage = getErrorMessage(error48);
45956
+ if (!isFillTransactionUnsupportedError(primaryMessage)) {
45957
+ throw new SignerDOError(
45958
+ `Failed to broadcast transaction: ${primaryMessage}`,
45959
+ "BROADCAST_FAILED"
45960
+ );
45961
+ }
45962
+ console.warn("[SignerDO] broadcast fallback activated", {
45963
+ reason: "eth_filltransaction_unsupported",
45964
+ chainId
45965
+ });
45966
+ try {
45967
+ const txHash = await this.sendWithRawFallback(
45968
+ txParams,
45969
+ nonce,
45970
+ chainId,
45971
+ feeParams,
45972
+ publicClient,
45973
+ walletClient,
45974
+ account
45975
+ );
45976
+ console.info("[SignerDO] broadcast fallback success", { chainId, txHash });
45977
+ return txHash;
45978
+ } catch (fallbackError) {
45979
+ const fallbackMessage = getErrorMessage(fallbackError);
45980
+ const message = `${primaryMessage}; fallback failed: ${fallbackMessage}`;
45981
+ console.error("[SignerDO] broadcast fallback failed", {
45982
+ chainId,
45983
+ primaryError: primaryMessage,
45984
+ fallbackError: fallbackMessage
45985
+ });
45986
+ throw new SignerDOError(
45987
+ `Failed to broadcast transaction: ${message}`,
45988
+ "BROADCAST_FAILED"
45989
+ );
45990
+ }
45991
+ }
45992
+ }
45993
+ async sendWithPrimaryPath(txParams, nonce, chainId, feeParams, walletClient, account) {
45994
+ return walletClient.sendTransaction({
45995
+ ...txParams,
45996
+ nonce,
45997
+ account,
45998
+ chain: { id: chainId },
45999
+ maxFeePerGas: feeParams.maxFeePerGas,
46000
+ maxPriorityFeePerGas: feeParams.maxPriorityFeePerGas
46001
+ });
46002
+ }
46003
+ async sendWithRawFallback(txParams, nonce, chainId, feeParams, publicClient, walletClient, account) {
46004
+ const gas = await publicClient.estimateGas({
46005
+ account: account.address,
46006
+ to: txParams.to,
46007
+ data: txParams.data,
46008
+ value: txParams.value,
46009
+ nonce,
46010
+ maxFeePerGas: feeParams.maxFeePerGas,
46011
+ maxPriorityFeePerGas: feeParams.maxPriorityFeePerGas,
46012
+ authorizationList: txParams.authorizationList
46013
+ });
46014
+ const request = buildRawFallbackBroadcastRequest({
46015
+ txParams,
46016
+ nonce,
46017
+ chainId,
46018
+ account,
46019
+ gas,
46020
+ feeParams
46021
+ });
46022
+ const serializedTransaction = await walletClient.signTransaction(request);
46023
+ if (!serializedTransaction) {
45840
46024
  throw new SignerDOError(
45841
- `Failed to broadcast transaction: ${message}`,
46025
+ "Fallback signing returned empty serialized transaction",
45842
46026
  "BROADCAST_FAILED"
45843
46027
  );
45844
46028
  }
46029
+ return publicClient.sendRawTransaction({ serializedTransaction });
45845
46030
  }
45846
46031
  /**
45847
46032
  * Encode an intent to bytes for the Orchestrator.execute() call
@@ -46007,9 +46192,18 @@ var SignerDO = class extends DurableObject {
46007
46192
  staleThresholdPerGas: config2.triggerThresholdWei
46008
46193
  });
46009
46194
  if (!triggerReplacement) {
46195
+ const resolution = resolveNonTriggeredReplacement(attempts, config2.maxAttempts);
46010
46196
  this.sql.exec(
46011
- "UPDATE pending_transactions SET status = ? WHERE id = ?",
46012
- "pending",
46197
+ `
46198
+ UPDATE pending_transactions
46199
+ SET status = ?,
46200
+ replacement_attempts = ?,
46201
+ last_replacement_at = ?
46202
+ WHERE id = ?
46203
+ `,
46204
+ resolution.status,
46205
+ resolution.nextAttempts,
46206
+ nowMs,
46013
46207
  txId
46014
46208
  );
46015
46209
  return "skipped";
@@ -46522,6 +46716,7 @@ var SignerPoolDO = class extends DurableObject2 {
46522
46716
 
46523
46717
  // src/durable-objects/bundle-status.do.ts
46524
46718
  import { DurableObject as DurableObject3 } from "cloudflare:workers";
46719
+ var DEFAULT_BUNDLE_UNRESOLVED_SLA_MS = 3e5;
46525
46720
  var BundleStatusDO = class extends DurableObject3 {
46526
46721
  static {
46527
46722
  __name(this, "BundleStatusDO");
@@ -46546,6 +46741,7 @@ var BundleStatusDO = class extends DurableObject3 {
46546
46741
  CREATE INDEX IF NOT EXISTS idx_bundle_transactions_tx_id ON bundle_transactions(tx_id);
46547
46742
  CREATE INDEX IF NOT EXISTS idx_bundle_transactions_signer ON bundle_transactions(signer_name);
46548
46743
  `);
46744
+ this.ensureBundleTransactionsSchema();
46549
46745
  this.sql.exec(`
46550
46746
  CREATE TABLE IF NOT EXISTS pending_bundles (
46551
46747
  bundle_id TEXT PRIMARY KEY,
@@ -46664,6 +46860,73 @@ var BundleStatusDO = class extends DurableObject3 {
46664
46860
  }
46665
46861
  }
46666
46862
  }
46863
+ /**
46864
+ * Backward-compatible migration for bundle_transactions metadata.
46865
+ * Adds created_at for unresolved-bundle SLA tracking.
46866
+ */
46867
+ ensureBundleTransactionsSchema() {
46868
+ const columns = new Set(
46869
+ this.sql.exec("PRAGMA table_info(bundle_transactions)").toArray().map((row) => String(row.name ?? ""))
46870
+ );
46871
+ if (!columns.has("created_at")) {
46872
+ this.sql.exec(
46873
+ "ALTER TABLE bundle_transactions ADD COLUMN created_at INTEGER NOT NULL DEFAULT 0"
46874
+ );
46875
+ }
46876
+ const now = Date.now();
46877
+ this.sql.exec(
46878
+ "UPDATE bundle_transactions SET created_at = ? WHERE created_at IS NULL OR created_at <= 0",
46879
+ now
46880
+ );
46881
+ }
46882
+ getChainIdFromName() {
46883
+ const name = this.ctx.id.name;
46884
+ if (!name) return 0;
46885
+ const parts = name.split("-");
46886
+ if (parts.length !== 3 || parts[0] !== "bundle" || parts[1] !== "status") return 0;
46887
+ const chainId = Number.parseInt(parts[2], 10);
46888
+ return Number.isFinite(chainId) ? chainId : 0;
46889
+ }
46890
+ getBundleUnresolvedSlaMs() {
46891
+ const parsed = Number.parseInt(
46892
+ this.env.BUNDLE_UNRESOLVED_SLA_MS ?? String(DEFAULT_BUNDLE_UNRESOLVED_SLA_MS),
46893
+ 10
46894
+ );
46895
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_BUNDLE_UNRESOLVED_SLA_MS;
46896
+ }
46897
+ async fetchTxStatusFromSigner(txId, signerName) {
46898
+ try {
46899
+ const signerId = this.env.SIGNER.idFromName(signerName);
46900
+ const signer = this.env.SIGNER.get(signerId);
46901
+ const response = await signer.fetch(`http://do/get_tx_status?txId=${txId}`);
46902
+ if (!response.ok) return null;
46903
+ return await response.json();
46904
+ } catch (error48) {
46905
+ logger.warn(
46906
+ {
46907
+ event: "bundle_status_signer_fetch_failed",
46908
+ txId,
46909
+ signerName,
46910
+ error: getErrorMessage(error48)
46911
+ },
46912
+ "failed to fetch transaction status from signer"
46913
+ );
46914
+ return null;
46915
+ }
46916
+ }
46917
+ async probeSignerForTxStatus(txId, chainId, skipSignerName) {
46918
+ const signerCount = Number.parseInt(this.env.RELAYER_COUNT ?? "1", 10);
46919
+ const maxSigners = Number.isFinite(signerCount) && signerCount > 0 ? signerCount : 1;
46920
+ for (let index2 = 0; index2 < maxSigners; index2 += 1) {
46921
+ const signerName = `signer-${chainId}-${index2}`;
46922
+ if (skipSignerName && signerName === skipSignerName) continue;
46923
+ const txStatus = await this.fetchTxStatusFromSigner(txId, signerName);
46924
+ if (txStatus) {
46925
+ return { txStatus, signerName };
46926
+ }
46927
+ }
46928
+ return { txStatus: null, signerName: null };
46929
+ }
46667
46930
  /**
46668
46931
  * HTTP handler for BundleStatusDO endpoints
46669
46932
  */
@@ -46716,10 +46979,11 @@ var BundleStatusDO = class extends DurableObject3 {
46716
46979
  */
46717
46980
  async add_bundle_tx(bundleId, txId, signerName) {
46718
46981
  this.sql.exec(
46719
- "INSERT OR IGNORE INTO bundle_transactions (bundle_id, tx_id, signer_name) VALUES (?, ?, ?)",
46982
+ "INSERT OR IGNORE INTO bundle_transactions (bundle_id, tx_id, signer_name, created_at) VALUES (?, ?, ?, ?)",
46720
46983
  bundleId,
46721
46984
  txId,
46722
- signerName ?? null
46985
+ signerName ?? null,
46986
+ Date.now()
46723
46987
  );
46724
46988
  }
46725
46989
  /**
@@ -46727,7 +46991,7 @@ var BundleStatusDO = class extends DurableObject3 {
46727
46991
  */
46728
46992
  async get_bundle_status(bundleId) {
46729
46993
  const txRows = this.sql.exec(
46730
- "SELECT tx_id, signer_name FROM bundle_transactions WHERE bundle_id = ?",
46994
+ "SELECT tx_id, signer_name, created_at FROM bundle_transactions WHERE bundle_id = ?",
46731
46995
  bundleId
46732
46996
  ).toArray();
46733
46997
  if (txRows.length === 0) {
@@ -46749,26 +47013,113 @@ var BundleStatusDO = class extends DurableObject3 {
46749
47013
  };
46750
47014
  }
46751
47015
  const transactions = [];
47016
+ const chainId = this.getChainIdFromName();
46752
47017
  for (const row of txRows) {
46753
47018
  const txId = row.tx_id;
46754
47019
  const signerName = row.signer_name;
46755
47020
  if (!signerName) {
46756
- continue;
47021
+ logger.warn(
47022
+ {
47023
+ event: "bundle_status_missing_signer_name",
47024
+ bundleId,
47025
+ txId
47026
+ },
47027
+ "bundle transaction missing signer_name; probing signers"
47028
+ );
46757
47029
  }
46758
47030
  try {
46759
- const signerId = this.env.SIGNER.idFromName(signerName);
46760
- const signer = this.env.SIGNER.get(signerId);
46761
- const response = await signer.fetch(`http://do/get_tx_status?txId=${txId}`);
46762
- if (!response.ok) {
47031
+ let txStatus = signerName ? await this.fetchTxStatusFromSigner(txId, signerName) : null;
47032
+ let resolvedSignerName = signerName;
47033
+ if (!txStatus && chainId > 0) {
47034
+ const probed = await this.probeSignerForTxStatus(
47035
+ txId,
47036
+ chainId,
47037
+ signerName ?? void 0
47038
+ );
47039
+ txStatus = probed.txStatus;
47040
+ resolvedSignerName = probed.signerName;
47041
+ }
47042
+ if (!txStatus) {
47043
+ logger.warn(
47044
+ {
47045
+ event: "bundle_status_signer_probe_failed",
47046
+ bundleId,
47047
+ txId,
47048
+ signerName,
47049
+ chainId
47050
+ },
47051
+ "failed to resolve signer tx status for bundle transaction"
47052
+ );
46763
47053
  continue;
46764
47054
  }
46765
- const txStatus = await response.json();
47055
+ if (resolvedSignerName && resolvedSignerName !== signerName) {
47056
+ try {
47057
+ this.sql.exec(
47058
+ "UPDATE bundle_transactions SET signer_name = ? WHERE bundle_id = ? AND tx_id = ?",
47059
+ resolvedSignerName,
47060
+ bundleId,
47061
+ txId
47062
+ );
47063
+ } catch (error48) {
47064
+ logger.warn(
47065
+ {
47066
+ event: "bundle_status_signer_cache_update_failed",
47067
+ bundleId,
47068
+ txId,
47069
+ signerName,
47070
+ resolvedSignerName,
47071
+ chainId,
47072
+ error: getErrorMessage(error48)
47073
+ },
47074
+ "failed to cache resolved signer name for bundle transaction"
47075
+ );
47076
+ }
47077
+ }
46766
47078
  transactions.push(txStatus);
46767
- } catch {
47079
+ } catch (error48) {
47080
+ logger.warn(
47081
+ {
47082
+ event: "bundle_status_tx_resolution_failed",
47083
+ bundleId,
47084
+ txId,
47085
+ signerName,
47086
+ chainId,
47087
+ error: getErrorMessage(error48)
47088
+ },
47089
+ "failed during bundle transaction status resolution"
47090
+ );
46768
47091
  continue;
46769
47092
  }
46770
47093
  }
46771
47094
  if (transactions.length === 0) {
47095
+ const oldestCreatedAt = txRows.reduce((oldest, row) => {
47096
+ const createdAt = Number(row.created_at ?? 0);
47097
+ if (!Number.isFinite(createdAt) || createdAt <= 0) return oldest;
47098
+ return Math.min(oldest, createdAt);
47099
+ }, Number.MAX_SAFE_INTEGER);
47100
+ const safeOldestCreatedAt = oldestCreatedAt === Number.MAX_SAFE_INTEGER ? Date.now() : oldestCreatedAt;
47101
+ const ageMs = Date.now() - safeOldestCreatedAt;
47102
+ const unresolvedSlaMs = this.getBundleUnresolvedSlaMs();
47103
+ if (ageMs >= unresolvedSlaMs) {
47104
+ logger.error(
47105
+ {
47106
+ event: "bundle_unresolvable_sla_failed",
47107
+ reason: "unresolvable_bundle_tracking_timeout",
47108
+ bundleId,
47109
+ ageMs,
47110
+ unresolvedSlaMs,
47111
+ txCount: txRows.length,
47112
+ chainId
47113
+ },
47114
+ "bundle unresolved beyond SLA; terminalizing as failed"
47115
+ );
47116
+ return {
47117
+ bundleId,
47118
+ status: "failed",
47119
+ statusCode: 300,
47120
+ receipts: []
47121
+ };
47122
+ }
46772
47123
  return {
46773
47124
  bundleId,
46774
47125
  status: "pending",
@@ -47478,6 +47829,7 @@ var HttpAuthNonceDO = class extends DurableObject5 {
47478
47829
  };
47479
47830
 
47480
47831
  // src/index.ts
47832
+ var MAX_MONITOR_ATTEMPTS = 30;
47481
47833
  var app = new Hono2();
47482
47834
  app.use("*", async (c2, next) => {
47483
47835
  const allowedOrigins = c2.env.CORS_ALLOWED_ORIGINS;
@@ -47562,24 +47914,70 @@ async function handleQueue(batch, env) {
47562
47914
  for (const msg of batch.messages) {
47563
47915
  const job = msg.body;
47564
47916
  try {
47565
- const jobType = "type" in job ? job.type : "monitor";
47917
+ const jobType = resolveQueueJobType(job);
47566
47918
  switch (jobType) {
47567
47919
  case "monitor":
47568
47920
  await handleMonitorJob(msg, env);
47569
47921
  break;
47570
47922
  default:
47571
- logger.warn({ job }, "unknown job type");
47923
+ logger.warn({ job, attempts: msg.attempts }, "queue invalid job payload");
47572
47924
  msg.ack();
47573
47925
  }
47574
47926
  } catch (error48) {
47575
- logger.error({ job, error: getErrorMessage(error48) }, "queue processing error");
47927
+ logger.error(
47928
+ { job, attempts: msg.attempts, error: getErrorMessage(error48) },
47929
+ "queue processing error"
47930
+ );
47576
47931
  msg.retry({ delaySeconds: 30 });
47577
47932
  }
47578
47933
  }
47579
47934
  }
47580
47935
  __name(handleQueue, "handleQueue");
47936
+ function resolveQueueJobType(job) {
47937
+ if (!job || typeof job !== "object") return "unknown";
47938
+ if (!("type" in job)) return "monitor";
47939
+ return job.type === "monitor" ? "monitor" : "unknown";
47940
+ }
47941
+ __name(resolveQueueJobType, "resolveQueueJobType");
47942
+ function getMonitorAttempt(msg) {
47943
+ return msg.attempts ?? msg.body.attempt ?? 0;
47944
+ }
47945
+ __name(getMonitorAttempt, "getMonitorAttempt");
47946
+ async function notifySignerFinalization(env, signerName, txId, txHash, status) {
47947
+ try {
47948
+ const signerId = env.SIGNER.idFromName(signerName);
47949
+ const signer = env.SIGNER.get(signerId);
47950
+ const response = await signer.fetch(`http://do/finalized?signerName=${signerName}`, {
47951
+ method: "POST",
47952
+ headers: { "Content-Type": "application/json" },
47953
+ body: JSON.stringify({ txId, txHash, status })
47954
+ });
47955
+ return response.ok;
47956
+ } catch {
47957
+ return false;
47958
+ }
47959
+ }
47960
+ __name(notifySignerFinalization, "notifySignerFinalization");
47961
+ async function finalizeMonitorAsFailed(msg, env, reason) {
47962
+ const { txId, txHash, signerName } = msg.body;
47963
+ const finalized = await notifySignerFinalization(env, signerName, txId, txHash, "failed");
47964
+ if (finalized) {
47965
+ logger.error(
47966
+ { txId, txHash, signerName, attempts: getMonitorAttempt(msg), reason },
47967
+ "monitor job finalized as failed"
47968
+ );
47969
+ return true;
47970
+ }
47971
+ logger.error(
47972
+ { txId, txHash, signerName, attempts: getMonitorAttempt(msg), reason },
47973
+ "failed to finalize monitor job as failed"
47974
+ );
47975
+ return false;
47976
+ }
47977
+ __name(finalizeMonitorAsFailed, "finalizeMonitorAsFailed");
47581
47978
  async function handleMonitorJob(msg, env) {
47582
- const { txId, txHash, signerName, attempt } = msg.body;
47979
+ const { txId, txHash, signerName } = msg.body;
47980
+ const attempt = getMonitorAttempt(msg);
47583
47981
  const fallbackChainIds = getChainIds(env);
47584
47982
  const chainId = msg.body.chainId ?? (fallbackChainIds.length === 1 ? fallbackChainIds[0] : void 0);
47585
47983
  if (!chainId) {
@@ -47590,24 +47988,33 @@ async function handleMonitorJob(msg, env) {
47590
47988
  const receipt = await getTransactionReceipt2(getChainRpcUrl(chainId, env), txHash);
47591
47989
  if (receipt) {
47592
47990
  await logBundleGasTelemetry(env, txId, chainId, txHash, receipt.gasUsed);
47593
- const signerId = env.SIGNER.idFromName(signerName);
47594
- const signer = env.SIGNER.get(signerId);
47595
- await signer.fetch(`http://do/finalized?signerName=${signerName}`, {
47596
- method: "POST",
47597
- headers: { "Content-Type": "application/json" },
47598
- body: JSON.stringify({
47599
- txId,
47600
- txHash,
47601
- status: receipt.status === "0x1" ? "confirmed" : "failed"
47602
- })
47603
- });
47991
+ const finalStatus = receipt.status === "0x1" ? "confirmed" : "failed";
47992
+ const finalized = await notifySignerFinalization(env, signerName, txId, txHash, finalStatus);
47993
+ if (!finalized) {
47994
+ msg.retry({ delaySeconds: 30 });
47995
+ logger.error(
47996
+ { txId, txHash, signerName, attempt },
47997
+ "failed to notify signer finalization, retrying"
47998
+ );
47999
+ return;
48000
+ }
47604
48001
  msg.ack();
47605
- logger.info(
47606
- { txId, txHash, status: receipt.status === "0x1" ? "confirmed" : "failed" },
47607
- "transaction finalized"
47608
- );
48002
+ logger.info({ txId, txHash, status: finalStatus }, "transaction finalized");
47609
48003
  } else {
47610
- const delaySeconds = Math.min(Math.pow(2, attempt + 1), 60);
48004
+ if (attempt >= MAX_MONITOR_ATTEMPTS) {
48005
+ const finalized = await finalizeMonitorAsFailed(
48006
+ msg,
48007
+ env,
48008
+ "monitor retries exhausted without receipt"
48009
+ );
48010
+ if (finalized) {
48011
+ msg.ack();
48012
+ } else {
48013
+ msg.retry({ delaySeconds: 30 });
48014
+ }
48015
+ return;
48016
+ }
48017
+ const delaySeconds = Math.min(Math.pow(2, attempt), 60);
47611
48018
  msg.retry({ delaySeconds });
47612
48019
  logger.debug({ txId, txHash, attempt, delaySeconds }, "transaction pending, retrying");
47613
48020
  }