LayerHopDocsLaunch app (opens in a new tab)

Wallets and embedded wallets

Execute LayerHop quotes from any wallet stack with @layerhop/wagmi and @layerhop/solana. EIP-5792 batching sends approve + swap (+ fee) in one confirmation, with snippets for wagmi/Reown, Privy, Dynamic, thirdweb and Coinbase Smart Wallet.

Two small packages turn a LayerHop quote into a confirmed transaction in the user's wallet. @layerhop/wagmi covers EVM chains through wagmi or a viem WalletClient. @layerhop/solana covers Solana through @solana/wallet-adapter, Wallet Standard or an injected provider. Both build the transaction, ask the wallet to sign it, and report the hash with reportSubmitted so order tracking and webhooks work. Neither one holds keys: every transaction is confirmed by the user in their wallet.

Test keys (lh_test_…) return placeholder transactions. Both packages detect them (sandbox: true), never pass them to the wallet, and report a placeholder hash so the test order fills. See test mode.

Install#

Terminal
# EVM (wagmi v2 / viem v2)
npm install @layerhop/wagmi @layerhop/sdk wagmi viem @tanstack/react-query
# Solana
npm install @layerhop/solana @layerhop/sdk @solana/web3.js

@layerhop/wagmi and @layerhop/solana are on npm from 0.7.0: npm install @layerhop/wagmi or npm install @layerhop/solana.

One confirmation with EIP-5792#

An EVM route can need up to three calls: an ERC-20 approve (see approvals), the swap or payment, and extraTxs such as LayerHop's fee transfer on a same-token checkout payment. executeQuote handles them like this:

  1. It switches the wallet to the transaction's chain if needed.
  2. It reads the allowance. The approval call is added only when the allowance is short, and only for the exact amount (never unlimited).
  3. If there is more than one call and wallet_getCapabilities reports atomic batching for the chain (atomic.status is supported or ready), every call goes in one wallet_sendCalls request with atomicRequired: true. The user confirms once and the calls succeed or fail together. The returned hash is the mined transaction's hash, read from wallet_getCallsStatus.
  4. Otherwise it sends them one by one: approve and wait for it, then the main call, then each extra call. An extra call that fails or is rejected never fails the payment. If the wallet rejects the batch as unsupported (codes 4200, 5700, 5710, 5740, 5750, 5760 or -32601), it falls back to the same sequential flow. A user rejection (4001) is never retried.

Pass batch: "never" to always send sequentially, or batch: "require" to throw instead of falling back. Smart-contract wallets (Coinbase Smart Wallet, Safe) and EIP-7702 accounts (for example MetaMask smart accounts) batch. Most plain EOA extensions send sequentially.

wagmi and Reown AppKit#

executeQuote(config, { quote }) is the action and useExecuteQuote() is the hook. Both work with any wagmi config. With Reown AppKit, use the adapter's config (wagmiAdapter.wagmiConfig). This is how the LayerHop app itself sends.

TSX
import { LayerHopProvider, useExecuteQuote, useLayerHopQuote, useOrderStatus } from "@layerhop/wagmi";
import { useAccount } from "wagmi";

// Inside <WagmiProvider> + <QueryClientProvider> + <LayerHopProvider options={{ integrator: "myapp" }}>
function Swap() {
  const { address } = useAccount();
  const quote = useLayerHopQuote(address && {
    srcChain: "base", srcToken: "USDC", amount: "25000000",
    dstChain: "arbitrum", dstToken: "ETH", sender: address, recipient: address,
  });
  const exec = useExecuteQuote();
  const best = quote.data?.best;
  const order = useOrderStatus(best?.provider, exec.hash);
  return (
    <button disabled={!best || exec.isPending} onClick={() => best && exec.execute({ quote: best })}>
      {exec.isPending ? exec.status : exec.mode === "batch" ? "Sent in one confirmation" : "Swap"}
    </button>
  );
}

exec.status goes idle → building → (switching) → (approving) → signing → reporting → submitted, or ends in error. exec.mode is batch, sequential or sandbox. Without React, use the action:

TypeScript
import { executeQuote } from "@layerhop/wagmi/actions";
import { LayerHop } from "@layerhop/sdk";

const lh = new LayerHop({ apiKey: process.env.LAYERHOP_API_KEY });
const { best } = await lh.quote(params);
const sent = await executeQuote(wagmiConfig, { quote: best, client: lh });
await sent.wait(); // true once confirmed

For Solana with Reown AppKit, wrap the Solana walletProvider:

TypeScript
import { useAppKitProvider } from "@reown/appkit/react";
import type { Provider } from "@reown/appkit-adapter-solana/react";
import { executeSolanaQuote, fromInjectedProvider } from "@layerhop/solana";

const { walletProvider } = useAppKitProvider<Provider>("solana");
await executeSolanaQuote(fromInjectedProvider(walletProvider), { quote: best, client: lh, connection });

Coinbase Smart Wallet#

Coinbase Smart Wallet supports EIP-5792, so approve + swap (+ fee) take one passkey confirmation. With plain wagmi, add the connector. smartWalletOnly hides the extension and mobile app, and all offers both.

TypeScript
import { createConfig, http } from "wagmi";
import { base } from "wagmi/chains";
import { coinbaseWallet } from "wagmi/connectors";

export const wagmiConfig = createConfig({
  chains: [base],
  connectors: [coinbaseWallet({ appName: "My App", preference: "smartWalletOnly" })],
  transports: { [base.id]: http() },
});
// then: useExecuteQuote() / executeQuote(wagmiConfig, { quote })

Reown AppKit includes the Coinbase connector by default. Its coinbasePreference option takes the same values (the LayerHop app sets "all").

Privy#

Privy's wagmi integration gives you a normal wagmi config, so useExecuteQuote works unchanged. Import createConfig and WagmiProvider from @privy-io/wagmi and nest PrivyProvider → QueryClientProvider → WagmiProvider.

TSX
import { PrivyProvider } from "@privy-io/react-auth";
import { createConfig, WagmiProvider } from "@privy-io/wagmi";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { http } from "wagmi";
import { base } from "viem/chains";
import { LayerHopProvider } from "@layerhop/wagmi";

const wagmiConfig = createConfig({ chains: [base], transports: { [base.id]: http() } });
const queryClient = new QueryClient();

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <PrivyProvider appId="your-privy-app-id">
      <QueryClientProvider client={queryClient}>
        <WagmiProvider config={wagmiConfig}>
          <LayerHopProvider options={{ integrator: "myapp" }}>{children}</LayerHopProvider>
        </WagmiProvider>
      </QueryClientProvider>
    </PrivyProvider>
  );
}

Privy's Solana hooks work with bytes. Wrap them in a SolanaSigner:

TypeScript
import { useSignAndSendTransaction, useSignTransaction, useWallets } from "@privy-io/react-auth/solana";
import { VersionedTransaction } from "@solana/web3.js";
import { bytesToBase58, executeSolanaQuote, type SolanaSigner } from "@layerhop/solana";

const { wallets } = useWallets();
const { signTransaction } = useSignTransaction();
const { signAndSendTransaction } = useSignAndSendTransaction();
const wallet = wallets[0]!;
const signer: SolanaSigner = {
  signTransaction: async (tx) =>
    VersionedTransaction.deserialize((await signTransaction({ transaction: tx.serialize(), wallet })).signedTransaction),
  signAndSendTransaction: async (tx) =>
    bytesToBase58((await signAndSendTransaction({ transaction: tx.serialize(), wallet })).signature),
};
await executeSolanaQuote(signer, { quote: best, client: lh, connection });

Dynamic#

Dynamic syncs its wallets into wagmi through DynamicWagmiConnector. Inside it, useExecuteQuote works unchanged.

TSX
import { DynamicContextProvider } from "@dynamic-labs/sdk-react-core";
import { EthereumWalletConnectors } from "@dynamic-labs/ethereum";
import { SolanaWalletConnectors } from "@dynamic-labs/solana";
import { DynamicWagmiConnector } from "@dynamic-labs/wagmi-connector";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { createConfig, http, WagmiProvider } from "wagmi";
import { base } from "viem/chains";

const wagmiConfig = createConfig({ chains: [base], multiInjectedProviderDiscovery: false, transports: { [base.id]: http() } });
const queryClient = new QueryClient();

<DynamicContextProvider settings={{ environmentId: "ENV_ID", walletConnectors: [EthereumWalletConnectors, SolanaWalletConnectors] }}>
  <WagmiProvider config={wagmiConfig}>
    <QueryClientProvider client={queryClient}>
      <DynamicWagmiConnector>{/* your app: useExecuteQuote() */}</DynamicWagmiConnector>
    </QueryClientProvider>
  </WagmiProvider>
</DynamicContextProvider>;

For Solana, Dynamic's signer has signTransaction and signAndSendTransaction (it resolves { signature }), which is the shape fromInjectedProvider expects:

TypeScript
import { useDynamicContext } from "@dynamic-labs/sdk-react-core";
import { isSolanaWallet } from "@dynamic-labs/solana";
import { executeSolanaQuote, fromInjectedProvider } from "@layerhop/solana";

const { primaryWallet } = useDynamicContext();
if (primaryWallet && isSolanaWallet(primaryWallet)) {
  const signer = await primaryWallet.getSigner();
  await executeSolanaQuote(fromInjectedProvider(signer), { quote: best, client: lh, connection });
}

thirdweb#

thirdweb v5 converts its wallets (in-app, smart and external) to a viem WalletClient. Pass that client to executeQuoteWithWalletClient.

TypeScript
import { viemAdapter } from "thirdweb/adapters/viem";
import { base } from "thirdweb/chains";
import { useActiveWallet } from "thirdweb/react";
import { executeQuoteWithWalletClient } from "@layerhop/wagmi/actions";

const wallet = useActiveWallet();
// `client` is your thirdweb client (createThirdwebClient)
const walletClient = viemAdapter.wallet.toViem({ client, chain: base, wallet: wallet! });
await executeQuoteWithWalletClient(walletClient, { quote: best, client: lh });

executeQuoteWithWalletClient works with any viem WalletClient, including createWalletClient({ transport: custom(provider) }) over an EIP-1193 provider. Reads such as the allowance and receipts go through the wallet's provider. Pass publicClient to use your own RPC instead.

Plain @solana/wallet-adapter#

TypeScript
import { useConnection, useWallet } from "@solana/wallet-adapter-react";
import { executeSolanaQuote, fromWalletAdapter } from "@layerhop/solana";

const wallet = useWallet();
const { connection } = useConnection();
const sent = await executeSolanaQuote(fromWalletAdapter(wallet), { quote: best, client: lh, connection });
await sent.wait();

executeSolanaQuote picks the send mode from the transaction itself:

  • submitViaApi: the wallet only signs and the signed bytes go to POST /v1/orders/submit (see submit via API).
  • Co-signed: the provider already signed it. The wallet only signs, and your connection broadcasts the exact bytes.
  • Plain: the wallet signs and sends.

For Wallet Standard wallets, use fromWalletStandard(wallet, account).

Last updated