Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions typescript/ccip-server/src/services/CCTPAttestationService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,21 @@ import { Logger } from 'pino';

import { PrometheusMetrics } from '../utils/prometheus.js';

// https://developers.circle.com/api-reference/cctp/all/get-messages-v-2
type DelayReason =
| 'insufficient_fee'
| 'amount_above_max'
| 'insufficient_allowance_available';
type Status = 'complete' | 'pending_confirmations';

interface CCTPMessageEntry {
attestation: string;
message: string;
eventNonce: string;
// CCTP v2 only
cctpVersion?: string;
status?: Status;
delayReason?: DelayReason;
}

interface CCTPData {
Expand Down Expand Up @@ -151,6 +163,21 @@ class CCTPAttestationService {

const json: CCTPData = await resp.json();

json.messages.forEach((message) => {
if (message.attestation === 'PENDING') {
const errorString = `CCTP attestation is pending due to ${message.delayReason}`;
switch (message.delayReason) {
case 'insufficient_fee':
case 'amount_above_max':
case 'insufficient_allowance_available':
PrometheusMetrics.logUnhandledError(this.serviceName);
}
logger.error(context, errorString);
throw new Error(errorString);
}
});
Comment on lines +166 to +178
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Handle undefined delayReason to avoid confusing error messages.

Right now, if delayReason is undefined (which it can be for normal pending states), you'll get an error message like "CCTP attestation is pending due to undefined". Not exactly helpful when you're trying to figure out what went wrong.

Consider this approach:

 json.messages.forEach((message) => {
   if (message.attestation === 'PENDING') {
-    const errorString = `CCTP attestation is pending due to ${message.delayReason}`;
+    const errorString = message.delayReason 
+      ? `CCTP attestation is pending due to ${message.delayReason}`
+      : 'CCTP attestation is still pending';
     switch (message.delayReason) {
       case 'insufficient_fee':
       case 'amount_above_max':
       case 'insufficient_allowance_available':
         PrometheusMetrics.logUnhandledError(this.serviceName);
+        break;
     }
     logger.error(context, errorString);
     throw new Error(errorString);
   }
 });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
json.messages.forEach((message) => {
if (message.attestation === 'PENDING') {
const errorString = `CCTP attestation is pending due to ${message.delayReason}`;
switch (message.delayReason) {
case 'insufficient_fee':
case 'amount_above_max':
case 'insufficient_allowance_available':
PrometheusMetrics.logUnhandledError(this.serviceName);
}
logger.error(context, errorString);
throw new Error(errorString);
}
});
json.messages.forEach((message) => {
if (message.attestation === 'PENDING') {
const errorString = message.delayReason
? `CCTP attestation is pending due to ${message.delayReason}`
: 'CCTP attestation is still pending';
switch (message.delayReason) {
case 'insufficient_fee':
case 'amount_above_max':
case 'insufficient_allowance_available':
PrometheusMetrics.logUnhandledError(this.serviceName);
break;
}
logger.error(context, errorString);
throw new Error(errorString);
}
});
🤖 Prompt for AI Agents
In typescript/ccip-server/src/services/CCTPAttestationService.ts around lines
166-178, the error string uses message.delayReason directly which can be
undefined and produce "due to undefined"; update the code to normalize the
delayReason (e.g. const reason = message.delayReason ?? 'unknown' or 'not
provided') and use that normalized value in the errorString and log, keep the
existing PrometheusMetrics logging for the specific known delay reasons, and
ensure logs/errors remain descriptive by including the fallback reason when
delayReason is missing.


// TODO: handle multiple messages in one tx hash
return [json.messages[0].message, json.messages[0].attestation];
}
}
Expand Down
25 changes: 17 additions & 8 deletions typescript/sdk/src/ism/metadata/ccipread.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { utils } from 'ethers';

import { AbstractCcipReadIsm__factory } from '@hyperlane-xyz/core';
import { WithAddress } from '@hyperlane-xyz/utils';
import { WithAddress, ensure0x } from '@hyperlane-xyz/utils';

import { HyperlaneCore } from '../../core/HyperlaneCore.js';
import { IsmType, OffchainLookupIsmConfig } from '../types.js';
Expand Down Expand Up @@ -58,11 +58,11 @@ export class OffchainLookupMetadataBuilder implements MetadataBuilder {
const url = urlTemplate
.replace('{sender}', sender)
.replace('{data}', callDataHex);

let res: Response;
try {
let responseJson: any;
if (urlTemplate.includes('{data}')) {
const res = await fetch(url);
responseJson = await res.json();
res = await fetch(url);
} else {
const signature = await signer.signMessage(
utils.arrayify(
Expand All @@ -73,20 +73,29 @@ export class OffchainLookupMetadataBuilder implements MetadataBuilder {
),
),
);
const res = await fetch(url, {
res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ sender, data: callDataHex, signature }),
});
responseJson = await res.json();
}
const rawHex = responseJson.data as string;
return rawHex.startsWith('0x') ? rawHex : `0x${rawHex}`;
} catch (error: any) {
this.core.logger.warn(
`CCIP-read metadata fetch failed for ${url}: ${error}`,
);
// try next URL
continue;
}

const responseJson = await res.json();
if (!res.ok) {
this.core.logger.warn(
`Server at ${url} responded with error: ${responseJson.error}`,
);
// try next URL
continue;
} else {
return ensure0x(responseJson.data);
}
Comment on lines +90 to 99
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Mind the mud when parsing responses.

If a server answers with plain text or malformed JSON, res.json() will throw before we can slog on to the next URL, recreating the opaque failure we’re trying to fix. Please catch that parse error and keep looping so we always have a fallback path.

-      const responseJson = await res.json();
-      if (!res.ok) {
-        this.core.logger.warn(
-          `Server at ${url} responded with error: ${responseJson.error}`,
-        );
-        // try next URL
-        continue;
-      } else {
-        return ensure0x(responseJson.data);
-      }
+      let responseJson: any;
+      try {
+        responseJson = await res.json();
+      } catch (error) {
+        this.core.logger.warn(
+          `Server at ${url} returned non-JSON payload: ${error}`,
+        );
+        continue;
+      }
+      if (!res.ok) {
+        this.core.logger.warn(
+          `Server at ${url} responded with error: ${responseJson?.error}`,
+        );
+        continue;
+      }
+      return ensure0x(responseJson.data);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const responseJson = await res.json();
if (!res.ok) {
this.core.logger.warn(
`Server at ${url} responded with error: ${responseJson.error}`,
);
// try next URL
continue;
} else {
return ensure0x(responseJson.data);
}
let responseJson: any;
try {
responseJson = await res.json();
} catch (error) {
this.core.logger.warn(
`Server at ${url} returned non-JSON payload: ${error}`,
);
continue;
}
if (!res.ok) {
this.core.logger.warn(
`Server at ${url} responded with error: ${responseJson?.error}`,
);
continue;
}
return ensure0x(responseJson.data);
🤖 Prompt for AI Agents
In typescript/sdk/src/ism/metadata/ccipread.ts around lines 90 to 99, calling
await res.json() can throw on plain-text or malformed responses which breaks the
retry loop; wrap the JSON parse in a try/catch, on parse failure read res.text()
(or use a safe parse) and log a warning containing the URL and raw text/error,
then continue to the next URL instead of letting the exception bubble up; keep
the existing behavior of returning ensure0x(responseJson.data) only when parsing
succeeds and res.ok is true.

}

Expand Down
Loading