docs: add MetaMask integration guide with USDC token setup - #100
docs: add MetaMask integration guide with USDC token setup#100zkasuran wants to merge 8 commits into
Conversation
Adds comprehensive guide for integrating Arc Testnet with MetaMask, including the critical wallet_watchAsset call to register USDC. Without this step, users see no USDC balance in MetaMask after receiving tokens, causing confusion and making DApps appear broken. Includes: - Complete onboarding flow with wallet_addEthereumChain - wallet_watchAsset call for USDC token registration - Contract addresses and chain configuration - Why this matters section explaining the impact Fixes circlefin#97 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Great guide — this addresses the exact pain point in #97. One more MetaMask behaviour on Arc Testnet that's worth documenting alongside the Even after the network has been added, calling // ❌ Unreliable on Arc Testnet — may resolve without actually switching
await window.ethereum.request({
method: 'wallet_switchEthereumChain',
params: [{ chainId: '0x4CEF52' }],
});
// ✅ Reliable — works whether network is already added or not
await window.ethereum.request({
method: 'wallet_addEthereumChain',
params: [{
chainId: '0x4CEF52',
chainName: 'Arc Testnet',
nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 },
rpcUrls: ['https://rpc.drpc.testnet.arc.network'],
blockExplorerUrls: ['https://testnet.arcscan.app'],
}],
});Note the Also: the |
Three corrections to the MetaMask guide, all verified against the repo config, the MetaMask spec and the live testnet RPC: - chainId was 0x4CF252 (5042002 transposed to 5042770). The testnet chain id is 5042002, which is 0x4CEF52 (hardhat.config.ts, defaults.rs). - nativeCurrency was USDC/6 decimals. MetaMask only supports 18-decimal native currencies and rejects decimals != 18, so the add call has to use ETH/18 even though gas is paid in USDC (issue circlefin#95). The USDC ERC-20 watchAsset call keeps decimals: 6, which is correct. - rpcUrls was https://rpc.arc.network, which does not resolve. Switched to https://rpc.drpc.testnet.arc.network, which returns chain id 0x4cef52 and Access-Control-Allow-Origin: * for browser DApps (circlefin#90). Also documents that wallet_switchEthereumChain fails silently or throws 4902 on Arc Testnet, and that wallet_addEthereumChain should be used as both add and switch (issue circlefin#89).
|
Thanks @osr21, this was a good catch and it surfaced more than the switch-chain issue. I verified everything against the repo config, the MetaMask spec and the live testnet RPC, and pushed a fix. Three corrections plus your switch-chain note:
AI disclosure: these fixes were prepared with help from Claude (Anthropic). I verified each value against the repo, the MetaMask |
|
Thanks for the thorough fixes — the chainId, RPC, and nativeCurrency corrections all match what we see in a working implementation. Two small additions:
// ✅ Correct — await the chain switch first
await window.ethereum.request({ method: 'wallet_addEthereumChain', params: [arcConfig] });
await window.ethereum.request({ method: 'wallet_watchAsset', params: [usdcConfig] });Dead block explorer URLs — worth a note in the guide that |
|
Thanks @osr21, added both, pushed in b33caaa.
AI disclosure: these doc edits were prepared with help from Claude (Anthropic). I verified the three explorer hosts directly (DNS and HTTP) before pushing; the |
|
The ordering and explorer fixes look correct — One more edge case worth documenting here, since it bites right at this exact point in the onboarding flow: even after We hit this in the Arc Relay Bridge on a Base Sepolia → Arc Testnet CCTP bridge:
The message itself was correct (decoded: source=6, dest=26). Only the submission chain was wrong. Root cause: MetaMask updates its Fix: Verify via // After wallet_addEthereumChain resolves, verify the provider transport has caught up
async function waitForProviderChain(expectedChainId: number, retries = 6): Promise<void> {
for (let i = 0; i < retries; i++) {
if (i > 0) await new Promise(r => setTimeout(r, 500 * i));
const provider = new ethers.BrowserProvider(window.ethereum);
const network = await provider.getNetwork();
if (Number(network.chainId) === expectedChainId) return;
}
throw new Error('Network did not stabilise — please switch manually and retry.');
}
// Usage
await window.ethereum.request({ method: 'wallet_addEthereumChain', params: [arcConfig] });
await waitForProviderChain(5042002); // ← this is the missing step
await window.ethereum.request({ method: 'wallet_watchAsset', params: [usdcConfig] });
// now safe to send transactionsA note in the guide's "send a transaction" section (or even a callout in the network setup section) would save a lot of head-scratching. Happy to draft a short addition if helpful. |
|
Thanks @osr21, that race is nasty and exactly the kind of thing this guide should cover. Added in b879beb: a section on the eth_chainId vs routing gap linking #130, your waitForProviderChain approach (with a note that each attempt needs a fresh BrowserProvider since ethers caches the network per instance) and a pointer from the onboarding flow for flows that go straight into a transaction. |
|
Good addition in b879beb — the note about fresh To confirm: our implementation in the Arc Relay Bridge already follows this pattern. Our // getProvider() — called inside each retry attempt
export async function getProvider(): Promise<ethers.BrowserProvider> {
if (!window.ethereum) throw new Error("No wallet detected.");
return new ethers.BrowserProvider(window.ethereum); // always a fresh instance
}
async function getSignerOnChain(expectedChainId: number, maxRetries = 6) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
if (attempt > 0) await new Promise(r => setTimeout(r, 500 * attempt));
const provider = await getProvider(); // ← fresh BrowserProvider every time
const signer = await provider.getSigner();
const network = await provider.getNetwork(); // reads live from RPC, not cache
if (Number(network.chainId) === expectedChainId) return signer;
}
throw new Error("Wallet did not stabilise on expected network.");
}Worth spelling out for anyone adapting this: in ethers v6, If the guide's code example makes that explicit (even just a comment like |
|
im working on building various dapps on Arc Testnet, will update on any patches or comprehensive guides |
The prose said to construct a fresh provider per attempt, the code example did not. A reader adapting the loop can hoist the provider out and then wait through every retry for a cached network value that never changes. The comment now sits on the line that matters and the note explains the failure.
|
Done in The comment now sits on the line it applies to: // Must be a new instance each iteration: ethers caches the network per
// BrowserProvider, so a reused one keeps returning the stale chain.
const provider = new ethers.BrowserProvider(window.ethereum);I also spelled out the failure in the note under the snippet: ethers v6 caches the network from the first |
|
The inline comment placement and caching explanation in Same trap in viemFor anyone adapting this to viem or wagmi: import { createPublicClient, custom } from 'viem'
import { arc } from './chains' // your Arc Testnet chain definition
// ❌ stale — reads the value baked in at construction
const staleChainId = client.chain.id;
// ✅ live — goes to RPC every time, equivalent to provider.getNetwork() in ethers
async function waitForViemChain(expectedChainId: number, retries = 6): Promise<void> {
for (let i = 0; i < retries; i++) {
if (i > 0) await new Promise(r => setTimeout(r, 500 * i));
// Construct a fresh client each iteration for the same reason as BrowserProvider
const client = createPublicClient({ transport: custom(window.ethereum) });
const chainId = await client.getChainId(); // live RPC call
if (chainId === expectedChainId) return;
}
throw new Error('Wallet did not stabilise on expected network.');
}
|
Re-verified every hard value in the guide against Circle's Arc docs, this repo's own config and the live testnet, then folded in the viem half of the review on circlefin#100. - nativeCurrency is USDC with 18 decimals, not ETH. The native balance carries 18 decimals (1 USDC = 1e18 base units) while the ERC-20 interface at 0x3600000000000000000000000000000000000000 reports 6 and truncates, both over one balance. MetaMask only requires decimals to equal 18 and accepts any 1 to 6 character symbol, so the ETH workaround in circlefin#95 is not needed. - RPC hosts point at the arc.io domain Circle documents now. All four published testnet endpoints answer eth_chainId with 0x4cef52. - Fixed the two MetaMask reference links, which 404 since their docs moved under json-rpc-methods, and pointed the Arc docs link at docs.arc.io to match the domain migration in circlefin#70. - Corrected the ethers caching note. A reused BrowserProvider throws "network changed" unless it was built with the "any" network, and one pinned to a network or built with staticNetwork stops re-reading at all. - Added the viem equivalent, where the trap is client.chain.id being fixed at construction while getChainId() reads live. - The onboarding flow now checks for window.ethereum and treats the 4001 user rejection as an outcome rather than a failure.
|
Brought this up to date with The conflictOne hunk, in Nothing upstream duplicates the guide. Re-verified valuesThe PR is two months old, so I checked every hard value again against Circle's Arc docs, this repo and the live network.
Two things were stale beyond the conflict. The RPC hosts, per the table. And both MetaMask reference links in the guide now 404, because their docs moved the JSON-RPC methods under The nativeCurrency correctionThis one contradicts #95 and my own earlier commit, so here is the evidence. MetaMask's validator requires 18 decimals is not a workaround here, it is the correct scale. Arc's native balance carries 18 decimals and the ERC-20 view truncates it to 6, over one shared balance. On the live network that is 74183684466781322809 from One consequence needed a new section. With the network registered as USDC, MetaMask's native row already reads as the user's USDC balance, so the token registration adds a second row for the same funds at 6 decimals. That is still the view a DApp transacts against, since transfers, approvals and allowances go through the ERC-20 interface, but nobody should add the two rows together. The guide says so now and points at Circle's advice to read On the last two suggestionsTook the viem trap. Rewritten rather than pasted, for two reasons. On a client built without a Left the Your inline comment placement from @osr21 if your bridge is running with Verification
Thanks @osr21 for the repeated reviews here. Every correction in the guide except the last one started as one of your reports. |
|
Confirmed — and you were right to flag it.
|
Summary
Adds comprehensive MetaMask integration guide for Arc Testnet, including the critical
wallet_watchAssetcall to register USDC.Problem
Issue #97 identified that there's no documented way to add Arc Testnet USDC to MetaMask's token list. Without this:
Solution
New guide at
docs/metamask-integration.mdcovering:wallet_addEthereumChainwallet_watchAsset(the missing piece)Changes
docs/metamask-integration.mdREADME.mdto link to new guideTesting
Code examples follow MetaMask's official API documentation:
Impact
Every DApp on Arc Testnet that involves USDC transfers benefits from this documentation.
Fixes #97