();
const handleProviderAnnouncement = (
event: WindowEventMap["eip6963:announceProvider"],
) => {
providers.set(event.detail.info.uuid, event.detail);
};
window.addEventListener(
"eip6963:announceProvider",
handleProviderAnnouncement,
);
window.dispatchEvent(new Event("eip6963:requestProvider"));
await new Promise((resolve) => window.setTimeout(resolve, 250));
window.removeEventListener(
"eip6963:announceProvider",
handleProviderAnnouncement,
);
return [...providers.values()];
}
```
### 2.2. Connect the wallet and request account access
After you select a provider, request account access before calling an App Kit
SDK method. This should happen in a user-triggered action such as a
`Connect wallet` button.
```typescript TypeScript theme={null}
async function connectWallet(provider: EIP1193Provider) {
await provider.request({
method: "eth_requestAccounts",
params: undefined, // Required by the provider type even though this method has no params.
});
const accounts = (await provider.request({
method: "eth_accounts",
params: undefined, // Required by the provider type even though this method has no params.
})) as string[];
return {
connectedAddress: accounts[0] ?? null,
};
}
```
Keep wallet connection and App Kit actions as separate user actions. This avoids
overlapping wallet permission or chain-switch requests while a previous wallet
prompt is still pending.
### 2.3. Create a Viem adapter from the selected wallet provider
Use the discovered provider to request account access, then create the App Kit
adapter that signs transactions in the browser:
```typescript TypeScript theme={null}
import { createViemAdapterFromProvider } from "@circle-fin/adapter-viem-v2";
async function connectBrowserWallet() {
const providers = await discoverBrowserWallets();
const selectedWallet =
providers.find(
({ info }) => info.rdns === "io.metamask" || info.name === "MetaMask",
) ?? providers[0];
if (!selectedWallet) {
throw new Error("No EIP-6963 browser wallet found");
}
const { connectedAddress } = await connectWallet(selectedWallet.provider);
const adapter = await createViemAdapterFromProvider({
provider: selectedWallet.provider,
});
return {
adapter,
connectedAddress,
walletName: selectedWallet.info.name,
};
}
```
If multiple EVM wallets are installed, explicitly choose the wallet you want to
use instead of relying on the first announced provider. The browser demo used to
validate this quickstart prefers MetaMask when it is available.
## Step 3. Bridge tokens
### 3.1. Pass the browser wallet adapter into `kit.bridge()`
This is the only App Kit-specific bridge call you need after the wallet is
connected:
```typescript TypeScript theme={null}
import { AppKit } from "@circle-fin/app-kit";
const kit = new AppKit();
async function bridgeWithBrowserWallet() {
const { adapter, connectedAddress, walletName } =
await connectBrowserWallet();
let result = await kit.bridge({
from: { adapter, chain: "Ethereum_Sepolia" },
to: { adapter, chain: "Arc_Testnet" },
amount: "1.00",
});
if (result.state === "error") {
result = await kit.retryBridge(result, {
from: adapter,
to: adapter,
});
}
console.log(`Submitted bridge with ${walletName}`, {
connectedAddress,
result,
});
return result;
}
```
This example omits `token`, which causes the bridge call to default to USDC.
A [non-USDC token](https://developers.circle.com/cctp/expanded-assets) is any
configured ERC-20, such as EURC.
To bridge a non-USDC token, pass a
[token supported by Bridge](/app-kit/references/supported-blockchains#supported-tokens)
(such as `EURC`) as `token`. For any other configured asset, pass its `tokenId`
instead.
```typescript TypeScript theme={null}
import { AppKit } from "@circle-fin/app-kit";
const kit = new AppKit();
async function bridgeWithBrowserWallet() {
const { adapter, connectedAddress, walletName } =
await connectBrowserWallet();
let result = await kit.bridge({
from: { adapter, chain: "Ethereum_Sepolia" },
to: { adapter, chain: "Arc_Testnet" },
amount: "1.00",
token: "EURC", // or "wETH" | "cirBTC" | bytes32 tokenId
});
if (result.state === "error") {
result = await kit.retryBridge(result, {
from: adapter,
to: adapter,
});
}
console.log(`Submitted bridge with ${walletName}`, {
connectedAddress,
result,
});
return result;
}
```
These routes are EVM-to-EVM only. Both blockchains must support
[CCTP for non-USDC](https://developers.circle.com/cctp/expanded-assets/concepts/supported-chains-and-domains),
and the token must be configured on both.
Token deployment on a chain does not mean every bridge route is live yet. To see
which routes a token supports, query Iris with that token's `tokenId` (sandbox
example for EURC):
```bash theme={null}
curl "https://iris-api-sandbox.circle.com/v2/cctpx/tokens?tokenid=0x2587821a0ee7daa174b95436b5dab1731cfa1844775b010217d3c0dd02a4eecd"
```
`amount` is a human-readable decimal string in whole-token units (same shape as
USDC). Token decimals differ. For example, EURC uses 6, cirBTC uses 8, and wETH
uses 18.
CCTP for non-USDC tokens does not support
[custom bridge fees](/app-kit/tutorials/bridge/collect-bridge-fee). To inspect
the quoted fee before you submit,
[estimate costs](/app-kit/tutorials/bridge/estimate-costs) and pass the returned
`quote` into `kit.bridge()`.
Download the runnable
[browser demo](https://github.com/circlefin/docs-examples/tree/master/app-kit-bridge-evm)
to see the EVM-to-EVM bridge flow in action.
### 3.2. Observe bridge lifecycle events
To inspect the bridge flow while it runs, subscribe to Bridge Kit events before
you call `kit.bridge()`. This is useful for seeing the runtime step order and
payload shape. The companion browser demos render these events into an on-page
`` element, but logging them to the console is enough for the core
integration.
```typescript TypeScript theme={null}
kit.on("*", (payload) => {
console.log("Action:", payload);
});
```
Using other EVM chains? Change the `chain` values in `kit.bridge()` and ensure
the connected wallet holds the token you want to bridge on the source chain and
enough gas to complete the transfer flow.
You can customize your bridges to
[collect a fee](/app-kit/tutorials/bridge/collect-bridge-fee), use the
[Forwarding Service](/app-kit/tutorials/bridge/use-forwarding-service), or
[estimate gas and provider fees](/app-kit/tutorials/bridge/estimate-costs)
before bridging. Proceed only if the cost works for you.
### 3.3. Verify the transaction
After `kit.bridge()` resolves, inspect the returned `steps` array. Each
transaction step includes an `explorerUrl`. Use those links to confirm the
approve, burn, and mint steps for the amount you bridged.
The following code is an example of how an `approve` event might look in the
browser console after a successful bridge. The values are examples only and are
not a real transaction:
```bash Shell theme={null}
Event received: {
protocol: "cctp",
version: "v2",
traceId: "550afd44ba4c6d1d1bf4880b9ded3840",
values: {
name: "approve",
state: "success",
txHash: "0xdeadbeefcafebabe1234567890abcdef1234567890abcdef1234567890abcd",
data: {
txHash:
"0xdeadbeefcafebabe1234567890abcdef1234567890abcdef1234567890abcd",
status: "success",
cumulativeGasUsed: 17138643n,
gasUsed: 38617n,
blockNumber: 8778959n,
blockHash:
"0xbeadfacefeed1234567890abcdef1234567890abcdef1234567890abcdef12",
transactionIndex: 173,
effectiveGasPrice: 1037232n,
},
explorerUrl:
"https://explorer.testnet.arc.io/tx/0xdeadbeefcafebabe1234567890abcdef1234567890abcdef1234567890abcd",
},
method: "approve",
}
```
## Extend: Add a Solana source wallet
If you want a Solana browser wallet as the source, keep the EVM destination
adapter from the browser wallet flow above and add a Solana source adapter. The
examples use Solana Devnet and Arc Testnet, but you can use Solana and any
[supported EVM chain](/app-kit/references/supported-blockchains) as the
destination.
In this browser wallet flow, both wallets run in the browser and the user signs
transactions in wallet extensions. Treat wallet connection and bridging as
separate user actions: connect the destination EVM wallet first, connect the
Solana source wallet second, then call `kit.bridge()` after both adapters are
available.
### Additional prerequisites
Before you begin, ensure that you have:
* Worked through the EVM browser wallet flow above first. This Solana path adds
a Solana source wallet to that same browser-wallet pattern.
* Installed a Solana browser wallet that exposes `window.solana`.
* Funded your Solana wallet with testnet USDC from the
[Circle Faucet](https://faucet.circle.com/).
* Funded your Solana wallet with SOL for Solana Devnet transaction fees from the
[Solana Faucet](https://faucet.solana.com/).
### Add the Solana dependencies
Add the Solana adapter dependency to the same project:
```bash Shell theme={null}
npm install @circle-fin/adapter-solana
```
### Connect the Solana source wallet
This step extends the EVM browser wallet flow by adding a Solana source wallet.
You will connect the Solana wallet, create a Solana source adapter, keep the EVM
destination adapter, and pass both adapters into `kit.bridge()`.
The snippets below keep each part of the flow in small helper functions for
readability. The companion browser demo wires this same sequence through
`handleEvmConnect()`, `handleSolanaConnect()`, and `handleBridge()` in a
runnable UI.
#### Connect the Solana wallet and request account access
This pattern assumes a Solana browser wallet that exposes `window.solana`. Keep
wallet connection and App Kit actions as separate user actions so the wallet is
fully connected before you call an App Kit SDK method.
```typescript TypeScript theme={null}
import type { CreateSolanaAdapterFromProviderParams } from "@circle-fin/adapter-solana";
type SolanaWalletProvider = CreateSolanaAdapterFromProviderParams["provider"];
declare global {
interface Window {
solana?: SolanaWalletProvider;
}
}
async function connectSolanaWallet(provider: SolanaWalletProvider) {
const connection = await provider.connect();
return {
connectedAddress:
connection.publicKey?.toString() ??
provider.publicKey?.toString() ??
null,
};
}
```
#### Keep the EVM destination adapter and add a Solana source adapter
This Solana path builds on the EVM browser wallet flow above. Reuse the
connected EVM wallet provider from that flow, then add a Solana provider and
create one adapter for each chain:
```typescript TypeScript theme={null}
import { createViemAdapterFromProvider } from "@circle-fin/adapter-viem-v2";
import { createSolanaAdapterFromProvider } from "@circle-fin/adapter-solana";
import type { EIP1193Provider } from "viem";
async function createBridgeAdapters(
evmProvider: EIP1193Provider,
solanaProvider: SolanaWalletProvider,
) {
const evmAdapter = await createViemAdapterFromProvider({
provider: evmProvider,
});
const solanaAdapter = await createSolanaAdapterFromProvider({
provider: solanaProvider,
});
return {
evmAdapter,
solanaAdapter,
};
}
```
#### Pass the browser wallet adapters into `kit.bridge()`
After you have a connected EVM provider from the earlier browser-wallet flow and
a connected Solana provider from `window.solana`, create both adapters and pass
them into `kit.bridge()`:
```typescript TypeScript theme={null}
import { AppKit } from "@circle-fin/app-kit";
import type { EIP1193Provider } from "viem";
const kit = new AppKit();
async function bridgeUSDCWithSolanaBrowserWallet(
evmProvider: EIP1193Provider,
solanaProvider: SolanaWalletProvider,
) {
const { evmAdapter, solanaAdapter } = await createBridgeAdapters(
evmProvider,
solanaProvider,
);
const result = await kit.bridge({
from: { adapter: solanaAdapter, chain: "Solana_Devnet" },
to: { adapter: evmAdapter, chain: "Arc_Testnet" },
amount: "1.00",
});
console.log(
"Submitted bridge from Solana browser wallet to EVM destination",
{
result,
},
);
return result;
}
```
#### Retry a failed bridge attempt
If the first bridge attempt returns `state: "error"`, retry it with the same
freshly created adapters:
```typescript TypeScript theme={null}
let result = await kit.bridge({
from: { adapter: solanaAdapter, chain: "Solana_Devnet" },
to: { adapter: evmAdapter, chain: "Arc_Testnet" },
amount: "1.00",
});
if (result.state === "error") {
result = await kit.retryBridge(result, {
from: solanaAdapter,
to: evmAdapter,
});
}
```
Download the runnable
[browser demo](https://github.com/circlefin/docs-examples/tree/master/app-kit-bridge-solana)
to see the Solana-to-EVM bridge flow in action.
#### Observe bridge lifecycle events
If you added the `kit.on("*", (payload) => { ... })` listener in the previous
step, it already captures Solana bridge events, and no additional subscription
is needed.
```typescript TypeScript theme={null}
kit.on("*", (payload) => {
console.log("Action:", payload);
});
```
Using a different EVM chain as the destination? Change the `to.chain` value and
ensure the connected Solana wallet holds USDC on the source chain and enough
native gas to complete the transfer flow.
You can customize your bridges to
[collect a fee](/app-kit/tutorials/bridge/collect-bridge-fee), use the
[Forwarding Service](/app-kit/tutorials/bridge/use-forwarding-service), or
[estimate gas and provider fees](/app-kit/tutorials/bridge/estimate-costs)
before bridging. Proceed only if the cost works for you.
#### Verify the transaction
After `kit.bridge()` resolves, inspect the returned `steps` array. Each
transaction step includes an `explorerUrl`. Use those links to confirm the burn,
attestation, and mint steps for the amount you bridged.
The following code is an example of how a `burn` step might look in the browser
console after a successful bridge. The values are examples only and are not a
real transaction:
```bash Shell theme={null}
steps: [
{
name: "burn",
state: "success",
txHash: "5UfgJ5vVZxUxefDGqzqkVLHzHxVTyYH9StYyHKSNc7WLyFTmgL5RFGujWNqEbUBdNKRkHmx7ZRQR3FVhdEwxKHm",
data: {
txHash:
"5UfgJ5vVZxUxefDGqzqkVLHzHxVTyYH9StYyHKSNc7WLyFTmgL5RFGujWNqEbUBdNKRkHmx7ZRQR3FVhdEwxKHm",
status: "success",
blockNumber: 312456789n,
blockHash: "HxVTyYH9StYyHKSNc7WLyFTmgL5RFGujWNqEbUBdNK",
transactionIndex: 0,
gasUsed: 25000n,
cumulativeGasUsed: 0n,
effectiveGasPrice: 5000n,
explorerUrl:
"https://solscan.io/tx/5UfgJ5vVZxUxefDGqzqkVLHzHxVTyYH9StYyHKSNc7WLyFTmgL5RFGujWNqEbUBdNKRkHmx7ZRQR3FVhdEwxKHm?cluster=devnet",
},
},
];
```