Skip to content

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:

ts
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 ​

FieldTypeRequiredDescription
metadata.idstring✅Unique lowercase kebab-case ID: "my-wallet"
metadata.namestring✅Human-readable name shown in the modal
metadata.iconstring—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
adapterApiVersionstring—Set to WALLET_ADAPTER_API_VERSION from core
capabilitiesWalletCapabilities✅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.

ts
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.

ts
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.

ts
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.

ts
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.

ts
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.

ts
isAvailable(): boolean {
  return typeof window !== "undefined" && !!window.MyWallet;
}

Factory function pattern ​

Wrap your adapter in a factory so callers can pass options:

ts
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:

CodeWhen to use
WALLET_NOT_FOUNDAdapter ID is not registered
WALLET_NOT_INSTALLED / WALLET_NOT_AVAILABLERequired wallet/provider is missing
CONNECTION_REJECTEDUser cancelled the connect prompt
SIGN_REJECTEDUser cancelled or rejected signing
REQUEST_TIMEOUTWallet didn't respond within a reasonable time
NETWORK_MISMATCHWallet returned a different network
UNSUPPORTED_METHODWallet cannot perform the requested operation
UNKNOWN_ERRORUnexpected error not covered above
ts
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:

ts
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:

ts
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.

→ Full guide: Building Adapters with AI

Activate the skill at the start of your Claude Code session:

/xrpl-wallet-kit-adapter-developer

Then 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.

Released under the MIT License.