viemv2 for EVM blockchainsethersv6 for EVM blockchainssolanafor the Solana blockchaincircle-walletsfor Circle-managed developer-controlled and user-controlled wallets
Before setting up an adapter, you should already have created the wallet,
account, API keys, and policies in your wallet provider.
- Viem
- Ethers
- Solana
- Circle Wallets
Use the Viem adapter for EVM apps that use
viem accounts or wallet clients.Server-side wallet
Use a server-side wallet when your backend signs transactions. The signer can be a private key or a wallet provider.- Private key
- Turnkey
Create one adapter from your wallet private key. The adapter works across EVM
blockchains and uses built-in public RPC endpoints.
Default RPC URLs are shared and may be rate-limited. For a more stable
connection, configure a custom RPC.
TypeScript
import { createViemAdapterFromPrivateKey } from "@circle-fin/adapter-viem-v2";
const adapter = createViemAdapterFromPrivateKey({
privateKey: process.env.PRIVATE_KEY as string,
});
Use Turnkey Company Wallets with the Viem adapter for backend signing. The setup
uses Adapter setup:What to know about this setup:
@turnkey/viem to create a Turnkey-backed viem account, then passes that
account to the Viem adapter.Set these environment variables from your Turnkey account:.env
TURNKEY_API_PUBLIC_KEY=YOUR_TURNKEY_API_PUBLIC_KEY
TURNKEY_API_PRIVATE_KEY=YOUR_TURNKEY_API_PRIVATE_KEY
TURNKEY_ORGANIZATION_ID=YOUR_TURNKEY_ORGANIZATION_ID
TURNKEY_WALLET_ADDRESS=YOUR_TURNKEY_ETHEREUM_WALLET_ADDRESS
TypeScript
import { AppKit } from "@circle-fin/app-kit";
import { ViemAdapter } from "@circle-fin/adapter-viem-v2";
import { Turnkey as TurnkeyServerSDK } from "@turnkey/sdk-server";
import { createAccount } from "@turnkey/viem";
import { createPublicClient, createWalletClient, http } from "viem";
const turnkey = new TurnkeyServerSDK({
apiBaseUrl: "https://api.turnkey.com",
apiPublicKey: process.env.TURNKEY_API_PUBLIC_KEY!,
apiPrivateKey: process.env.TURNKEY_API_PRIVATE_KEY!,
defaultOrganizationId: process.env.TURNKEY_ORGANIZATION_ID!,
});
const account = await createAccount({
client: turnkey.apiClient(),
organizationId: process.env.TURNKEY_ORGANIZATION_ID!,
signWith: process.env.TURNKEY_WALLET_ADDRESS!,
});
const appKit = new AppKit();
const supportedChains = (await appKit.getSupportedChains()).filter(
(chain) => chain.type === "evm",
);
const adapter = new ViemAdapter(
{
getPublicClient: ({ chain }) =>
createPublicClient({
chain,
transport: http(),
}),
getWalletClient: ({ chain }) =>
createWalletClient({
account,
chain,
transport: http(),
}),
},
{
addressContext: "user-controlled",
supportedChains,
},
);
- Turnkey supports both
viemandethers. This setup usesviembecause@turnkey/viemcreates a Turnkey-backed account directly, which keeps the adapter setup shorter. - Use this setup only on your backend.
TURNKEY_API_PRIVATE_KEYauthenticates requests to Turnkey and must not be exposed in browser code. - For crosschain flows, create one adapter per blockchain if your wallet client or RPC setup is blockchain-specific.
Browser wallet
This setup expects a wallet extension in the browser (for examplewindow.ethereum or window.solana), not a Node.js server. You can use wallets
like MetaMask or Phantom:- MetaMask (EIP-6963)
- window.ethereum
Use EIP-6963 to discover injected browser wallets, select a provider, request
access to the user’s accounts, then pass the provider to the Viem adapter.The example selects MetaMask by reverse-DNS identifier. To use any injected
wallet, omit
requiredRdns.TypeScript
import {
createViemAdapterFromProvider,
type CreateViemAdapterFromProviderParams,
} from "@circle-fin/adapter-viem-v2";
type BrowserWalletProvider = CreateViemAdapterFromProviderParams["provider"];
type EIP6963ProviderDetail = {
info: {
uuid: string;
name: string;
icon: string;
rdns: string;
};
provider: BrowserWalletProvider;
};
declare global {
interface WindowEventMap {
"eip6963:announceProvider": CustomEvent<EIP6963ProviderDetail>;
}
}
async function getInjectedWalletProvider(
requiredRdns?: string,
): Promise<BrowserWalletProvider> {
const providers = new Map<string, EIP6963ProviderDetail>();
const onAnnounce = ((event: CustomEvent<EIP6963ProviderDetail>) => {
providers.set(event.detail.info.uuid, event.detail);
}) as EventListener;
window.addEventListener("eip6963:announceProvider", onAnnounce);
window.dispatchEvent(new Event("eip6963:requestProvider"));
await new Promise((resolve) => window.setTimeout(resolve, 250));
window.removeEventListener("eip6963:announceProvider", onAnnounce);
const provider = requiredRdns
? [...providers.values()].find(({ info }) => info.rdns === requiredRdns)
?.provider
: [...providers.values()][0]?.provider;
if (!provider) {
throw new Error(
requiredRdns
? `No EIP-6963 wallet found for ${requiredRdns}`
: "No EIP-6963 browser wallet found",
);
}
return provider;
}
const provider = await getInjectedWalletProvider("io.metamask");
// For any injected wallet, use:
// const provider = await getInjectedWalletProvider();
await provider.request({
method: "eth_requestAccounts",
params: undefined,
});
const adapter = await createViemAdapterFromProvider({
provider,
});
Use
window.ethereum only when the wallet does not support EIP-6963 or your app
intentionally accepts the default injected provider.TypeScript
import { createViemAdapterFromProvider } from "@circle-fin/adapter-viem-v2";
import type { EIP1193Provider } from "viem";
declare global {
interface Window {
ethereum?: EIP1193Provider;
}
}
if (!window.ethereum) {
throw new Error("No wallet provider found");
}
const adapter = await createViemAdapterFromProvider({
provider: window.ethereum,
});
Custom RPC
By default, adapters use built-in RPC endpoints, which may be rate-limited or unreliable. Override them with your own provider. Alchemy, QuickNode, and chainlist.org are common places to source endpoints. This example uses Alchemy.ThegetPublicClient/getWalletClient override pairs with any signer setup
that uses viem, such as a private key or browser wallet.TypeScript
import { createViemAdapterFromPrivateKey } from "@circle-fin/adapter-viem-v2";
import { EthereumSepolia, ArcTestnet } from "@circle-fin/app-kit/chains";
import { createPublicClient, createWalletClient, http } from "viem";
const RPC_BY_CHAIN_NAME: Record<string, string> = {
[EthereumSepolia.name]: `https://eth-sepolia.g.alchemy.com/v2/${process.env.ALCHEMY_KEY}`,
[ArcTestnet.name]: `https://arc-testnet.g.alchemy.com/v2/${process.env.ALCHEMY_KEY}`,
};
const adapter = createViemAdapterFromPrivateKey({
privateKey: process.env.PRIVATE_KEY as string,
getPublicClient: ({ chain }) => {
const rpcUrl = RPC_BY_CHAIN_NAME[chain.name];
if (!rpcUrl) {
throw new Error(`No RPC configured for chain: ${chain.name}`);
}
return createPublicClient({
chain,
transport: http(rpcUrl, {
retryCount: 3,
timeout: 10000,
}),
});
},
getWalletClient: ({ chain, account }) => {
const rpcUrl = RPC_BY_CHAIN_NAME[chain.name];
if (!rpcUrl) {
throw new Error(`No RPC configured for chain: ${chain.name}`);
}
return createWalletClient({
account,
chain,
transport: http(rpcUrl, {
retryCount: 3,
timeout: 10000,
}),
});
},
});
Use the Ethers adapter for EVM apps that use
ethers providers or signers.Server-side wallet
Use a server-side wallet when your backend signs transactions. The signer can be a private key or a wallet provider.- Private key
- Privy
Create one adapter from your wallet private key. The adapter works across EVM
blockchains and uses built-in public RPC endpoints.
Default RPC URLs are shared and may be rate-limited. For a more stable
connection, configure a custom RPC.
TypeScript
import { createEthersAdapterFromPrivateKey } from "@circle-fin/adapter-ethers-v6";
const adapter = createEthersAdapterFromPrivateKey({
privateKey: process.env.PRIVATE_KEY as string,
});
Use Privy server wallets with the Ethers adapter when Privy signs for your
backend. Privy server wallets sign through the Privy wallet API by Use a server-owned Privy wallet. After adding the helper, create the adapter:What to know about this setup:
walletId,
so the setup needs a small custom ethers signer.Set these environment variables from your Privy app and server wallet:.env
PRIVY_APP_ID=YOUR_PRIVY_APP_ID
PRIVY_APP_SECRET=YOUR_PRIVY_APP_SECRET
PRIVY_AUTHORIZATION_PRIVATE_KEY=YOUR_PRIVY_AUTHORIZATION_PRIVATE_KEY
PRIVY_WALLET_ID=YOUR_PRIVY_SERVER_WALLET_ID
PRIVY_WALLET_ADDRESS=YOUR_PRIVY_SERVER_WALLET_ADDRESS
PRIVY_AUTHORIZATION_PRIVATE_KEY must belong
to the wallet owner or an authorized signer for PRIVY_WALLET_ID.Create the PrivyServerSigner.ts helper first. The adapter setup imports this
helper in the next step.TypeScript
import { PrivyClient, type Hex, type Quantity } from "@privy-io/server-auth";
import { ethers } from "ethers";
interface PrivyServerSignerOptions {
walletId: string;
walletAddress: string;
privy: PrivyClient;
provider: ethers.Provider;
}
type JsonSafe =
string | number | boolean | null | JsonSafe[] | { [key: string]: JsonSafe };
type PrivySignTransactionRequest = Parameters<
PrivyClient["walletApi"]["ethereum"]["signTransaction"]
>[0];
type PrivySignMessageRequest = Parameters<
PrivyClient["walletApi"]["ethereum"]["signMessage"]
>[0];
type PrivySignTypedDataRequest = Parameters<
PrivyClient["walletApi"]["ethereum"]["signTypedData"]
>[0];
export class PrivyServerSigner extends ethers.AbstractSigner {
private walletId: string;
private walletAddress: string;
private privy: PrivyClient;
constructor(options: PrivyServerSignerOptions) {
super(options.provider);
this.walletId = options.walletId;
this.walletAddress = options.walletAddress;
this.privy = options.privy;
}
connect(provider: ethers.Provider): PrivyServerSigner {
return new PrivyServerSigner({
walletId: this.walletId,
walletAddress: this.walletAddress,
privy: this.privy,
provider,
});
}
async getAddress(): Promise<string> {
return this.walletAddress;
}
async signTransaction(tx: ethers.TransactionRequest): Promise<string> {
const toQuantity = (
value: ethers.BigNumberish | null | undefined,
): Quantity | undefined =>
value != null ? (ethers.toBeHex(value) as Hex) : undefined;
const toHex = (value: unknown): Hex | undefined =>
value != null ? (value.toString() as Hex) : undefined;
const request: PrivySignTransactionRequest = {
walletId: this.walletId,
chainType: "ethereum",
transaction: {
to: toHex(tx.to),
nonce: tx.nonce != null ? Number(tx.nonce) : undefined,
chainId: tx.chainId != null ? Number(tx.chainId) : undefined,
data: toHex(tx.data),
value: toQuantity(tx.value),
type: (tx.type ?? 2) as 0 | 1 | 2,
gasLimit: toQuantity(tx.gasLimit),
maxFeePerGas: toQuantity(tx.maxFeePerGas),
maxPriorityFeePerGas: toQuantity(tx.maxPriorityFeePerGas),
},
};
const { signedTransaction } =
await this.privy.walletApi.ethereum.signTransaction(request);
return signedTransaction;
}
async signMessage(message: string | Uint8Array): Promise<string> {
const request: PrivySignMessageRequest = {
walletId: this.walletId,
chainType: "ethereum",
message: typeof message === "string" ? message : ethers.hexlify(message),
};
const { signature } =
await this.privy.walletApi.ethereum.signMessage(request);
return signature;
}
async signTypedData(
domain: ethers.TypedDataDomain,
types: Record<string, ethers.TypedDataField[]>,
value: Record<string, unknown>,
): Promise<string> {
const toJsonSafe = (input: unknown): JsonSafe => {
if (typeof input === "bigint") {
return input.toString();
}
if (Array.isArray(input)) {
return input.map(toJsonSafe);
}
if (input && typeof input === "object") {
return Object.fromEntries(
Object.entries(input).map(([key, nestedValue]) => [
key,
toJsonSafe(nestedValue),
]),
);
}
return input as JsonSafe;
};
const primaryType =
Object.keys(types).find((key) => key !== "EIP712Domain") ?? "";
const jsonSafeDomain = toJsonSafe(domain) as Record<string, unknown>;
const jsonSafeMessage = toJsonSafe(value) as Record<string, unknown>;
const request: PrivySignTypedDataRequest = {
walletId: this.walletId,
typedData: {
domain: jsonSafeDomain,
types,
message: jsonSafeMessage,
primaryType,
},
};
const { signature } =
await this.privy.walletApi.ethereum.signTypedData(request);
return signature;
}
}
TypeScript
import { AppKit } from "@circle-fin/app-kit";
import { EthersAdapter } from "@circle-fin/adapter-ethers-v6";
import { PrivyClient } from "@privy-io/server-auth";
import { ethers } from "ethers";
import { PrivyServerSigner } from "./PrivyServerSigner";
const privy = new PrivyClient(
process.env.PRIVY_APP_ID!,
process.env.PRIVY_APP_SECRET!,
{
walletApi: {
authorizationPrivateKey: process.env.PRIVY_AUTHORIZATION_PRIVATE_KEY!,
},
},
);
const appKit = new AppKit();
const supportedChains = (await appKit.getSupportedChains()).filter(
(chain) => chain.type === "evm",
);
const arcTestnet = supportedChains.find(
(chain) => chain.chain === "Arc_Testnet",
);
if (!arcTestnet) {
throw new Error("Arc Testnet is not supported");
}
const provider = new ethers.JsonRpcProvider(arcTestnet.rpcEndpoints[0]);
const signer = new PrivyServerSigner({
walletId: process.env.PRIVY_WALLET_ID!,
walletAddress: process.env.PRIVY_WALLET_ADDRESS!,
privy,
provider,
});
const adapter = new EthersAdapter(
{
signer,
getProvider: ({ chain }) =>
new ethers.JsonRpcProvider(chain.rpcEndpoints[0]),
},
{
addressContext: "user-controlled",
supportedChains,
},
);
- Privy’s stock
createEthersSigneris built for user-linked wallets. Server wallets sign bywalletId, which is why this setup usesPrivyServerSigner. - User-owned embedded wallets are not server wallets. For this setup, create a server-owned wallet or add your backend key as an authorized signer.
- Automatic chain switching does not support this custom signer. For crosschain flows, create one adapter per blockchain.
Browser wallet
This setup expects a wallet extension in the browser (for examplewindow.ethereum or window.solana), not a Node.js server. You can use wallets
like MetaMask or Phantom:In crosschain flows, use the Viem adapter for injected browser wallets. The
Ethers adapter can work for same-chain browser wallet operations, but the
BrowserProvider from ethers can throw a network-change error when the wallet
switches blockchains during a bridge or other crosschain flow.- MetaMask (EIP-6963)
- window.ethereum
Use EIP-6963 to discover injected browser wallets, select a provider, request
access to the user’s accounts, then pass the provider to the Ethers adapter.The example selects MetaMask by reverse-DNS identifier. To use any injected
wallet, omit
requiredRdns.TypeScript
import {
createEthersAdapterFromProvider,
type CreateEthersAdapterFromProviderParams,
} from "@circle-fin/adapter-ethers-v6";
type BrowserWalletProvider = CreateEthersAdapterFromProviderParams["provider"];
type EIP6963ProviderDetail = {
info: {
uuid: string;
name: string;
icon: string;
rdns: string;
};
provider: BrowserWalletProvider;
};
declare global {
interface WindowEventMap {
"eip6963:announceProvider": CustomEvent<EIP6963ProviderDetail>;
}
}
async function getInjectedWalletProvider(
requiredRdns?: string,
): Promise<BrowserWalletProvider> {
const providers = new Map<string, EIP6963ProviderDetail>();
const onAnnounce = ((event: CustomEvent<EIP6963ProviderDetail>) => {
providers.set(event.detail.info.uuid, event.detail);
}) as EventListener;
window.addEventListener("eip6963:announceProvider", onAnnounce);
window.dispatchEvent(new Event("eip6963:requestProvider"));
await new Promise((resolve) => window.setTimeout(resolve, 250));
window.removeEventListener("eip6963:announceProvider", onAnnounce);
const provider = requiredRdns
? [...providers.values()].find(({ info }) => info.rdns === requiredRdns)
?.provider
: [...providers.values()][0]?.provider;
if (!provider) {
throw new Error(
requiredRdns
? `No EIP-6963 wallet found for ${requiredRdns}`
: "No EIP-6963 browser wallet found",
);
}
return provider;
}
const provider = await getInjectedWalletProvider("io.metamask");
// For any injected wallet, use:
// const provider = await getInjectedWalletProvider();
await provider.request({
method: "eth_requestAccounts",
params: undefined,
});
const adapter = await createEthersAdapterFromProvider({
provider,
});
Use
window.ethereum only when the wallet does not support EIP-6963 or your app
intentionally accepts the default injected provider.TypeScript
import { createEthersAdapterFromProvider } from "@circle-fin/adapter-ethers-v6";
import type { Eip1193Provider } from "ethers";
declare global {
interface Window {
ethereum?: Eip1193Provider;
}
}
if (!window.ethereum) {
throw new Error("No wallet provider found");
}
const adapter = await createEthersAdapterFromProvider({
provider: window.ethereum,
});
Custom RPC
By default, adapters use built-in RPC endpoints, which may be rate-limited or unreliable. Override them with your own provider. Alchemy, QuickNode, and chainlist.org are common places to source endpoints. This example uses Alchemy.ThegetProvider override pairs with any signer setup that uses ethers, such
as a private key or browser wallet.TypeScript
import { createEthersAdapterFromPrivateKey } from "@circle-fin/adapter-ethers-v6";
import { EthereumSepolia, ArcTestnet } from "@circle-fin/app-kit/chains";
import { JsonRpcProvider } from "ethers";
const RPC_BY_CHAIN_NAME: Record<string, string> = {
[EthereumSepolia.name]: `https://eth-sepolia.g.alchemy.com/v2/${process.env.ALCHEMY_KEY}`,
[ArcTestnet.name]: `https://arc-testnet.g.alchemy.com/v2/${process.env.ALCHEMY_KEY}`,
};
const adapter = createEthersAdapterFromPrivateKey({
privateKey: process.env.PRIVATE_KEY as string,
getProvider: ({ chain }) => {
const rpcUrl = RPC_BY_CHAIN_NAME[chain.name];
if (!rpcUrl) {
throw new Error(`No RPC configured for chain: ${chain.name}`);
}
return new JsonRpcProvider(rpcUrl);
},
});
Use the Solana adapter for Solana wallets and RPC clients.
Server-side wallet
Use a server-side wallet when your backend signs transactions.Private key
Create one adapter from your Solana private key. Solana accepts Base58, Base64, or JSON array format private keys.Default RPC URLs are shared and may be rate-limited. For a more stable
connection, configure a custom RPC.
TypeScript
import { createSolanaKitAdapterFromPrivateKey } from "@circle-fin/adapter-solana-kit";
const adapter = createSolanaKitAdapterFromPrivateKey({
privateKey: process.env.PRIVATE_KEY as string,
});
Browser wallet
This setup expects a wallet extension in the browser (for examplewindow.ethereum or window.solana), not a Node.js server. You can use wallets
like MetaMask or Phantom:TypeScript
import { createSolanaKitAdapterFromProvider } from "@circle-fin/adapter-solana-kit";
type SolanaKitWalletProvider = Parameters<
typeof createSolanaKitAdapterFromProvider
>[0]["provider"];
declare global {
interface Window {
solana?: SolanaKitWalletProvider;
}
}
if (!window.solana) {
throw new Error("No Solana wallet provider found");
}
const adapter = await createSolanaKitAdapterFromProvider({
provider: window.solana,
});
Custom RPC
By default, adapters use built-in RPC endpoints, which may be rate-limited or unreliable. Override them with your own provider. Alchemy, QuickNode, and chainlist.org are common places to source endpoints. This example uses Alchemy.ThegetRpc override pairs with any Solana signer setup, such as a private key
or browser wallet.TypeScript
import { createSolanaKitAdapterFromPrivateKey } from "@circle-fin/adapter-solana-kit";
import { createSolanaRpc } from "@solana/kit";
const adapter = createSolanaKitAdapterFromPrivateKey({
privateKey: process.env.SOLANA_PRIVATE_KEY as string,
getRpc: () =>
createSolanaRpc(
`https://solana-devnet.g.alchemy.com/v2/${process.env.ALCHEMY_KEY}`,
),
});
server-side onlyUse the Circle Wallets adapter if you already
manage wallets through Circle. The
adapter is suited for backend apps and works with
developer-controlled or
user-controlled wallets
so you can use the App Kit SDK on
supported blockchains
without managing private keys yourself.Pick the custody model that matches your app.
- Developer-controlled
- User-controlled
Use the developer-controlled Circle Wallets adapter when your backend initiates
wallet actions.Use the wallet address for the same blockchain as
Setup
The developer-controlled wallets adapter requires these credentials from the Circle Console:- API Key:
Environment-prefixed (examples:
TEST_API_KEY:abc123:def456,LIVE_API_KEY:xyz:uvw) or Base64-encoded - Entity Secret: 64 lowercase alphanumeric characters
TypeScript
import { createCircleWalletsAdapter } from "@circle-fin/adapter-circle-wallets";
const adapter = createCircleWalletsAdapter({
apiKey: process.env.CIRCLE_API_KEY!,
entitySecret: process.env.CIRCLE_ENTITY_SECRET!,
});
Usage
When you use the developer-controlled adapter in an App Kit operation, pass the wallet address in the operation context. The address tells the App Kit SDK which Circle Wallet acts on that blockchain:TypeScript
from: {
adapter,
chain: "Base_Sepolia",
address: walletAddress,
}
chain.- The wallet address you pass to App Kit operations determines whether Circle handles the transaction as a dev-controlled externally owned account (EOA) or smart contract account (SCA) wallet. See Account type comparison to understand the difference.
- Dev-controlled EOA: Each Circle Wallet exists on one blockchain. For crosschain flows, provision one wallet per source blockchain. The onchain addresses differ.
- Dev-controlled SCA: Each Circle Wallet exists on one blockchain. For crosschain flows, provision one SCA per source blockchain.
The Circle Wallets adapter does not support
gas sponsorship for
crosschain transfers that originate on Solana. For those transactions, the
user’s Solana wallet must hold sufficient SOL to pay network fees.
Use the user-controlled adapter when end users approve wallet actions in your
app. The adapter runs on your server. Challenge completion runs in the client
with the
User-Controlled Wallets Web SDK.
Setup
Before creating a user-controlled wallet adapter, the user must have a configured signing method and credentials from your existing user-controlled wallet login flow. If not, adapter creation fails with Circle error code155110.The user-controlled adapter requires these server-side credentials:- API Key:
Environment-prefixed (examples:
TEST_API_KEY:abc123:def456,LIVE_API_KEY:xyz:uvw) or Base64-encoded userToken: From your existing UCW authentication flow. See Authentication methods.
encryptionKey from that same login (and your Circle
App ID) to complete challenges with the Web SDK. The adapter does not take
encryptionKey.This code block initializes the user-controlled Circle Wallets adapter:TypeScript
import { createCircleUserWalletAdapter } from "@circle-fin/adapter-circle-wallets/ucw/server";
const adapter = await createCircleUserWalletAdapter({
apiKey: process.env.CIRCLE_API_KEY!,
userToken, // from your existing UCW login
chain: "Arc_Testnet",
onChallenge: ({ challengeId }) => {
// notify your client to complete the challenge
},
});
Usage
When you use the user-controlled adapter in an App Kit operation, pass onlyadapter and chain in the operation context. Omit address; the adapter
resolves the wallet address:TypeScript
from: {
adapter,
chain: "Arc_Testnet",
}
- Account type: The user-controlled adapter uses smart contract account (SCA) wallets. See Account type comparison.
-
Approval flow: An App Kit method such as
kit.sendwaits for user approval. The adapter callsonChallengewith achallengeId; use it to notify the client only. Do not return a signature. The client completes the challenge with the Web SDK, while the adapter polls Circle until approval completes. Keep the client-side challenge UI and listener available for the entire duration of the server-side call. -
When challenges fire: Passing
chainorchainscan find or create the user’s SCA wallet, so a challenge may fire during adapter creation or a later App Kit operation. Crosschain operations may emit multiple challenges; keep listening until the operation finishes. -
Chain selection: Pass
chainfor a single-chain operation. For crosschain operations, passchains(an array of every blockchain involved) when creating the adapter. Use the same adapter instance for both the source (from) and destination (to):TypeScriptfrom: { adapter, chain: "Arc_Testnet" }, to: { adapter, chain: "Base_Sepolia" }, -
Challenge delivery: Deliver each
challengeIdto the client through your own channel, such as SSE, WebSocket, or polling.