Add injective-cctp skill#9
Conversation
Headless CCTP V2 transfer CLI for moving native USDC to and from
Injective EVM. Covers both directions (deposit and withdraw) across
the 6 V2-enabled EVM chains (Ethereum, Arbitrum, Base, OP, Polygon,
Avalanche) with Injective EVM as the other side.
The skill walks the standard 5-step CCTP flow — approve, depositForBurn,
attestation poll against iris-api, switch, receiveMessage — and includes
a --resume flag so a stuck transfer can be picked up by anyone holding
the burn tx hash. No relayer, no aggregator, no custodian.
Layout:
skills/injective-cctp/
SKILL.md — frontmatter + flow + runbook
README.md — repo-browse blurb
scripts/ — runnable Node CLI (viem)
package.json
cctp.mjs — entrypoint
chains.mjs — chain configs (V2 contracts, USDC, RPCs)
abis.mjs — minimal V2 + ERC-20 ABIs
.gitignore — node_modules, .env, *.log
references/
architecture.md — deeper CCTP V2 protocol notes
domains.md — Circle's chain reference
Same flow as the wallet UI at https://usdc.inj.so
(source: github.com/ckhbtc/usdc-widget) — this is the agent/script
flavor of the same logic.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Warning Rate limit exceeded
You’ve run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (4)
📝 WalkthroughWalkthroughThis PR adds the ChangesCCTP V2 Bridge Implementation
Sequence Diagram(s)sequenceDiagram
participant User
participant CLI
participant SourceChain
participant CircleAPI
participant DestChain
User->>CLI: run (key, --from, --to, amount / --resume)
CLI->>SourceChain: approve() if needed -> depositForBurn (burn)
SourceChain-->>CLI: burnTxHash
CLI->>CircleAPI: poll attestation (iris-api)
CircleAPI-->>CLI: attestation (complete)
CLI->>DestChain: receiveMessage(message, attestation)
DestChain-->>CLI: mintTxHash
CLI-->>User: output burnHash, mintHash
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@skills/injective-cctp/references/architecture.md`:
- Around line 28-30: Several fenced code blocks in architecture.md lack language
tags (triggering markdownlint MD040); update the fences for the shown snippets
so they include appropriate languages: add "solidity" to the
USDC.approve(TokenMessenger, amount) and the receiveMessage(bytes message, bytes
attestation) snippets, add "solidity" to the depositForBurn(...) signature
block, and add "http" to the GET https://iris-api.circle.com/... example; locate
these by the unique snippets USDC.approve(TokenMessenger, amount),
depositForBurn(...), GET
https://iris-api.circle.com/v2/messages/{srcDomain}?transactionHash={txHash},
and receiveMessage(bytes message, bytes attestation) and update the opening
triple-backticks to include the language identifier.
In `@skills/injective-cctp/scripts/cctp.mjs`:
- Around line 127-143: The polling code around the fetch(url) call swallows
non-OK HTTP responses and network errors; update the logic in the try/catch
around fetch + res.ok (the block that reads res, data, msg, status, lastStatus,
elapsed) to log non-2xx responses (include res.status and res.statusText) when
res.ok is false and to log the caught exception (e) in the catch block,
preserving the existing retry behavior; ensure the log messages include context
like "attestation poll" and the URL or elapsed time so operators can see 429/5xx
errors and network blips while still retrying.
- Around line 197-198: Validate the --resume value before assigning it to
burnHash: check args.resume is a hex string of 64 chars (optionally prefixed
with "0x"), normalize by adding "0x" if missing, and reject/exit with a clear
error if it fails validation so it is not passed to the attestation
URL/transactionHash param (used later where burnHash is sent to the Circle
iris-api and in the poll loop). Update the assignment around burnHash and any
code that constructs the attestation URL to use the validated/normalized hash.
In `@skills/injective-cctp/SKILL.md`:
- Around line 91-93: Add the fenced code block language identifier "http" to the
existing block containing the HTTP GET example (the block with "GET
https://iris-api.circle.com/v2/messages/{srcDomain}?transactionHash={burnTxHash}")
so the fence becomes ```http to satisfy markdownlint MD040; locate the block in
SKILL.md and update the opening backticks only.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 26a5aada-0bd6-4f99-957e-07221e9d8399
📒 Files selected for processing (9)
skills/injective-cctp/README.mdskills/injective-cctp/SKILL.mdskills/injective-cctp/references/architecture.mdskills/injective-cctp/references/domains.mdskills/injective-cctp/scripts/.gitignoreskills/injective-cctp/scripts/abis.mjsskills/injective-cctp/scripts/cctp.mjsskills/injective-cctp/scripts/chains.mjsskills/injective-cctp/scripts/package.json
| ``` | ||
| USDC.approve(TokenMessenger, amount) | ||
| ``` |
There was a problem hiding this comment.
Specify fence languages for unlabeled code blocks.
At Line 28, Line 43, Line 67, and Line 93, fenced blocks are missing language tags, which can trigger markdownlint MD040.
Proposed fix
-```
+```solidity
USDC.approve(TokenMessenger, amount)- +solidity
depositForBurn(
uint256 amount,
uint32 destinationDomain, // Circle's domain id, NOT EVM chain id
bytes32 mintRecipient, // bytes32-padded destination address
address burnToken, // USDC contract on the source chain
bytes32 destinationCaller, // bytes32(0) = anyone can submit the mint
uint256 maxFee, // 0 for standard transfer
uint32 minFinalityThreshold // 2000 for standard, 1000 for fast
)
-```
+```http
GET https://iris-api.circle.com/v2/messages/{srcDomain}?transactionHash={txHash}
- +solidity
receiveMessage(bytes message, bytes attestation)
Also applies to: 43-53, 67-69, 93-95
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)
[warning] 28-28: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@skills/injective-cctp/references/architecture.md` around lines 28 - 30,
Several fenced code blocks in architecture.md lack language tags (triggering
markdownlint MD040); update the fences for the shown snippets so they include
appropriate languages: add "solidity" to the USDC.approve(TokenMessenger,
amount) and the receiveMessage(bytes message, bytes attestation) snippets, add
"solidity" to the depositForBurn(...) signature block, and add "http" to the GET
https://iris-api.circle.com/... example; locate these by the unique snippets
USDC.approve(TokenMessenger, amount), depositForBurn(...), GET
https://iris-api.circle.com/v2/messages/{srcDomain}?transactionHash={txHash},
and receiveMessage(bytes message, bytes attestation) and update the opening
triple-backticks to include the language identifier.
| let burnHash = args.resume || null; | ||
|
|
There was a problem hiding this comment.
--resume hash is not validated before being used in the attestation URL.
args.resume is passed directly to Circle's iris-api as transactionHash. A mistyped or incorrectly formatted value (wrong length, non-hex, missing 0x) produces no early error — the poll loop runs silently for 30 minutes before timing out.
🛠️ Proposed fix — validate tx hash format at startup
let burnHash = args.resume || null;
+ if (burnHash && !/^0x[0-9a-fA-F]{64}$/.test(burnHash)) {
+ err(`invalid --resume value: expected a 0x-prefixed 32-byte hex hash, got: ${burnHash}`);
+ process.exit(1);
+ }
+
if (!burnHash) {📝 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.
| let burnHash = args.resume || null; | |
| let burnHash = args.resume || null; | |
| if (burnHash && !/^0x[0-9a-fA-F]{64}$/.test(burnHash)) { | |
| err(`invalid --resume value: expected a 0x-prefixed 32-byte hex hash, got: ${burnHash}`); | |
| process.exit(1); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@skills/injective-cctp/scripts/cctp.mjs` around lines 197 - 198, Validate the
--resume value before assigning it to burnHash: check args.resume is a hex
string of 64 chars (optionally prefixed with "0x"), normalize by adding "0x" if
missing, and reject/exit with a clear error if it fails validation so it is not
passed to the attestation URL/transactionHash param (used later where burnHash
is sent to the Circle iris-api and in the poll loop). Update the assignment
around burnHash and any code that constructs the attestation URL to use the
validated/normalized hash.
There was a problem hiding this comment.
this is an example of the differentiated error handling that should be handled within the polling function.
see: #9 (comment)
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
gh api repos/InjectiveLabs/agent-skills/pulls/9/comments --jq '.[] | select(.id == 3206142460) | {id: .id, body: .body, path: .path, line: .line}'
Length of output: 4030
@bguiz, understood — thanks for the clarification. The connection makes sense: if an invalid or mistyped --resume hash is supplied, Circle's iris-api will respond with a non-2xx status, and with the differentiated error handling from that comment in place (logging res.status for non-OK responses and e.message for network errors), the user would immediately see something like:
[cctp] Circle API returned HTTP 404 — retrying (5s elapsed)
…rather than 30 minutes of silence. That's a reasonable design choice — surface the problem through meaningful API error feedback rather than a pre-flight regex check.
I'll withdraw the upfront format-validation suggestion in favour of the polling-level fix in r3206142460, which is the right layer to handle this.
✏️ Learnings added
Learnt from: bguiz
Repo: InjectiveLabs/agent-skills PR: 9
File: skills/injective-cctp/scripts/cctp.mjs:197-198
Timestamp: 2026-05-08T05:02:36.818Z
Learning: In `skills/injective-cctp/scripts/cctp.mjs` (InjectiveLabs/agent-skills), invalid `--resume` tx hash values should be surfaced through differentiated error handling inside the `pollAttestation` function (logging non-2xx HTTP responses and network errors) rather than upfront regex/format validation at startup. The Circle iris-api will return a non-2xx response for invalid hashes, which the improved poll loop will log clearly.
Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.
There was a problem hiding this comment.
Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!
| ``` | ||
| GET https://iris-api.circle.com/v2/messages/{srcDomain}?transactionHash={burnTxHash} | ||
| ``` |
There was a problem hiding this comment.
Add a language identifier to the fenced code block at Line 91.
This block currently omits a fence language and can trigger markdownlint MD040.
Proposed fix
- ```
+ ```http
GET https://iris-api.circle.com/v2/messages/{srcDomain}?transactionHash={burnTxHash}
```📝 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.
| ``` | |
| GET https://iris-api.circle.com/v2/messages/{srcDomain}?transactionHash={burnTxHash} | |
| ``` |
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)
[warning] 91-91: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@skills/injective-cctp/SKILL.md` around lines 91 - 93, Add the fenced code
block language identifier "http" to the existing block containing the HTTP GET
example (the block with "GET
https://iris-api.circle.com/v2/messages/{srcDomain}?transactionHash={burnTxHash}")
so the fence becomes ```http to satisfy markdownlint MD040; locate the block in
SKILL.md and update the opening backticks only.
| | `TokenMinterV2` | `0xfd78EE919681417d192449715b2594ab58f5D002` | | ||
| | `MessageV2` | `0xec546b6B005471ECf012e5aF77FBeC07e0FD8f78` | | ||
|
|
||
| ## Per-chain values used by this skill |
There was a problem hiding this comment.
I suggest including testnet values as well for this, as users will likely not want to try this with real funds initially. Also good for demo purposes.
There was a problem hiding this comment.
done. added a testnet chain table to references/domains.md + a commented-out testnet section template in chains.mjs. held off hardcoding the testnet v2 contract addresses since they're a separate set from mainnet's deterministic ones – left a pointer to circle's docs instead. bf97753 / 196a119
| | Ethereum, Avalanche, OP, Arbitrum, Base, Polygon | ✓ | ✓ | ✓ | | ||
| | Injective EVM | ✓ | — | — | | ||
|
|
||
| This skill only uses **standard transfer** so Fast/Forwarding are out of scope. Adding Fast for non-Injective routes is a small change in `cctp.mjs` (lower `minFinalityThreshold`, non-zero `maxFee`, plus a fee fetch from Circle's `/v2/burn/USDC/fees/{src}/{dst}` endpoint). |
There was a problem hiding this comment.
Circle does not support CCTP fast transfers for Injective, mainly because there is no need.
CCTP fast transfers on networks that support them are (ironically) slower than CCTP standard transfers on networks that do no support fast transfers.
If a network has a very fast block time and doesn't require multiple block confirmations for finality - e.g. ~600ms and instant finality for Injective, then there is essentially no need to speed things up further.
However for Ethereum, where block times are over 10s and requires multiple block confirmations, the wait time is over a minute, hence the need for CCTP fast transfer mode.
There was a problem hiding this comment.
rewrote that section – frame is "feature unnecessary, by virtue of injective being fast enough already" instead of "missing feature". kept the bit about source-side fast transfer for inbound from ethereum since that wait IS still ~13min and worth doing. 2ab3ede / 3dc9d07
|
|
||
| ## Why chain ID ≠ domain ID | ||
|
|
||
| Circle's CCTP domains pre-date most L2s and use a separate, smaller integer space (currently 0-29). They have no relationship to EIP-155 chain IDs. The CLI never confuses them because each chain entry stores both fields explicitly — `id` is for viem's chain config (and EIP-712 signing), `domain` is for `depositForBurn`'s `destinationDomain` arg. |
There was a problem hiding this comment.
Another reason is that different instances of the same network share the same domain ID, while maintaining separate chain IDs. E.g. Injective Testnet and Injective Mainnet are both 29.
There was a problem hiding this comment.
added an explicit section explaining this. injective mainnet (1776) and testnet (1439) both = domain 29, with the ethereum/sepolia parallel called out for symmetry. be83cab / 0198f64
| } | ||
|
|
||
| // ─── Main ───────────────────────────────────────────────────────────────────── | ||
| async function main() { |
There was a problem hiding this comment.
this main function is a little unwield-y - I suggest splitting it into multiple functions, each of which handle 1 stage of the process (e.g. burn/ attestation polling/ mint), and then have the main function call them in sequence.
There was a problem hiding this comment.
agreed. split into setUp / approveIfNeeded / burn / pollAttestation / mint. main is just orchestration now and each stage is independently testable. 4bb5ff6 / af812ca
| let burnHash = args.resume || null; | ||
|
|
There was a problem hiding this comment.
this is an example of the differentiated error handling that should be handled within the polling function.
see: #9 (comment)
| @@ -0,0 +1,280 @@ | |||
| #!/usr/bin/env node | |||
There was a problem hiding this comment.
rather than write this functionality ourselves, I think it would be better to instead use the bridge kit SDK maintained by circle: https://www.npmjs.com/package/@circle-fin/bridge-kit
There was a problem hiding this comment.
... however, the issue is that bridge kit has not been updated to include Injective configurations yet.
their devrel @huijing says that they already have a branch/ PR for this in the works, so it should be soon.
I'm planning to include this within https://docs.injective.network/developers-defi/usdc-cctp-tutorial (or as an additional page) when that happens.
There was a problem hiding this comment.
noted. once @huijing's PR lands and bridge-kit ships injective configs, the burn / poll / mint stages collapse into a few SDK calls – the staging refactor i just did should make that swap surgical.
|
|
||
| `scripts/cctp.mjs` accepts a `--resume <burnTxHash> --from <chain>` flag that does steps 2-4 automatically. | ||
|
|
||
| ## Reference |
There was a problem hiding this comment.
this page should be included as a reference: https://docs.injective.network/developers-defi/usdc-stablecoin
There was a problem hiding this comment.
added. it's the first link in a new "External docs" block at the top of "Reference," alongside the in-flight cctp tutorial page and circle's docs. f8633ad / 63aa190
The original framing read "Injective doesn't support Fast Transfer" which sounds like a missing feature. Brendan's review pointed out the actual reason: Injective's ~600ms blocks + instant finality already beat what Fast Transfer offers on slower chains, so Fast Transfer is *unnecessary*, not *unavailable*. Replaced the matrix paragraph in domains.md with a "Why Injective doesn't need Fast Transfer" section that explains finality vs Fast mechanics, then updated the SKILL.md constraints bullet to match. Capabilities matrix kept; the "—" cell now reads "(not needed; see above)" so the framing matches. Source-side Fast Transfer for inbound deposits from Ethereum still makes sense (the wait is on Ethereum's finality, not Injective's) — that path is documented as a small CLI extension but unwired today. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Brendan flagged that the previous testnet framing missed an important nuance: each chain family has ONE CCTP domain id, shared between its mainnet and testnet instances. Concretely, Injective mainnet (chain id 1776) and Injective testnet (chain id 1439) both report CCTP domain 29. The chain id is what tells JSON-RPC apart; the domain id is what depositForBurn uses to route. Added a dedicated section to references/domains.md citing Circle's own quote and walking through the Ethereum + Injective example so the distinction is unambiguous. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Brendan asked to include testnet values so users can experiment without
risking real funds. Added across three places:
• domains.md — split V2 contract addresses into separate (mainnet)
and (testnet) sections; renamed the per-chain table to "Mainnet —
chain reference" and added a parallel "Testnet — chain reference"
table covering Sepolia/Fuji/Amoy/Injective-testnet with chain ids
+ domain ids.
• chains.mjs — added a commented testnet-section template right
before viemChain, with the chain id + domain id for each testnet
so adding a key like `injective-testnet` is a copy-paste from the
nearest mainnet entry.
• SKILL.md — new constraint bullet noting the CLI ships mainnet
only and pointing at the docs/template for adding testnet support.
Held back from hardcoding testnet V2 contract addresses since they're
a separate Circle deployment from the deterministic mainnet ones —
Circle updates them periodically and shipping stale ones is risky.
Pointer to developers.circle.com/cctp/references/contract-addresses
in three places so it's not missable.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Brendan suggested using @circle-fin/bridge-kit instead of writing the V2 flow ourselves, then noted in a follow-up that bridge-kit doesn't include Injective configurations yet — Circle DevRel (@huijing) has a PR in progress and Brendan plans to make docs.injective.network/developers-defi/usdc-cctp-tutorial the canonical walkthrough when it lands. Captured the migration plan in two places so it's not orphaned: • references/domains.md — new "Future: bridge-kit migration" section linking the npm package and the in-flight tutorial page, explaining that when Injective lands the CLI's burn/poll/mint stages collapse to a few SDK calls. • scripts/cctp.mjs — code comment right under the usage block calling out the deferred migration so the next person editing the file sees the plan immediately. Not migrating yet because we'd block the skill on a Circle PR landing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Brendan asked for docs.injective.network/developers-defi/usdc-stablecoin to be cited as an authoritative chain-side USDC reference. Added an "External docs" sub-section at the top of ## Reference covering: • Injective USDC stablecoin overview (chain-side primer) • Injective USDC + CCTP tutorial (the in-flight canonical walkthrough) • Circle CCTP docs (protocol authority) • Circle bridge-kit npm (cross-link to the migration plan) Also clarified that the deterministic V2 contract addresses below are mainnet-only — testnets use a separate set, per references/domains.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
main() was 100+ lines mixing arg parsing, validation, allowance check,
approve, burn, attestation poll, and mint into a single function.
Brendan suggested splitting it so each stage is independently
readable and replaceable (the bridge-kit migration in particular
will swap out 4 of these in one shot).
Extracted five stage functions:
• setUp(argv) — arg parse, env check, account/chain
validation, recipient/amount parsing.
Throws (instead of process.exit-ing) so
it's testable.
• approveIfNeeded(ctx) — allowance read + approve write, skipped
when allowance is already sufficient.
Returns the approve hash or null.
• burn(ctx) — depositForBurn + receipt wait, returns
the burn hash.
• pollAttestation(d, h) — unchanged in this commit; the next
commit rewrites its error handling.
• mint(ctx) — receiveMessage on the destination chain.
main() is now orchestration only — pulls the ctx from setUp, builds
the four viem clients, and calls the stages in sequence.
No behavior change in this commit. Verified with `node --check` and
`node cctp.mjs --help`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous pollAttestation wrapped the fetch in one try/catch that
swallowed every non-2xx response and every network failure alike.
Effect: a 30-min stalled poll produced zero output between the
initial "polling …" log and the eventual timeout throw — impossible
to tell from outside whether Circle was rate-limiting, the API was
down, or the network was just slow.
Rewrote with one branch per failure mode:
404 Circle hasn't seen the burn yet (normal at start).
Log once per status transition, then keep polling.
429 Rate-limited. Log, then back off 30s before next poll.
5xx Circle API problem. Log with status code, retry on the
normal 5s cadence (servers usually recover).
other 4xx Unexpected. Log status + statusText, retry.
network error DNS/connection-level failure. Log the error message
itself (not just "blip — retry"), retry.
malformed JSON Log the parse error, retry.
pending complete 200 + status==='complete' + attestation !== 'PENDING'
→ return.
pending in-flight Log only when the status string changes.
Now the operator gets a continuous stream of diagnostics during long
polls, can tell exactly which kind of failure they're seeing, and the
30-min timeout is the path of last resort instead of the only signal.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
Adds a new skill,
injective-cctp, for moving native USDC into and out of Injective EVM via Circle's Cross-Chain Transfer Protocol V2. Burns USDC on the source chain, polls Circle's iris-api for the attestation, and submits the mint on the destination chain — no relayer, no aggregator, no custodian.Same flow as the wallet UI live at https://usdc.inj.so (source) — this is the agent/script flavor of the same logic.
Coverage
Standard CCTP V2 transfer only — Injective doesn't support Fast Transfer per Circle's supported-chains list.
Layout
Quick start
Test plan
node --checkpasses on all three .mjs filesnode cctp.mjs --helprenders without errorsnpm installresolves cleanly (only viem, transitive deps)🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Documentation
Chores