From d31ecabf810d516b51d4109dc1d73a6243f7f552 Mon Sep 17 00:00:00 2001 From: nullxnothing Date: Mon, 1 Jun 2026 18:35:27 -0600 Subject: [PATCH 01/47] feat: add SAID Protocol agent identity integration Adds SAID Protocol (on-chain agent identity, verification, and trust on Solana) to the Integration Command Center. SaidProtocolService reads agent identity and the 0-100 trust score from the public SAID API; the registry tile exposes a read-only identity check plus directory/docs links and a gated registration preview. No signing paths are enabled yet. --- electron/ipc/said.ts | 15 ++ electron/main/index.ts | 2 + electron/preload/index.ts | 5 + electron/services/SaidProtocolService.ts | 148 ++++++++++++++++++ electron/shared/types.ts | 26 +++ .../IntegrationCommandCenter/actionRunner.ts | 65 ++++++++ .../IntegrationCommandCenter/registry.ts | 21 +++ src/types/daemon.d.ts | 10 ++ .../IntegrationCommandCenter.registry.test.ts | 95 ++++++++++- test/services/SaidProtocolService.test.ts | 111 +++++++++++++ 10 files changed, 497 insertions(+), 1 deletion(-) create mode 100644 electron/ipc/said.ts create mode 100644 electron/services/SaidProtocolService.ts create mode 100644 test/services/SaidProtocolService.test.ts diff --git a/electron/ipc/said.ts b/electron/ipc/said.ts new file mode 100644 index 00000000..c5ec7ea6 --- /dev/null +++ b/electron/ipc/said.ts @@ -0,0 +1,15 @@ +import { ipcMain } from 'electron' +import { ipcHandler } from '../services/IpcHandlerFactory' +import * as SaidProtocolService from '../services/SaidProtocolService' + +export function registerSaidHandlers() { + ipcMain.handle('said:get-identity', ipcHandler(async (_event, wallet: string) => { + if (typeof wallet !== 'string' || !wallet) throw new Error('Invalid wallet address') + return SaidProtocolService.getIdentity(wallet.trim()) + })) + + ipcMain.handle('said:get-trust', ipcHandler(async (_event, wallet: string) => { + if (typeof wallet !== 'string' || !wallet) throw new Error('Invalid wallet address') + return SaidProtocolService.getTrustScore(wallet.trim()) + })) +} diff --git a/electron/main/index.ts b/electron/main/index.ts index 705f9149..5b8443c5 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -42,6 +42,7 @@ import { registerLaunchHandlers } from '../ipc/launch' import { registerDashboardHandlers } from '../ipc/dashboard' import { registerForensicsHandlers } from '../ipc/forensics' import { registerRegistryHandlers } from '../ipc/registry' +import { registerSaidHandlers } from '../ipc/said' import { registerColosseumHandlers } from '../ipc/colosseum' import { registerIdleHandlers } from '../ipc/idle' import { registerMeterflowHandlers } from '../ipc/meterflow' @@ -325,6 +326,7 @@ function registerAllIpc() { registerDashboardHandlers() registerForensicsHandlers() registerRegistryHandlers() + registerSaidHandlers() registerColosseumHandlers() registerIdleHandlers() registerMeterflowHandlers() diff --git a/electron/preload/index.ts b/electron/preload/index.ts index 9b180550..a8a6a23e 100644 --- a/electron/preload/index.ts +++ b/electron/preload/index.ts @@ -601,6 +601,11 @@ contextBridge.exposeInMainWorld('daemon', { importToken: (mint: string, walletId: string) => ipcRenderer.invoke('dashboard:import-token', mint, walletId), }, + said: { + getIdentity: (wallet: string) => ipcRenderer.invoke('said:get-identity', wallet), + getTrust: (wallet: string) => ipcRenderer.invoke('said:get-trust', wallet), + }, + forensics: { scan: (input: object) => ipcRenderer.invoke('forensics:scan', input), expand: (input: object) => ipcRenderer.invoke('forensics:expand', input), diff --git a/electron/services/SaidProtocolService.ts b/electron/services/SaidProtocolService.ts new file mode 100644 index 00000000..31ca0bb1 --- /dev/null +++ b/electron/services/SaidProtocolService.ts @@ -0,0 +1,148 @@ +import { PublicKey } from '@solana/web3.js' +import type { SaidIdentity, SaidTrustScore } from '../shared/types' + +// SAID Protocol — on-chain identity, verification, and trust for AI agents on Solana. +// DAEMON only consumes the public read API here. Registration/verification/staking sign +// transactions and must be added behind SignerGuardService + a transaction preview. +const SAID_API_BASE = process.env.DAEMON_SAID_API_URL || 'https://api.saidprotocol.com' +const REQUEST_TIMEOUT_MS = 10_000 + +// Program IDs are docs-sourced. Verify on a Solana explorer before wiring any signing flow. +export const SAID_PROGRAM_ID = { + mainnet: '5dpw6KEQPn248pnkkaYyWfHwu2nfb3LUMbTucb6LaA8G', + devnet: 'ESPreFucjVwtDmZbhtL3JLJ9VxCethNEYtosMQhkcurv', +} as const + +function isValidWallet(address: string): boolean { + try { + new PublicKey(address) + return address.length >= 32 && address.length <= 44 + } catch { + return false + } +} + +async function fetchJson(path: string): Promise<{ status: number; body: T | null }> { + const url = `${SAID_API_BASE}${path}` + const controller = new AbortController() + const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS) + try { + const response = await fetch(url, { + method: 'GET', + headers: { Accept: 'application/json' }, + signal: controller.signal, + }) + if (response.status === 404) return { status: 404, body: null } + if (!response.ok) { + const text = await response.text().catch(() => '') + throw new Error(`SAID request failed (${response.status}): ${text.slice(0, 180)}`) + } + const body = (await response.json()) as T + return { status: response.status, body } + } catch (err) { + if (err instanceof Error && err.name === 'AbortError') { + throw new Error('SAID request timed out') + } + throw err + } finally { + clearTimeout(timer) + } +} + +// The numeric trust score lives on /api/agents/:wallet under trustScore.score. +// /api/trust/:wallet only returns a coarse tier + registered/verified booleans, and +// /api/agents/:wallet already carries verification, reputation, and the economic +// breakdown — so a single agents read is the source of truth for both calls. +interface RawTrustScore { + score?: number + economic?: number + badges?: string[] +} + +interface RawAgent { + wallet?: string + pda?: string + owner?: string + name?: string + description?: string + isVerified?: boolean + image?: string + twitter?: string + website?: string + serviceTypes?: string[] + skills?: string[] + reputationScore?: number + feedbackCount?: number + trustScore?: number | RawTrustScore + passportMint?: string +} + +function readScore(value: number | RawTrustScore | undefined): number | null { + if (typeof value === 'number') return value + if (value && typeof value.score === 'number') return value.score + return null +} + +function isStaked(trust: number | RawTrustScore | undefined): boolean { + if (typeof trust !== 'object' || !trust) return false + if (Array.isArray(trust.badges) && trust.badges.includes('staked')) return true + return typeof trust.economic === 'number' && trust.economic > 0 +} + +export async function getTrustScore(wallet: string): Promise { + if (!isValidWallet(wallet)) throw new Error('Invalid Solana wallet address') + const { status, body } = await fetchJson(`/api/agents/${wallet}`) + if (status === 404 || !body || !body.wallet) { + return { score: 0, verified: false, staked: false, reputation: null } + } + const score = readScore(body.trustScore) ?? 0 + return { + score: Math.max(0, Math.min(100, Math.round(score))), + verified: Boolean(body.isVerified), + staked: isStaked(body.trustScore), + reputation: body.reputationScore ?? null, + } +} + +export async function getIdentity(wallet: string): Promise { + if (!isValidWallet(wallet)) throw new Error('Invalid Solana wallet address') + const { status, body } = await fetchJson(`/api/agents/${wallet}`) + if (status === 404 || !body || !body.wallet) { + return { + wallet, + pda: null, + owner: null, + name: null, + description: null, + isVerified: false, + image: null, + twitter: null, + website: null, + serviceTypes: [], + skills: [], + reputationScore: null, + feedbackCount: null, + trustScore: null, + passportMint: null, + registered: false, + } + } + return { + wallet: body.wallet, + pda: body.pda ?? null, + owner: body.owner ?? null, + name: body.name ?? null, + description: body.description ?? null, + isVerified: Boolean(body.isVerified), + image: body.image ?? null, + twitter: body.twitter ?? null, + website: body.website ?? null, + serviceTypes: Array.isArray(body.serviceTypes) ? body.serviceTypes : [], + skills: Array.isArray(body.skills) ? body.skills : [], + reputationScore: body.reputationScore ?? null, + feedbackCount: body.feedbackCount ?? null, + trustScore: readScore(body.trustScore), + passportMint: body.passportMint ?? null, + registered: true, + } +} diff --git a/electron/shared/types.ts b/electron/shared/types.ts index 74b9eb32..99fe302f 100644 --- a/electron/shared/types.ts +++ b/electron/shared/types.ts @@ -1111,6 +1111,32 @@ export interface RicoMapsEmbedStatus { error: string | null } +export interface SaidTrustScore { + score: number + verified: boolean + staked: boolean + reputation: number | null +} + +export interface SaidIdentity { + wallet: string + pda: string | null + owner: string | null + name: string | null + description: string | null + isVerified: boolean + image: string | null + twitter: string | null + website: string | null + serviceTypes: string[] + skills: string[] + reputationScore: number | null + feedbackCount: number | null + trustScore: number | null + passportMint: string | null + registered: boolean +} + export type SolanaTransactionPreviewKind = 'send-sol' | 'send-token' | 'swap' | 'launch' export interface SolanaTransactionPreviewInput { diff --git a/src/panels/IntegrationCommandCenter/actionRunner.ts b/src/panels/IntegrationCommandCenter/actionRunner.ts index da3bcc02..e2bcc4d1 100644 --- a/src/panels/IntegrationCommandCenter/actionRunner.ts +++ b/src/panels/IntegrationCommandCenter/actionRunner.ts @@ -295,6 +295,71 @@ export async function runIntegrationAction(actionId: string, context: Integratio } } + if (actionId === 'check-said-identity') { + const wallet = context.defaultWallet + if (!wallet) { + return { + title: 'No wallet selected', + status: 'warning', + detail: 'Set a default wallet in the Wallet panel before checking its SAID identity.', + } + } + + const identity = await daemon.said.getIdentity(wallet.address) + if (!identity.ok || !identity.data) { + return { + title: 'SAID lookup failed', + status: 'error', + detail: identity.error ?? 'DAEMON could not reach the SAID directory.', + items: [wallet.address], + } + } + + if (!identity.data.registered) { + return { + title: 'Not registered on SAID', + status: 'info', + detail: `${wallet.name} has no SAID identity yet. Register it to earn a verifiable trust score and appear in the directory.`, + items: [wallet.address], + } + } + + const trustRes = await daemon.said.getTrust(wallet.address) + const score = trustRes.ok && trustRes.data ? trustRes.data.score : identity.data.trustScore + const badges = [ + identity.data.isVerified ? 'verified' : 'unverified', + trustRes.ok && trustRes.data?.staked ? 'staked' : 'no stake', + ] + return { + title: identity.data.name ? `SAID: ${identity.data.name}` : 'SAID identity found', + status: 'success', + detail: `Trust score ${score ?? 'n/a'}/100 (${badges.join(', ')}).`, + items: [ + wallet.address, + ...(identity.data.pda ? [`PDA ${identity.data.pda}`] : []), + ...(typeof identity.data.feedbackCount === 'number' ? [`${identity.data.feedbackCount} feedback`] : []), + ], + } + } + + if (actionId === 'open-said-directory') { + void daemon.shell.openExternal('https://www.saidprotocol.com/agents') + return { + title: 'Opening SAID directory', + status: 'success', + detail: 'Launched the SAID public agent directory in your browser.', + } + } + + if (actionId === 'open-said-docs') { + void daemon.shell.openExternal('https://www.saidprotocol.com/docs') + return { + title: 'Opening SAID docs', + status: 'success', + detail: 'Launched the SAID docs in your browser.', + } + } + return { title: 'Preview only', status: 'info', diff --git a/src/panels/IntegrationCommandCenter/registry.ts b/src/panels/IntegrationCommandCenter/registry.ts index bcf64064..175a41f9 100644 --- a/src/panels/IntegrationCommandCenter/registry.ts +++ b/src/panels/IntegrationCommandCenter/registry.ts @@ -323,6 +323,27 @@ export const INTEGRATION_REGISTRY: IntegrationDefinition[] = [ { id: 'preview-squads-vault', label: 'Preview vault', description: 'Planned: multisig and vault preview before proposal creation or execution.', kind: 'planned', risk: 'read-only' }, ], }, + { + id: 'said-protocol', + name: 'SAID Protocol', + tagline: 'On-chain identity + trust for AI agents', + description: 'Give agents a verifiable Solana identity with a verification badge, a 0–100 trust score, and on-chain reputation. Resolve agents by wallet/name/DID across 10 chains and discover them in a public directory. Complements the DAEMON work registry: SAID proves who an agent is, the registry proves what it did.', + category: 'agent', + docsUrl: 'https://www.saidprotocol.com/docs', + installCommand: 'pnpm add @said-protocol/agent said-sdk', + recommendedFor: ['agent identity', 'agent verification', 'trust scores', 'agent reputation', 'agent discovery', 'A2A messaging'], + requirements: [ + { type: 'package', key: '@said-protocol/agent', label: '@said-protocol/agent package', optional: true }, + { type: 'wallet', key: 'default-wallet', label: 'Default DAEMON wallet (for register/verify signing)' }, + { type: 'external-url', key: 'https://www.saidprotocol.com/agents', label: 'SAID agent directory' }, + ], + actions: [ + { id: 'check-said-identity', label: 'Check identity', description: 'Look up the default wallet on SAID and show its agent identity, verification badge, and trust score.', kind: 'safe-check', risk: 'read-only' }, + { id: 'open-said-directory', label: 'Browse directory', description: 'Open the SAID public agent directory in your browser.', kind: 'setup', risk: 'read-only' }, + { id: 'open-said-docs', label: 'Open docs', description: 'Open the SAID docs in your browser.', kind: 'setup', risk: 'read-only' }, + { id: 'preview-said-register', label: 'Preview registration', description: 'Review the register → verify flow and on-chain costs before any signing is enabled.', kind: 'planned', risk: 'requires-confirmation' }, + ], + }, ] export function getIntegration(id: string): IntegrationDefinition | undefined { diff --git a/src/types/daemon.d.ts b/src/types/daemon.d.ts index dd2ef787..84697be2 100644 --- a/src/types/daemon.d.ts +++ b/src/types/daemon.d.ts @@ -25,6 +25,8 @@ import type { ForensicsGraphData, ForensicsGraphNode, RicoMapsEmbedStatus, + SaidIdentity, + SaidTrustScore, JupiterTokenSearchResult, ClaudeMdData, ClaudeConnection, @@ -178,6 +180,8 @@ export type { ForensicsGraphData, ForensicsGraphNode, RicoMapsEmbedStatus, + SaidIdentity, + SaidTrustScore, JupiterTokenSearchResult, ClaudeMdData, ClaudeConnection, @@ -1607,6 +1611,7 @@ declare global { launch: DaemonLaunch dashboard: DaemonDashboard forensics: DaemonForensics + said: DaemonSaid registry: DaemonRegistry colosseum: DaemonColosseum idle: DaemonIdle @@ -1747,6 +1752,11 @@ declare global { startRicoMaps: () => Promise> } + interface DaemonSaid { + getIdentity: (wallet: string) => Promise> + getTrust: (wallet: string) => Promise> + } + interface DaemonBrowser { navigate: (url: string) => Promise> capture: (pageId: string, url: string, title: string, content: string) => Promise> diff --git a/test/panels/IntegrationCommandCenter.registry.test.ts b/test/panels/IntegrationCommandCenter.registry.test.ts index 6f91624c..61b6365e 100644 --- a/test/panels/IntegrationCommandCenter.registry.test.ts +++ b/test/panels/IntegrationCommandCenter.registry.test.ts @@ -1,8 +1,19 @@ -import { describe, expect, it } from 'vitest' +import { afterEach, describe, expect, it, vi } from 'vitest' import { INTEGRATION_REGISTRY } from '../../src/panels/IntegrationCommandCenter/registry' import { runIntegrationAction } from '../../src/panels/IntegrationCommandCenter/actionRunner' import { resolveIntegrationStatus, type IntegrationContext } from '../../src/panels/IntegrationCommandCenter/status' +function stubDaemon(said: { getIdentity?: unknown; getTrust?: unknown }) { + const daemon = { + said: { + getIdentity: said.getIdentity ?? vi.fn(), + getTrust: said.getTrust ?? vi.fn(), + }, + shell: { openExternal: vi.fn() }, + } + vi.stubGlobal('window', { daemon }) +} + describe('Integration Command Center registry', () => { it('keeps integration ids, actions, and docs valid', () => { const ids = new Set() @@ -216,4 +227,86 @@ describe('Integration Command Center registry', () => { expect(routeStack.title).toBe('IDLE execution prerequisites ready') expect(routeStack.items).toContain('x402 payment tooling available') }) + + describe('SAID Protocol integration', () => { + afterEach(() => { + vi.unstubAllGlobals() + }) + + const baseContext: IntegrationContext = { + envFiles: [], + mcps: [], + packages: new Set(), + walletReady: true, + defaultWallet: { id: 'wallet-1', name: 'Main Wallet', address: 'So11111111111111111111111111111111111111112', is_default: 1, created_at: 1, assigned_project_ids: [] }, + secureKeys: {}, + toolchain: null, + } + + it('registers SAID as an agent identity integration', () => { + const said = INTEGRATION_REGISTRY.find((integration) => integration.id === 'said-protocol') + expect(said).toBeDefined() + expect(said!.category).toBe('agent') + expect(said!.docsUrl).toBe('https://www.saidprotocol.com/docs') + expect(said!.installCommand).toContain('@said-protocol/agent') + expect(said!.requirements).toContainEqual({ + type: 'wallet', + key: 'default-wallet', + label: 'Default DAEMON wallet (for register/verify signing)', + }) + expect(said!.actions.map((action) => action.id)).toEqual([ + 'check-said-identity', + 'open-said-directory', + 'open-said-docs', + 'preview-said-register', + ]) + // register/verify/stake must stay gated behind confirmation, never auto-executed + const register = said!.actions.find((action) => action.id === 'preview-said-register') + expect(register!.kind).toBe('planned') + expect(register!.risk).toBe('requires-confirmation') + }) + + it('reports a registered, verified identity with its trust score', async () => { + stubDaemon({ + getIdentity: vi.fn(async () => ({ + ok: true, + data: { registered: true, name: 'DAEMON Agent', isVerified: true, pda: 'PdA1', trustScore: 80, feedbackCount: 2 }, + })), + getTrust: vi.fn(async () => ({ ok: true, data: { score: 88, verified: true, staked: true, reputation: 10 } })), + }) + + const result = await runIntegrationAction('check-said-identity', baseContext) + expect(result.status).toBe('success') + expect(result.title).toBe('SAID: DAEMON Agent') + expect(result.detail).toContain('88/100') + expect(result.detail).toContain('verified') + expect(result.detail).toContain('staked') + expect(result.items).toContain('PDA PdA1') + }) + + it('treats an unregistered wallet as actionable info, not an error', async () => { + stubDaemon({ + getIdentity: vi.fn(async () => ({ ok: true, data: { registered: false, wallet: baseContext.defaultWallet!.address } })), + }) + const result = await runIntegrationAction('check-said-identity', baseContext) + expect(result.status).toBe('info') + expect(result.title).toBe('Not registered on SAID') + }) + + it('surfaces lookup failures as errors', async () => { + stubDaemon({ + getIdentity: vi.fn(async () => ({ ok: false, error: 'network down' })), + }) + const result = await runIntegrationAction('check-said-identity', baseContext) + expect(result.status).toBe('error') + expect(result.detail).toBe('network down') + }) + + it('warns when no default wallet is set', async () => { + stubDaemon({}) + const result = await runIntegrationAction('check-said-identity', { ...baseContext, defaultWallet: null, walletReady: false }) + expect(result.status).toBe('warning') + expect(result.title).toBe('No wallet selected') + }) + }) }) diff --git a/test/services/SaidProtocolService.test.ts b/test/services/SaidProtocolService.test.ts new file mode 100644 index 00000000..bc5791bf --- /dev/null +++ b/test/services/SaidProtocolService.test.ts @@ -0,0 +1,111 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import * as SaidProtocolService from '../../electron/services/SaidProtocolService' + +const VALID_WALLET = 'So11111111111111111111111111111111111111112' + +function jsonResponse(body: unknown, status = 200): Response { + return new Response(JSON.stringify(body), { + status, + headers: { 'Content-Type': 'application/json' }, + }) +} + +describe('SaidProtocolService', () => { + beforeEach(() => { + vi.restoreAllMocks() + }) + + afterEach(() => { + vi.unstubAllGlobals() + }) + + it('rejects invalid wallet addresses before making a request', async () => { + const fetchSpy = vi.fn() + vi.stubGlobal('fetch', fetchSpy) + await expect(SaidProtocolService.getTrustScore('not-a-wallet')).rejects.toThrow('Invalid Solana wallet address') + await expect(SaidProtocolService.getIdentity('short')).rejects.toThrow('Invalid Solana wallet address') + expect(fetchSpy).not.toHaveBeenCalled() + }) + + it('derives the numeric score from the agent trustScore object', async () => { + vi.stubGlobal('fetch', vi.fn(async () => jsonResponse({ + wallet: VALID_WALLET, + trustScore: { score: 87.6, economic: 3, badges: ['verified', 'staked'] }, + isVerified: true, + reputationScore: 42, + }))) + + const trust = await SaidProtocolService.getTrustScore(VALID_WALLET) + expect(trust).toEqual({ score: 88, verified: true, staked: true, reputation: 42 }) + }) + + it('infers stake from a positive economic component when no staked badge is present', async () => { + vi.stubGlobal('fetch', vi.fn(async () => jsonResponse({ + wallet: VALID_WALLET, + trustScore: { score: 40, economic: 2, badges: ['verified'] }, + isVerified: true, + }))) + const trust = await SaidProtocolService.getTrustScore(VALID_WALLET) + expect(trust.staked).toBe(true) + }) + + it('returns a zero score for an unregistered wallet', async () => { + vi.stubGlobal('fetch', vi.fn(async () => new Response('not found', { status: 404 }))) + const trust = await SaidProtocolService.getTrustScore(VALID_WALLET) + expect(trust).toEqual({ score: 0, verified: false, staked: false, reputation: null }) + }) + + it('clamps trust score into the 0-100 range', async () => { + vi.stubGlobal('fetch', vi.fn(async () => jsonResponse({ wallet: VALID_WALLET, trustScore: { score: 250 }, isVerified: false }))) + const trust = await SaidProtocolService.getTrustScore(VALID_WALLET) + expect(trust.score).toBe(100) + expect(trust.verified).toBe(false) + }) + + it('returns an unregistered identity on 404', async () => { + vi.stubGlobal('fetch', vi.fn(async () => new Response('not found', { status: 404 }))) + const identity = await SaidProtocolService.getIdentity(VALID_WALLET) + expect(identity.registered).toBe(false) + expect(identity.wallet).toBe(VALID_WALLET) + expect(identity.isVerified).toBe(false) + expect(identity.serviceTypes).toEqual([]) + }) + + it('maps a full agent payload into a normalized identity', async () => { + vi.stubGlobal('fetch', vi.fn(async () => jsonResponse({ + wallet: VALID_WALLET, + pda: 'PdA1111111111111111111111111111111111111111', + owner: 'Own1111111111111111111111111111111111111111', + name: 'DAEMON Agent', + description: 'ships code', + isVerified: true, + image: 'https://img', + twitter: 'daemon', + website: 'https://daemonide.tech', + serviceTypes: ['dev'], + skills: ['anchor'], + reputationScore: 12, + feedbackCount: 3, + trustScore: 91, + passportMint: 'Mint111111111111111111111111111111111111111', + }))) + + const identity = await SaidProtocolService.getIdentity(VALID_WALLET) + expect(identity.registered).toBe(true) + expect(identity.name).toBe('DAEMON Agent') + expect(identity.isVerified).toBe(true) + expect(identity.trustScore).toBe(91) + expect(identity.feedbackCount).toBe(3) + expect(identity.skills).toEqual(['anchor']) + }) + + it('throws a descriptive error on a 5xx response', async () => { + vi.stubGlobal('fetch', vi.fn(async () => new Response('boom', { status: 500 }))) + await expect(SaidProtocolService.getTrustScore(VALID_WALLET)).rejects.toThrow(/SAID request failed \(500\)/) + }) + + it('exposes docs-sourced program ids for both clusters', () => { + expect(SaidProtocolService.SAID_PROGRAM_ID.mainnet).toMatch(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/) + expect(SaidProtocolService.SAID_PROGRAM_ID.devnet).toMatch(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/) + }) +}) From b6988dae5f19a37a273a8919028317d97987e411 Mon Sep 17 00:00:00 2001 From: nullxnothing Date: Wed, 3 Jun 2026 11:35:24 -0600 Subject: [PATCH 02/47] @ feat: daemon wallet adapter (multi-wallet, solflare-recommended) Add a vendor-neutral wallet adapter on the wallet panel so any Solana wallet can connect, with Solflare featured as the recommended signer. Generalizes the prior solflare-only external signing path: the wallet- standard preference now routes prepare/sign/submit through a registry of providers. Adds shortvec-correct tx deserialization shared with the solflare lib, real wallet logos, and adapter unit tests. @ --- electron/shared/types.ts | 4 + public/wallet-logos/backpack.png | Bin 0 -> 2184 bytes public/wallet-logos/phantom.png | Bin 0 -> 37882 bytes public/wallet-logos/solflare.png | Bin 0 -> 6493 bytes src/lib/solanaReadiness.ts | 8 +- src/lib/solflareWallet.ts | 29 +- src/lib/walletAdapter/index.ts | 88 +++++ .../providers/comingSoonProvider.ts | 44 +++ .../providers/solflareProvider.ts | 54 +++ src/lib/walletAdapter/registry.ts | 64 +++ src/lib/walletAdapter/serialization.ts | 45 +++ src/lib/walletAdapter/types.ts | 53 +++ src/panels/WalletPanel/WalletAdapterCard.tsx | 197 ++++++++++ src/panels/WalletPanel/WalletPanel.css | 368 +++++++++++------- src/panels/WalletPanel/WalletSettings.tsx | 32 +- src/panels/WalletPanel/tabs/WalletTab.tsx | 226 +++++++---- test/lib/walletAdapter.test.ts | 136 +++++++ 17 files changed, 1080 insertions(+), 268 deletions(-) create mode 100644 public/wallet-logos/backpack.png create mode 100644 public/wallet-logos/phantom.png create mode 100644 public/wallet-logos/solflare.png create mode 100644 src/lib/walletAdapter/index.ts create mode 100644 src/lib/walletAdapter/providers/comingSoonProvider.ts create mode 100644 src/lib/walletAdapter/providers/solflareProvider.ts create mode 100644 src/lib/walletAdapter/registry.ts create mode 100644 src/lib/walletAdapter/serialization.ts create mode 100644 src/lib/walletAdapter/types.ts create mode 100644 src/panels/WalletPanel/WalletAdapterCard.tsx create mode 100644 test/lib/walletAdapter.test.ts diff --git a/electron/shared/types.ts b/electron/shared/types.ts index 99fe302f..2ae917d0 100644 --- a/electron/shared/types.ts +++ b/electron/shared/types.ts @@ -1448,6 +1448,10 @@ export interface SubmitExternalSignedTransactionInput { id: string publicKey: string signedTransactionBase64: string + // Which Daemon Wallet Adapter provider signed (e.g. 'solflare'); drives perk + // attribution in the settlement event. Defaults to 'solflare' for the legacy + // path when omitted. + signerProvider?: string } export interface TransferTokenInput { diff --git a/public/wallet-logos/backpack.png b/public/wallet-logos/backpack.png new file mode 100644 index 0000000000000000000000000000000000000000..c5a28ff7deafc87894d577474ac556579bd64291 GIT binary patch literal 2184 zcmb_eYd8}O8{VAFVL8Ua#J3J}4tu>A=2&w~n~{-2(VUuWG-t{x6{R#p8%2&oh{ckm zkt`~wUM%v}izy+D#q0g9>-&CuKfmj{uj_v9>$#sl&vie4o~vFSE^;6U2mk=c9Xsmi zy{ojpDJ8L6)#!bcU6Cdp^@{@lWR(6U5KvUCwrc|8yj|=8Os>Ymv^NFfnuI6CIEv>#bJBDbu5rgPW?MW#{@frOv7LB`;n+|zIMydmp09`mj`Yo@9Q4XwOVY`t3SFvV4S8BAiMfg=cBjttu!h3C-Ic~ouGlr>+p zaXAo=S?=_WzRx(AsK8vATcsq|v=i-0lWM9H6&#*8a>$nWMvw*vzJ#rMH-Df?c^_uE zVPUb6OVtyPWF)F3C52kt5uy6JPc0J7$NPvB@Q6x(Fbw?kWG41c#$7Qz^}0;0ieLqj z*Vh+hM9H(LV-1j42BJkK%~6sJ#}i~-wcidR8oPh=gUMTg>%2cP_ZFfMR&1v#l}E&v zBnvx?tvQ)=`=4iE{sE9OwK84qe4l-A^vA;5pgWPW!+l>k&R9D*JAO!^gH-qdZDQJkp^o$W9{HYj&&NYgK+TZ(`4 zUU~=?t#$l2qK7!;1#(;+h+7(z&La9Q{4`24dlp zYO@curjRc;eRw3Y)v_hK$}I+Y^$7A_kvr@PX@EUq)VO{=pm_?4)}@!56nD)`MUJ&N&;czTdUZNw&ghj|?`Z zn$K{a^b^_ifxxN|NzwxZe=HtnaK!gi7<y;4}$t;%NKv#g|M@I!lVy{;Iv#n!Cl`o=|Ne72vNZ&BPuNnqVR??Aa$+ z*C^>bX^jsjvkkg>{G?+Wyw{BM_>=L4ZgJHf&tvqF=+-oRNzn;2d`ymIHf-2yX*Lp? zeR;h}Ju2l>u%_S8R0xD(l%{i|qb1Eev7VC9jxc3_d6I}?y?-L(m!2}6EfSaXOTrWFFTy{CGv8oUr4@ch# z9Dk8Nh1YMX!E||GMaZyY4)O`Qk#~Apr359R+VxJPcW zpgJh7VsqwW12iMn@|Le1?eA7ef@!@XZ-+Fsejhv%6oU{>1!-?W)#6efGpfcy(ZHs& zV~Jp&vYRH?3#odXeH(C-pqiy|CoI!t_*xGN`5E zUH11W-2G=A;(4CoRv@IB&+vj#fTE^@xjJ=83iVz!HG%y(&#iTZJ}`eGSF$`sp-cGM z$qQQ74KZ_Z4teMHV54yAX@2wU=C@c&vmSX@=Dn8+KS3vPK)Uq;YQ)xoKD33{J09HD zE8Fc9re=7gn|Mo|dhLxd{xo%3$5eK2;r}{HL_4V(hb3LgD;ry^T%|zx^c#e>qV|$< zA(pgiqSDbHJK8#T_%~>==R$x=$^jhHshK}2G-P0A*5t-$irUz?F z6pHcL*3I$-b9wiIr99R3FDu`Me^TE)gRAzpZ}Al^piQY)QF~=T^;cHloRn*L2haa& z5dW2|8vXnOnVYV4o##v{H8SuZ;JM&8o6mYgZ@TV4KFRHF6ho)HUL=K&vlSWr-QVZq{;>K?0!+7K+p)|vF+LQi~0@i2tiEF>0tC^E&?OD%y$$cD$4dZm?J8X*(?6jqmHr|1S$`k*htB4+n-0337j KaAevCW&Q=46AE_# literal 0 HcmV?d00001 diff --git a/public/wallet-logos/phantom.png b/public/wallet-logos/phantom.png new file mode 100644 index 0000000000000000000000000000000000000000..1a847e444753eae32f6aa1bd70cbc3419cffbc3e GIT binary patch literal 37882 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 export type SolflareTransaction = Transaction | VersionedTransaction @@ -120,7 +121,7 @@ export async function signSerializedSolflareTransaction(transactionBase64: strin const publicKey = activeWallet.publicKey?.toBase58() if (!publicKey) throw new Error('Connect Solflare before signing a transaction') - const transaction = Transaction.from(base64ToBytes(transactionBase64)) + const transaction = deserializeTransaction(base64ToBytes(transactionBase64)) const signedTransaction = await activeWallet.signTransaction(transaction) return { publicKey, @@ -128,13 +129,6 @@ export async function signSerializedSolflareTransaction(transactionBase64: strin } } -export async function signAndSendSolflareTransaction(transaction: SolflareTransaction, options?: SendOptions): Promise { - const activeWallet = requireConnectedWallet() - const signature = await activeWallet.signAndSendTransaction(transaction, options) - setState({ ...state, status: 'connected', error: null, lastSignature: signature }) - return signature -} - async function getWallet(network: SolflareSupportedCluster): Promise { if (wallet && walletNetwork === network) return wallet if (wallet) await disconnectSolflareWallet() @@ -182,20 +176,3 @@ function describeSolflareError(error: unknown): string { return 'Solflare request failed' } -function base64ToBytes(value: string): Uint8Array { - const binary = window.atob(value) - const bytes = new Uint8Array(binary.length) - for (let i = 0; i < binary.length; i += 1) { - bytes[i] = binary.charCodeAt(i) - } - return bytes -} - -function bytesToBase64(bytes: Uint8Array): string { - let binary = '' - const chunkSize = 0x8000 - for (let i = 0; i < bytes.length; i += chunkSize) { - binary += String.fromCharCode(...bytes.subarray(i, i + chunkSize)) - } - return window.btoa(binary) -} diff --git a/src/lib/walletAdapter/index.ts b/src/lib/walletAdapter/index.ts new file mode 100644 index 00000000..cb6f5927 --- /dev/null +++ b/src/lib/walletAdapter/index.ts @@ -0,0 +1,88 @@ +import { + getActiveProvider, + getProvider, + getWalletAdapterState, + listProviders, + setActiveProvider, + setWalletAdapterState, + subscribeWalletAdapter, +} from './registry' +import type { + DaemonWalletProvider, + WalletAdapterMessageSignature, + WalletAdapterSignedTransaction, + WalletAdapterState, +} from './types' + +export type { + DaemonWalletProvider, + WalletAdapterMessageSignature, + WalletAdapterSignedTransaction, + WalletAdapterState, + WalletAdapterStatus, + WalletPerkTier, +} from './types' + +export { + getActiveProvider, + getProvider, + getWalletAdapterState, + listProviders, + subscribeWalletAdapter, +} + +export function getProviders(): DaemonWalletProvider[] { + return listProviders() +} + +export async function connectWallet( + providerId: string, + cluster: WalletInfrastructureSettings['cluster'], +): Promise { + const provider = getProvider(providerId) + if (!provider) throw new Error(`Unknown wallet provider: ${providerId}`) + + setActiveProvider(providerId) + try { + return await provider.connect(cluster) + } catch (error) { + setActiveProvider(null) + throw error + } +} + +export async function disconnectWallet(): Promise { + const provider = getActiveProvider() + if (!provider) { + const idle: WalletAdapterState = { + provider: null, + status: 'idle', + network: null, + publicKey: null, + error: null, + lastSignature: null, + } + setWalletAdapterState(idle) + return idle + } + + try { + return await provider.disconnect() + } finally { + setActiveProvider(null) + } +} + +export async function signMessage(message: string): Promise { + return requireActiveProvider().signMessage(message) +} + +export async function signSerializedTransaction(transactionBase64: string): Promise { + return requireActiveProvider().signSerializedTransaction(transactionBase64) +} + +function requireActiveProvider(): DaemonWalletProvider { + const provider = getActiveProvider() + if (!provider) throw new Error('Connect a wallet before signing') + return provider +} diff --git a/src/lib/walletAdapter/providers/comingSoonProvider.ts b/src/lib/walletAdapter/providers/comingSoonProvider.ts new file mode 100644 index 00000000..b335b0f2 --- /dev/null +++ b/src/lib/walletAdapter/providers/comingSoonProvider.ts @@ -0,0 +1,44 @@ +import type { DaemonWalletProvider, WalletAdapterState } from '../types' + +interface ComingSoonConfig { + id: string + name: string + icon: string + subtitle: string +} + +// Phantom / Backpack appear in the picker for parity with the web adapter, but +// the sandboxed Electron renderer has no injected extension to talk to, so they +// stay unavailable until an SDK/deeplink path is wired (phase 2). +function createComingSoonProvider(config: ComingSoonConfig): DaemonWalletProvider { + const unavailable = (): never => { + throw new Error(`${config.name} connection is coming soon to the desktop app`) + } + return { + id: config.id, + name: config.name, + icon: config.icon, + subtitle: config.subtitle, + isHighlighted: false, + perkTier: 'standard', + isAvailable: () => false, + connect: unavailable as () => Promise, + disconnect: unavailable as () => Promise, + signMessage: unavailable as DaemonWalletProvider['signMessage'], + signSerializedTransaction: unavailable as DaemonWalletProvider['signSerializedTransaction'], + } +} + +export const phantomProvider = createComingSoonProvider({ + id: 'phantom', + name: 'Phantom', + icon: '/wallet-logos/phantom.png', + subtitle: 'Solana', +}) + +export const backpackProvider = createComingSoonProvider({ + id: 'backpack', + name: 'Backpack', + icon: '/wallet-logos/backpack.png', + subtitle: 'Solana · xNFT', +}) diff --git a/src/lib/walletAdapter/providers/solflareProvider.ts b/src/lib/walletAdapter/providers/solflareProvider.ts new file mode 100644 index 00000000..2de99fb1 --- /dev/null +++ b/src/lib/walletAdapter/providers/solflareProvider.ts @@ -0,0 +1,54 @@ +import { + connectSolflareWallet, + disconnectSolflareWallet, + getSolflareNetwork, + signSerializedSolflareTransaction, + signSolflareMessage, +} from '../../solflareWallet' +import type { DaemonWalletProvider, WalletAdapterState } from '../types' + +const SOLFLARE_ICON = '/wallet-logos/solflare.png' + +// Solflare is the launch partner: it backs the existing, fully wired external +// signing path, so the adapter wraps those functions rather than duplicating +// the SDK glue. Highlighted + partner perk tier drive the featured placement. +export const solflareProvider: DaemonWalletProvider = { + id: 'solflare', + name: 'Solflare', + icon: SOLFLARE_ICON, + subtitle: 'Recommended for daemon · external signing', + isHighlighted: true, + perkTier: 'partner', + + isAvailable(cluster) { + return getSolflareNetwork(cluster) !== null + }, + + async connect(cluster): Promise { + const state = await connectSolflareWallet(cluster) + return toAdapterState(state) + }, + + async disconnect(): Promise { + const state = await disconnectSolflareWallet() + return toAdapterState(state) + }, + + signMessage(message) { + return signSolflareMessage(message) + }, + + signSerializedTransaction(transactionBase64) { + return signSerializedSolflareTransaction(transactionBase64) + }, +} + +function toAdapterState(state: { + status: WalletAdapterState['status'] + network: WalletAdapterState['network'] + publicKey: string | null + error: string | null + lastSignature: string | null +}): WalletAdapterState { + return { provider: solflareProvider.id, ...state } +} diff --git a/src/lib/walletAdapter/registry.ts b/src/lib/walletAdapter/registry.ts new file mode 100644 index 00000000..6ed8f9e2 --- /dev/null +++ b/src/lib/walletAdapter/registry.ts @@ -0,0 +1,64 @@ +import { subscribeSolflareWallet } from '../solflareWallet' +import { backpackProvider, phantomProvider } from './providers/comingSoonProvider' +import { solflareProvider } from './providers/solflareProvider' +import type { DaemonWalletProvider, WalletAdapterListener, WalletAdapterState } from './types' + +const providers = new Map() +const listeners = new Set() + +let activeProviderId: string | null = null +let state: WalletAdapterState = { + provider: null, + status: 'idle', + network: null, + publicKey: null, + error: null, + lastSignature: null, +} + +export function registerProvider(provider: DaemonWalletProvider): void { + providers.set(provider.id, provider) +} + +export function listProviders(): DaemonWalletProvider[] { + // Highlighted partner first, then registration order. + return [...providers.values()].sort((a, b) => Number(b.isHighlighted) - Number(a.isHighlighted)) +} + +export function getProvider(id: string): DaemonWalletProvider | undefined { + return providers.get(id) +} + +export function getActiveProvider(): DaemonWalletProvider | null { + return activeProviderId ? providers.get(activeProviderId) ?? null : null +} + +export function setActiveProvider(id: string | null): void { + activeProviderId = id && providers.has(id) ? id : null +} + +export function getWalletAdapterState(): WalletAdapterState { + return state +} + +export function subscribeWalletAdapter(listener: WalletAdapterListener): () => void { + listeners.add(listener) + listener(state) + return () => listeners.delete(listener) +} + +export function setWalletAdapterState(next: WalletAdapterState): void { + state = next + listeners.forEach((listener) => listener(state)) +} + +// Pre-register the launch partner (featured) plus the parity providers, then +// relay Solflare's underlying SDK state into the adapter observable so existing +// Solflare subscribers and the new card see one source of truth. +registerProvider(solflareProvider) +registerProvider(phantomProvider) +registerProvider(backpackProvider) +subscribeSolflareWallet((solflareState) => { + if (activeProviderId && activeProviderId !== solflareProvider.id) return + setWalletAdapterState({ provider: solflareProvider.id, ...solflareState }) +}) diff --git a/src/lib/walletAdapter/serialization.ts b/src/lib/walletAdapter/serialization.ts new file mode 100644 index 00000000..989f23e3 --- /dev/null +++ b/src/lib/walletAdapter/serialization.ts @@ -0,0 +1,45 @@ +import { Transaction, VersionedTransaction } from '@solana/web3.js' +import type { WalletAdapterTransaction } from './types' + +// A serialized transaction begins with a compact-u16 signature count followed +// by that many 64-byte signatures; only after them does the message start, and +// only the message's first byte carries the version marker (high bit set = v0+). +// Skip the signatures to read that marker so v0 txns (e.g. Jupiter swaps) parse +// as versioned while legacy txns parse as legacy. +export function deserializeTransaction(bytes: Uint8Array): WalletAdapterTransaction { + const { value: signatureCount, length: prefixLength } = decodeCompactU16(bytes) + const messageOffset = prefixLength + signatureCount * 64 + const isVersioned = (bytes[messageOffset] & 0x80) !== 0 + return isVersioned ? VersionedTransaction.deserialize(bytes) : Transaction.from(bytes) +} + +// Solana's compact-u16 (shortvec) encoding: 7 bits per byte, high bit = continue. +function decodeCompactU16(bytes: Uint8Array): { value: number; length: number } { + let value = 0 + let length = 0 + for (;;) { + const byte = bytes[length] + value |= (byte & 0x7f) << (length * 7) + length += 1 + if ((byte & 0x80) === 0) break + } + return { value, length } +} + +export function base64ToBytes(value: string): Uint8Array { + const binary = window.atob(value) + const bytes = new Uint8Array(binary.length) + for (let i = 0; i < binary.length; i += 1) { + bytes[i] = binary.charCodeAt(i) + } + return bytes +} + +export function bytesToBase64(bytes: Uint8Array): string { + let binary = '' + const chunkSize = 0x8000 + for (let i = 0; i < bytes.length; i += chunkSize) { + binary += String.fromCharCode(...bytes.subarray(i, i + chunkSize)) + } + return window.btoa(binary) +} diff --git a/src/lib/walletAdapter/types.ts b/src/lib/walletAdapter/types.ts new file mode 100644 index 00000000..f69936cf --- /dev/null +++ b/src/lib/walletAdapter/types.ts @@ -0,0 +1,53 @@ +import type { Transaction, VersionedTransaction } from '@solana/web3.js' + +export type WalletAdapterSupportedCluster = Exclude +export type WalletAdapterTransaction = Transaction | VersionedTransaction +export type WalletAdapterStatus = 'idle' | 'connecting' | 'connected' | 'disconnecting' | 'error' + +// Solflare is the launch partner and gets featured placement plus perk +// attribution; other providers default to 'standard'. +export type WalletPerkTier = 'partner' | 'standard' + +export interface WalletAdapterState { + provider: string | null + status: WalletAdapterStatus + network: WalletAdapterSupportedCluster | null + publicKey: string | null + error: string | null + lastSignature: string | null +} + +export interface WalletAdapterMessageSignature { + publicKey: string + signature: string + signatureBytes: number[] +} + +export interface WalletAdapterSignedTransaction { + publicKey: string + signedTransactionBase64: string +} + +/** + * A concrete wallet behind the Daemon Wallet Adapter. Shaped to the Wallet + * Standard feature set (connect / disconnect / signTransaction / signMessage) + * but registered manually: the Electron renderer runs sandboxed with no + * injected extensions, so Wallet Standard's `getWallets()` discovery is always + * empty and providers must be wired in by hand. + */ +export interface DaemonWalletProvider { + readonly id: string + readonly name: string + readonly icon: string + readonly subtitle: string + readonly isHighlighted: boolean + readonly perkTier: WalletPerkTier + /** True when the provider can actually be used in the current environment. */ + isAvailable(cluster: WalletInfrastructureSettings['cluster']): boolean + connect(cluster: WalletInfrastructureSettings['cluster']): Promise + disconnect(): Promise + signMessage(message: string): Promise + signSerializedTransaction(transactionBase64: string): Promise +} + +export type WalletAdapterListener = (state: WalletAdapterState) => void diff --git a/src/panels/WalletPanel/WalletAdapterCard.tsx b/src/panels/WalletPanel/WalletAdapterCard.tsx new file mode 100644 index 00000000..fd1ce4a6 --- /dev/null +++ b/src/panels/WalletPanel/WalletAdapterCard.tsx @@ -0,0 +1,197 @@ +import { useEffect, useState } from 'react' +import { + connectWallet, + disconnectWallet, + getProviders, + getWalletAdapterState, + signMessage, + subscribeWalletAdapter, + type DaemonWalletProvider, + type WalletAdapterState, +} from '../../lib/walletAdapter' +import { compactAddress } from '../../utils/textDisplay' + +interface WalletAdapterCardProps { + cluster: WalletInfrastructureSettings['cluster'] + preferredWallet: WalletInfrastructureSettings['preferredWallet'] + onPreferWalletStandard: () => Promise + onTrackWallet: (address: string) => Promise +} + +export function WalletAdapterCard({ + cluster, + preferredWallet, + onPreferWalletStandard, + onTrackWallet, +}: WalletAdapterCardProps) { + const [state, setState] = useState(getWalletAdapterState()) + const [busy, setBusy] = useState(false) + const [message, setMessage] = useState(null) + + useEffect(() => subscribeWalletAdapter(setState), []) + + const providers = getProviders() + const featured = providers.find((provider) => provider.isHighlighted) ?? providers[0] + const rest = providers.filter((provider) => provider !== featured) + const isConnected = state.status === 'connected' && Boolean(state.publicKey) + const isBusy = busy || state.status === 'connecting' || state.status === 'disconnecting' + const isLocalnet = cluster === 'localnet' + const isPreferred = preferredWallet === 'wallet-standard' + + const connect = async (provider: DaemonWalletProvider) => { + if (!provider.isAvailable(cluster)) { + setMessage(`${provider.name} is not available on this network yet`) + return + } + setBusy(true) + setMessage(null) + try { + await connectWallet(provider.id, cluster) + setMessage(`${provider.name} connected`) + } catch (error) { + setMessage(error instanceof Error ? error.message : `${provider.name} connection failed`) + } finally { + setBusy(false) + } + } + + const disconnect = async () => { + setBusy(true) + setMessage(null) + try { + await disconnectWallet() + setMessage('Wallet disconnected') + } catch (error) { + setMessage(error instanceof Error ? error.message : 'Disconnect failed') + } finally { + setBusy(false) + } + } + + const signCheck = async () => { + setBusy(true) + setMessage(null) + try { + const signature = await signMessage(`DAEMON wallet adapter check on ${cluster}`) + setMessage(`Signed ${compactAddress(signature.signature)}`) + } catch (error) { + setMessage(error instanceof Error ? error.message : 'Signature failed') + } finally { + setBusy(false) + } + } + + const trackWallet = async () => { + if (!state.publicKey) return + setBusy(true) + setMessage(null) + try { + await onTrackWallet(state.publicKey) + setMessage('Address tracked') + } catch (error) { + setMessage(error instanceof Error ? error.message : 'Could not track address') + } finally { + setBusy(false) + } + } + + return ( +
+
+
+
DAEMON Wallet Adapter
+
+ Connect any Solana wallet. Solflare is the recommended partner for external signing while DAEMON keeps local signers available for internal execution. +
+
+ + {isConnected ? 'Connected' : isPreferred ? 'Preferred' : 'Available'} + +
+ + {isConnected ? ( + + ) : ( +
+ connect(featured)} disabled={isBusy} /> + {rest.length > 0 &&
MORE WALLETS
} + {rest.map((provider) => ( + connect(provider)} + disabled={isBusy} + /> + ))} +
+ )} + + {state.error &&
{state.error}
} + {message &&
{message}
} + +
+ {isConnected && ( + + )} + + + +
+
+ ) +} + +interface ProviderRowProps { + provider: DaemonWalletProvider + cluster: WalletInfrastructureSettings['cluster'] + featured?: boolean + disabled?: boolean + onClick: () => void +} + +function ProviderRow({ provider, cluster, featured = false, disabled = false, onClick }: ProviderRowProps) { + const available = provider.isAvailable(cluster) + return ( + + ) +} + +function ConnectedAccount({ state, cluster }: { state: WalletAdapterState; cluster: WalletInfrastructureSettings['cluster'] }) { + return ( +
+
+
Provider
+
{state.provider ?? '—'}
+
Active external signer.
+
+
+
Address
+
{state.publicKey ? compactAddress(state.publicKey) : 'Not connected'}
+
{state.network ? `Connected on ${state.network}` : `Network ${cluster}`}
+
+
+ ) +} diff --git a/src/panels/WalletPanel/WalletPanel.css b/src/panels/WalletPanel/WalletPanel.css index a21b06df..d007c698 100644 --- a/src/panels/WalletPanel/WalletPanel.css +++ b/src/panels/WalletPanel/WalletPanel.css @@ -27,43 +27,11 @@ align-self: flex-start; } +/* Header KPI strip — fused hairline grid via the shared KpiGrid primitive. + Constrain width so it doesn't span the whole header. */ .wallet-workspace-metrics { - display: grid; - grid-template-columns: repeat(3, 110px); - gap: 12px; -} - -.wallet-panel .wallet-workspace-metric { - display: flex; - flex-direction: column; - gap: 4px; - padding: 10px 12px; - border: 1px solid var(--s5); - border-radius: var(--drawer-card-radius); - background: var(--s1); - box-shadow: none; - min-width: 0; -} - -.wallet-workspace-metric-label { - font-size: var(--fs-9); - color: var(--t4); - text-transform: uppercase; - letter-spacing: 0.08em; -} - -.wallet-workspace-metric-value { - font-size: var(--fs-16); - font-weight: 700; - line-height: 1.1; - color: var(--t1); - font-variant-numeric: tabular-nums; -} - -@container wallet (min-width: 1000px) { - .wallet-workspace-metrics { - grid-template-columns: repeat(3, 128px); - } + grid-template-columns: repeat(3, minmax(0, 1fr)); + max-width: 420px; } /* ── Tabs ── */ @@ -146,7 +114,7 @@ .wallet-action-btn { height: 40px; font-size: var(--fs-13); - border-radius: 10px; + border-radius: var(--r-panel); } .wallet-feed-row, @@ -301,39 +269,9 @@ flex-direction: column; } +/* Balance KPIs — fused hairline grid via the shared KpiGrid primitive. */ .wallet-portfolio-grid { - display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 10px; -} - -.wallet-portfolio-card { - display: flex; - flex-direction: column; - gap: 5px; - padding: 12px; - border: 1px solid var(--s4); - border-radius: 12px; - background: var(--s2); -} - -.wallet-portfolio-label { - font-size: var(--fs-9); - color: var(--t4); - text-transform: uppercase; - letter-spacing: 0.08em; -} - -.wallet-portfolio-value { - font-size: var(--fs-15); - line-height: 1.2; - color: var(--t1); -} - -.wallet-portfolio-meta { - font-size: var(--fs-11); - color: var(--t3); - line-height: 1.35; } /* ── Quick Actions (Send / Swap / Receive / Vault) ── */ @@ -375,7 +313,7 @@ .wallet-action-btn.active { color: var(--green); - border-color: rgba(62, 207, 142, 0.35); + border-color: var(--green); background: var(--green-glow); } @@ -401,8 +339,7 @@ } .wallet-input:focus { - border-color: var(--green); - box-shadow: 0 0 0 2px var(--green-glow); + border-color: var(--accent-line); } .wallet-input::placeholder { @@ -425,36 +362,39 @@ border-top: 1px solid var(--s4); } +/* Wallet buttons inherit the shared Solana grammar (flat hairline, + muted-green primary) so they read identically to .sol-btn across + the three panels. Class names kept to avoid churn in form markup. */ .wallet-btn { - height: 32px; - padding: 0 14px; - border-radius: var(--radius-lg); + height: var(--btn-h-lg); + padding: 0 var(--space-md); + border-radius: var(--r); font-size: var(--fs-12); - font-weight: 500; - color: var(--t2); - background: var(--s2); - border: 1px solid var(--s5); + font-weight: var(--fw-medium); + color: var(--t1); + background: transparent; + border: 1px solid var(--line-2); cursor: pointer; - transition: all 0.12s; + transition: background var(--transition-fast), border-color var(--transition-fast), color var(--transition-fast); font-family: var(--font-ui); } .wallet-btn:hover { - background: var(--s3); + background: var(--s1); color: var(--t1); - border-color: var(--s6); + border-color: var(--line-strong); } .wallet-btn.primary { - background: var(--green); - border-color: var(--green); - color: var(--bg); - font-weight: 600; + background: var(--btn-pri-bg); + border-color: var(--accent-line); + color: var(--btn-pri-text); + font-weight: var(--fw-medium); } .wallet-btn.primary:hover { - background: var(--green-dim); - border-color: var(--green-dim); + background: color-mix(in srgb, var(--green) 18%, var(--btn-pri-bg)); + border-color: var(--accent-line); } .wallet-btn.primary:disabled { @@ -464,13 +404,13 @@ .wallet-btn.primary-soft { color: var(--green); - border-color: rgba(62, 207, 142, 0.26); - background: rgba(62, 207, 142, 0.1); + border-color: var(--accent-line); + background: var(--accent-line); } .wallet-btn.primary-soft:hover { - border-color: rgba(62, 207, 142, 0.38); - background: rgba(62, 207, 142, 0.16); + border-color: var(--green); + background: var(--accent-line); color: var(--green); } @@ -481,14 +421,14 @@ .wallet-btn.danger:hover { background: var(--red-glow); - border-color: rgba(239, 83, 80, 0.3); + border-color: color-mix(in srgb, var(--red) 30%, transparent); color: var(--red); } .wallet-icon-btn { height: 28px; padding: 0 10px; - border-radius: 6px; + border-radius: var(--r-panel); font-size: var(--fs-11); font-weight: 500; color: var(--t3); @@ -552,11 +492,19 @@ font-variant-numeric: tabular-nums; } -.wallet-list, +.wallet-list { + display: flex; + flex-direction: column; + gap: 0; +} + +/* Holdings preview — single bordered container; flush DataRows fuse on hairlines. */ .wallet-holdings { display: flex; flex-direction: column; gap: 0; + border: 1px solid var(--line); + background: var(--s1); } .wallet-holding-main { @@ -654,8 +602,8 @@ .wallet-transaction-preview { border: 1px solid var(--s5); - border-radius: 10px; - background: rgba(255, 255, 255, 0.03); + border-radius: var(--r-panel); + background: var(--wash); padding: 12px; display: flex; flex-direction: column; @@ -755,10 +703,10 @@ .wallet-transaction-preview-warning { padding: 8px 10px; - border: 1px solid rgba(239, 68, 68, 0.28); + border: 1px solid color-mix(in srgb, var(--red) 28%, transparent); border-radius: var(--radius-lg); color: var(--red); - background: rgba(239, 68, 68, 0.08); + background: color-mix(in srgb, var(--red) 8%, transparent); font-size: var(--fs-12); line-height: 1.45; } @@ -914,7 +862,7 @@ padding: 0 8px; border-radius: var(--radius-pill); border: 1px solid var(--s5); - background: rgba(255, 255, 255, 0.03); + background: var(--wash); } @media (max-width: 900px) { @@ -986,7 +934,7 @@ .wallet-state-badge.live { color: var(--green); - border-color: rgba(62, 207, 142, 0.22); + border-color: var(--accent-line); background: var(--green-glow); } @@ -1010,9 +958,9 @@ gap: 8px; min-width: 0; padding: 12px; - border: 1px solid rgba(255, 255, 255, 0.06); - border-radius: 12px; - background: rgba(255, 255, 255, 0.02); + border: 1px solid var(--line); + border-radius: var(--r-panel); + background: var(--wash); } .wallet-runtime-summary-label { @@ -1068,8 +1016,8 @@ align-items: flex-start; justify-content: space-between; gap: 18px; - border-color: color-mix(in srgb, var(--green) 18%, var(--border) 82%); - background: linear-gradient(135deg, rgba(62, 207, 142, 0.1), rgba(255, 255, 255, 0.02)); + border-color: var(--accent-line); + background: var(--accent-fill); } .wallet-first-run-title { @@ -1103,13 +1051,13 @@ .wallet-row-card { padding: 14px; border: 1px solid var(--s4); - border-radius: 12px; - background: rgba(255, 255, 255, 0.02); + border-radius: var(--r-panel); + background: var(--wash); } .wallet-row-card:hover { border-color: var(--s5); - background: rgba(255, 255, 255, 0.03); + background: var(--wash); } .wallet-row-main-top { @@ -1144,7 +1092,7 @@ padding: 0 8px; border-radius: var(--radius-pill); border: 1px solid var(--s5); - background: rgba(255, 255, 255, 0.03); + background: var(--wash); font-size: var(--fs-10); font-weight: 600; letter-spacing: 0.04em; @@ -1154,8 +1102,8 @@ .wallet-pill.live { color: var(--green); - border-color: rgba(62, 207, 142, 0.22); - background: rgba(62, 207, 142, 0.1); + border-color: var(--accent-line); + background: var(--accent-line); } .wallet-pill.muted { @@ -1175,7 +1123,7 @@ gap: 12px; flex-wrap: wrap; padding-top: 12px; - border-top: 1px solid rgba(255, 255, 255, 0.05); + border-top: 1px solid var(--line); } .wallet-actions-card-main, @@ -1246,10 +1194,13 @@ font-size: var(--fs-11); } +/* Readiness 4-up — fused hairline grid (the 1px gap is the shared border). */ .wallet-readiness-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: var(--space-sm); + gap: 1px; + background: var(--line); + border: 1px solid var(--line); } .wallet-readiness-item { @@ -1257,6 +1208,10 @@ grid-template-columns: auto minmax(0, 1fr); gap: var(--space-sm); align-items: flex-start; + padding: 16px 18px; + border: 0; + border-radius: 0; + background: var(--bg); } .wallet-readiness-item strong { @@ -1462,7 +1417,7 @@ justify-content: center; width: 20px; height: 20px; - border-radius: 4px; + border-radius: var(--r); color: var(--t4); cursor: pointer; background: none; @@ -1500,7 +1455,7 @@ display: flex; align-items: center; justify-content: center; - border-radius: 6px; + border-radius: var(--r-panel); color: var(--t3); background: none; border: 1px solid var(--s5); @@ -1516,13 +1471,13 @@ .wallet-agent-action-btn.active { color: var(--green); - border-color: rgba(62, 207, 142, 0.35); + border-color: var(--green); background: var(--green-glow); } .wallet-agent-action-btn--danger:hover { color: var(--red); - border-color: rgba(239, 83, 80, 0.3); + border-color: color-mix(in srgb, var(--red) 30%, transparent); background: var(--red-glow); } @@ -1532,7 +1487,7 @@ gap: 8px; padding: 10px 12px; background: var(--s1); - border-radius: 6px; + border-radius: var(--r-panel); border: 1px solid var(--s4); } @@ -1563,7 +1518,7 @@ margin-top: 8px; padding: 8px 12px; background: var(--red-glow); - border-radius: 6px; + border-radius: var(--r-panel); } .wallet-success-msg { @@ -1576,7 +1531,7 @@ margin-top: 8px; padding: 8px 12px; background: var(--green-glow); - border-radius: 6px; + border-radius: var(--r-panel); } .wallet-success-msg span { @@ -1677,7 +1632,7 @@ background: var(--s2); color: var(--t2); padding: 10px 12px; - border-radius: 6px; + border-radius: var(--r-panel); border: 1px solid var(--s4); word-break: break-all; margin-top: 8px; @@ -1733,7 +1688,6 @@ border: 1px solid var(--card-border); border-radius: var(--radius-card); background: var(--card-bg); - box-shadow: var(--shadow-sm); } .wallet-onramp-head { @@ -1818,7 +1772,7 @@ align-items: center; justify-content: center; padding: 0; - border-radius: 6px; + border-radius: var(--r-panel); } .wallet-swap-mint-row { @@ -1965,7 +1919,7 @@ padding: 4px; display: flex; align-items: center; - border-radius: 4px; + border-radius: var(--r); transition: color 0.12s; } @@ -1994,7 +1948,7 @@ color: var(--red); padding: 8px 12px; background: var(--red-glow); - border-radius: 6px; + border-radius: var(--r-panel); } .vault-actions { @@ -2005,7 +1959,7 @@ .vault-btn { font-size: var(--fs-12); padding: 6px 12px; - border-radius: 6px; + border-radius: var(--r-panel); border: 1px solid var(--s5); background: var(--s2); color: var(--t2); @@ -2017,12 +1971,12 @@ .vault-btn.primary { background: var(--green-glow); - border-color: rgba(62, 207, 142, 0.25); + border-color: var(--accent-line); color: var(--green); } .vault-btn.primary:hover { - background: rgba(62, 207, 142, 0.18); + background: var(--accent-line); } .vault-btn:disabled { opacity: 0.4; cursor: default; } @@ -2041,7 +1995,7 @@ .vault-select { font-size: var(--fs-12); padding: 8px 10px; - border-radius: 6px; + border-radius: var(--r-panel); border: 1px solid var(--s5); background: var(--s2); color: var(--t1); @@ -2051,7 +2005,7 @@ .vault-textarea { font-size: var(--fs-12); padding: 8px 10px; - border-radius: 6px; + border-radius: var(--r-panel); border: 1px solid var(--s5); background: var(--s2); color: var(--t1); @@ -2125,7 +2079,7 @@ .vault-btn-sm { font-size: var(--fs-11); padding: 4px 10px; - border-radius: 4px; + border-radius: var(--r); border: 1px solid var(--s5); background: var(--s2); color: var(--t2); @@ -2135,7 +2089,7 @@ .vault-btn-sm:hover { background: var(--s3); color: var(--t1); } .vault-btn-sm.active { border-color: var(--green); color: var(--green); } -.vault-btn-sm.danger { border-color: rgba(239, 83, 80, 0.3); color: var(--red); } +.vault-btn-sm.danger { border-color: color-mix(in srgb, var(--red) 30%, transparent); color: var(--red); } .vault-btn-sm.danger:hover { background: var(--red-glow); } .vault-reveal { @@ -2151,7 +2105,7 @@ color: var(--t2); background: var(--bg); padding: 10px 12px; - border-radius: 6px; + border-radius: var(--r-panel); border: 1px solid var(--s4); max-height: 140px; overflow: auto; @@ -2165,7 +2119,7 @@ .pnl-sync-btn { height: 24px; padding: 0 10px; - border-radius: 6px; + border-radius: var(--r-panel); font-size: var(--fs-10); font-weight: 600; color: var(--t3); @@ -2177,7 +2131,7 @@ .pnl-sync-btn:hover:not(:disabled) { color: var(--green); - border-color: rgba(62, 207, 142, 0.3); + border-color: color-mix(in srgb, var(--green) 30%, transparent); background: var(--green-glow); } @@ -2267,16 +2221,13 @@ .wallet-workspace-metrics { grid-template-columns: 1fr; width: 100%; + max-width: none; } .wallet-workspace-actions { width: 100%; } - .wallet-panel .wallet-workspace-metric { - width: 100%; - } - .wallet-tabs { padding-inline: var(--space-lg); } @@ -2379,7 +2330,6 @@ letter-spacing: 0.1em; } -.wallet-portfolio-card, .wallet-settings-card, .wallet-overview-card, .wallet-move-card, @@ -2426,12 +2376,10 @@ border-color: var(--panel-border); } -.wallet-portfolio-card, .wallet-settings-card, .wallet-runtime-summary-item, .wallet-row-card, .wallet-readiness-hero, -.wallet-readiness-item, .wallet-overview-card, .wallet-move-card, .wallet-send-form, @@ -2444,7 +2392,6 @@ border-color: var(--card-border); border-radius: var(--radius-card); background: var(--card-bg); - box-shadow: var(--shadow-sm); } .wallet-row-card:hover, @@ -2513,9 +2460,7 @@ .wallet-first-run, .wallet-empty-route { - background: - linear-gradient(180deg, color-mix(in srgb, var(--green) 8%, transparent), transparent), - var(--card-bg); + background: var(--card-bg); } .wallet-error-msg, @@ -2524,3 +2469,134 @@ background: var(--red-glow); border-color: color-mix(in srgb, var(--red) 28%, var(--card-border)); } + +/* ── Daemon Wallet Adapter ── */ + +.wallet-adapter-list { + display: flex; + flex-direction: column; + gap: 8px; + margin: 12px 0; +} + +.wallet-adapter-sublabel { + font-family: var(--font-mono); + font-size: var(--fs-11); + letter-spacing: 0.12em; + color: var(--t4); + padding: 6px 2px 2px; +} + +.wallet-adapter-row { + display: flex; + align-items: center; + gap: 12px; + width: 100%; + text-align: left; + padding: 11px 12px; + background: var(--s2); + border: 1px solid var(--line); + border-radius: var(--radius-lg); + cursor: pointer; + transition: background 0.12s, border-color 0.12s; +} + +.wallet-adapter-row:hover:not(:disabled) { + background: var(--s3); + border-color: var(--s6); +} + +.wallet-adapter-row:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.wallet-adapter-row.featured { + background: var(--green-glow); + border-color: var(--accent-line); + padding: 13px 12px; +} + +.wallet-adapter-logo { + flex: none; + display: block; + border-radius: 7px; + object-fit: cover; +} + +.wallet-adapter-id { + display: flex; + flex-direction: column; + gap: 3px; + min-width: 0; + flex: 1; +} + +.wallet-adapter-name { + display: inline-flex; + align-items: center; + gap: 7px; + font-size: var(--fs-13); + font-weight: 600; + color: var(--t1); +} + +.wallet-adapter-sub { + font-family: var(--font-mono); + font-size: var(--fs-11); + color: var(--t3); +} + +.wallet-adapter-cue { + flex: none; + font-family: var(--font-mono); + font-size: var(--fs-11); + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--t4); +} + +.wallet-adapter-row.featured .wallet-adapter-cue { + color: var(--green); +} + +/* ── Primary view nav (SegmentedControl + utility actions) ── + Replaces the old 7-button .wallet-quick-actions bar. */ +.wallet-nav-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--space-md); + margin-top: var(--space-lg); + flex-wrap: wrap; +} + +.wallet-nav-seg { + flex: 1; + min-width: 0; +} + +/* Wallet cards now compose Surface + DataRow; this class only adds the + action cluster spacing below the identity row. */ +.wallet-row-card { + display: flex; + flex-direction: column; + gap: var(--space-md); +} + +.wallet-actions-card { + display: flex; + flex-direction: column; + gap: var(--space-sm); +} + +.wallet-feed-delta { + font-family: var(--mono); + font-variant-numeric: tabular-nums; + font-size: var(--fs-12); +} + +@container wallet (max-width: 460px) { + .wallet-nav-row { flex-direction: column; align-items: stretch; } + .wallet-nav-seg { width: 100%; } +} diff --git a/src/panels/WalletPanel/WalletSettings.tsx b/src/panels/WalletPanel/WalletSettings.tsx index bf5c5111..8fa8d557 100644 --- a/src/panels/WalletPanel/WalletSettings.tsx +++ b/src/panels/WalletPanel/WalletSettings.tsx @@ -1,6 +1,7 @@ import { useEffect, useState } from 'react' import { Toggle } from '../../components/Toggle' import { SolflareWalletCard } from './SolflareWalletCard' +import { WalletAdapterCard } from './WalletAdapterCard' import './WalletPanel.css' interface WalletSettingsProps { @@ -82,6 +83,12 @@ export function WalletSettings({ await onSaveInfrastructure(next) } + const preferWalletStandard = async () => { + const next = { ...draftInfra, preferredWallet: 'wallet-standard' as const } + setDraftInfra(next) + await onSaveInfrastructure(next) + } + return (
@@ -102,7 +109,7 @@ export function WalletSettings({ token launch execution, toolbox guidance, and the Solana runtime it will keep expanding.
- + {rpcStatusLabel} @@ -303,7 +310,7 @@ export function WalletSettings({ > - +