Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.usewraith.xyz/llms.txt

Use this file to discover all available pages before exploring further.

Chain connectors are the abstraction layer between Wraith’s chain-agnostic agent core and blockchain-specific logic. Each connector implements a standard interface, allowing the same agent engine to operate across any supported chain.

Interface

Every chain connector implements ChainConnector:
interface ChainConnector {
  readonly chain: string;
  readonly nativeAsset: string;
  readonly addressFormat: "evm" | "stellar" | "solana" | "ckb";

  deriveKeys(seed: Uint8Array): Promise<DerivedKeys>;

  sendPayment(params: SendPaymentParams): Promise<TxResult>;
  scanPayments(stealthKeys: ChainStealthKeys): Promise<DetectedPayment[]>;
  getBalance(address: string): Promise<ChainBalance>;
  withdraw(params: WithdrawParams): Promise<TxResult>;
  withdrawAll(params: WithdrawAllParams): Promise<WithdrawAllResult>;

  registerName(name: string, stealthKeys: ChainStealthKeys): Promise<TxResult>;
  resolveName(name: string): Promise<ResolvedName | null>;

  fundWallet(address: string): Promise<TxResult>;

  getExplorerUrl(type: "tx" | "address", value: string): string;
}

Supporting Types

interface DerivedKeys {
  address: string;
  stealthKeys: ChainStealthKeys;
  metaAddress: string;
}

interface ChainStealthKeys {
  [key: string]: unknown;  // opaque to the core — each chain stores what it needs
}

interface SendPaymentParams {
  senderAddress: string;
  senderStealthKeys: ChainStealthKeys;
  recipientMetaAddress: string;
  amount: string;
  asset?: string;
}

interface WithdrawParams {
  stealthKeys: ChainStealthKeys;
  from: string;
  to: string;
  amount?: string;  // undefined = withdraw max
}

interface DetectedPayment {
  stealthAddress: string;
  balance: string;
  ephemeralPubKey: string;
}

interface ChainBalance {
  native: string;
  tokens: Record<string, string>;
}

interface TxResult {
  txHash: string;
  txLink: string;
}

EVM Connector

A single EVMConnector class covers all EVM-compatible chains. Different chains use different configuration — same code.

Configuration

interface EVMConnectorConfig {
  chainId: number;
  rpcUrl: string;
  explorerUrl: string;
  contracts: {
    announcer: `0x${string}`;
    registry: `0x${string}`;
    sender: `0x${string}`;
    names: `0x${string}`;
  };
  subgraphUrl?: string;
  faucetUrl?: string;
  tokens?: Record<string, { address: string; decimals: number }>;
}

How Methods Map to SDK Primitives

Interface MethodImplementation
deriveKeys(seed)SHA-256 seed -> privateKeyToAccount -> sign message -> deriveStealthKeys(sig) -> encodeStealthMetaAddress
sendPaymentdecodeStealthMetaAddress -> generateStealthAddress -> writeContract(WraithSender, "sendETH")
scanPaymentsQuery subgraph for Announcement events -> scanAnnouncements(events, ...) -> fetch balances
getBalancepublicClient.getBalance + readContract(erc20, balanceOf) per token
withdrawderiveStealthPrivateKey -> privateKeyToAccount -> sendTransaction
registerNamesignNameRegistration(name, metaBytes, spendingKey) -> writeContract(WraithNames, "register")
resolveNamereadContract(WraithNames, "resolve", [name]) -> decode meta-address
fundWalletPOST to faucet API (testnet)

Adding a New EVM Chain

No code changes required. Register a new chain with its config:
import { Chain } from "@wraith-protocol/sdk";

// Horizen Testnet
chainRegistry.register(Chain.Horizen, new EVMConnector({
  chainId: 2651420,
  rpcUrl: "https://horizen-testnet.rpc.caldera.xyz/http",
  explorerUrl: "https://horizen-testnet.explorer.caldera.xyz",
  contracts: {
    announcer: "0x8AE65c05E7eb48B9bA652781Bc0a3DBA09A484F3",
    registry: "0x953E6cEdcdfAe321796e7637d33653F6Ce05c527",
    sender: "0x226C5eb4e139D9fa01cc09eA318638b090b12095",
    names: "0x3d46f709a99A3910f52bD292211Eb5D557F882D6",
  },
  subgraphUrl: "https://api.goldsky.com/api/public/...",
  tokens: {
    ETH: { address: "native", decimals: 18 },
    ZEN: { address: "0x4b36cb6E...", decimals: 18 },
    USDC: { address: "0x01c7AEb2...", decimals: 6 },
  },
}));

// Ethereum Mainnet — same connector, different config
chainRegistry.register(Chain.Ethereum, new EVMConnector({
  chainId: 1,
  rpcUrl: "https://eth-mainnet.g.alchemy.com/v2/...",
  explorerUrl: "https://etherscan.io",
  contracts: {
    announcer: "0x...",
    registry: "0x...",
    sender: "0x...",
    names: "0x...",
  },
}));
Requirements for each new EVM chain:
  1. Deploy the 4 Solidity contracts (Announcer, Registry, Sender, Names)
  2. Set up a subgraph to index Announcement events
  3. Add config to the registry

Stellar Connector

How Methods Map to SDK Primitives

Interface MethodImplementation
deriveKeys(seed)SHA-256 seed -> ed25519 seed -> sign message -> deriveStealthKeys(sig) -> encodeStealthMetaAddress
sendPaymentdecodeStealthMetaAddress -> generateStealthAddress -> Operation.createAccount -> Soroban announcer
scanPaymentsFetch Soroban events -> scanAnnouncements(events, ...) -> fetch balances from Horizon
getBalanceGET /accounts/{key} from Horizon
withdrawderiveStealthPrivateScalar -> build payment tx -> signStellarTransaction -> submit to Horizon
registerNameCall Soroban WraithNames register(name, metaAddress)
resolveNameSimulate Soroban WraithNames resolve(name)
fundWalletStellar Friendbot GET /friendbot?addr={key}

Stellar-Specific Considerations

  • Account creation: Stellar requires accounts to exist with a minimum balance (1 XLM). Sending to a new stealth address uses Operation.createAccount, not Operation.payment.
  • Signing: Stealth private keys are derived scalars. Must use signWithScalar() from the SDK — can’t use Keypair.fromRawEd25519Seed().
  • Events: Soroban contract events are fetched via sorobanServer.getEvents(), not a subgraph.

Configuration

const connector = new StellarConnector({
  networkPassphrase: Networks.TESTNET,
  horizonUrl: "https://horizon-testnet.stellar.org",
  sorobanUrl: "https://soroban-testnet.stellar.org",
  contracts: {
    announcer: "CCJLJ2QRBJAAKIG6ELNQVXLLWMKKWVN5O2FKWUETHZGMPAD4MHK7WVWL",
    names: "CC...",
  },
});

Solana Connector

Handles Solana using ed25519 and Anchor programs.

How Methods Map to SDK Primitives

Interface MethodImplementation
deriveKeys(seed)SHA-256 seed -> ed25519 seed -> sign message -> deriveStealthKeys(sig) -> encodeStealthMetaAddress
sendPaymentdecodeStealthMetaAddress -> generateStealthAddress -> Anchor instruction wraith_sender.send_sol (transfer + announce atomically)
scanPaymentsFetch announcer program transaction logs -> parse Anchor events -> scanAnnouncements(events, ...) -> fetch balances
getBalanceconnection.getBalance(address) for SOL, connection.getTokenAccountsByOwner() for SPL tokens
withdrawderiveStealthPrivateScalar -> signWithScalar to sign SystemProgram.transfer -> sendRawTransaction
registerNameAnchor instruction wraith_names.register with name + 64-byte meta-address
resolveNameDerive PDA from ["name", nameBytes] -> fetch and decode NameRecord account
fundWalletconnection.requestAirdrop(address, lamports) (devnet)

Solana-Specific Considerations

  • No account deployment: An ed25519 public key is a valid Solana address. Send SOL directly — no createAccount needed.
  • Rent exemption: Accounts need ~0.00089 SOL minimum. When withdrawing all, send balance - 5000 lamports (tx fee).
  • SPL tokens: Use associated token accounts (ATAs). The ATA must be created for the stealth address before transferring SPL tokens.
  • Signing: Uses signWithScalar() from the SDK — stealth scalars can’t be used with standard Keypair.
  • Events: Anchor emit!() writes to program logs. Parse via transaction history, not subgraph.

Configuration

interface SolanaConnectorConfig {
  rpcUrl: string;
  explorerUrl: string;
  contracts: {
    announcer: string;  // program ID (base58)
    sender: string;
    names: string;
  };
  cluster: "devnet" | "testnet" | "mainnet-beta";
}
import { Chain } from "@wraith-protocol/sdk";

chainRegistry.register(Chain.Solana, new SolanaConnector({
  rpcUrl: "https://api.devnet.solana.com",
  explorerUrl: "https://explorer.solana.com",
  contracts: {
    announcer: "...",  // deployed program ID
    sender: "...",
    names: "...",
  },
  cluster: "devnet",
}));

CKB Connector

Handles Nervos CKB using secp256k1 and the UTXO-based Cell model. CKB is architecturally unique: there is no separate announcer contract. Cells are the announcements — the ephemeral public key and stealth address hash are embedded directly in the Cell’s lock script args.

How Methods Map to SDK Primitives

Interface MethodImplementation
deriveKeys(seed)SHA-256 seed -> privateKeyToAccount -> sign message -> deriveStealthKeys(sig) -> encodeStealthMetaAddress
sendPaymentdecodeStealthMetaAddress -> generateStealthAddress -> create Cell with stealth-lock (args = ephemeral_pub || blake160)
scanPaymentsget_cells RPC filtered by stealth-lock code hash -> scanStealthCells(cells, ...) -> return matched Cells with balances
getBalanceSum capacity of all matched stealth Cells
withdrawderiveStealthPrivateKey -> sign transaction consuming stealth Cell -> create destination Cell
registerNamebuildRegisterName({ name, spendingPubKey, viewingPubKey }) -> create Cell with wraith-names-type type script, lock = owner
resolveNamebuildResolveName({ name }) -> get_cells RPC filtered by type script -> metaAddressFromNameData(data)
fundWalletCKB testnet faucet

CKB-Specific Considerations

  • UTXO model: CKB uses Cells (UTXOs), not accounts. Sending creates a Cell; withdrawing consumes it. There’s no balance to query on an address — you query live Cells.
  • No separate announcer: The Cell’s lock script args contain the ephemeral public key. No event infrastructure needed.
  • Minimum capacity: A stealth-lock Cell requires at least 61 CKB due to the 53-byte args field.
  • blake160: Address hashing uses blake2b with "ckb-default-hash" personalization, truncated to 20 bytes.
  • SHA-256 for shared secret: Unlike EVM (keccak256), CKB uses SHA-256 for hashing the ECDH shared secret.
  • No view tags: Every Cell is fully checked. The get_cells RPC pre-filters by code hash, keeping the scan set small.
  • Name ownership via lock script: Unlike EVM/Solana where names are owned by the spending key, CKB name ownership is determined by the Cell’s lock script. Whoever can spend the Cell controls the name.

Configuration

interface CKBConnectorConfig {
  rpcUrl: string;
  explorerUrl: string;
  contracts: {
    stealthLockCodeHash: string;
    namesTypeCodeHash: string;
  };
  cellDeps: {
    stealthLock: { txHash: string; index: number };
    namesType: { txHash: string; index: number };
  };
  network: "testnet" | "mainnet";
}
import { Chain } from "@wraith-protocol/sdk";

chainRegistry.register(Chain.CKB, new CKBConnector({
  rpcUrl: "https://testnet.ckbapp.dev",
  explorerUrl: "https://pudge.explorer.nervos.org",
  contracts: {
    stealthLockCodeHash: "0x...",
    namesTypeCodeHash: "0xc133817d433f72ea16a2404adaf961524e9572c8378829a21968710d6182e20d",
  },
  cellDeps: {
    stealthLock: { txHash: "0x...", index: 0 },
    namesType: { txHash: "0x9acd640d35eadd893b358dddd415f4061fe81cb249e8ace51a866fee314141b8", index: 0 },
  },
  network: "testnet",
}));

Chain Registry

The TEE server maintains a registry of available chain connectors:
class ChainRegistry {
  private connectors = new Map<string, ChainConnector>();

  register(chain: string, connector: ChainConnector): void {
    this.connectors.set(chain, connector);
  }

  get(chain: string): ChainConnector {
    const c = this.connectors.get(chain);
    if (!c) throw new Error(`Unsupported chain: "${chain}". Available: ${this.supportedChains().join(", ")}`);
    return c;
  }

  supportedChains(): string[] {
    return Array.from(this.connectors.keys());
  }
}

Usage in Agent Service

async sendPayment(agentId: string, recipient: string, amount: string) {
  const agent = await this.db.agents.findOneBy({ id: agentId });
  const connector = this.chainRegistry.get(agent.chain);
  const stealthKeys = await this.tee.deriveAgentStealthKeys(agentId, agent.chain);
  return connector.sendPayment({
    senderAddress: agent.address,
    senderStealthKeys: stealthKeys,
    recipientMetaAddress: recipient,
    amount,
  });
}

Adding a New Chain Family

To add support for a completely new chain family (e.g., Solana):
  1. Create a connector class implementing ChainConnector
  2. Implement all methods using the chain’s SDK and cryptographic primitives
  3. Write the crypto module at @wraith-protocol/sdk/chains/solana
  4. Deploy stealth address contracts on the target chain
  5. Register the connector in the chain registry
The agent core, AI engine, storage, notifications, and scheduling work automatically — no changes needed.

Key Differences Between Chain Families

AspectEVMStellarSolanaCKB
Curvesecp256k1ed25519ed25519secp256k1
Address format0x... (20 bytes)G... (56 chars)Base58bech32m
Meta-address prefixst:eth:0xst:xlm:st:sol:st:ckb:
ContractsSoliditySoroban (Rust)Solana ProgramsRISC-V Lock Script
AnnouncementsEVM events / subgraphSoroban events / HorizonProgram log eventsEmbedded in Cell args
Native assetETHXLMSOLCKB
Account modelBalance-basedAccount must exist firstBalance-basedUTXO (Cell)