From 51273d54864c132fc659299ededae3b91e7c6c58 Mon Sep 17 00:00:00 2001 From: Hallab Date: Mon, 24 Aug 2026 22:22:40 +0100 Subject: [PATCH 1/2] feat(wallet): add unified cross-chain wallet adapters --- examples/multichain-scan/.env.example | 8 ++- examples/multichain-scan/README.md | 63 ++++++++++------- examples/multichain-scan/index.ts | 73 ++++++++++++++----- examples/multichain-scan/package.json | 3 +- examples/multichain-scan/tsconfig.json | 16 +++++ src/chains/stellar/signer.ts | 17 ++++- src/index.ts | 20 ++++++ src/wallet/adapter.ts | 69 ++++++++++++++++++ src/wallet/adapters/freighter.ts | 60 ++++++++++++++++ src/wallet/adapters/solana.ts | 32 +++++++++ src/wallet/adapters/viem.ts | 40 +++++++++++ src/wallet/index.ts | 15 ++++ test/chains/stellar/signer.test.ts | 3 + test/wallet/adapter.test.ts | 97 ++++++++++++++++++++++++++ 14 files changed, 468 insertions(+), 48 deletions(-) create mode 100644 examples/multichain-scan/tsconfig.json create mode 100644 src/wallet/adapter.ts create mode 100644 src/wallet/adapters/freighter.ts create mode 100644 src/wallet/adapters/solana.ts create mode 100644 src/wallet/adapters/viem.ts create mode 100644 src/wallet/index.ts create mode 100644 test/wallet/adapter.test.ts diff --git a/examples/multichain-scan/.env.example b/examples/multichain-scan/.env.example index d5666e1..bf81843 100644 --- a/examples/multichain-scan/.env.example +++ b/examples/multichain-scan/.env.example @@ -1,6 +1,8 @@ -# Your secret key (64-byte hex string, 128 hex chars) -# Used to derive stealth keys for all chains. -SECRET_KEY="your-64-byte-hex-secret-key-here" +# Module exporting a Map named `walletAdapters`. +WALLET_REGISTRY_MODULE="./wallet-registry.ts" + +# Optional legacy signature for also scanning Solana and CKB. +SECRET_KEY="" # Optional: limit scan range (ISO timestamp or empty for full range) FROM_TIMESTAMP="" diff --git a/examples/multichain-scan/README.md b/examples/multichain-scan/README.md index e9db905..ace725f 100644 --- a/examples/multichain-scan/README.md +++ b/examples/multichain-scan/README.md @@ -1,34 +1,47 @@ -# Multichain Stealth Payment Scanner - -A CLI that scans for incoming stealth payments across all 4 supported chains in parallel — Stellar, EVM, Solana, and CKB. - -## How it works - -1. Derives stealth keys from a single secret key -2. Fetches announcements on all 4 chains simultaneously via `Promise.all` -3. Filters announcements owned by this wallet using each chain's scan function -4. Prints matched payments grouped by chain - -## Supported Chains +# Multichain Wallet Scanner + +A CLI that uses one `WalletAdapter` registry to request the chain-specific +derivation signatures and scan the Wraith Stellar and Horizen test networks. +An optional legacy signature can also enable the existing Solana and CKB scans. + +## Wallet registry + +Create `wallet-registry.ts` next to the example. The SDK adapters are structural: +the Freighter, viem, and Solana wallet packages remain optional and are never +imported by the SDK adapters themselves. + +```ts +import { + FreighterWalletAdapter, + ViemWalletAdapter, + type WalletAdapter, +} from '@wraith-protocol/sdk'; +import { createWalletClient, custom } from 'viem'; +import { horizenTestnet } from './your-chain-config'; +import * as freighter from '@stellar/freighter-api'; + +const evmClient = createWalletClient({ + chain: horizenTestnet, + transport: custom(window.ethereum), +}); + +export const walletAdapters = new Map([ + ['stellar', new FreighterWalletAdapter(freighter)], + ['evm', new ViemWalletAdapter(evmClient)], +]); +``` -| Chain | Fetch function | Scan function | Crypto | -| ------- | -------------------- | ------------------- | --------- | -| Stellar | `fetchAnnouncements` | `scanAnnouncements` | ed25519 | -| EVM | `fetchAnnouncements` | `scanAnnouncements` | secp256k1 | -| Solana | `fetchAnnouncements` | `scanAnnouncements` | ed25519 | -| CKB | `fetchStealthCells` | `scanStealthCells` | secp256k1 | +The same registry can include a `SolanaWalletAdapter` from an +`@solana/wallet-adapter` wallet when a Solana scan is needed. ## Usage ```bash -# 1. Copy and fill in the environment variables cp .env.example .env -# Edit .env with your SECRET_KEY - -# 2. Run the scanner +# Set WALLET_REGISTRY_MODULE to the registry module above. npm start ``` -## Output - -The script prints results per chain — total announcements found, matched payments, and details for each match including stealth address, ephemeral public key, and derived private key/scalar. +The CLI asks both wallets to sign their chain's fixed, non-transactional Wraith +message, derives the correct key shape through `deriveStealthKeysFromWallet`, +then scans Stellar testnet and Horizen testnet concurrently. diff --git a/examples/multichain-scan/index.ts b/examples/multichain-scan/index.ts index aa95bec..9ffc445 100644 --- a/examples/multichain-scan/index.ts +++ b/examples/multichain-scan/index.ts @@ -1,12 +1,18 @@ #!/usr/bin/env node -import { deriveStealthKeys as stellarDerive } from '@wraith-protocol/sdk/chains/stellar'; +import { + deriveStealthKeysFromWallet, + type EvmWalletAdapter, + type StellarWalletAdapter, + type WalletAdapter, +} from '@wraith-protocol/sdk'; import { fetchAnnouncementsStream as stellarFetch } from '@wraith-protocol/sdk/chains/stellar'; import { scanAnnouncements as stellarScan } from '@wraith-protocol/sdk/chains/stellar'; import { bytesToHex as stellarHex } from '@wraith-protocol/sdk/chains/stellar'; +import type { StealthKeys as StellarStealthKeys } from '@wraith-protocol/sdk/chains/stellar'; -import { deriveStealthKeys as evmDerive } from '@wraith-protocol/sdk/chains/evm'; import { fetchAnnouncements as evmFetch } from '@wraith-protocol/sdk/chains/evm'; import { scanAnnouncements as evmScan } from '@wraith-protocol/sdk/chains/evm'; +import type { StealthKeys as EvmStealthKeys } from '@wraith-protocol/sdk/chains/evm'; import { deriveStealthKeys as solanaDerive } from '@wraith-protocol/sdk/chains/solana'; import { fetchAnnouncements as solanaFetch } from '@wraith-protocol/sdk/chains/solana'; @@ -32,9 +38,7 @@ function hexToBytes(hex: string): Uint8Array { return new Uint8Array(hex.match(/.{1,2}/g)!.map((b) => parseInt(b, 16))); } -async function scanStellar(sigHex: string): Promise { - const sig = hexToBytes(sigHex); - const keys = stellarDerive(sig); +async function scanStellar(keys: StellarStealthKeys): Promise { const announcements = []; for await (const ann of stellarFetch('stellar')) { announcements.push(ann); @@ -56,9 +60,7 @@ async function scanStellar(sigHex: string): Promise { }; } -async function scanEvm(sigHex: string): Promise { - const sig = `0x${sigHex}` as `0x${string}`; - const keys = evmDerive(sig); +async function scanEvm(keys: EvmStealthKeys): Promise { const announcements = await evmFetch('horizen'); const matches = evmScan(announcements, keys.viewingKey, keys.spendingPubKey, keys.spendingKey); return { @@ -110,20 +112,27 @@ async function scanCkb(sigHex: string): Promise { async function main() { console.log('=== Wraith Multichain Scanner ===\n'); + const registryModule = getEnv('WALLET_REGISTRY_MODULE'); const secretKey = getEnv('SECRET_KEY'); - if (!secretKey) { - console.error('ERROR: Missing SECRET_KEY'); - console.error('Copy .env.example to .env and fill in the values.'); + if (!registryModule) { + console.error('ERROR: Missing WALLET_REGISTRY_MODULE'); + console.error('Export a WalletAdapter registry as shown in the README.'); process.exit(1); } - console.log(`Scanning all chains with key: ${secretKey.slice(0, 16)}...\n`); - const scanners = [ - scanStellar(secretKey), - scanEvm(secretKey), - scanSolana(secretKey), - scanCkb(secretKey), - ]; + const registry = await loadWalletRegistry(registryModule); + const stellarAdapter = requireAdapter(registry, 'stellar'); + const evmAdapter = requireAdapter(registry, 'evm'); + const [stellarKeys, evmKeys, stellarAddress, evmAddress] = await Promise.all([ + deriveStealthKeysFromWallet(stellarAdapter), + deriveStealthKeysFromWallet(evmAdapter), + stellarAdapter.getAddress(), + evmAdapter.getAddress(), + ]); + console.log(`Signed with Stellar ${stellarAddress} and EVM ${evmAddress}.\n`); + + const scanners = [scanStellar(stellarKeys), scanEvm(evmKeys)]; + if (secretKey) scanners.push(scanSolana(secretKey), scanCkb(secretKey)); const results = await Promise.allSettled(scanners); let totalMatches = 0; @@ -147,6 +156,34 @@ async function main() { console.log(`=== Done — ${totalMatches} total match(es) across all chains ===`); } +async function loadWalletRegistry(modulePath: string): Promise> { + const loaded = (await import(modulePath)) as { + walletAdapters?: Map; + default?: Map; + }; + const registry = loaded.walletAdapters ?? loaded.default; + if (!(registry instanceof Map)) { + throw new TypeError('The wallet registry module must export a Map as `walletAdapters`.'); + } + return registry; +} + +function requireAdapter( + registry: Map, + chain: 'stellar', +): StellarWalletAdapter; +function requireAdapter(registry: Map, chain: 'evm'): EvmWalletAdapter; +function requireAdapter( + registry: Map, + chain: 'stellar' | 'evm', +): StellarWalletAdapter | EvmWalletAdapter { + const adapter = registry.get(chain); + if (!adapter || adapter.chain !== chain) { + throw new Error(`Wallet registry is missing a ${chain} adapter.`); + } + return adapter as StellarWalletAdapter | EvmWalletAdapter; +} + main().catch((err) => { console.error('Fatal error:', err); process.exit(1); diff --git a/examples/multichain-scan/package.json b/examples/multichain-scan/package.json index 71e66e7..01c0b1e 100644 --- a/examples/multichain-scan/package.json +++ b/examples/multichain-scan/package.json @@ -4,7 +4,8 @@ "type": "module", "main": "index.ts", "scripts": { - "start": "npx tsx index.ts" + "start": "npx tsx index.ts", + "typecheck": "tsc --noEmit" }, "dependencies": { "@wraith-protocol/sdk": "file:../.." diff --git a/examples/multichain-scan/tsconfig.json b/examples/multichain-scan/tsconfig.json new file mode 100644 index 0000000..fec30a6 --- /dev/null +++ b/examples/multichain-scan/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "skipLibCheck": true, + "types": ["node"], + "baseUrl": ".", + "paths": { + "@wraith-protocol/sdk": ["../../src/index.ts"], + "@wraith-protocol/sdk/chains/*": ["../../src/chains/*/index.ts"] + } + }, + "include": ["index.ts", "wallet-registry.ts"] +} diff --git a/src/chains/stellar/signer.ts b/src/chains/stellar/signer.ts index a2e8a68..150fd3d 100644 --- a/src/chains/stellar/signer.ts +++ b/src/chains/stellar/signer.ts @@ -1,5 +1,6 @@ import { sha256 } from '@noble/hashes/sha256'; import { KeyDerivationFailedError } from '../../errors'; +import type { StellarWalletAdapter } from '../../wallet/adapter'; /** * Minimal signing capability required to derive Stellar stealth keys. @@ -84,6 +85,8 @@ export interface WebAuthnPasskeyStealthSignerOptions { credentials?: WebAuthnCredentialsContainer; /** Relying party ID passed through to `navigator.credentials.get()`. */ rpId?: string; + /** Stellar smart-account address controlled by this passkey. */ + address?: string; } /** @@ -110,14 +113,17 @@ export interface WebAuthnPasskeyStealthSignerOptions { * * @see {@link StellarStealthSigner} */ -export class WebAuthnPasskeyStealthSigner implements StellarStealthSigner { +export class WebAuthnPasskeyStealthSigner implements StellarStealthSigner, StellarWalletAdapter { + readonly chain = 'stellar' as const; private readonly credentialId: Uint8Array; private readonly credentials: WebAuthnCredentialsContainer; private readonly rpId?: string; + private readonly address?: string; constructor(options: WebAuthnPasskeyStealthSignerOptions) { this.credentialId = options.credentialId; this.rpId = options.rpId; + this.address = options.address; const globalCredentials = (globalThis as { navigator?: { credentials?: unknown } }).navigator ?.credentials as WebAuthnCredentialsContainer | undefined; @@ -158,6 +164,15 @@ export class WebAuthnPasskeyStealthSigner implements StellarStealthSigner { combined.set(new Uint8Array(results.second).subarray(0, 32), 32); return combined; } + + async getAddress(): Promise { + if (!this.address) { + throw new KeyDerivationFailedError( + 'No Stellar smart-account address was configured for this passkey signer.', + ); + } + return this.address; + } } function derivePRFSalt(message: Uint8Array, label: 'first' | 'second'): Uint8Array { diff --git a/src/index.ts b/src/index.ts index 75a73eb..a37af3b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,6 +5,26 @@ export { Chain } from './agent/types'; */ export { installReactNativePolyfills } from './compat'; export { scanAll } from './scanner/unified'; +export { + deriveStealthKeysFromWallet, + FreighterWalletAdapter, + createFreighterWalletAdapter, + ViemWalletAdapter, + createViemWalletAdapter, + SolanaWalletAdapter, + createSolanaWalletAdapter, +} from './wallet'; +export type { + WalletAdapterChain, + BaseWalletAdapter, + StellarWalletAdapter, + EvmWalletAdapter, + SolanaChainWalletAdapter, + WalletAdapter, + FreighterWalletApi, + ViemWalletClient, + SolanaWalletAdapterLike, +} from './wallet'; export type { WraithConfig, AgentConfig, diff --git a/src/wallet/adapter.ts b/src/wallet/adapter.ts new file mode 100644 index 0000000..099955a --- /dev/null +++ b/src/wallet/adapter.ts @@ -0,0 +1,69 @@ +import { STEALTH_SIGNING_MESSAGE as EVM_SIGNING_MESSAGE } from '../chains/evm/constants'; +import { deriveStealthKeys as deriveEvmStealthKeys } from '../chains/evm/keys'; +import type { HexString, StealthKeys as EvmStealthKeys } from '../chains/evm/types'; +import { STEALTH_SIGNING_MESSAGE as SOLANA_SIGNING_MESSAGE } from '../chains/solana/constants'; +import { deriveStealthKeys as deriveSolanaStealthKeys } from '../chains/solana/keys'; +import type { StealthKeys as SolanaStealthKeys } from '../chains/solana/types'; +import { STEALTH_SIGNING_MESSAGE as STELLAR_SIGNING_MESSAGE } from '../chains/stellar/constants'; +import { deriveStealthKeys as deriveStellarStealthKeys } from '../chains/stellar/keys'; +import type { StealthKeys as StellarStealthKeys } from '../chains/stellar/types'; + +/** Chains supported by the unified wallet adapter. */ +export type WalletAdapterChain = 'stellar' | 'evm' | 'solana'; + +/** Common wallet capabilities used by cross-chain applications. */ +export interface BaseWalletAdapter { + readonly chain: TChain; + signMessage(message: Uint8Array): Promise; + getAddress(): Promise; +} + +/** Wallet adapter for Stellar-compatible ed25519 wallets. */ +export type StellarWalletAdapter = BaseWalletAdapter<'stellar', Uint8Array>; + +/** Wallet adapter for EVM wallets returning a 65-byte hex signature. */ +export type EvmWalletAdapter = BaseWalletAdapter<'evm', HexString>; + +/** Wallet adapter for Solana-compatible ed25519 wallets. */ +export type SolanaChainWalletAdapter = BaseWalletAdapter<'solana', Uint8Array>; + +/** Discriminated union accepted by the unified wallet registry and derivation router. */ +export type WalletAdapter = StellarWalletAdapter | EvmWalletAdapter | SolanaChainWalletAdapter; + +export function deriveStealthKeysFromWallet( + adapter: StellarWalletAdapter, +): Promise; +export function deriveStealthKeysFromWallet(adapter: EvmWalletAdapter): Promise; +export function deriveStealthKeysFromWallet( + adapter: SolanaChainWalletAdapter, +): Promise; +export function deriveStealthKeysFromWallet( + adapter: WalletAdapter, +): Promise; + +/** + * Signs the chain-specific Wraith derivation message and routes the signature + * through that chain's existing stealth-key derivation implementation. + */ +export async function deriveStealthKeysFromWallet( + adapter: WalletAdapter, +): Promise { + switch (adapter.chain) { + case 'stellar': { + const signature = await adapter.signMessage(encode(STELLAR_SIGNING_MESSAGE)); + return deriveStellarStealthKeys(signature); + } + case 'evm': { + const signature = await adapter.signMessage(encode(EVM_SIGNING_MESSAGE)); + return deriveEvmStealthKeys(signature); + } + case 'solana': { + const signature = await adapter.signMessage(encode(SOLANA_SIGNING_MESSAGE)); + return deriveSolanaStealthKeys(signature); + } + } +} + +function encode(message: string): Uint8Array { + return new TextEncoder().encode(message); +} diff --git a/src/wallet/adapters/freighter.ts b/src/wallet/adapters/freighter.ts new file mode 100644 index 0000000..f03b04e --- /dev/null +++ b/src/wallet/adapters/freighter.ts @@ -0,0 +1,60 @@ +import type { StellarWalletAdapter } from '../adapter'; + +/** Minimal Freighter API surface used by the adapter. */ +export interface FreighterWalletApi { + signMessage(message: string): Promise<{ + signedMessage?: Uint8Array | string; + error?: string | { message?: string }; + }>; + getAddress(): Promise; +} + +/** Freighter reference adapter with no dependency on `@stellar/freighter-api`. */ +export class FreighterWalletAdapter implements StellarWalletAdapter { + readonly chain = 'stellar' as const; + + constructor(private readonly wallet: FreighterWalletApi) { + if ( + !wallet || + typeof wallet.signMessage !== 'function' || + typeof wallet.getAddress !== 'function' + ) { + throw new TypeError( + 'A Freighter-compatible wallet with signMessage and getAddress is required.', + ); + } + } + + async signMessage(message: Uint8Array): Promise { + const result = await this.wallet.signMessage(new TextDecoder().decode(message)); + if (!result.signedMessage) + throw new Error(readError(result.error, 'Freighter did not sign the message.')); + return typeof result.signedMessage === 'string' + ? decodeBase64(result.signedMessage) + : result.signedMessage; + } + + async getAddress(): Promise { + const result = await this.wallet.getAddress(); + if (typeof result === 'string') return result; + if (!result.address) throw new Error(readError(result.error, 'Freighter is not connected.')); + return result.address; + } +} + +/** Creates a unified adapter from an installed Freighter API object. */ +export function createFreighterWalletAdapter(wallet: FreighterWalletApi): FreighterWalletAdapter { + return new FreighterWalletAdapter(wallet); +} + +function decodeBase64(value: string): Uint8Array { + if (typeof globalThis.atob === 'function') { + return Uint8Array.from(globalThis.atob(value), (character) => character.charCodeAt(0)); + } + return new Uint8Array(Buffer.from(value, 'base64')); +} + +function readError(error: string | { message?: string } | undefined, fallback: string): string { + if (typeof error === 'string') return error; + return error?.message ?? fallback; +} diff --git a/src/wallet/adapters/solana.ts b/src/wallet/adapters/solana.ts new file mode 100644 index 0000000..2d78969 --- /dev/null +++ b/src/wallet/adapters/solana.ts @@ -0,0 +1,32 @@ +import type { SolanaChainWalletAdapter } from '../adapter'; + +/** Structural subset exposed by `@solana/wallet-adapter` wallets. */ +export interface SolanaWalletAdapterLike { + publicKey: { toBase58(): string } | null; + signMessage?: (message: Uint8Array) => Promise; +} + +/** `@solana/wallet-adapter` reference adapter with no package import. */ +export class SolanaWalletAdapter implements SolanaChainWalletAdapter { + readonly chain = 'solana' as const; + + constructor(private readonly wallet: SolanaWalletAdapterLike) { + if (!wallet || typeof wallet.signMessage !== 'function') { + throw new TypeError('A Solana wallet-adapter wallet with signMessage is required.'); + } + } + + async signMessage(message: Uint8Array): Promise { + return this.wallet.signMessage!(message); + } + + async getAddress(): Promise { + if (!this.wallet.publicKey) throw new Error('The Solana wallet is not connected.'); + return this.wallet.publicKey.toBase58(); + } +} + +/** Creates a unified adapter from an `@solana/wallet-adapter` wallet. */ +export function createSolanaWalletAdapter(wallet: SolanaWalletAdapterLike): SolanaWalletAdapter { + return new SolanaWalletAdapter(wallet); +} diff --git a/src/wallet/adapters/viem.ts b/src/wallet/adapters/viem.ts new file mode 100644 index 0000000..5388997 --- /dev/null +++ b/src/wallet/adapters/viem.ts @@ -0,0 +1,40 @@ +import type { HexString } from '../../chains/evm/types'; +import type { EvmWalletAdapter } from '../adapter'; + +/** Structural subset of a viem WalletClient; viem is not imported at runtime. */ +export interface ViemWalletClient { + account?: { address: string } | null; + getAddresses?: () => Promise; + signMessage(args: { + account?: { address: string } | string; + message: { raw: Uint8Array }; + }): Promise; +} + +/** viem WalletClient reference adapter. */ +export class ViemWalletAdapter implements EvmWalletAdapter { + readonly chain = 'evm' as const; + + constructor(private readonly client: ViemWalletClient) { + if (!client || typeof client.signMessage !== 'function') { + throw new TypeError('A viem-compatible WalletClient with signMessage is required.'); + } + } + + async signMessage(message: Uint8Array): Promise { + const account = this.client.account ?? (await this.getAddress()); + return this.client.signMessage({ account, message: { raw: message } }); + } + + async getAddress(): Promise { + if (this.client.account?.address) return this.client.account.address; + const addresses = await this.client.getAddresses?.(); + if (!addresses?.[0]) throw new Error('The viem wallet client has no connected account.'); + return addresses[0]; + } +} + +/** Creates a unified adapter from a viem WalletClient-shaped object. */ +export function createViemWalletAdapter(client: ViemWalletClient): ViemWalletAdapter { + return new ViemWalletAdapter(client); +} diff --git a/src/wallet/index.ts b/src/wallet/index.ts new file mode 100644 index 0000000..41e8957 --- /dev/null +++ b/src/wallet/index.ts @@ -0,0 +1,15 @@ +export { deriveStealthKeysFromWallet } from './adapter'; +export type { + WalletAdapterChain, + BaseWalletAdapter, + StellarWalletAdapter, + EvmWalletAdapter, + SolanaChainWalletAdapter, + WalletAdapter, +} from './adapter'; +export { FreighterWalletAdapter, createFreighterWalletAdapter } from './adapters/freighter'; +export type { FreighterWalletApi } from './adapters/freighter'; +export { ViemWalletAdapter, createViemWalletAdapter } from './adapters/viem'; +export type { ViemWalletClient } from './adapters/viem'; +export { SolanaWalletAdapter, createSolanaWalletAdapter } from './adapters/solana'; +export type { SolanaWalletAdapterLike } from './adapters/solana'; diff --git a/test/chains/stellar/signer.test.ts b/test/chains/stellar/signer.test.ts index 332ced1..8f8affd 100644 --- a/test/chains/stellar/signer.test.ts +++ b/test/chains/stellar/signer.test.ts @@ -70,6 +70,7 @@ describe('WebAuthnPasskeyStealthSigner', () => { const session1 = new WebAuthnPasskeyStealthSigner({ credentialId, credentials: mockPasskeyCredentials(credentialId), + address: 'GSMARTACCOUNT', }); const session2 = new WebAuthnPasskeyStealthSigner({ credentialId, @@ -83,6 +84,8 @@ describe('WebAuthnPasskeyStealthSigner', () => { expect(keys1.viewingKey).toEqual(keys2.viewingKey); expect(keys1.spendingPubKey).toEqual(keys2.spendingPubKey); expect(keys1.viewingPubKey).toEqual(keys2.viewingPubKey); + expect(session1.chain).toBe('stellar'); + expect(await session1.getAddress()).toBe('GSMARTACCOUNT'); }); test('different credentials derive different keys', async () => { diff --git a/test/wallet/adapter.test.ts b/test/wallet/adapter.test.ts new file mode 100644 index 0000000..ea92b97 --- /dev/null +++ b/test/wallet/adapter.test.ts @@ -0,0 +1,97 @@ +import { describe, expect, test, vi } from 'vitest'; +import { deriveStealthKeys as deriveEvmKeys } from '../../src/chains/evm/keys'; +import { STEALTH_SIGNING_MESSAGE as EVM_MESSAGE } from '../../src/chains/evm/constants'; +import { deriveStealthKeys as deriveSolanaKeys } from '../../src/chains/solana/keys'; +import { STEALTH_SIGNING_MESSAGE as SOLANA_MESSAGE } from '../../src/chains/solana/constants'; +import { deriveStealthKeys as deriveStellarKeys } from '../../src/chains/stellar/keys'; +import { STEALTH_SIGNING_MESSAGE as STELLAR_MESSAGE } from '../../src/chains/stellar/constants'; +import { deriveStealthKeysFromWallet } from '../../src/wallet/adapter'; +import { FreighterWalletAdapter } from '../../src/wallet/adapters/freighter'; +import { SolanaWalletAdapter } from '../../src/wallet/adapters/solana'; +import { ViemWalletAdapter } from '../../src/wallet/adapters/viem'; + +describe('deriveStealthKeysFromWallet', () => { + test('routes Stellar wallets with the Stellar derivation message', async () => { + const signature = new Uint8Array(64).fill(0x11); + const signMessage = vi.fn(async () => signature); + const keys = await deriveStealthKeysFromWallet({ + chain: 'stellar', + getAddress: async () => 'GABC', + signMessage, + }); + + expect(new TextDecoder().decode(signMessage.mock.calls[0][0])).toBe(STELLAR_MESSAGE); + expect(keys).toEqual(deriveStellarKeys(signature)); + }); + + test('routes EVM wallets with the EVM derivation message', async () => { + const signature = `0x${'22'.repeat(65)}` as `0x${string}`; + const signMessage = vi.fn(async () => signature); + const keys = await deriveStealthKeysFromWallet({ + chain: 'evm', + getAddress: async () => '0x1234', + signMessage, + }); + + expect(new TextDecoder().decode(signMessage.mock.calls[0][0])).toBe(EVM_MESSAGE); + expect(keys).toEqual(deriveEvmKeys(signature)); + }); + + test('routes Solana wallets with the Solana derivation message', async () => { + const signature = new Uint8Array(64).fill(0x33); + const signMessage = vi.fn(async () => signature); + const keys = await deriveStealthKeysFromWallet({ + chain: 'solana', + getAddress: async () => 'So1ana', + signMessage, + }); + + expect(new TextDecoder().decode(signMessage.mock.calls[0][0])).toBe(SOLANA_MESSAGE); + expect(keys).toEqual(deriveSolanaKeys(signature)); + }); +}); + +describe('reference wallet adapters', () => { + test('wraps Freighter without importing its package', async () => { + const signature = new Uint8Array(64).fill(0x44); + const wallet = { + getAddress: vi.fn(async () => ({ address: 'GTEST' })), + signMessage: vi.fn(async () => ({ signedMessage: signature })), + }; + const adapter = new FreighterWalletAdapter(wallet); + + expect(adapter.chain).toBe('stellar'); + expect(await adapter.getAddress()).toBe('GTEST'); + expect(await adapter.signMessage(new TextEncoder().encode('hello'))).toEqual(signature); + expect(wallet.signMessage).toHaveBeenCalledWith('hello'); + }); + + test('wraps a viem WalletClient structurally', async () => { + const signature = `0x${'55'.repeat(65)}` as `0x${string}`; + const signMessage = vi.fn(async () => signature); + const adapter = new ViemWalletAdapter({ + account: { address: '0xabc' }, + signMessage, + }); + const message = new Uint8Array([1, 2, 3]); + + expect(await adapter.getAddress()).toBe('0xabc'); + expect(await adapter.signMessage(message)).toBe(signature); + expect(signMessage).toHaveBeenCalledWith({ + account: { address: '0xabc' }, + message: { raw: message }, + }); + }); + + test('wraps a Solana wallet-adapter wallet structurally', async () => { + const signature = new Uint8Array(64).fill(0x66); + const adapter = new SolanaWalletAdapter({ + publicKey: { toBase58: () => 'So1anaAddress' }, + signMessage: async () => signature, + }); + + expect(adapter.chain).toBe('solana'); + expect(await adapter.getAddress()).toBe('So1anaAddress'); + expect(await adapter.signMessage(new Uint8Array([1]))).toEqual(signature); + }); +}); From 0713ca7dda251faf4423682669f785783b3a86e4 Mon Sep 17 00:00:00 2001 From: Hallab Date: Wed, 26 Aug 2026 08:53:41 +0100 Subject: [PATCH 2/2] chore: refresh api-extractor baseline --- etc/sdk-solana.api.md | 4 +- etc/sdk-stellar.api.md | 9 ++- etc/sdk.api.md | 164 +++++++++++++++++++++++++++++++++++++---- 3 files changed, 158 insertions(+), 19 deletions(-) diff --git a/etc/sdk-solana.api.md b/etc/sdk-solana.api.md index 2715fe4..c5b8c83 100644 --- a/etc/sdk-solana.api.md +++ b/etc/sdk-solana.api.md @@ -134,10 +134,10 @@ export function decodeStealthMetaAddress(metaAddress: string): StealthMetaAddres // @public (undocumented) export const DEPLOYMENTS: Record; -// Warning: (ae-forgotten-export) The symbol "StealthKeys_2" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "StealthKeys$1" needs to be exported by the entry point index.d.ts // // @public -export function deriveStealthKeys(signature: Uint8Array): StealthKeys_2; +export function deriveStealthKeys(signature: Uint8Array): StealthKeys$1; // @public export function deriveStealthPrivateScalar(spendingScalar: bigint, viewingKey: Uint8Array, ephemeralPubKey: Uint8Array): bigint; diff --git a/etc/sdk-stellar.api.md b/etc/sdk-stellar.api.md index 385c011..d2348e1 100644 --- a/etc/sdk-stellar.api.md +++ b/etc/sdk-stellar.api.md @@ -707,15 +707,22 @@ export interface WebAuthnCredentialsContainer { get(options: Record): Promise; } +// Warning: (ae-forgotten-export) The symbol "StellarWalletAdapter" needs to be exported by the entry point index.d.ts +// // @public -export class WebAuthnPasskeyStealthSigner implements StellarStealthSigner { +export class WebAuthnPasskeyStealthSigner implements StellarStealthSigner, StellarWalletAdapter { constructor(options: WebAuthnPasskeyStealthSignerOptions); // (undocumented) + readonly chain: "stellar"; + // (undocumented) + getAddress(): Promise; + // (undocumented) signMessage(message: Uint8Array): Promise; } // @public export interface WebAuthnPasskeyStealthSignerOptions { + address?: string; credentialId: Uint8Array; credentials?: WebAuthnCredentialsContainer; rpId?: string; diff --git a/etc/sdk.api.md b/etc/sdk.api.md index 706a09d..c3b3a39 100644 --- a/etc/sdk.api.md +++ b/etc/sdk.api.md @@ -40,6 +40,16 @@ export interface Balance { tokens: Record; } +// @public +export interface BaseWalletAdapter { + // (undocumented) + readonly chain: TChain; + // (undocumented) + getAddress(): Promise; + // (undocumented) + signMessage(message: Uint8Array): Promise; +} + // @public (undocumented) export enum Chain { // (undocumented) @@ -75,13 +85,13 @@ export interface CkbChainInput { // (undocumented) source: AsyncIterable; // (undocumented) - spendingKey: HexString; + spendingKey: HexString_2; // (undocumented) - spendingPubKey: HexString; - // Warning: (ae-forgotten-export) The symbol "HexString" needs to be exported by the entry point index.d.ts + spendingPubKey: HexString_2; + // Warning: (ae-forgotten-export) The symbol "HexString_2" needs to be exported by the entry point index.d.ts // // (undocumented) - viewingKey: HexString; + viewingKey: HexString_2; } // @public (undocumented) @@ -105,6 +115,33 @@ export interface Conversation { updatedAt: string; } +// @public +export function createFreighterWalletAdapter(wallet: FreighterWalletApi): FreighterWalletAdapter; + +// @public +export function createSolanaWalletAdapter(wallet: SolanaWalletAdapterLike): SolanaWalletAdapter; + +// @public +export function createViemWalletAdapter(client: ViemWalletClient): ViemWalletAdapter; + +// Warning: (ae-forgotten-export) The symbol "StealthKeys$1" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +export function deriveStealthKeysFromWallet(adapter: StellarWalletAdapter): Promise; + +// Warning: (ae-forgotten-export) The symbol "StealthKeys" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +export function deriveStealthKeysFromWallet(adapter: EvmWalletAdapter): Promise; + +// Warning: (ae-forgotten-export) The symbol "StealthKeys_2" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +export function deriveStealthKeysFromWallet(adapter: SolanaChainWalletAdapter): Promise; + +// @public (undocumented) +export function deriveStealthKeysFromWallet(adapter: WalletAdapter): Promise; + // @public (undocumented) export class ECDHFailedError extends WraithCryptoError { constructor(reason: string); @@ -119,13 +156,45 @@ export interface EvmChainInput { // (undocumented) source: AsyncIterable; // (undocumented) - spendingKey: HexString_2; + spendingKey: HexString; // (undocumented) - spendingPubKey: HexString_2; - // Warning: (ae-forgotten-export) The symbol "HexString_2" needs to be exported by the entry point index.d.ts + spendingPubKey: HexString; + // Warning: (ae-forgotten-export) The symbol "HexString" needs to be exported by the entry point index.d.ts // // (undocumented) - viewingKey: HexString_2; + viewingKey: HexString; +} + +// @public +export type EvmWalletAdapter = BaseWalletAdapter<'evm', HexString>; + +// @public +export class FreighterWalletAdapter implements StellarWalletAdapter { + constructor(wallet: FreighterWalletApi); + // (undocumented) + readonly chain: "stellar"; + // (undocumented) + getAddress(): Promise; + // (undocumented) + signMessage(message: Uint8Array): Promise; +} + +// @public +export interface FreighterWalletApi { + // (undocumented) + getAddress(): Promise; + // (undocumented) + signMessage(message: string): Promise<{ + signedMessage?: Uint8Array | string; + error?: string | { + message?: string; + }; + }>; } // @public (undocumented) @@ -212,12 +281,12 @@ export type MatchedAnnouncement = { chain: 'stellar'; timestamp: number; seq: number; - announcement: MatchedAnnouncement_3; + announcement: MatchedAnnouncement$1; } | { chain: 'solana'; timestamp: number; seq: number; - announcement: MatchedAnnouncement_4; + announcement: MatchedAnnouncement_3; } | { chain: 'ckb'; timestamp: number; @@ -338,10 +407,10 @@ export interface Schedule { // @public (undocumented) export interface SolanaChainInput { - // Warning: (ae-forgotten-export) The symbol "Announcement_3" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "Announcement_2" needs to be exported by the entry point index.d.ts // // (undocumented) - source: AsyncIterable; + source: AsyncIterable; // (undocumented) spendingPubKey: Uint8Array; // (undocumented) @@ -350,12 +419,36 @@ export interface SolanaChainInput { viewingKey: Uint8Array; } +// @public +export type SolanaChainWalletAdapter = BaseWalletAdapter<'solana', Uint8Array>; + +// @public +export class SolanaWalletAdapter implements SolanaChainWalletAdapter { + constructor(wallet: SolanaWalletAdapterLike); + // (undocumented) + readonly chain: "solana"; + // (undocumented) + getAddress(): Promise; + // (undocumented) + signMessage(message: Uint8Array): Promise; +} + +// @public +export interface SolanaWalletAdapterLike { + // (undocumented) + publicKey: { + toBase58(): string; + } | null; + // (undocumented) + signMessage?: (message: Uint8Array) => Promise; +} + // @public (undocumented) export interface StellarChainInput { - // Warning: (ae-forgotten-export) The symbol "Announcement_2" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "Announcement$1" needs to be exported by the entry point index.d.ts // // (undocumented) - source: AsyncIterable; + source: AsyncIterable; // (undocumented) spendingPubKey: Uint8Array; // (undocumented) @@ -364,6 +457,9 @@ export interface StellarChainInput { viewingKey: Uint8Array; } +// @public +export type StellarWalletAdapter = BaseWalletAdapter<'stellar', Uint8Array>; + // @public (undocumented) export type SupportedChain = 'evm' | 'stellar' | 'solana' | 'ckb'; @@ -392,6 +488,36 @@ export class UnsupportedAssetError extends WraithBuilderError { readonly code = "WRAITH/BUILDER/UNSUPPORTED_ASSET"; } +// @public +export class ViemWalletAdapter implements EvmWalletAdapter { + constructor(client: ViemWalletClient); + // (undocumented) + readonly chain: "evm"; + // (undocumented) + getAddress(): Promise; + // (undocumented) + signMessage(message: Uint8Array): Promise; +} + +// @public +export interface ViemWalletClient { + // (undocumented) + account?: { + address: string; + } | null; + // (undocumented) + getAddresses?: () => Promise; + // (undocumented) + signMessage(args: { + account?: { + address: string; + } | string; + message: { + raw: Uint8Array; + }; + }): Promise; +} + // @public (undocumented) export class ViewTagMismatchError extends WraithCryptoError { constructor(expectedTag: number, actualTag: number); @@ -399,6 +525,12 @@ export class ViewTagMismatchError extends WraithCryptoError { readonly code = "WRAITH/CRYPTO/VIEW_TAG_MISMATCH"; } +// @public +export type WalletAdapter = StellarWalletAdapter | EvmWalletAdapter | SolanaChainWalletAdapter; + +// @public +export type WalletAdapterChain = 'stellar' | 'evm' | 'solana'; + // @public (undocumented) export class Wraith { constructor(config: WraithConfig); @@ -504,8 +636,8 @@ export abstract class WraithNetworkError extends WraithError { // Warnings were encountered during analysis: // // dist/index.d.ts:207:5 - (ae-forgotten-export) The symbol "MatchedAnnouncement_2" needs to be exported by the entry point index.d.ts -// dist/index.d.ts:212:5 - (ae-forgotten-export) The symbol "MatchedAnnouncement_3" needs to be exported by the entry point index.d.ts -// dist/index.d.ts:217:5 - (ae-forgotten-export) The symbol "MatchedAnnouncement_4" needs to be exported by the entry point index.d.ts +// dist/index.d.ts:212:5 - (ae-forgotten-export) The symbol "MatchedAnnouncement$1" needs to be exported by the entry point index.d.ts +// dist/index.d.ts:217:5 - (ae-forgotten-export) The symbol "MatchedAnnouncement_3" needs to be exported by the entry point index.d.ts // dist/index.d.ts:222:5 - (ae-forgotten-export) The symbol "MatchedStealthCell" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package)