Skip to main content

Prerequisites

  • Sender wallet funded with native asset (ETH or SOL)
  • Recipient registered on the target chain (or pass their 66-byte meta-address directly)
  • ethereumWalletClient / ethereumProvider (EVM) or solanaWallet (Solana)

High-level: one method

import { parseEther } from "viem";

const result = await client.sendStealthPayment({
  chain: "ethereum",
  recipient: "0xRecipientEoa...",   // or meta-address hex, or Solana base58
  amount: parseEther("0.01"),
  announce: true,                   // default
  relay: false,                     // set true for cross-chain UAB
});

console.log({
  stealthAddress: result.stealthAddress,
  txHash: result.txHash,
  announceTxHash: result.announceTxHash,  // Ethereum only (separate tx)
  metaAddressHex: result.metaAddressHex,
});
Solana bundles transfer + announce in one transaction (unless the announcement is delayed, see below). Ethereum submits the transfer first, then the announce (two tx hashes).

Delayed announcement (anonymity set)

Set delayAnnouncement (ms) to decouple the transfer from the announcement and break the timing correlation between the two on-chain events:
const result = await client.sendStealthPayment({
  chain: "ethereum",
  recipient,
  amount: parseEther("0.01"),
  delayAnnouncement: 5 * 60_000,   // announce 5 minutes later
});
const announceTx = await result.announcePromise;  // keep the process alive until this resolves
See Anonymity utilities for dummy-announcement noise as well.

Cross-chain send

await client.sendStealthPayment({
  chain: "solana",
  recipient: recipientMetaOrPubkey,
  amount: 1_000_000n,  // lamports
  relay: true,
  batchId: 42,         // optional Wormhole nonce
});

Low-level: prepare + submit yourself

Use this when you need custom gas, batching, or UI control.
// 1. Resolve recipient
const { metaAddressHex } = await client.resolveRecipientMetaAddress(recipientEoa);
if (!metaAddressHex) throw new Error("Not registered");

// 2. Derive stealth material
const send = client.prepareStealthSend(metaAddressHex);
/*
  send.stealthAddress      -> fund this address
  send.ephemeralPublicKey  -> goes in announce
  send.ephemeralPrivateKey -> store if you need ghost/relay later
  send.metadata            -> view tag byte (+ PSR extension if applicable)
*/

// 3. Transfer
await walletClient.sendTransaction({
  to: send.stealthAddress,
  value: parseEther("0.01"),
  chain: sepolia,
});

// 4. Announce
const announce = client.buildAnnounceTransactionRequest(send);
await walletClient.sendTransaction({
  to: announce.to,
  data: announce.data,
  chain: sepolia,
});

Cross-chain relay (manual)

const relay = await client.buildAnnounceWithRelay("ethereum", send);
await walletClient.sendTransaction({
  to: relay.to,
  data: relay.data,
  value: relay.value,
  chain: sepolia,
});
On Solana, buildAnnounceWithRelay returns instructions + extra signers (Wormhole message keypair):
const relay = await client.buildAnnounceWithRelay("solana", send);
// relay.instructions, relay.signers: co-sign with solanaWallet

Sending tokens (ERC-20 / SPL)

Set token to send an ERC-20 (Ethereum) or SPL mint (Solana) instead of the native asset. amount is the token’s smallest unit (decimals-aware). The announcement is identical to a native send, so the recipient discovers it the same way.
// USDC on Ethereum (6 decimals)
await client.sendStealthPayment({
  chain: "ethereum",
  recipient,
  token: "0xUSDC...",
  amount: 25_000000n,   // 25 USDC
});

// SPL mint on Solana (sender pays the ~0.002 SOL ATA rent for the recipient)
await client.sendStealthPayment({
  chain: "solana",
  recipient,
  token: "EPjF...USDCmint",
  amount: 25_000000n,
});
A fresh stealth address holds the token but no native gas, so the recipient cannot move it without help. Either set gasDrop to also send a little native asset to the stealth address, or have the recipient withdraw via a gasless sweep.
await client.sendStealthPayment({
  chain: "ethereum",
  recipient,
  token: "0xUSDC...",
  amount: 25_000000n,
  gasDrop: parseEther("0.0005"),  // recipient can self-sweep later
});

Recipient resolution

recipient accepts every identity form client.resolveRecipient understands:
recipient valueBehavior
66-byte meta-address (0x + 132 hex, st:opq: prefix accepted)Validated and used directly
Ethereum 0x addressRegistry lookup on Ethereum
Solana base58 pubkeyRegistry lookup on Solana
ipfs://CIDDID document fetch (configure ipfs on the client)
*.eth nameENS com.opaque.meta text record (configure ens on the client)
See resolveRecipient for setup details.