import React from 'react' import { Box, Text } from 'ink' import { Surface } from '../../../ui/Surface.js' import { Select } from '../../../ui/Select.js' import { Spinner } from '../../../ui/Spinner.js' import { TextInput } from '../../../ui/TextInput.js' import { theme } from '../../../ui/theme.js' import { useAppInput } from '../../../app/input/AppInputProvider.js' import { openExternalUrl } from '../../../utils/openExternal.js' import type { EthagentIdentity } from '../../../storage/config.js' import type { BrowserWalletReady } from '../../wallet/browserWallet.js' import type { TokenTransferProgress } from '../shared/effects/types.js' import { readCustodyMode } from '../custody/state.js' import { shortAddress, shortCid } from '../shared/model/format.js' import { FlowTimeline } from '../shared/components/FlowTimeline.js' import { FieldRow } from '../shared/components/FieldRow.js' import { OPEN_BROWSER_HINT } from '../shared/components/WalletApprovalScreen.js' const TRANSFER_STEPS = ['Choose Receiver', 'Sender Signs', 'Receiver Signs', 'Sender Updates URI', 'Transfer Token'] type TokenTransferTargetScreenProps = { identity: EthagentIdentity tokenNetworkLabel: string error?: string initialValue?: string onSubmit: (value: string) => void onBack: () => void } export const TokenTransferTargetScreen: React.FC = ({ identity, tokenNetworkLabel, error, initialValue, onSubmit, onBack, }) => { const tokenValue = identity.agentId ? `#${identity.agentId}` : 'not created' const senderValue = shortAddress(identity.ownerAddress ?? identity.address) const custodyMode = readCustodyMode(identity.state) return ( } footer={↵ next · esc back} > {custodyMode === 'advanced' ? ( Advanced custody: owner wallet signs. ) : null} No token approval is requested. validateTargetInput(value)} onSubmit={onSubmit} onCancel={onBack} /> {error ? {error} : null} ) } type TokenTransferSigningScreenProps = { identity: EthagentIdentity tokenNetworkLabel: string targetHandle: string targetAddress: string progress: TokenTransferProgress | null walletSession: BrowserWalletReady | null onCancel: () => void } export const TokenTransferSigningScreen: React.FC = ({ identity, tokenNetworkLabel, targetHandle, targetAddress, progress, walletSession, onCancel, }) => { useAppInput((input, key) => { if (key.escape || (key.ctrl && input === 'c')) onCancel() if (key.return && walletSession?.url) { openExternalUrl(walletSession.url) } }, { isActive: true }) const senderAddress = identity.ownerAddress ?? identity.address const resolvedProgress = progress ?? { phase: 'sender-sign' as const, walletRole: 'sender' as const, expectedAddress: senderAddress as TokenTransferProgress['expectedAddress'], title: 'Use Sender Wallet', detail: 'Sign to save a transfer snapshot.', walletAction: 'Sign Snapshot', label: 'preparing transfer snapshot...', } const phase = resolvedProgress.phase const spinnerLabel = tokenTransferSpinnerLabel(resolvedProgress) const activeRoleLabel = resolvedProgress.walletRole === 'receiver' ? 'Receiver' : 'Sender' const otherWallet = resolvedProgress.walletRole === 'sender' ? { label: 'Receiver', value: `${shortAddress(targetAddress)}${targetHandle !== targetAddress ? ` (${targetHandle})` : ''}` } : { label: 'Sender', value: shortAddress(senderAddress) } return ( } footer={esc back} > {resolvedProgress.title} {resolvedProgress.detail} {resolvedProgress.expectedAddress ? ( <> ) : null} {walletSession ? ( {walletSession.url} {OPEN_BROWSER_HINT} ) : null} ) } type TokenTransferReadyScreenProps = { identity: EthagentIdentity tokenNetworkLabel: string targetHandle: string targetAddress: string snapshotCid: string txHash: string footer: React.ReactNode backHint: string onBack: () => void } export const TokenTransferReadyScreen: React.FC = ({ identity, tokenNetworkLabel, targetHandle, targetAddress, snapshotCid, txHash, footer, backHint, onBack, }) => ( } footer={footer} > Transfer the token externally, then restore with the receiver wallet. No token approval is requested. options={[ { value: 'back', role: 'section', label: 'Navigation' }, { value: 'back', label: 'Back', hint: backHint, role: 'utility' }, ]} hintLayout="inline" onSubmit={onBack} onCancel={onBack} /> ) type StatusRowProps = { label: string; value: string; tone?: 'ok' | 'warn'; emphasize?: boolean } const StatusRow: React.FC = ({ label, value, tone, emphasize }) => { const color = emphasize ? theme.accentPeriwinkle : tone === 'ok' ? theme.text : tone === 'warn' ? theme.accentPeriwinkle : theme.text return ( {value}} /> ) } function transferTimelineStep(phase: TokenTransferProgress['phase'] | undefined): number { switch (phase) { case 'sender-sign': return 2 case 'target-sign': return 3 case 'pinning': case 'sender-transaction': case 'confirming': return 4 default: return 2 } } function validateTargetInput(value: string): string | null { const trimmed = value.trim() if (!trimmed) return 'Enter the receiver wallet ENS name or 0x address' if (trimmed.startsWith('0x') && !/^0x[0-9a-fA-F]{40}$/.test(trimmed)) return 'Enter a valid 0x address' if (!trimmed.startsWith('0x') && !trimmed.includes('.')) return 'Enter an ENS name or 0x address' return null } function shortHash(hash: string): string { return hash.length > 14 ? `${hash.slice(0, 10)}...${hash.slice(-6)}` : hash } function tokenTransferSpinnerLabel(progress: TokenTransferProgress): string { const label = progress.walletRole === 'none' ? progress.title : progress.walletAction ?? progress.title return `${label.replace(/[.]+$/g, '')}...` }