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#
# 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/wagmiand@layerhop/solanaare on npm from 0.7.0:npm install @layerhop/wagmiornpm 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:
- It switches the wallet to the transaction's chain if needed.
- It reads the allowance. The approval call is added only when the allowance is short, and only for the exact amount (never unlimited).
- If there is more than one call and
wallet_getCapabilitiesreports atomic batching for the chain (atomic.statusissupportedorready), every call goes in onewallet_sendCallsrequest withatomicRequired: true. The user confirms once and the calls succeed or fail together. The returnedhashis the mined transaction's hash, read fromwallet_getCallsStatus. - 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.
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:
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 confirmedFor Solana with Reown AppKit, wrap the Solana walletProvider:
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.
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.
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:
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.
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:
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.
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#
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 toPOST /v1/orders/submit(see submit via API).- Co-signed: the provider already signed it. The wallet only signs, and your
connectionbroadcasts the exact bytes. - Plain: the wallet signs and sends.
For Wallet Standard wallets, use fromWalletStandard(wallet, account).
Related#
- React hooks:
useQuote,useSwapanduseOrderStatus, wallet-agnostic. - TypeScript SDK:
quote,buildTx,submit,reportSubmitted. - Approvals and non-custodial design.
Last updated