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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ Arc is an open EVM-compatible layer 1 built on [Malachite](https://github.com/ci

- 🚀 **[Execution](crates/node/README.md)** - Execution binary and configuration
- 🗳️ **[Consensus](crates/malachite-app/README.md)** - Consensus binary and configuration
- 🦊 **[MetaMask Integration](docs/metamask-integration.md)** - Connect MetaMask and register USDC token
- More: see Arc [developer docs](https://docs.arc.network/arc/concepts/welcome-to-arc) for guides, APIs, and specs

## Install and Run a Node
Expand Down
200 changes: 200 additions & 0 deletions docs/metamask-integration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
# MetaMask Integration Guide

Guide for integrating Arc Testnet with MetaMask, including USDC token setup.

## Add Arc Testnet to MetaMask

Use `wallet_addEthereumChain` to add Arc Testnet:

```typescript
await window.ethereum.request({
method: "wallet_addEthereumChain",
params: [{
chainId: "0x4CEF52", // 5042002 in hex
chainName: "Arc Testnet",
nativeCurrency: {
name: "ETH",
symbol: "ETH",
decimals: 18,
},
rpcUrls: ["https://rpc.drpc.testnet.arc.network"],
blockExplorerUrls: ["https://testnet.arcscan.app"],
}],
});
```

### Why the native currency is "ETH" and not "USDC"

Arc pays gas in USDC, but the `nativeCurrency` here still has to be `{ name: "ETH", symbol: "ETH", decimals: 18 }`. MetaMask only supports 18-decimal native currencies and validates that `decimals` equals 18, so a config with `decimals: 6` is rejected. If you pass `symbol: "USDC"` with `decimals: 18` the network is accepted but MetaMask shows the gas balance 10^12 times too high.

The practical consequences:

- MetaMask labels gas costs in "ETH" (for example "0.000000021 ETH") rather than USDC. Show the real USDC gas estimate in your own DApp UI if that matters to your users.
- This only affects the native gas display. The USDC ERC-20 token you register below keeps `decimals: 6` and shows the correct balance.

See issue [#95](https://github.com/circlefin/arc-node/issues/95) for the full background.

## Switching to Arc Testnet

Do not use `wallet_switchEthereumChain` to move the user to Arc Testnet. On Arc Testnet it fails silently or throws a `4902` (chain not found) even after the network has already been added (issue [#89](https://github.com/circlefin/arc-node/issues/89)).

Call `wallet_addEthereumChain` instead. It both adds the network if it is missing and switches to it if it is already present, so it is reliable in both cases:

```typescript
// Unreliable on Arc Testnet, may resolve without switching or throw 4902
// await window.ethereum.request({
// method: "wallet_switchEthereumChain",
// params: [{ chainId: "0x4CEF52" }],
// });

// Reliable, works whether the network is already added or not
await window.ethereum.request({
method: "wallet_addEthereumChain",
params: [{
chainId: "0x4CEF52",
chainName: "Arc Testnet",
nativeCurrency: { name: "ETH", symbol: "ETH", decimals: 18 },
rpcUrls: ["https://rpc.drpc.testnet.arc.network"],
blockExplorerUrls: ["https://testnet.arcscan.app"],
}],
});
```

## Wait for the RPC Transport Before Sending Transactions

`wallet_addEthereumChain` resolving does not mean transactions route to Arc Testnet yet. For a short window (roughly 200 to 500 ms) MetaMask already reports the new chain from `eth_chainId` while its internal JSON-RPC router still points at the previous endpoint. A transaction sent in that window lands on the old chain (issue [#130](https://github.com/circlefin/arc-node/issues/130)). The reported case was a CCTP `receiveMessage` meant for Arc Testnet that landed on Base Sepolia instead and reverted with `"Invalid destination domain"`.

Polling `eth_chainId` does not close the gap because it flips before the routing does. Verify through `provider.getNetwork()` instead. It goes through the same transport as `eth_sendTransaction`, so once it returns the Arc Testnet chain ID, transactions route there too:

```typescript
import { ethers } from "ethers";

// After wallet_addEthereumChain resolves, wait for the provider
// transport to catch up before sending any transaction.
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 stabilize. Switch manually and retry.");
}

await window.ethereum.request({
method: "wallet_addEthereumChain",
params: [arcTestnetConfig],
});
await waitForProviderChain(5042002);
// Now safe to send transactions
```

Construct a fresh `BrowserProvider` on each attempt. ethers caches the network per provider instance, so reusing one instance can keep returning the stale chain.

The wait is only needed when a transaction follows the network switch in the same flow. Reading balances or registering tokens is not affected.

## Register USDC Token

**Important:** MetaMask does not automatically show USDC in the token list for custom chains. After adding the network, you must register USDC using `wallet_watchAsset`:

```typescript
await window.ethereum.request({
method: "wallet_watchAsset",
params: {
type: "ERC20",
options: {
address: "0x3600000000000000000000000000000000000000",
symbol: "USDC",
decimals: 6,
image: "https://cryptologos.cc/logos/usd-coin-usdc-logo.png",
},
},
});
```

MetaMask will show a confirmation dialog. Once accepted, USDC will appear in the user's token list with the correct balance. Note that USDC uses `decimals: 6` here, which is correct for the ERC-20 token and separate from the 18-decimal native currency above.

**Order matters:** call `wallet_watchAsset` only after the `wallet_addEthereumChain` promise has resolved. If it fires while the user is still on a different network, MetaMask registers USDC against the wrong chain and the balance never shows up. The onboarding flow below awaits the chain add before registering the token for this reason.

## Complete Onboarding Flow

Recommended sequence for DApp wallet connection:

```typescript
async function connectWallet() {
try {
// 1. Request account access
const accounts = await window.ethereum.request({
method: "eth_requestAccounts",
});

// 2. Add Arc Testnet network (also switches to it if already added)
await window.ethereum.request({
method: "wallet_addEthereumChain",
params: [{
chainId: "0x4CEF52",
chainName: "Arc Testnet",
nativeCurrency: {
name: "ETH",
symbol: "ETH",
decimals: 18,
},
rpcUrls: ["https://rpc.drpc.testnet.arc.network"],
blockExplorerUrls: ["https://testnet.arcscan.app"],
}],
});

// 3. Register USDC token
await window.ethereum.request({
method: "wallet_watchAsset",
params: {
type: "ERC20",
options: {
address: "0x3600000000000000000000000000000000000000",
symbol: "USDC",
decimals: 6,
image: "https://cryptologos.cc/logos/usd-coin-usdc-logo.png",
},
},
});

console.log("Wallet connected:", accounts[0]);
return accounts[0];
} catch (error) {
console.error("Wallet connection failed:", error);
throw error;
}
}
```

If the flow continues straight into a transaction, run `waitForProviderChain` from the section above between steps 2 and 3.

## Why This Matters

Without the `wallet_watchAsset` call:
- Users see no USDC balance in MetaMask after receiving tokens
- Users assume transactions failed
- DApps appear broken

Every DApp on Arc Testnet that involves USDC transfers should include this step in their onboarding flow.

## Contract Addresses

**Arc Testnet:**
- USDC: `0x3600000000000000000000000000000000000000`
- Chain ID: `5042002` (hex: `0x4CEF52`)
- RPC: `https://rpc.drpc.testnet.arc.network`
- Explorer: `https://testnet.arcscan.app`

The public RPC `https://rpc.testnet.arc.network` works too, but `rpc.drpc.testnet.arc.network` returns `Access-Control-Allow-Origin: *`, which is the safest choice for browser DApps calling the endpoint directly (see issue [#90](https://github.com/circlefin/arc-node/issues/90)).

`https://testnet.arcscan.app` is the only public block explorer that currently works, so use it in the `blockExplorerUrls` field and in any transaction-link examples. `explorer.testnet.arc.network` no longer resolves. `explorer.arc.io` resolves but sits behind Circle's internal Cloudflare Access login rather than serving a public explorer.

## References

- [MetaMask wallet_watchAsset documentation](https://docs.metamask.io/wallet/reference/wallet_watchasset/)
- [MetaMask wallet_addEthereumChain documentation](https://docs.metamask.io/wallet/reference/wallet_addethereumchain/)
- [Arc Network documentation](https://docs.arc.network/)