Writing a Custom Adapter
Every wallet in XRPL Wallet Kit is implemented as an adapter — a small object that wraps the wallet's native SDK behind a unified interface. If your target wallet isn't in the official list, you can write your own.
Adapter interface
An adapter must implement WalletAdapter from @xrpl-wallet-kit/core:
import type { WalletAdapter } from "@xrpl-wallet-kit/core";
import { WALLET_ADAPTER_API_VERSION } from "@xrpl-wallet-kit/core";
const myAdapter: WalletAdapter = {
adapterApiVersion: WALLET_ADAPTER_API_VERSION,
metadata: {
id: "my-wallet",
name: "My Wallet",
type: "extension",
icon: "https://my-wallet.io/logo.png",
},
capabilities: {
connect: true,
disconnect: true,
signTransaction: true,
details: {
supportedNetworks: ["mainnet", "testnet"],
transactionModes: ["sign-only"],
},
},
async connect(options) { /* ... */ },
async disconnect() { /* ... */ },
async signTransaction(request) { /* ... */ },
// Optional but recommended:
async signMessage(request) { /* ... */ },
async restoreSession(session) { /* ... */ },
isAvailable() { /* ... */ },
};Metadata fields
| Field | Type | Required | Description |
|---|---|---|---|
metadata.id | string | ✅ | Unique lowercase kebab-case ID: "my-wallet" |
metadata.name | string | ✅ | Human-readable name shown in the modal |
metadata.icon | string | — | URL or data URI for the wallet logo |
metadata.type | "extension" | "mobile" | "walletconnect" | "snap" | "hardware" | "embedded" | "web" | ✅ | Wallet integration type; web for hosted popup wallets |
adapterApiVersion | string | — | Set to WALLET_ADAPTER_API_VERSION from core |
capabilities | WalletCapabilities | ✅ | Methods and granular support the adapter actually implements |
Required methods
connect(options): Promise<ConnectResult>
Opens the wallet, prompts the user to choose an account, and returns the session.
import type { ConnectOptions, ConnectResult } from "@xrpl-wallet-kit/core";
import { WalletKitError, WalletKitErrorCode } from "@xrpl-wallet-kit/core";
async connect(options: ConnectOptions): Promise<ConnectResult> {
if (!window.MyWallet) {
throw new WalletKitError(
WalletKitErrorCode.WALLET_NOT_INSTALLED,
"My Wallet extension is not installed."
);
}
const response = await window.MyWallet.connect();
if (!response || !response.address) {
throw new WalletKitError(
WalletKitErrorCode.CONNECTION_REJECTED,
"User rejected the connection."
);
}
return {
account: {
address: response.address,
publicKey: response.publicKey, // include if available
},
raw: response,
};
}disconnect(): Promise<void>
Cleans up the wallet connection. Must not throw if already disconnected.
async disconnect(): Promise<void> {
try {
await window.MyWallet?.disconnect();
} catch {
// ignore — already disconnected
}
}signTransaction(request): Promise<SignTransactionResult>
Asks the user to sign a prepared transaction. Returns the signed blob.
import type { SignTransactionResult } from "@xrpl-wallet-kit/core";
async signTransaction(
request: SignTransactionRequest
): Promise<SignTransactionResult> {
const result = await window.MyWallet.signTransaction({ transaction: request.txJson });
if (!result || result.cancelled) {
throw new WalletKitError(WalletKitErrorCode.SIGN_REJECTED, "User cancelled signing.");
}
return {
txBlob: result.blob,
signed: true,
raw: result,
};
}Optional methods
signMessage(request): Promise<SignMessageResult>
Signs an arbitrary text message. Required if you want to use @xrpl-wallet-kit/auth.
import type { SignMessageResult } from "@xrpl-wallet-kit/core";
async signMessage(
request: SignMessageRequest
): Promise<SignMessageResult> {
const result = await window.MyWallet.signMessage({ message: request.message });
return {
proof: result.signature,
signature: result.signature, // hex compact signature
publicKey: result.publicKey, // hex public key (optional)
signatureKind: "signature", // "signature" | "signedTx"
};
}signatureKind
Use "signature" if the wallet returns a raw compact ECDSA/EdDSA signature.
Use "signedTx" if the wallet returns a full signed transaction blob.
The auth verifier branches automatically based on signatureKind.
restoreSession(session): Promise<ConnectResult | null>
Attempts to restore a previous session without user interaction. Called on page load.
async restoreSession(stored: WalletSession): Promise<ConnectResult | null> {
try {
const session = await window.MyWallet.getConnectedSession();
if (!session?.address) return null;
if (session.address !== stored.account.address) return null;
return { account: { ...stored.account, address: session.address }, session: stored };
} catch {
return null;
}
}isAvailable(): boolean
Returns true if the wallet is detectable in the current browser.
isAvailable(): boolean {
return typeof window !== "undefined" && !!window.MyWallet;
}Factory function pattern
Wrap your adapter in a factory so callers can pass options:
import type { WalletAdapter } from "@xrpl-wallet-kit/core";
import { WALLET_ADAPTER_API_VERSION } from "@xrpl-wallet-kit/core";
export interface MyWalletAdapterOptions {
clientId?: string;
network?: "mainnet" | "testnet";
}
export function createMyWalletAdapter(options: MyWalletAdapterOptions = {}): WalletAdapter {
return {
adapterApiVersion: WALLET_ADAPTER_API_VERSION,
metadata: {
id: "my-wallet",
name: "My Wallet",
icon: "https://my-wallet.io/logo.png",
type: "extension",
homepage: "https://my-wallet.io",
},
capabilities: { connect: true, disconnect: true, signTransaction: true },
async connect(connectOptions) {
// Read factory options.clientId and per-request connectOptions separately.
},
async disconnect() { /* ... */ },
async signTransaction(request) { /* ... */ },
isAvailable() {
return typeof window !== "undefined" && !!window.MyWallet;
},
};
}Error handling
Use WalletKitError with the correct code so the manager and UI can handle errors consistently:
| Code | When to use |
|---|---|
WALLET_NOT_FOUND | Adapter ID is not registered |
WALLET_NOT_INSTALLED / WALLET_NOT_AVAILABLE | Required wallet/provider is missing |
CONNECTION_REJECTED | User cancelled the connect prompt |
SIGN_REJECTED | User cancelled or rejected signing |
REQUEST_TIMEOUT | Wallet didn't respond within a reasonable time |
NETWORK_MISMATCH | Wallet returned a different network |
UNSUPPORTED_METHOD | Wallet cannot perform the requested operation |
UNKNOWN_ERROR | Unexpected error not covered above |
import { WalletKitError, WalletKitErrorCode } from "@xrpl-wallet-kit/core";
throw new WalletKitError(
WalletKitErrorCode.SIGN_REJECTED,
"User cancelled the sign request."
);Register the adapter
Pass an instance to WalletManager:
import { WalletManager } from "@xrpl-wallet-kit/core";
import { createMyWalletAdapter } from "./my-wallet-adapter";
const manager = new WalletManager({
adapters: [
createMyWalletAdapter({ clientId: "..." }),
// ...other adapters
],
network: "mainnet",
});XRPL and Xahau Chain Support
Legacy adapters without a supportedChains declaration remain XRPL-only. Declare only chains and transaction types the provider can actually sign:
details: {
supportedChains: { families: ["xrpl", "xahau"] },
transactionModes: ["sign-only", "sign-and-submit"],
}This is a declaration, not a complete implementation. Non-XRPL requests require a registered protocol before connection. Use its prepared fields and signed blob verification; never run Xahau transactions through an XRPL-only codec or drop NetworkID/HookParameters. Criteria in a chain filter are combined; empty arrays deny support, rather than meaning every network.
Return network context in connection results and network-change events. A chain-aware adapter emitting networkChanged without a network is disconnected by the manager. Xahau sessions require explicit reconnect after reload. See Xahau setup and the skill's references/chain-support.md before implementing this path.
Native login is a separate optional signIn() method/capability. Do not alias it to signMessage(); follow the native sign-in contract. Publish the adapter guide using the common skill reference references/adapter-docs.md.
Hard rules
Never include these in an adapter:
- No private keys, seeds, or secrets — adapters must never generate or store key material.
- No business logic —
signAuthPayload,setPrimary, domain lookup, identity verification, etc. belong in the app layer, not the adapter. - No DOM modals or framework code — adapters must be pure TypeScript with no UI dependencies.
- No hardcoded WalletConnect
projectId— always require caller injection for credentials.
Building with AI
Adapter developer skill
The project skill covers the adapter contract, XRPL/Xahau chain rules, capabilities, native sign-in, session safety and consumer documentation format. Use it with current provider documentation and review the generated implementation.
Activate the skill at the start of your Claude Code session:
/xrpl-wallet-kit-adapter-developerThen describe the wallet's API to the agent. It will scaffold the factory function, set capability flags correctly, map errors to WalletKitErrorCode, implement restoreSession() with passive-only rules, and generate a test scaffold — all following the adapter contract automatically.