diff --git a/app/src/agentworld/assets.test.ts b/app/src/agentworld/assets.test.ts new file mode 100644 index 000000000..b00b4406f --- /dev/null +++ b/app/src/agentworld/assets.test.ts @@ -0,0 +1,57 @@ +/** + * Tests for Agent World asset helpers — symbol/decimals resolution and the + * base-unit → human amount formatter shared by marketplace prices, bounty + * rewards, and the x402 confirm dialog. + */ +import { describe, expect, test } from 'vitest'; + +import { decimalsForAsset, formatAssetAmount, formatUnits, resolveAssetSymbol } from './assets'; + +describe('formatUnits', () => { + test('shifts the decimal point by the asset decimals', () => { + expect(formatUnits('30000000', 6)).toBe('30'); + expect(formatUnits('10500000', 6)).toBe('10.5'); + expect(formatUnits('1', 6)).toBe('0.000001'); + }); + + test('returns the raw string unchanged for zero decimals', () => { + expect(formatUnits('42', 0)).toBe('42'); + }); +}); + +describe('formatAssetAmount', () => { + test('humanizes a base-unit USDC price so display matches decimal input', () => { + // A 30 USDC listing is stored as "30000000" base units (USDC = 6 decimals). + // It MUST render as the human "30 USDC" — the same value a user types into + // AmountCommitDialog — not the raw base-unit string. + expect(formatAssetAmount('30000000', 'USDC')).toBe('30 USDC'); + }); + + test('keeps the fractional part of a non-round base-unit amount', () => { + expect(formatAssetAmount('10500000', 'USDC')).toBe('10.5 USDC'); + }); + + test('groups large amounts with thousands separators', () => { + // 1,234,000 USDC = 1234000 * 10^6 base units. + expect(formatAssetAmount('1234000000000', 'USDC')).toBe('1,234,000 USDC'); + }); + + test('resolves a USDC mint address to symbol + decimals', () => { + // Mainnet USDC SPL mint → 30 USDC. + expect(formatAssetAmount('30000000', 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v')).toBe( + '30 USDC' + ); + }); + + test('renders an unknown (0-decimal) asset verbatim', () => { + // No known decimals → no division, no double-conversion of an unknown unit. + expect(formatAssetAmount('500', 'MYSTERY')).toBe('500 MYSTERY'); + }); +}); + +describe('asset resolution sanity', () => { + test('USDC resolves to 6 decimals', () => { + expect(decimalsForAsset('USDC')).toBe(6); + expect(resolveAssetSymbol('USDC')).toBe('USDC'); + }); +}); diff --git a/app/src/agentworld/assets.ts b/app/src/agentworld/assets.ts index 289f5f1bf..42d7ec3f8 100644 --- a/app/src/agentworld/assets.ts +++ b/app/src/agentworld/assets.ts @@ -46,3 +46,54 @@ export function decimalsForAsset(asset: string | undefined, walletDecimals?: num if (typeof walletDecimals === 'number') return walletDecimals; return decimalsForSymbol(resolveAssetSymbol(asset)); } + +/** + * Format a raw base-unit integer string to a decimal string with `decimals`. + * + * e.g. `formatUnits("30000000", 6)` → `"30"`, `formatUnits("10500000", 6)` → + * `"10.5"`. Pure string math (no float) so large amounts never lose precision. + * + * Lives here (not in X402ConfirmDialog) because both the confirm dialog AND the + * marketplace price/reward formatters need it; X402ConfirmDialog re-exports it so + * its existing public API is unchanged. + */ +export function formatUnits(raw: string, decimals: number): string { + if (decimals <= 0) return raw; + const negative = raw.startsWith('-'); + const digits = (negative ? raw.slice(1) : raw).padStart(decimals + 1, '0'); + const whole = digits.slice(0, digits.length - decimals); + const frac = digits.slice(digits.length - decimals).replace(/0+$/, ''); + const body = frac ? `${whole}.${frac}` : whole; + return negative ? `-${body}` : body; +} + +/** Group the integer part of a numeric amount with thousands separators. */ +function groupThousands(amount: string): string { + if (!Number.isFinite(Number(amount))) return amount; + const negative = amount.startsWith('-'); + const body = negative ? amount.slice(1) : amount; + const [intPart, fracPart] = body.split('.'); + const grouped = Number(intPart).toLocaleString('en-US'); + const out = fracPart != null ? `${grouped}.${fracPart}` : grouped; + return negative ? `-${out}` : out; +} + +/** + * Format a marketplace / x402 amount that is stored in the asset's smallest BASE + * units (e.g. a 30 USDC price is stored as `"30000000"`) into a human-readable + * string like `"30 USDC"`. + * + * The asset's decimals are resolved from its symbol/mint via {@link decimalsForAsset} + * (USDC → 6) because marketplace price objects carry no `decimals` field — we + * assume the conventional decimals for the known symbol and default to 0 for + * unknown assets (rendered verbatim, since we can't safely humanize them). + * + * Display unit MUST match the human-decimal INPUT unit used by AmountCommitDialog + * — both go through the same decimals, so a price shown as "30 USDC" is the value + * a user types when bidding/offering. + */ +export function formatAssetAmount(amount: string, asset: string): string { + const decimals = decimalsForAsset(asset); + const display = decimals > 0 ? formatUnits(amount, decimals) : amount; + return `${groupThousands(display)} ${resolveAssetSymbol(asset)}`; +} diff --git a/app/src/agentworld/components/AmountCommitDialog.test.tsx b/app/src/agentworld/components/AmountCommitDialog.test.tsx index 62f256a99..1f3bbc102 100644 --- a/app/src/agentworld/components/AmountCommitDialog.test.tsx +++ b/app/src/agentworld/components/AmountCommitDialog.test.tsx @@ -1,39 +1,90 @@ /** * Tests for AmountCommitDialog — the amount-entry dialog for x402 bid/offer - * commitments. Generic placeholders only. + * commitments. The dialog now accepts a HUMAN decimal amount and emits BASE + * units (× 10^decimals) to the existing onSubmit contract. Generic placeholders + * only. */ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { describe, expect, test, vi } from 'vitest'; -import AmountCommitDialog from './AmountCommitDialog'; +import AmountCommitDialog, { parseHumanAmount } from './AmountCommitDialog'; function baseProps() { return { title: 'Bid on @handle', asset: 'USDC', - submitLabel: 'Place bid', + decimals: 6, + submitLabel: 'Continue', onSubmit: vi.fn(), onCancel: vi.fn(), }; } +describe('parseHumanAmount', () => { + test('scales a human decimal amount to base units', () => { + expect(parseHumanAmount('1.5', 6)).toMatchObject({ base: '1500000', valid: true }); + expect(parseHumanAmount('1', 6)).toMatchObject({ base: '1000000', valid: true }); + expect(parseHumanAmount('0.000001', 6)).toMatchObject({ base: '1', valid: true }); + expect(parseHumanAmount('35', 6)).toMatchObject({ base: '35000000', valid: true }); + }); + + test('empty / partial input is not valid but carries no error', () => { + expect(parseHumanAmount('', 6)).toMatchObject({ valid: false, errorKey: null }); + expect(parseHumanAmount('.', 6)).toMatchObject({ valid: false, errorKey: null }); + }); + + test('rejects more decimal places than the asset supports', () => { + expect(parseHumanAmount('1.1234567', 6)).toMatchObject({ + valid: false, + errorKey: 'agentWorld.trading.amountTooManyDecimals', + }); + }); + + test('rejects zero and non-positive amounts', () => { + expect(parseHumanAmount('0', 6)).toMatchObject({ + valid: false, + errorKey: 'agentWorld.trading.amountMustBePositive', + }); + expect(parseHumanAmount('0.0', 6)).toMatchObject({ + valid: false, + errorKey: 'agentWorld.trading.amountMustBePositive', + }); + }); +}); + describe('AmountCommitDialog', () => { - test('submit is disabled until a numeric amount is entered', async () => { + test('submit is disabled until a valid amount is entered', async () => { render(); expect(screen.getByTestId('commit-submit')).toBeDisabled(); - await userEvent.type(screen.getByTestId('commit-amount-input'), '500'); + await userEvent.type(screen.getByTestId('commit-amount-input'), '5'); expect(screen.getByTestId('commit-submit')).toBeEnabled(); }); - test('input strips non-digits and submit reports the sanitized amount', async () => { + test('a human decimal amount is converted to base units on submit', async () => { const props = baseProps(); render(); const input = screen.getByTestId('commit-amount-input') as HTMLInputElement; - await userEvent.type(input, '1a2b3'); - expect(input.value).toBe('123'); + await userEvent.type(input, '1.5'); + expect(input.value).toBe('1.5'); await userEvent.click(screen.getByTestId('commit-submit')); - expect(props.onSubmit).toHaveBeenCalledWith('123'); + expect(props.onSubmit).toHaveBeenCalledWith('1500000'); + }); + + test('input strips letters and collapses to a single decimal point', async () => { + render(); + const input = screen.getByTestId('commit-amount-input') as HTMLInputElement; + await userEvent.type(input, '1a2.3.4b'); + // letters dropped, only the first dot kept: "12.34" + expect(input.value).toBe('12.34'); + }); + + test('shows an error and blocks submit when too many decimals are entered', async () => { + const props = baseProps(); + render(); + await userEvent.type(screen.getByTestId('commit-amount-input'), '1.1234567'); + expect(screen.getByTestId('commit-amount-error')).toBeInTheDocument(); + expect(screen.getByTestId('commit-submit')).toBeDisabled(); }); test('cancel calls onCancel', async () => { diff --git a/app/src/agentworld/components/AmountCommitDialog.tsx b/app/src/agentworld/components/AmountCommitDialog.tsx index b9746fe56..3b6cef3ff 100644 --- a/app/src/agentworld/components/AmountCommitDialog.tsx +++ b/app/src/agentworld/components/AmountCommitDialog.tsx @@ -1,16 +1,21 @@ /** - * AmountCommitDialog — amount-entry confirm dialog for x402 *commitments* - * (bids and offers). + * AmountCommitDialog — amount-entry dialog for x402 *commitments* (bids/offers). * * Unlike X402ConfirmDialog (which gates an immediate on-chain spend), a bid/offer * is a signed authorization that only settles if accepted — so there is no * balance gate and no on-chain transfer at submit time. The user enters an - * amount and submits; the parent owns the RPC call. + * amount; the parent owns the next step (a confirm review, then the RPC). + * + * UX: the input accepts a HUMAN decimal amount (e.g. "1.5" USDC). It is converted + * to base units (× 10^decimals) before `onSubmit`, so the user never has to type + * raw lamport-style integers and the downstream RPC contract (a base-units + * string) is unchanged. */ import { useState } from 'react'; import Button from '../../components/ui/Button'; import { ModalShell } from '../../components/ui/ModalShell'; +import { useT } from '../../lib/i18n/I18nContext'; export interface AmountCommitDialogProps { /** Header title, e.g. "Bid on @handle". */ @@ -19,30 +24,102 @@ export interface AmountCommitDialogProps { subtitle?: string; /** Asset symbol shown next to the amount input (e.g. "USDC"). */ asset: string; - /** Submit-button label (e.g. "Place bid" / "Submit offer"). */ + /** Decimals for the asset (USDC = 6). Used to scale the human amount → base units. */ + decimals: number; + /** Submit-button label (e.g. "Continue" → review step). */ submitLabel: string; - /** Busy label while the commitment is in flight. */ + /** Busy label while the next step is in flight. */ busyLabel?: string; busy?: boolean; - /** Called with the entered amount (raw string) on submit. */ + /** Called with the amount in BASE units (string) on submit. */ onSubmit: (amount: string) => void; onCancel: () => void; } +export interface ParsedAmount { + /** Amount in base units, ready for the RPC. Only set when `valid`. */ + base: string | null; + valid: boolean; + /** i18n key for the validation problem, or null when valid / empty. */ + errorKey: string | null; +} + +/** + * Parse a human decimal amount string into base units. + * + * Rules: + * - empty → not valid, no error (submit just stays disabled). + * - more fractional digits than `decimals` → invalid (`tooManyDecimals`). + * - zero / non-positive → invalid (`mustBePositive`). + * - otherwise → base-units integer string with no leading zeros. + * + * Input is assumed already sanitized to `[0-9.]` with at most one dot (the + * onChange handler enforces that), but we re-validate defensively. + */ +export function parseHumanAmount(input: string, decimals: number): ParsedAmount { + const trimmed = input.trim(); + if (trimmed === '' || trimmed === '.') { + return { base: null, valid: false, errorKey: null }; + } + // Reject anything that isn't digits + at most one dot. + if (!/^\d*\.?\d*$/.test(trimmed)) { + return { base: null, valid: false, errorKey: 'agentWorld.trading.amountInvalid' }; + } + const [wholeRaw, fracRaw = ''] = trimmed.split('.'); + if (fracRaw.length > decimals) { + return { base: null, valid: false, errorKey: 'agentWorld.trading.amountTooManyDecimals' }; + } + const whole = wholeRaw === '' ? '0' : wholeRaw; + const frac = fracRaw.padEnd(decimals, '0'); + // Concatenate then strip leading zeros (BigInt-safe; keeps it exact). + const combined = `${whole}${frac}`.replace(/^0+(?=\d)/, ''); + let base: bigint; + try { + base = BigInt(combined); + } catch { + return { base: null, valid: false, errorKey: 'agentWorld.trading.amountInvalid' }; + } + if (base <= 0n) { + return { base: null, valid: false, errorKey: 'agentWorld.trading.amountMustBePositive' }; + } + return { base: base.toString(), valid: true, errorKey: null }; +} + +/** Keep only digits + a single decimal point as the user types. */ +function sanitizeDecimalInput(value: string): string { + // Drop everything except digits and dots, then collapse to a single dot. + const cleaned = value.replace(/[^0-9.]/g, ''); + const firstDot = cleaned.indexOf('.'); + if (firstDot === -1) return cleaned; + return cleaned.slice(0, firstDot + 1) + cleaned.slice(firstDot + 1).replace(/\./g, ''); +} + export default function AmountCommitDialog({ title, subtitle, asset, + decimals, submitLabel, - busyLabel = 'Submitting…', + busyLabel, busy = false, onSubmit, onCancel, }: AmountCommitDialogProps) { + const { t } = useT(); const [amount, setAmount] = useState(''); - // Allow only digits (base-unit integer amount); empty disables submit. - const sanitized = amount.replace(/[^0-9]/g, ''); - const canSubmit = sanitized.length > 0 && !busy; + const parsed = parseHumanAmount(amount, decimals); + const canSubmit = parsed.valid && !busy; + + function handleSubmit() { + if (!parsed.valid || parsed.base == null) return; + console.debug('[agentworld:commit-amount] submit', { + asset, + decimals, + human: amount, + base: parsed.base, + }); + onSubmit(parsed.base); + } return ( - Amount ({asset}) + {t('agentWorld.trading.amountLabel', 'Amount')} ({asset}) setAmount(e.target.value.replace(/[^0-9]/g, ''))} + onChange={e => setAmount(sanitizeDecimalInput(e.target.value))} /> + {parsed.errorKey && ( +

+ {parsed.errorKey === 'agentWorld.trading.amountTooManyDecimals' + ? t( + 'agentWorld.trading.amountTooManyDecimals', + `Use at most ${decimals} decimal places.` + ) + : parsed.errorKey === 'agentWorld.trading.amountMustBePositive' + ? t( + 'agentWorld.trading.amountMustBePositive', + 'Enter an amount greater than zero.' + ) + : t('agentWorld.trading.amountInvalid', 'Enter a valid amount.')} +

+ )}

- This is a signed commitment — funds only move if it is accepted. + {t( + 'agentWorld.trading.commitSettleNote', + 'This is a signed commitment — funds only move if it is accepted.' + )}

diff --git a/app/src/agentworld/components/CommitFlow.tsx b/app/src/agentworld/components/CommitFlow.tsx new file mode 100644 index 000000000..1f71561de --- /dev/null +++ b/app/src/agentworld/components/CommitFlow.tsx @@ -0,0 +1,157 @@ +/** + * CommitFlow — two-phase confirm-before-spend flow for x402 *commitments* + * (identity bids and offers), giving them parity with the Buy flow. + * + * Flow: + * 1. `amount` — AmountCommitDialog: enter a human decimal amount (→ base units). + * 2. `review` — X402ConfirmDialog in `mode="commit"`: show amount + asset + + * balance with a SOFT insufficient-funds warning (commitments + * settle only on acceptance, so a low balance never blocks). + * 3. `submit` — call `apiClient.marketplace.bid|offer` and report the outcome. + * + * Unlike Buy, the bid/offer RPCs have no `confirmed:false` probe that returns a + * wallet balance, and the multichain wallet RPC only reports NATIVE balances + * (SOL), not the USDC token balance the commitment is denominated in. So the + * review step renders with an UNKNOWN balance — X402ConfirmDialog shows a + * "couldn't verify balance" note and still allows Confirm (honest: we don't + * pretend the wallet can cover it; the backend is the authoritative gate). + * + * The component never touches Solana/x402 directly — every spend path goes + * through the injected `submit` callback (→ apiClient.marketplace.*). + */ +import { useState } from 'react'; + +import { useT } from '../../lib/i18n/I18nContext'; +import AmountCommitDialog from './AmountCommitDialog'; +import X402ConfirmDialog, { type X402WalletBalance } from './X402ConfirmDialog'; + +export type CommitKind = 'bid' | 'offer'; + +export interface CommitFlowProps { + kind: CommitKind; + /** Display name of the listing/handle (e.g. "@auction"). */ + name: string; + /** Asset symbol shown in both dialogs (e.g. "USDC"). */ + asset: string; + /** Decimals for `asset` (USDC = 6) — scales the human amount → base units. */ + decimals: number; + /** Network label for the review step (transparency only). */ + network?: string; + /** + * Optional wallet balance for the review step. Bid/offer have no balance probe + * today, so callers usually pass `null` → the dialog shows "couldn't verify". + */ + balance?: X402WalletBalance | null; + /** Wallet address shown (truncated) in the review step. */ + walletAddress?: string; + /** + * Perform the commitment. Receives the amount in BASE units. Resolves on + * success, rejects on failure (CommitFlow surfaces both via callbacks). + */ + submit: (amountBase: string) => Promise; + /** Fired after a successful commitment (parent shows the success banner). */ + onSuccess: () => void; + /** Fired on failure with the error message (parent shows the error banner). */ + onError: (message: string) => void; + /** Cancel/close the whole flow. */ + onClose: () => void; +} + +type Phase = + | { step: 'amount' } + | { step: 'review'; amountBase: string } + | { step: 'submitting'; amountBase: string }; + +export default function CommitFlow({ + kind, + name, + asset, + decimals, + network, + balance = null, + walletAddress = '', + submit, + onSuccess, + onError, + onClose, +}: CommitFlowProps) { + const { t } = useT(); + const [phase, setPhase] = useState({ step: 'amount' }); + + // The i18n `t(key, fallback)` has NO interpolation, so the listing `name` + // cannot be a translation param — we translate a PREFIX and compose with the + // name in code. (Keys must exist in en.ts + every locale; a key with a `${name}` + // fallback would always fall through to English and slip past i18n parity.) + const amountTitle = + kind === 'bid' + ? `${t('agentWorld.trading.bidTitlePrefix')} ${name}` + : `${t('agentWorld.trading.offerTitlePrefix')} ${name}`; + const reviewTitle = + kind === 'bid' + ? `${t('agentWorld.trading.bidReviewTitlePrefix')} ${name}` + : `${t('agentWorld.trading.offerReviewTitlePrefix')} ${name}`; + const confirmLabel = + kind === 'bid' + ? t('agentWorld.trading.placeBid', 'Place bid') + : t('agentWorld.trading.submitOffer', 'Submit offer'); + + function handleAmount(amountBase: string) { + console.debug('[agentworld:commit-flow] amount → review', { kind, name, amountBase }); + setPhase({ step: 'review', amountBase }); + } + + function handleConfirm() { + if (phase.step !== 'review') return; + const { amountBase } = phase; + console.debug('[agentworld:commit-flow] confirm → submit', { kind, name, amountBase }); + setPhase({ step: 'submitting', amountBase }); + void submit(amountBase) + .then(() => { + console.debug('[agentworld:commit-flow] commit ok', { kind, name }); + onSuccess(); + }) + .catch((err: unknown) => { + const message = String(err); + console.debug('[agentworld:commit-flow] commit failed', { kind, name, message }); + onError(message); + }); + } + + if (phase.step === 'amount') { + return ( + + ); + } + + return ( + + ); +} diff --git a/app/src/agentworld/components/X402ConfirmDialog.test.tsx b/app/src/agentworld/components/X402ConfirmDialog.test.tsx index cb6143ce6..7c82e811b 100644 --- a/app/src/agentworld/components/X402ConfirmDialog.test.tsx +++ b/app/src/agentworld/components/X402ConfirmDialog.test.tsx @@ -12,6 +12,7 @@ import { describe, expect, test, vi } from 'vitest'; import { openUrl } from '../../utils/openUrl'; import X402ConfirmDialog, { + balanceStatus, formatUnits, fundingUrl, isInsufficient, @@ -64,6 +65,17 @@ describe('isInsufficient', () => { }); }); +describe('balanceStatus', () => { + test('classifies sufficient / insufficient / unknown', () => { + expect(balanceStatus(BALANCE, '10000000')).toBe('sufficient'); + expect(balanceStatus(BALANCE, '60000000')).toBe('insufficient'); + // Null balance is UNKNOWN — never collapsed into "sufficient". + expect(balanceStatus(null, '60000000')).toBe('unknown'); + // Unparseable raw is also unknown (not a verified balance). + expect(balanceStatus({ ...BALANCE, raw: 'nope' }, '1')).toBe('unknown'); + }); +}); + describe('fundingUrl', () => { test('builds the tiny.place fund URL with address + asset params', () => { expect(fundingUrl('GW1jU1ZoVpugX4Z164j7x7rpTsKQn1QvK4Techu7P6PH', 'USDC')).toBe( @@ -118,12 +130,38 @@ describe('X402ConfirmDialog', () => { ); }); - test('shows "Unknown" balance and still allows confirm when balance is null', () => { + test('null balance shows a "couldn\'t verify" note and still allows confirm (spend)', () => { render(); expect(screen.getByTestId('x402-balance')).toHaveTextContent('Unknown'); + // The unknown balance is NOT treated as sufficient: surface a distinct note, + // and never hard-block (no add-funds redirect, confirm stays enabled). + expect(screen.getByTestId('x402-balance-unverified')).toBeInTheDocument(); + expect(screen.queryByTestId('x402-add-funds')).not.toBeInTheDocument(); expect(screen.getByTestId('x402-confirm')).toBeEnabled(); }); + test('commit mode: insufficient balance SOFT-warns but still allows confirm', () => { + render(); + // No hard block: the Pay→Add-funds swap does not happen for commitments. + expect(screen.queryByTestId('x402-add-funds')).not.toBeInTheDocument(); + expect(screen.queryByTestId('x402-insufficient')).not.toBeInTheDocument(); + expect(screen.getByTestId('x402-commit-warning')).toBeInTheDocument(); + expect(screen.getByTestId('x402-confirm')).toBeEnabled(); + }); + + test('commit mode: null balance shows the unverified note and allows confirm', () => { + render(); + expect(screen.getByTestId('x402-balance-unverified')).toBeInTheDocument(); + expect(screen.queryByTestId('x402-commit-warning')).not.toBeInTheDocument(); + expect(screen.getByTestId('x402-confirm')).toBeEnabled(); + }); + + test('spend mode (default): proven shortfall HARD-blocks with add-funds', () => { + render(); + expect(screen.queryByTestId('x402-confirm')).not.toBeInTheDocument(); + expect(screen.getByTestId('x402-add-funds')).toBeInTheDocument(); + }); + test('busy state shows the busy label and disables both actions', () => { render(); const confirm = screen.getByTestId('x402-confirm'); diff --git a/app/src/agentworld/components/X402ConfirmDialog.tsx b/app/src/agentworld/components/X402ConfirmDialog.tsx index b3a6813f9..4bc919480 100644 --- a/app/src/agentworld/components/X402ConfirmDialog.tsx +++ b/app/src/agentworld/components/X402ConfirmDialog.tsx @@ -1,19 +1,37 @@ /** * X402ConfirmDialog — confirm-before-spend dialog for Agent World x402 flows. * - * Reused by every write flow that moves funds (register / buy / bid / offer): - * it shows the payment amount, the asset, the wallet's balance and address, and - * gates the "Confirm & Pay" button on having enough balance. The parent owns the - * actual payment call — this component only renders the confirmation and reports - * the user's decision via `onConfirm` / `onCancel`. + * Reused by every write flow that involves funds (register / buy / bid / offer) + * across two modes: * - * Money only moves after the user clicks Confirm (which the parent wires to the - * `confirmed: true` RPC) — this dialog never calls the backend itself. + * - `mode="spend"` (register / buy): an IMMEDIATE on-chain spend. A provably + * insufficient balance HARD-gates the Pay button (we replace it with an + * "Add funds" redirect) so we never broadcast a payment that must fail. + * - `mode="commit"` (bid / offer): a SIGNED COMMITMENT that only settles if the + * counterparty accepts it later — there is no transfer at submit time. A low + * balance is therefore a SOFT warning, not a block: we surface it but still + * allow Confirm. + * + * In BOTH modes an UNKNOWN balance (couldn't be fetched) is never treated as + * "sufficient" — we show an explicit "couldn't verify balance" note and allow + * Confirm (the backend remains the authoritative gate), instead of silently + * pretending the wallet can cover the amount. + * + * The parent owns the actual payment / commitment call — this component only + * renders the confirmation and reports the user's decision via `onConfirm` / + * `onCancel`. Money only moves after the user clicks Confirm. */ import Button from '../../components/ui/Button'; import { ModalShell } from '../../components/ui/ModalShell'; +import { useT } from '../../lib/i18n/I18nContext'; import { openUrl } from '../../utils/openUrl'; -import { decimalsForAsset, resolveAssetSymbol } from '../assets'; +import { decimalsForAsset, formatUnits, resolveAssetSymbol } from '../assets'; + +// Re-exported from `../assets` so existing importers (LedgerSection, BountiesSection, +// ExploreSection, tests) keep importing `formatUnits` from this module unchanged. +// The implementation lives in `assets.ts` because the marketplace price formatter +// needs it too, and `assets.ts` cannot import from here without a cycle. +export { formatUnits }; /** tiny.place hosted funding page — handles deposits / on-ramp for the wallet. */ const FUND_PAGE_URL = 'https://tiny.place/fund'; @@ -39,11 +57,25 @@ export interface X402WalletBalance { assetSymbol: string; } +/** + * Which kind of x402 write this dialog is confirming: + * - `spend` — immediate on-chain payment (register / buy). Insufficient balance + * HARD-blocks (Pay → "Add funds"). + * - `commit` — signed bid/offer commitment. Insufficient balance SOFT-warns but + * still allows Confirm (funds move only on acceptance). + */ +export type X402ConfirmMode = 'spend' | 'commit'; + export interface X402ConfirmDialogProps { /** Title shown in the modal header (e.g. "Register @handle"). */ title: string; /** Optional subtitle / context line. */ subtitle?: string; + /** + * Confirmation mode. Defaults to `spend` (the original immediate-payment + * behaviour) so existing register/buy call sites are unchanged. + */ + mode?: X402ConfirmMode; /** Payment amount in raw base units (from the x402 challenge). */ amount: string; /** Asset symbol, e.g. "USDC". */ @@ -58,29 +90,42 @@ export interface X402ConfirmDialogProps { busy?: boolean; /** Label shown on the confirm button while `busy` (e.g. "Broadcasting…"). */ busyLabel?: string; + /** Override the confirm-button label (e.g. "Confirm bid" in commit mode). */ + confirmLabel?: string; onConfirm: () => void; onCancel: () => void; } -/** Format a raw base-unit integer string to a decimal string with `decimals`. */ -export function formatUnits(raw: string, decimals: number): string { - if (decimals <= 0) return raw; - const negative = raw.startsWith('-'); - const digits = (negative ? raw.slice(1) : raw).padStart(decimals + 1, '0'); - const whole = digits.slice(0, digits.length - decimals); - const frac = digits.slice(digits.length - decimals).replace(/0+$/, ''); - const body = frac ? `${whole}.${frac}` : whole; - return negative ? `-${body}` : body; +/** + * Three-way balance assessment against an amount: + * - `unknown` — balance is null/unparseable (couldn't verify). NEVER treated + * as sufficient; the dialog surfaces this distinctly. + * - `insufficient` — balance is provably below `amount`. + * - `sufficient` — balance provably covers `amount`. + * + * This replaces the old boolean-only check whose `null → false` collapsed the + * "unknown" case into "sufficient", silently bypassing the gate. Callers that + * only need the provable-shortfall signal can use `isInsufficient` below. + */ +export type BalanceStatus = 'unknown' | 'insufficient' | 'sufficient'; + +export function balanceStatus(balance: X402WalletBalance | null, amount: string): BalanceStatus { + if (!balance) return 'unknown'; + try { + return BigInt(balance.raw) < BigInt(amount) ? 'insufficient' : 'sufficient'; + } catch { + // A balance row we can't parse is not a verified balance. + return 'unknown'; + } } -/** True when the wallet provably cannot cover `amount`. Unknown balance → false. */ +/** + * True ONLY when the wallet provably cannot cover `amount`. Unknown balance → + * false (the shortfall is not proven). Retained for callers that want just the + * hard-block signal; prefer `balanceStatus` when the unknown case matters. + */ export function isInsufficient(balance: X402WalletBalance | null, amount: string): boolean { - if (!balance) return false; - try { - return BigInt(balance.raw) < BigInt(amount); - } catch { - return false; - } + return balanceStatus(balance, amount) === 'insufficient'; } function truncateAddress(addr: string): string { @@ -106,6 +151,7 @@ export function friendlyNetwork(network?: string): string { export default function X402ConfirmDialog({ title, subtitle, + mode = 'spend', amount, asset, network, @@ -113,16 +159,41 @@ export default function X402ConfirmDialog({ walletAddress, busy = false, busyLabel = 'Processing…', + confirmLabel, onConfirm, onCancel, }: X402ConfirmDialogProps) { + const { t } = useT(); // `asset` may arrive as a mint address; resolve to a display symbol + decimals // (preferring the wallet's own resolution when present). const assetSymbol = resolveAssetSymbol(asset, balance?.assetSymbol); const decimals = decimalsForAsset(asset, balance?.decimals); const amountDisplay = formatUnits(amount, decimals); - const insufficient = isInsufficient(balance, amount); - const confirmDisabled = busy || insufficient; + const status = balanceStatus(balance, amount); + const insufficient = status === 'insufficient'; + const unknownBalance = status === 'unknown'; + + // HARD block (replace Pay with Add-funds) applies ONLY to immediate spends with + // a PROVEN shortfall. Commitments never hard-block; unknown balance never + // hard-blocks (we couldn't prove a shortfall — surface a note instead). + const hardBlock = mode === 'spend' && insufficient; + // SOFT warning: a proven shortfall on a commitment (allowed, but flagged). + const softWarn = mode === 'commit' && insufficient; + const confirmDisabled = busy || hardBlock; + + console.debug('[agentworld:x402-confirm] render', { + mode, + status, + hardBlock, + softWarn, + unknownBalance, + busy, + }); + + const defaultConfirmLabel = + mode === 'commit' + ? t('agentWorld.trading.confirmCommit', 'Confirm') + : t('agentWorld.trading.confirmPay', 'Confirm & Pay'); return (
- + {amountDisplay} {assetSymbol} - + {friendlyNetwork(network)} - + - {balance ? `${balance.formatted} ${balance.assetSymbol}` : 'Unknown'} + {balance + ? `${balance.formatted} ${balance.assetSymbol}` + : t('agentWorld.trading.balanceUnknown', 'Unknown')} - + {truncateAddress(walletAddress)}
- {insufficient ? ( + {hardBlock ? (

- Insufficient {assetSymbol} balance to complete this payment. Add funds to your wallet to - continue. + {t( + 'agentWorld.trading.spendInsufficient', + `Insufficient ${assetSymbol} balance to complete this payment. Add funds to your wallet to continue.` + )} +

+ ) : softWarn ? ( + // Commitment with a proven shortfall — allowed, but flag it so the user + // knows the wallet may not cover it if the commitment is accepted. +

+ {t( + 'agentWorld.trading.commitInsufficientWarning', + `Your ${assetSymbol} balance may not cover this if the commitment is accepted. You can still submit it — funds only move on acceptance.` + )} +

+ ) : unknownBalance ? ( + // Balance couldn't be verified. Do NOT pretend it's sufficient — say so, + // and let the user proceed (the backend is the authoritative gate). +

+ {t( + 'agentWorld.trading.balanceUnverified', + "We couldn't verify your wallet balance. You can still continue — the payment is checked when it is submitted." + )} +

+ ) : mode === 'commit' ? ( +

+ {t( + 'agentWorld.trading.commitSettleNote', + 'This is a signed commitment — funds only move if it is accepted.' + )}

) : (

- Your wallet will sign and broadcast this payment on {friendlyNetwork(network)}. + {t( + 'agentWorld.trading.spendBroadcastNote', + 'Your wallet will sign and broadcast this payment on' + )}{' '} + {friendlyNetwork(network)}.

)}
- {insufficient ? ( - // Not enough balance — send the user to the tiny.place fund page for - // the exact wallet + asset instead of a dead, disabled Pay button. + {hardBlock ? ( + // Not enough balance for an immediate spend — send the user to the + // tiny.place fund page for the exact wallet + asset instead of a dead, + // disabled Pay button. ) : ( )}
diff --git a/app/src/agentworld/pages/IdentitiesSection.test.tsx b/app/src/agentworld/pages/IdentitiesSection.test.tsx index f1584bfcc..c980c4579 100644 --- a/app/src/agentworld/pages/IdentitiesSection.test.tsx +++ b/app/src/agentworld/pages/IdentitiesSection.test.tsx @@ -562,7 +562,8 @@ describe('Registry tab', () => { seller: 'seller-one', updatedAt: '2026-02-03T00:00:00Z', status: 'active', - price: { amount: '100', asset: 'USDC' }, + // Marketplace prices are BASE units (USDC = 6 decimals); 100 USDC = 100_000_000. + price: { amount: '100000000', asset: 'USDC' }, }, // no seller, no price → em-dash fallbacks; non-active status branch { listingId: 'listing-2', name: '@beta', updatedAt: 'not-a-date', status: 'pending' }, @@ -655,7 +656,7 @@ describe('Trading tab — floor prices', () => { test('shows a price when a floor card resolves with a price', async () => { vi.mocked(apiClient.marketplace.identityFloor).mockImplementation((length?: number) => { if (length === 3) - return Promise.resolve({ length: 3, price: { amount: '250', asset: 'USDC' } }); + return Promise.resolve({ length: 3, price: { amount: '250000000', asset: 'USDC' } }); return Promise.resolve({ length, price: undefined }); }); render(); @@ -679,14 +680,14 @@ describe('Trading tab — floor prices', () => { { listingId: 'sold-floor', name: '@soldfloor', - price: { amount: '50', asset: 'USDC' }, + price: { amount: '50000000', asset: 'USDC' }, status: 'sold', updatedAt: '2026-02-03T00:00:00Z', }, { listingId: 'active-floor', name: '@activefloor', - price: { amount: '250', asset: 'USDC' }, + price: { amount: '250000000', asset: 'USDC' }, status: 'active', updatedAt: '2026-02-03T00:00:00Z', }, @@ -710,7 +711,7 @@ describe('Trading tab — floor prices', () => { identities: Array.from({ length: 20 }, (_, index) => ({ listingId: `sold-floor-${index}`, name: `@soldfloor${index}`, - price: { amount: '50', asset: 'USDC' }, + price: { amount: '50000000', asset: 'USDC' }, status: 'sold', updatedAt: '2026-02-03T00:00:00Z', })), @@ -721,7 +722,7 @@ describe('Trading tab — floor prices', () => { { listingId: 'active-floor-page-2', name: '@activefloorpage2', - price: { amount: '250', asset: 'USDC' }, + price: { amount: '250000000', asset: 'USDC' }, status: 'active', updatedAt: '2026-02-03T00:00:00Z', }, @@ -785,7 +786,7 @@ describe('Trading tab — listed for sale', () => { { listingId: 'sale-1', name: '@forsale', - price: { amount: '42', asset: 'USDC' }, + price: { amount: '42000000', asset: 'USDC' }, listingType: 'auction', seller: 'seller-x', updatedAt: '2026-02-03T00:00:00Z', @@ -793,7 +794,7 @@ describe('Trading tab — listed for sale', () => { { listingId: 'sale-2', name: '@fixedone', - price: { amount: '7', asset: 'USDC' }, + price: { amount: '7000000', asset: 'USDC' }, listingType: 'fixed', updatedAt: '2026-02-03T00:00:00Z', }, @@ -928,7 +929,7 @@ describe('Trading tab — recent sales', () => { { saleId: 'sale-a', name: '@solddomain', - price: { amount: '999', asset: 'USDC' }, + price: { amount: '999000000', asset: 'USDC' }, buyer: 'buyer0123456789abcdef', createdAt: '2026-03-15T12:00:00Z', }, @@ -955,7 +956,7 @@ describe('Trading tab — recent sales', () => { test('renders all three Trading sub-views together when populated', async () => { vi.mocked(apiClient.marketplace.identityFloor).mockImplementation((length?: number) => length === 3 - ? Promise.resolve({ length: 3, price: { amount: '250', asset: 'USDC' } }) + ? Promise.resolve({ length: 3, price: { amount: '250000000', asset: 'USDC' } }) : Promise.resolve({ length, price: undefined }) ); vi.mocked(apiClient.marketplace.listIdentities).mockResolvedValue({ @@ -963,7 +964,7 @@ describe('Trading tab — recent sales', () => { { listingId: 'sale-1', name: '@listed', - price: { amount: '42', asset: 'USDC' }, + price: { amount: '42000000', asset: 'USDC' }, listingType: 'fixed', updatedAt: '2026-02-03T00:00:00Z', }, @@ -974,7 +975,7 @@ describe('Trading tab — recent sales', () => { { saleId: 'sale-a', name: '@sold', - price: { amount: '999', asset: 'USDC' }, + price: { amount: '999000000', asset: 'USDC' }, buyer: 'buyerabcdef0123456789', createdAt: '2026-03-15T12:00:00Z', }, @@ -1081,7 +1082,14 @@ const auctionListing = { }; describe('Trading tab — bid / offer commitments', () => { - test('Bid opens the amount dialog and submits a commitment', async () => { + // Helper: enter a human amount and advance past the amount dialog into the + // confirm-before-spend review step (the new second phase). + async function enterAmountAndReview(human: string) { + await userEvent.type(screen.getByTestId('commit-amount-input'), human); + await userEvent.click(screen.getByTestId('commit-submit')); // "Continue" → review + } + + test('Bid is two-phase: review appears BEFORE the RPC fires, then commits', async () => { vi.mocked(apiClient.marketplace.listIdentities).mockResolvedValue({ identities: [auctionListing], }); @@ -1089,10 +1097,17 @@ describe('Trading tab — bid / offer commitments', () => { await gotoTab('Trading'); await userEvent.click(await screen.findByRole('button', { name: 'Bid' })); - await userEvent.type(screen.getByTestId('commit-amount-input'), '35000000'); - await userEvent.click(screen.getByTestId('commit-submit')); + // Phase 1 — amount (human decimal). Continuing must NOT fire the RPC yet. + await enterAmountAndReview('35'); + expect(apiClient.marketplace.bid).not.toHaveBeenCalled(); + + // Phase 2 — review step (confirm-before-spend parity with Buy). + const confirm = await screen.findByTestId('x402-confirm'); + expect(apiClient.marketplace.bid).not.toHaveBeenCalled(); + await userEvent.click(confirm); await screen.findByTestId('commit-success'); + // Human "35" USDC → 35_000_000 base units (decimals = 6). expect(apiClient.marketplace.bid).toHaveBeenCalledWith('auc-1', { amount: '35000000', asset: 'USDC', @@ -1100,7 +1115,7 @@ describe('Trading tab — bid / offer commitments', () => { }); }); - test('Offer submits a commitment for the handle', async () => { + test('Offer submits a commitment for the handle after review', async () => { vi.mocked(apiClient.marketplace.listIdentities).mockResolvedValue({ identities: [auctionListing], }); @@ -1108,8 +1123,9 @@ describe('Trading tab — bid / offer commitments', () => { await gotoTab('Trading'); await userEvent.click(await screen.findByRole('button', { name: 'Offer' })); - await userEvent.type(screen.getByTestId('commit-amount-input'), '25000000'); - await userEvent.click(screen.getByTestId('commit-submit')); + await enterAmountAndReview('25'); + expect(apiClient.marketplace.offer).not.toHaveBeenCalled(); + await userEvent.click(await screen.findByTestId('x402-confirm')); await screen.findByTestId('commit-success'); expect(apiClient.marketplace.offer).toHaveBeenCalledWith('@auction', { @@ -1119,6 +1135,20 @@ describe('Trading tab — bid / offer commitments', () => { }); }); + test('the review step surfaces the "couldn\'t verify balance" note', async () => { + // Bid/offer have no balance probe, so the review renders an unknown balance: + // the dialog must say so (and still allow confirm), not pretend it's enough. + vi.mocked(apiClient.marketplace.listIdentities).mockResolvedValue({ + identities: [auctionListing], + }); + render(); + await gotoTab('Trading'); + await userEvent.click(await screen.findByRole('button', { name: 'Bid' })); + await enterAmountAndReview('5'); + expect(await screen.findByTestId('x402-balance-unverified')).toBeInTheDocument(); + expect(screen.getByTestId('x402-confirm')).toBeEnabled(); + }); + test('a failed commitment surfaces an error banner', async () => { vi.mocked(apiClient.marketplace.listIdentities).mockResolvedValue({ identities: [auctionListing], @@ -1127,13 +1157,13 @@ describe('Trading tab — bid / offer commitments', () => { render(); await gotoTab('Trading'); await userEvent.click(await screen.findByRole('button', { name: 'Bid' })); - await userEvent.type(screen.getByTestId('commit-amount-input'), '1'); - await userEvent.click(screen.getByTestId('commit-submit')); + await enterAmountAndReview('1'); + await userEvent.click(await screen.findByTestId('x402-confirm')); expect(await screen.findByTestId('commit-error')).toHaveTextContent('bid-rejected'); }); - test('Cancel closes the commitment dialog without calling the API', async () => { + test('Cancel on the amount dialog closes without calling the API', async () => { vi.mocked(apiClient.marketplace.listIdentities).mockResolvedValue({ identities: [auctionListing], }); diff --git a/app/src/agentworld/pages/IdentitiesSection.tsx b/app/src/agentworld/pages/IdentitiesSection.tsx index 1182b481b..f74757d82 100644 --- a/app/src/agentworld/pages/IdentitiesSection.tsx +++ b/app/src/agentworld/pages/IdentitiesSection.tsx @@ -29,7 +29,8 @@ import { type RegistryWalletBalance, } from '../../lib/agentworld/invokeApiClient'; import { apiClient } from '../AgentWorldShell'; -import AmountCommitDialog from '../components/AmountCommitDialog'; +import { decimalsForAsset, formatAssetAmount } from '../assets'; +import CommitFlow from '../components/CommitFlow'; import X402ConfirmDialog from '../components/X402ConfirmDialog'; import { explorerTxUrl as buyExplorerTxUrl, useX402Buy } from '../hooks/useX402Buy'; @@ -266,9 +267,17 @@ function ErrorBanner({ message }: { message: string }) { ); } -// Formats price amount + asset for display +// Formats a marketplace price for display. Listing/floor/sale `price.amount` +// strings are in the asset's smallest BASE units (same convention as the x402 +// buy challenge `amount` and bounty `reward.amount`) — a 30 USDC price arrives as +// "30000000". We humanize here via the shared {@link formatAssetAmount} so the +// DISPLAYED price ("30 USDC") matches the human-decimal value the user types in +// AmountCommitDialog when bidding/offering (which then scales ×10^decimals to base +// units). Rendering the raw base-unit string verbatim previously invited a +// catastrophic over-spend (user reads "30000000 USDC", types it, signs 30000000 × +// 10^6 base units). function formatPrice(amount: string, asset: string): string { - return `${amount} ${asset}`; + return formatAssetAmount(amount, asset); } // ── Register tab ────────────────────────────────────────────────────────────── @@ -716,12 +725,15 @@ function TradingTab() { setBuying(null); } - // x402 commitment flow (bid / offer) — no immediate spend. + // x402 commitment flow (bid / offer). Now two-phase (amount → review → submit) + // for confirm-before-spend parity with Buy — the CommitFlow component owns the + // amount/review state; here we only track which listing is in flight + the + // success/error banner state. const [commit, setCommit] = useState<{ kind: 'bid' | 'offer'; listing: IdentityListing } | null>( null ); const [commitState, setCommitState] = useState<{ - phase: 'idle' | 'busy' | 'success' | 'error'; + phase: 'idle' | 'success' | 'error'; message?: string; }>({ phase: 'idle' }); @@ -730,23 +742,28 @@ function TradingTab() { setCommitState({ phase: 'idle' }); } - function submitCommit(amount: string) { - if (!commit) return; + // Perform the actual commitment RPC. `amount` is in BASE units (CommitFlow has + // already converted the human decimal input). Returns a promise so CommitFlow + // can show its busy/review state and route the outcome back to the banners. + function performCommit(amount: string): Promise { + if (!commit) return Promise.resolve(); const { kind, listing } = commit; const price = { amount, asset: listing.price.asset, network: listing.price.network ?? '' }; - setCommitState({ phase: 'busy' }); const call = kind === 'bid' ? apiClient.marketplace.bid(listing.listingId, price) : apiClient.marketplace.offer(listing.name, price); - void call - .then(() => { - setCommit(null); - setCommitState({ phase: 'success' }); - }) - .catch((err: unknown) => { - setCommitState({ phase: 'error', message: String(err) }); - }); + return call.then(() => undefined); + } + + function handleCommitSuccess() { + setCommit(null); + setCommitState({ phase: 'success' }); + } + + function handleCommitError(message: string) { + setCommit(null); + setCommitState({ phase: 'error', message }); } return ( @@ -855,20 +872,18 @@ function TradingTab() {
)} - {/* Bid / offer amount dialog. */} + {/* Bid / offer commitment flow (amount → review → submit). */} {commit && ( - )} diff --git a/app/src/lib/i18n/ar.ts b/app/src/lib/i18n/ar.ts index b2de368b5..ef9777efc 100644 --- a/app/src/lib/i18n/ar.ts +++ b/app/src/lib/i18n/ar.ts @@ -5900,6 +5900,37 @@ const messages: TranslationMap = { 'userErrors.insufficientCredits.title': 'مطلوب رصيد المزود', 'userErrors.insufficientCredits.body': 'نفد رصيد المزود. أعد الشحن أو حدّث مفتاح API.', 'userErrors.scope.chat': 'الدردشة', + // Agent World — Identity trading (confirm-before-spend + balance gate) + 'agentWorld.trading.amountLabel': 'المبلغ', + 'agentWorld.trading.networkLabel': 'الشبكة', + 'agentWorld.trading.balanceLabel': 'رصيدك', + 'agentWorld.trading.walletLabel': 'المحفظة', + 'agentWorld.trading.balanceUnknown': 'غير معروف', + 'agentWorld.trading.cancel': 'إلغاء', + 'agentWorld.trading.addFunds': 'إضافة أموال', + 'agentWorld.trading.confirmPay': 'تأكيد والدفع', + 'agentWorld.trading.confirmCommit': 'تأكيد', + 'agentWorld.trading.continue': 'متابعة', + 'agentWorld.trading.submitting': 'جارٍ الإرسال…', + 'agentWorld.trading.placeBid': 'تقديم عرض السعر', + 'agentWorld.trading.submitOffer': 'تقديم العرض', + 'agentWorld.trading.bidTitlePrefix': 'عرض سعر على', + 'agentWorld.trading.offerTitlePrefix': 'عرض لـ', + 'agentWorld.trading.bidReviewTitlePrefix': 'تأكيد عرض السعر على', + 'agentWorld.trading.offerReviewTitlePrefix': 'تأكيد العرض لـ', + 'agentWorld.trading.spendInsufficient': + 'الرصيد غير كافٍ لإتمام هذه الدفعة. أضف أموالاً إلى محفظتك للمتابعة.', + 'agentWorld.trading.spendBroadcastNote': 'ستوقّع محفظتك هذه الدفعة وتبثّها على', + 'agentWorld.trading.commitInsufficientWarning': + 'قد لا يغطي رصيدك هذا الالتزام في حال قبوله. لا يزال بإمكانك تقديمه — لن تُحوّل الأموال إلا عند القبول.', + 'agentWorld.trading.commitSettleNote': 'هذا التزام موقّع — لن تُحوّل الأموال إلا إذا تم قبوله.', + 'agentWorld.trading.commitReviewSubtitle': 'راجع التزامك قبل تقديمه.', + 'agentWorld.trading.balanceUnverified': + 'تعذّر علينا التحقق من رصيد محفظتك. لا يزال بإمكانك المتابعة — يتم التحقق من الدفعة عند تقديمها.', + 'agentWorld.trading.amountTooManyDecimals': + 'يحتوي هذا المبلغ على عدد كبير جدًا من المنازل العشرية.', + 'agentWorld.trading.amountMustBePositive': 'أدخل مبلغًا أكبر من صفر.', + 'agentWorld.trading.amountInvalid': 'أدخل مبلغًا صالحًا.', }; export default messages; diff --git a/app/src/lib/i18n/bn.ts b/app/src/lib/i18n/bn.ts index 2b634e726..10293e4bf 100644 --- a/app/src/lib/i18n/bn.ts +++ b/app/src/lib/i18n/bn.ts @@ -6024,6 +6024,37 @@ const messages: TranslationMap = { 'userErrors.insufficientCredits.body': 'AI প্রদানকারীর ক্রেডিট শেষ। রিচার্জ করুন বা API কী বদলান।', 'userErrors.scope.chat': 'চ্যাট', + // Agent World — Identity trading (confirm-before-spend + balance gate) + 'agentWorld.trading.amountLabel': 'পরিমাণ', + 'agentWorld.trading.networkLabel': 'নেটওয়ার্ক', + 'agentWorld.trading.balanceLabel': 'আপনার ব্যালেন্স', + 'agentWorld.trading.walletLabel': 'ওয়ালেট', + 'agentWorld.trading.balanceUnknown': 'অজানা', + 'agentWorld.trading.cancel': 'বাতিল', + 'agentWorld.trading.addFunds': 'অর্থ যোগ করুন', + 'agentWorld.trading.confirmPay': 'নিশ্চিত করুন ও পরিশোধ করুন', + 'agentWorld.trading.confirmCommit': 'নিশ্চিত করুন', + 'agentWorld.trading.continue': 'চালিয়ে যান', + 'agentWorld.trading.submitting': 'জমা দেওয়া হচ্ছে…', + 'agentWorld.trading.placeBid': 'বিড করুন', + 'agentWorld.trading.submitOffer': 'অফার জমা দিন', + 'agentWorld.trading.bidTitlePrefix': 'বিড:', + 'agentWorld.trading.offerTitlePrefix': 'অফার:', + 'agentWorld.trading.bidReviewTitlePrefix': 'বিড নিশ্চিত করুন:', + 'agentWorld.trading.offerReviewTitlePrefix': 'অফার নিশ্চিত করুন:', + 'agentWorld.trading.spendInsufficient': + 'এই পেমেন্ট সম্পূর্ণ করার জন্য পর্যাপ্ত ব্যালেন্স নেই। চালিয়ে যেতে আপনার ওয়ালেটে অর্থ যোগ করুন।', + 'agentWorld.trading.spendBroadcastNote': 'আপনার ওয়ালেট এই পেমেন্টে স্বাক্ষর করে সম্প্রচার করবে', + 'agentWorld.trading.commitInsufficientWarning': + 'প্রতিশ্রুতিটি গৃহীত হলে আপনার ব্যালেন্স এটি কভার নাও করতে পারে। আপনি এটি জমা দিতে পারেন — অর্থ কেবল গ্রহণের সময়ই স্থানান্তরিত হয়।', + 'agentWorld.trading.commitSettleNote': + 'এটি একটি স্বাক্ষরিত প্রতিশ্রুতি — গৃহীত হলেই কেবল অর্থ স্থানান্তরিত হয়।', + 'agentWorld.trading.commitReviewSubtitle': 'জমা দেওয়ার আগে আপনার প্রতিশ্রুতি পর্যালোচনা করুন।', + 'agentWorld.trading.balanceUnverified': + 'আমরা আপনার ওয়ালেট ব্যালেন্স যাচাই করতে পারিনি। আপনি চালিয়ে যেতে পারেন — জমা দেওয়ার সময় পেমেন্ট যাচাই করা হয়।', + 'agentWorld.trading.amountTooManyDecimals': 'এই পরিমাণে অত্যধিক দশমিক স্থান রয়েছে।', + 'agentWorld.trading.amountMustBePositive': 'শূন্যের চেয়ে বড় একটি পরিমাণ লিখুন।', + 'agentWorld.trading.amountInvalid': 'একটি বৈধ পরিমাণ লিখুন।', }; export default messages; diff --git a/app/src/lib/i18n/de.ts b/app/src/lib/i18n/de.ts index e8cfb2014..7f2ee6d9f 100644 --- a/app/src/lib/i18n/de.ts +++ b/app/src/lib/i18n/de.ts @@ -6191,6 +6191,38 @@ const messages: TranslationMap = { 'userErrors.insufficientCredits.body': 'Deinem KI-Anbieter ist das Guthaben ausgegangen. Lade es auf oder aktualisiere den Schlüssel.', 'userErrors.scope.chat': 'Chat', + // Agent World — Identity trading (confirm-before-spend + balance gate) + 'agentWorld.trading.amountLabel': 'Betrag', + 'agentWorld.trading.networkLabel': 'Netzwerk', + 'agentWorld.trading.balanceLabel': 'Dein Guthaben', + 'agentWorld.trading.walletLabel': 'Wallet', + 'agentWorld.trading.balanceUnknown': 'Unbekannt', + 'agentWorld.trading.cancel': 'Abbrechen', + 'agentWorld.trading.addFunds': 'Guthaben aufladen', + 'agentWorld.trading.confirmPay': 'Bestätigen & zahlen', + 'agentWorld.trading.confirmCommit': 'Bestätigen', + 'agentWorld.trading.continue': 'Weiter', + 'agentWorld.trading.submitting': 'Wird gesendet…', + 'agentWorld.trading.placeBid': 'Gebot abgeben', + 'agentWorld.trading.submitOffer': 'Angebot senden', + 'agentWorld.trading.bidTitlePrefix': 'Gebot für', + 'agentWorld.trading.offerTitlePrefix': 'Angebot für', + 'agentWorld.trading.bidReviewTitlePrefix': 'Gebot bestätigen für', + 'agentWorld.trading.offerReviewTitlePrefix': 'Angebot bestätigen für', + 'agentWorld.trading.spendInsufficient': + 'Nicht genügend Guthaben, um diese Zahlung abzuschließen. Lade dein Wallet auf, um fortzufahren.', + 'agentWorld.trading.spendBroadcastNote': + 'Dein Wallet signiert diese Zahlung und überträgt sie auf', + 'agentWorld.trading.commitInsufficientWarning': + 'Dein Guthaben deckt dies möglicherweise nicht ab, falls die Zusage angenommen wird. Du kannst sie trotzdem absenden – Geld bewegt sich nur bei Annahme.', + 'agentWorld.trading.commitSettleNote': + 'Dies ist eine signierte Zusage – Geld bewegt sich nur bei Annahme.', + 'agentWorld.trading.commitReviewSubtitle': 'Überprüfe deine Zusage vor dem Absenden.', + 'agentWorld.trading.balanceUnverified': + 'Wir konnten dein Wallet-Guthaben nicht überprüfen. Du kannst trotzdem fortfahren – die Zahlung wird beim Absenden geprüft.', + 'agentWorld.trading.amountTooManyDecimals': 'Dieser Betrag hat zu viele Nachkommastellen.', + 'agentWorld.trading.amountMustBePositive': 'Gib einen Betrag größer als null ein.', + 'agentWorld.trading.amountInvalid': 'Gib einen gültigen Betrag ein.', }; export default messages; diff --git a/app/src/lib/i18n/en.ts b/app/src/lib/i18n/en.ts index e5366cffd..30f25f213 100644 --- a/app/src/lib/i18n/en.ts +++ b/app/src/lib/i18n/en.ts @@ -73,6 +73,39 @@ const en: TranslationMap = { 'explore.noBounties': 'No open bounties', 'explore.noAgents': 'No agents registered', 'agentWorld.jobs.deadlineFuture': 'Proposal deadline must be in the future', + // Agent World — Identity trading (confirm-before-spend + balance gate) + 'agentWorld.trading.amountLabel': 'Amount', + 'agentWorld.trading.networkLabel': 'Network', + 'agentWorld.trading.balanceLabel': 'Your balance', + 'agentWorld.trading.walletLabel': 'Wallet', + 'agentWorld.trading.balanceUnknown': 'Unknown', + 'agentWorld.trading.cancel': 'Cancel', + 'agentWorld.trading.addFunds': 'Add funds', + 'agentWorld.trading.confirmPay': 'Confirm & Pay', + 'agentWorld.trading.confirmCommit': 'Confirm', + 'agentWorld.trading.continue': 'Continue', + 'agentWorld.trading.submitting': 'Submitting…', + 'agentWorld.trading.placeBid': 'Place bid', + 'agentWorld.trading.submitOffer': 'Submit offer', + // Commit-dialog title PREFIXES — composed with the listing name in code + // (`t(...) + ' ' + name`) because `t(key, fallback)` has no interpolation. + 'agentWorld.trading.bidTitlePrefix': 'Bid on', + 'agentWorld.trading.offerTitlePrefix': 'Offer for', + 'agentWorld.trading.bidReviewTitlePrefix': 'Confirm bid on', + 'agentWorld.trading.offerReviewTitlePrefix': 'Confirm offer for', + 'agentWorld.trading.spendInsufficient': + 'Insufficient balance to complete this payment. Add funds to your wallet to continue.', + 'agentWorld.trading.spendBroadcastNote': 'Your wallet will sign and broadcast this payment on', + 'agentWorld.trading.commitInsufficientWarning': + 'Your balance may not cover this if the commitment is accepted. You can still submit it — funds only move on acceptance.', + 'agentWorld.trading.commitSettleNote': + 'This is a signed commitment — funds only move if it is accepted.', + 'agentWorld.trading.commitReviewSubtitle': 'Review your commitment before submitting.', + 'agentWorld.trading.balanceUnverified': + "We couldn't verify your wallet balance. You can still continue — the payment is checked when it is submitted.", + 'agentWorld.trading.amountTooManyDecimals': 'This amount has too many decimal places.', + 'agentWorld.trading.amountMustBePositive': 'Enter an amount greater than zero.', + 'agentWorld.trading.amountInvalid': 'Enter a valid amount.', // Agent World — Settings section UI 'nav.avatarMenu.account': 'Account', 'nav.avatarMenu.billing': 'Billing', diff --git a/app/src/lib/i18n/es.ts b/app/src/lib/i18n/es.ts index 622923b59..15836fa9a 100644 --- a/app/src/lib/i18n/es.ts +++ b/app/src/lib/i18n/es.ts @@ -6151,6 +6151,37 @@ const messages: TranslationMap = { 'userErrors.insufficientCredits.body': 'Tu proveedor de IA se quedó sin créditos. Recárgalo o actualiza su clave de API.', 'userErrors.scope.chat': 'Chat', + // Agent World — Identity trading (confirm-before-spend + balance gate) + 'agentWorld.trading.amountLabel': 'Importe', + 'agentWorld.trading.networkLabel': 'Red', + 'agentWorld.trading.balanceLabel': 'Tu saldo', + 'agentWorld.trading.walletLabel': 'Billetera', + 'agentWorld.trading.balanceUnknown': 'Desconocido', + 'agentWorld.trading.cancel': 'Cancelar', + 'agentWorld.trading.addFunds': 'Añadir fondos', + 'agentWorld.trading.confirmPay': 'Confirmar y pagar', + 'agentWorld.trading.confirmCommit': 'Confirmar', + 'agentWorld.trading.continue': 'Continuar', + 'agentWorld.trading.submitting': 'Enviando…', + 'agentWorld.trading.placeBid': 'Pujar', + 'agentWorld.trading.submitOffer': 'Enviar oferta', + 'agentWorld.trading.bidTitlePrefix': 'Pujar por', + 'agentWorld.trading.offerTitlePrefix': 'Oferta por', + 'agentWorld.trading.bidReviewTitlePrefix': 'Confirmar puja por', + 'agentWorld.trading.offerReviewTitlePrefix': 'Confirmar oferta por', + 'agentWorld.trading.spendInsufficient': + 'Saldo insuficiente para completar este pago. Añade fondos a tu billetera para continuar.', + 'agentWorld.trading.spendBroadcastNote': 'Tu billetera firmará y transmitirá este pago en', + 'agentWorld.trading.commitInsufficientWarning': + 'Tu saldo podría no cubrir esto si se acepta el compromiso. Aún puedes enviarlo: los fondos solo se mueven al aceptarse.', + 'agentWorld.trading.commitSettleNote': + 'Este es un compromiso firmado: los fondos solo se mueven si se acepta.', + 'agentWorld.trading.commitReviewSubtitle': 'Revisa tu compromiso antes de enviarlo.', + 'agentWorld.trading.balanceUnverified': + 'No pudimos verificar el saldo de tu billetera. Aún puedes continuar: el pago se comprueba al enviarlo.', + 'agentWorld.trading.amountTooManyDecimals': 'Este importe tiene demasiados decimales.', + 'agentWorld.trading.amountMustBePositive': 'Introduce un importe mayor que cero.', + 'agentWorld.trading.amountInvalid': 'Introduce un importe válido.', }; export default messages; diff --git a/app/src/lib/i18n/fr.ts b/app/src/lib/i18n/fr.ts index 49378b0e0..7136c05a6 100644 --- a/app/src/lib/i18n/fr.ts +++ b/app/src/lib/i18n/fr.ts @@ -6169,6 +6169,38 @@ const messages: TranslationMap = { 'userErrors.insufficientCredits.body': "Votre fournisseur IA n'a plus de crédits. Rechargez-le ou mettez à jour sa clé API.", 'userErrors.scope.chat': 'Chat', + // Agent World — Identity trading (confirm-before-spend + balance gate) + 'agentWorld.trading.amountLabel': 'Montant', + 'agentWorld.trading.networkLabel': 'Réseau', + 'agentWorld.trading.balanceLabel': 'Votre solde', + 'agentWorld.trading.walletLabel': 'Portefeuille', + 'agentWorld.trading.balanceUnknown': 'Inconnu', + 'agentWorld.trading.cancel': 'Annuler', + 'agentWorld.trading.addFunds': 'Ajouter des fonds', + 'agentWorld.trading.confirmPay': 'Confirmer et payer', + 'agentWorld.trading.confirmCommit': 'Confirmer', + 'agentWorld.trading.continue': 'Continuer', + 'agentWorld.trading.submitting': 'Envoi…', + 'agentWorld.trading.placeBid': 'Enchérir', + 'agentWorld.trading.submitOffer': 'Envoyer l’offre', + 'agentWorld.trading.bidTitlePrefix': 'Enchérir sur', + 'agentWorld.trading.offerTitlePrefix': 'Offre pour', + 'agentWorld.trading.bidReviewTitlePrefix': 'Confirmer l’enchère sur', + 'agentWorld.trading.offerReviewTitlePrefix': 'Confirmer l’offre pour', + 'agentWorld.trading.spendInsufficient': + 'Solde insuffisant pour effectuer ce paiement. Approvisionnez votre portefeuille pour continuer.', + 'agentWorld.trading.spendBroadcastNote': + 'Votre portefeuille signera et diffusera ce paiement sur', + 'agentWorld.trading.commitInsufficientWarning': + 'Votre solde pourrait ne pas couvrir cela si l’engagement est accepté. Vous pouvez quand même l’envoyer : les fonds ne bougent qu’à l’acceptation.', + 'agentWorld.trading.commitSettleNote': + 'Il s’agit d’un engagement signé : les fonds ne bougent qu’en cas d’acceptation.', + 'agentWorld.trading.commitReviewSubtitle': 'Vérifiez votre engagement avant de l’envoyer.', + 'agentWorld.trading.balanceUnverified': + 'Nous n’avons pas pu vérifier le solde de votre portefeuille. Vous pouvez quand même continuer : le paiement est vérifié lors de l’envoi.', + 'agentWorld.trading.amountTooManyDecimals': 'Ce montant comporte trop de décimales.', + 'agentWorld.trading.amountMustBePositive': 'Saisissez un montant supérieur à zéro.', + 'agentWorld.trading.amountInvalid': 'Saisissez un montant valide.', }; export default messages; diff --git a/app/src/lib/i18n/hi.ts b/app/src/lib/i18n/hi.ts index 2943ebb20..e0fb13359 100644 --- a/app/src/lib/i18n/hi.ts +++ b/app/src/lib/i18n/hi.ts @@ -6026,6 +6026,38 @@ const messages: TranslationMap = { 'userErrors.insufficientCredits.body': 'AI प्रदाता के क्रेडिट समाप्त। रिचार्ज करें या API कुंजी बदलें।', 'userErrors.scope.chat': 'चैट', + // Agent World — Identity trading (confirm-before-spend + balance gate) + 'agentWorld.trading.amountLabel': 'राशि', + 'agentWorld.trading.networkLabel': 'नेटवर्क', + 'agentWorld.trading.balanceLabel': 'आपका बैलेंस', + 'agentWorld.trading.walletLabel': 'वॉलेट', + 'agentWorld.trading.balanceUnknown': 'अज्ञात', + 'agentWorld.trading.cancel': 'रद्द करें', + 'agentWorld.trading.addFunds': 'फंड जोड़ें', + 'agentWorld.trading.confirmPay': 'पुष्टि करें और भुगतान करें', + 'agentWorld.trading.confirmCommit': 'पुष्टि करें', + 'agentWorld.trading.continue': 'जारी रखें', + 'agentWorld.trading.submitting': 'भेजा जा रहा है…', + 'agentWorld.trading.placeBid': 'बोली लगाएं', + 'agentWorld.trading.submitOffer': 'ऑफ़र भेजें', + 'agentWorld.trading.bidTitlePrefix': 'बोली:', + 'agentWorld.trading.offerTitlePrefix': 'ऑफ़र:', + 'agentWorld.trading.bidReviewTitlePrefix': 'बोली की पुष्टि करें:', + 'agentWorld.trading.offerReviewTitlePrefix': 'ऑफ़र की पुष्टि करें:', + 'agentWorld.trading.spendInsufficient': + 'यह भुगतान पूरा करने के लिए पर्याप्त बैलेंस नहीं है। जारी रखने के लिए अपने वॉलेट में फंड जोड़ें।', + 'agentWorld.trading.spendBroadcastNote': + 'आपका वॉलेट इस भुगतान पर हस्ताक्षर करके इसे प्रसारित करेगा', + 'agentWorld.trading.commitInsufficientWarning': + 'यदि प्रतिबद्धता स्वीकार की जाती है तो आपका बैलेंस इसे कवर नहीं कर सकता। आप फिर भी इसे भेज सकते हैं — फंड केवल स्वीकृति पर ही स्थानांतरित होते हैं।', + 'agentWorld.trading.commitSettleNote': + 'यह एक हस्ताक्षरित प्रतिबद्धता है — फंड केवल स्वीकार किए जाने पर ही स्थानांतरित होते हैं।', + 'agentWorld.trading.commitReviewSubtitle': 'भेजने से पहले अपनी प्रतिबद्धता की समीक्षा करें।', + 'agentWorld.trading.balanceUnverified': + 'हम आपके वॉलेट बैलेंस की पुष्टि नहीं कर सके। आप फिर भी जारी रख सकते हैं — भुगतान भेजे जाने पर जांचा जाता है।', + 'agentWorld.trading.amountTooManyDecimals': 'इस राशि में बहुत अधिक दशमलव स्थान हैं।', + 'agentWorld.trading.amountMustBePositive': 'शून्य से बड़ी राशि दर्ज करें।', + 'agentWorld.trading.amountInvalid': 'एक मान्य राशि दर्ज करें।', }; export default messages; diff --git a/app/src/lib/i18n/id.ts b/app/src/lib/i18n/id.ts index ec89fdea0..c3249a9a9 100644 --- a/app/src/lib/i18n/id.ts +++ b/app/src/lib/i18n/id.ts @@ -6045,6 +6045,38 @@ const messages: TranslationMap = { 'userErrors.insufficientCredits.body': 'Penyedia AI Anda kehabisan kredit. Isi ulang atau perbarui kunci API-nya.', 'userErrors.scope.chat': 'Obrolan', + // Agent World — Identity trading (confirm-before-spend + balance gate) + 'agentWorld.trading.amountLabel': 'Jumlah', + 'agentWorld.trading.networkLabel': 'Jaringan', + 'agentWorld.trading.balanceLabel': 'Saldo Anda', + 'agentWorld.trading.walletLabel': 'Dompet', + 'agentWorld.trading.balanceUnknown': 'Tidak diketahui', + 'agentWorld.trading.cancel': 'Batal', + 'agentWorld.trading.addFunds': 'Tambah dana', + 'agentWorld.trading.confirmPay': 'Konfirmasi & bayar', + 'agentWorld.trading.confirmCommit': 'Konfirmasi', + 'agentWorld.trading.continue': 'Lanjutkan', + 'agentWorld.trading.submitting': 'Mengirim…', + 'agentWorld.trading.placeBid': 'Ajukan tawaran', + 'agentWorld.trading.submitOffer': 'Kirim penawaran', + 'agentWorld.trading.bidTitlePrefix': 'Tawar untuk', + 'agentWorld.trading.offerTitlePrefix': 'Penawaran untuk', + 'agentWorld.trading.bidReviewTitlePrefix': 'Konfirmasi tawaran untuk', + 'agentWorld.trading.offerReviewTitlePrefix': 'Konfirmasi penawaran untuk', + 'agentWorld.trading.spendInsufficient': + 'Saldo tidak cukup untuk menyelesaikan pembayaran ini. Tambahkan dana ke dompet Anda untuk melanjutkan.', + 'agentWorld.trading.spendBroadcastNote': + 'Dompet Anda akan menandatangani dan menyiarkan pembayaran ini di', + 'agentWorld.trading.commitInsufficientWarning': + 'Saldo Anda mungkin tidak menutupi ini jika komitmen diterima. Anda tetap dapat mengirimkannya — dana hanya berpindah saat diterima.', + 'agentWorld.trading.commitSettleNote': + 'Ini adalah komitmen bertanda tangan — dana hanya berpindah jika diterima.', + 'agentWorld.trading.commitReviewSubtitle': 'Tinjau komitmen Anda sebelum mengirim.', + 'agentWorld.trading.balanceUnverified': + 'Kami tidak dapat memverifikasi saldo dompet Anda. Anda tetap dapat melanjutkan — pembayaran diperiksa saat dikirim.', + 'agentWorld.trading.amountTooManyDecimals': 'Jumlah ini memiliki terlalu banyak angka desimal.', + 'agentWorld.trading.amountMustBePositive': 'Masukkan jumlah lebih besar dari nol.', + 'agentWorld.trading.amountInvalid': 'Masukkan jumlah yang valid.', }; export default messages; diff --git a/app/src/lib/i18n/it.ts b/app/src/lib/i18n/it.ts index d0f37049b..e857c81a7 100644 --- a/app/src/lib/i18n/it.ts +++ b/app/src/lib/i18n/it.ts @@ -6136,6 +6136,38 @@ const messages: TranslationMap = { 'userErrors.insufficientCredits.body': 'Il tuo provider IA ha esaurito i crediti. Ricaricalo o aggiorna la chiave API.', 'userErrors.scope.chat': 'Chat', + // Agent World — Identity trading (confirm-before-spend + balance gate) + 'agentWorld.trading.amountLabel': 'Importo', + 'agentWorld.trading.networkLabel': 'Rete', + 'agentWorld.trading.balanceLabel': 'Il tuo saldo', + 'agentWorld.trading.walletLabel': 'Portafoglio', + 'agentWorld.trading.balanceUnknown': 'Sconosciuto', + 'agentWorld.trading.cancel': 'Annulla', + 'agentWorld.trading.addFunds': 'Aggiungi fondi', + 'agentWorld.trading.confirmPay': 'Conferma e paga', + 'agentWorld.trading.confirmCommit': 'Conferma', + 'agentWorld.trading.continue': 'Continua', + 'agentWorld.trading.submitting': 'Invio in corso…', + 'agentWorld.trading.placeBid': 'Fai un’offerta', + 'agentWorld.trading.submitOffer': 'Invia offerta', + 'agentWorld.trading.bidTitlePrefix': 'Offerta per', + 'agentWorld.trading.offerTitlePrefix': 'Proposta per', + 'agentWorld.trading.bidReviewTitlePrefix': 'Conferma offerta per', + 'agentWorld.trading.offerReviewTitlePrefix': 'Conferma proposta per', + 'agentWorld.trading.spendInsufficient': + 'Saldo insufficiente per completare questo pagamento. Aggiungi fondi al tuo portafoglio per continuare.', + 'agentWorld.trading.spendBroadcastNote': + 'Il tuo portafoglio firmerà e trasmetterà questo pagamento su', + 'agentWorld.trading.commitInsufficientWarning': + 'Il tuo saldo potrebbe non coprire questo importo se l’impegno viene accettato. Puoi comunque inviarlo: i fondi si spostano solo all’accettazione.', + 'agentWorld.trading.commitSettleNote': + 'Questo è un impegno firmato: i fondi si spostano solo se viene accettato.', + 'agentWorld.trading.commitReviewSubtitle': 'Controlla il tuo impegno prima di inviarlo.', + 'agentWorld.trading.balanceUnverified': + 'Non siamo riusciti a verificare il saldo del tuo portafoglio. Puoi comunque continuare: il pagamento viene verificato all’invio.', + 'agentWorld.trading.amountTooManyDecimals': 'Questo importo ha troppe cifre decimali.', + 'agentWorld.trading.amountMustBePositive': 'Inserisci un importo maggiore di zero.', + 'agentWorld.trading.amountInvalid': 'Inserisci un importo valido.', }; export default messages; diff --git a/app/src/lib/i18n/ko.ts b/app/src/lib/i18n/ko.ts index 275f69c21..310ed97f5 100644 --- a/app/src/lib/i18n/ko.ts +++ b/app/src/lib/i18n/ko.ts @@ -5967,6 +5967,37 @@ const messages: TranslationMap = { 'userErrors.insufficientCredits.title': '제공업체 크레딧 필요', 'userErrors.insufficientCredits.body': 'AI 제공업체 크레딧이 소진되었습니다.', 'userErrors.scope.chat': '채팅', + // Agent World — Identity trading (confirm-before-spend + balance gate) + 'agentWorld.trading.amountLabel': '금액', + 'agentWorld.trading.networkLabel': '네트워크', + 'agentWorld.trading.balanceLabel': '내 잔액', + 'agentWorld.trading.walletLabel': '지갑', + 'agentWorld.trading.balanceUnknown': '알 수 없음', + 'agentWorld.trading.cancel': '취소', + 'agentWorld.trading.addFunds': '자금 추가', + 'agentWorld.trading.confirmPay': '확인 및 결제', + 'agentWorld.trading.confirmCommit': '확인', + 'agentWorld.trading.continue': '계속', + 'agentWorld.trading.submitting': '제출 중…', + 'agentWorld.trading.placeBid': '입찰하기', + 'agentWorld.trading.submitOffer': '제안 보내기', + 'agentWorld.trading.bidTitlePrefix': '입찰:', + 'agentWorld.trading.offerTitlePrefix': '제안:', + 'agentWorld.trading.bidReviewTitlePrefix': '입찰 확인:', + 'agentWorld.trading.offerReviewTitlePrefix': '제안 확인:', + 'agentWorld.trading.spendInsufficient': + '이 결제를 완료하기에 잔액이 부족합니다. 계속하려면 지갑에 자금을 추가하세요.', + 'agentWorld.trading.spendBroadcastNote': '지갑이 이 결제에 서명하여 다음 네트워크에 전파합니다', + 'agentWorld.trading.commitInsufficientWarning': + '약정이 수락되면 잔액이 이를 충당하지 못할 수 있습니다. 그래도 제출할 수 있습니다 — 자금은 수락 시에만 이동합니다.', + 'agentWorld.trading.commitSettleNote': + '이것은 서명된 약정입니다 — 수락된 경우에만 자금이 이동합니다.', + 'agentWorld.trading.commitReviewSubtitle': '제출하기 전에 약정을 검토하세요.', + 'agentWorld.trading.balanceUnverified': + '지갑 잔액을 확인할 수 없습니다. 그래도 계속할 수 있습니다 — 결제는 제출 시 확인됩니다.', + 'agentWorld.trading.amountTooManyDecimals': '이 금액의 소수점 자릿수가 너무 많습니다.', + 'agentWorld.trading.amountMustBePositive': '0보다 큰 금액을 입력하세요.', + 'agentWorld.trading.amountInvalid': '유효한 금액을 입력하세요.', }; export default messages; diff --git a/app/src/lib/i18n/pl.ts b/app/src/lib/i18n/pl.ts index d271ee349..50355cc1f 100644 --- a/app/src/lib/i18n/pl.ts +++ b/app/src/lib/i18n/pl.ts @@ -6116,6 +6116,37 @@ const messages: TranslationMap = { 'userErrors.insufficientCredits.body': 'Twój dostawca AI nie ma już środków. Doładuj je lub zaktualizuj klucz API.', 'userErrors.scope.chat': 'Czat', + // Agent World — Identity trading (confirm-before-spend + balance gate) + 'agentWorld.trading.amountLabel': 'Kwota', + 'agentWorld.trading.networkLabel': 'Sieć', + 'agentWorld.trading.balanceLabel': 'Twoje saldo', + 'agentWorld.trading.walletLabel': 'Portfel', + 'agentWorld.trading.balanceUnknown': 'Nieznane', + 'agentWorld.trading.cancel': 'Anuluj', + 'agentWorld.trading.addFunds': 'Dodaj środki', + 'agentWorld.trading.confirmPay': 'Potwierdź i zapłać', + 'agentWorld.trading.confirmCommit': 'Potwierdź', + 'agentWorld.trading.continue': 'Kontynuuj', + 'agentWorld.trading.submitting': 'Wysyłanie…', + 'agentWorld.trading.placeBid': 'Złóż ofertę', + 'agentWorld.trading.submitOffer': 'Wyślij ofertę', + 'agentWorld.trading.bidTitlePrefix': 'Licytacja:', + 'agentWorld.trading.offerTitlePrefix': 'Oferta dla', + 'agentWorld.trading.bidReviewTitlePrefix': 'Potwierdź licytację:', + 'agentWorld.trading.offerReviewTitlePrefix': 'Potwierdź ofertę dla', + 'agentWorld.trading.spendInsufficient': + 'Niewystarczające saldo, aby zrealizować tę płatność. Dodaj środki do portfela, aby kontynuować.', + 'agentWorld.trading.spendBroadcastNote': 'Twój portfel podpisze i rozgłosi tę płatność w sieci', + 'agentWorld.trading.commitInsufficientWarning': + 'Twoje saldo może nie pokryć tego, jeśli zobowiązanie zostanie przyjęte. Nadal możesz je wysłać — środki przemieszczają się dopiero po przyjęciu.', + 'agentWorld.trading.commitSettleNote': + 'To podpisane zobowiązanie — środki przemieszczają się tylko po jego przyjęciu.', + 'agentWorld.trading.commitReviewSubtitle': 'Sprawdź swoje zobowiązanie przed wysłaniem.', + 'agentWorld.trading.balanceUnverified': + 'Nie udało nam się zweryfikować salda portfela. Nadal możesz kontynuować — płatność jest sprawdzana przy wysyłaniu.', + 'agentWorld.trading.amountTooManyDecimals': 'Ta kwota ma zbyt wiele miejsc po przecinku.', + 'agentWorld.trading.amountMustBePositive': 'Wprowadź kwotę większą od zera.', + 'agentWorld.trading.amountInvalid': 'Wprowadź prawidłową kwotę.', }; export default messages; diff --git a/app/src/lib/i18n/pt.ts b/app/src/lib/i18n/pt.ts index 667c7ff1e..f65adb4b8 100644 --- a/app/src/lib/i18n/pt.ts +++ b/app/src/lib/i18n/pt.ts @@ -6129,6 +6129,38 @@ const messages: TranslationMap = { 'userErrors.insufficientCredits.body': 'Seu provedor de IA ficou sem créditos. Recarregue-o ou atualize a chave de API.', 'userErrors.scope.chat': 'Chat', + // Agent World — Identity trading (confirm-before-spend + balance gate) + 'agentWorld.trading.amountLabel': 'Valor', + 'agentWorld.trading.networkLabel': 'Rede', + 'agentWorld.trading.balanceLabel': 'Seu saldo', + 'agentWorld.trading.walletLabel': 'Carteira', + 'agentWorld.trading.balanceUnknown': 'Desconhecido', + 'agentWorld.trading.cancel': 'Cancelar', + 'agentWorld.trading.addFunds': 'Adicionar fundos', + 'agentWorld.trading.confirmPay': 'Confirmar e pagar', + 'agentWorld.trading.confirmCommit': 'Confirmar', + 'agentWorld.trading.continue': 'Continuar', + 'agentWorld.trading.submitting': 'Enviando…', + 'agentWorld.trading.placeBid': 'Dar lance', + 'agentWorld.trading.submitOffer': 'Enviar oferta', + 'agentWorld.trading.bidTitlePrefix': 'Lance para', + 'agentWorld.trading.offerTitlePrefix': 'Oferta para', + 'agentWorld.trading.bidReviewTitlePrefix': 'Confirmar lance para', + 'agentWorld.trading.offerReviewTitlePrefix': 'Confirmar oferta para', + 'agentWorld.trading.spendInsufficient': + 'Saldo insuficiente para concluir este pagamento. Adicione fundos à sua carteira para continuar.', + 'agentWorld.trading.spendBroadcastNote': + 'Sua carteira vai assinar e transmitir este pagamento na', + 'agentWorld.trading.commitInsufficientWarning': + 'Seu saldo pode não cobrir isso se o compromisso for aceito. Você ainda pode enviá-lo — os fundos só se movem na aceitação.', + 'agentWorld.trading.commitSettleNote': + 'Este é um compromisso assinado — os fundos só se movem se for aceito.', + 'agentWorld.trading.commitReviewSubtitle': 'Revise seu compromisso antes de enviar.', + 'agentWorld.trading.balanceUnverified': + 'Não conseguimos verificar o saldo da sua carteira. Você ainda pode continuar — o pagamento é verificado no envio.', + 'agentWorld.trading.amountTooManyDecimals': 'Este valor tem casas decimais em excesso.', + 'agentWorld.trading.amountMustBePositive': 'Insira um valor maior que zero.', + 'agentWorld.trading.amountInvalid': 'Insira um valor válido.', }; export default messages; diff --git a/app/src/lib/i18n/ru.ts b/app/src/lib/i18n/ru.ts index 09a4a010d..b3dd4a458 100644 --- a/app/src/lib/i18n/ru.ts +++ b/app/src/lib/i18n/ru.ts @@ -6089,6 +6089,37 @@ const messages: TranslationMap = { 'userErrors.insufficientCredits.title': 'Требуются кредиты провайдера', 'userErrors.insufficientCredits.body': 'У провайдера закончились кредиты. Пополните их.', 'userErrors.scope.chat': 'Чат', + // Agent World — Identity trading (confirm-before-spend + balance gate) + 'agentWorld.trading.amountLabel': 'Сумма', + 'agentWorld.trading.networkLabel': 'Сеть', + 'agentWorld.trading.balanceLabel': 'Ваш баланс', + 'agentWorld.trading.walletLabel': 'Кошелёк', + 'agentWorld.trading.balanceUnknown': 'Неизвестно', + 'agentWorld.trading.cancel': 'Отмена', + 'agentWorld.trading.addFunds': 'Пополнить', + 'agentWorld.trading.confirmPay': 'Подтвердить и оплатить', + 'agentWorld.trading.confirmCommit': 'Подтвердить', + 'agentWorld.trading.continue': 'Продолжить', + 'agentWorld.trading.submitting': 'Отправка…', + 'agentWorld.trading.placeBid': 'Сделать ставку', + 'agentWorld.trading.submitOffer': 'Отправить предложение', + 'agentWorld.trading.bidTitlePrefix': 'Ставка на', + 'agentWorld.trading.offerTitlePrefix': 'Предложение для', + 'agentWorld.trading.bidReviewTitlePrefix': 'Подтвердите ставку на', + 'agentWorld.trading.offerReviewTitlePrefix': 'Подтвердите предложение для', + 'agentWorld.trading.spendInsufficient': + 'Недостаточно средств для совершения этого платежа. Пополните кошелёк, чтобы продолжить.', + 'agentWorld.trading.spendBroadcastNote': 'Ваш кошелёк подпишет и отправит этот платёж в сети', + 'agentWorld.trading.commitInsufficientWarning': + 'Вашего баланса может не хватить, если обязательство будет принято. Вы всё равно можете его отправить — средства списываются только при принятии.', + 'agentWorld.trading.commitSettleNote': + 'Это подписанное обязательство — средства списываются только при его принятии.', + 'agentWorld.trading.commitReviewSubtitle': 'Проверьте обязательство перед отправкой.', + 'agentWorld.trading.balanceUnverified': + 'Нам не удалось проверить баланс вашего кошелька. Вы всё равно можете продолжить — платёж проверяется при отправке.', + 'agentWorld.trading.amountTooManyDecimals': 'В этой сумме слишком много знаков после запятой.', + 'agentWorld.trading.amountMustBePositive': 'Введите сумму больше нуля.', + 'agentWorld.trading.amountInvalid': 'Введите корректную сумму.', }; export default messages; diff --git a/app/src/lib/i18n/zh-CN.ts b/app/src/lib/i18n/zh-CN.ts index 152dd7c85..d3977d193 100644 --- a/app/src/lib/i18n/zh-CN.ts +++ b/app/src/lib/i18n/zh-CN.ts @@ -5720,6 +5720,36 @@ const messages: TranslationMap = { 'userErrors.insufficientCredits.title': '需要提供商额度', 'userErrors.insufficientCredits.body': '提供商额度已用完,请充值或更新 API 密钥。', 'userErrors.scope.chat': '聊天', + // Agent World — Identity trading (confirm-before-spend + balance gate) + 'agentWorld.trading.amountLabel': '金额', + 'agentWorld.trading.networkLabel': '网络', + 'agentWorld.trading.balanceLabel': '你的余额', + 'agentWorld.trading.walletLabel': '钱包', + 'agentWorld.trading.balanceUnknown': '未知', + 'agentWorld.trading.cancel': '取消', + 'agentWorld.trading.addFunds': '添加资金', + 'agentWorld.trading.confirmPay': '确认并支付', + 'agentWorld.trading.confirmCommit': '确认', + 'agentWorld.trading.continue': '继续', + 'agentWorld.trading.submitting': '正在提交…', + 'agentWorld.trading.placeBid': '出价', + 'agentWorld.trading.submitOffer': '提交报价', + 'agentWorld.trading.bidTitlePrefix': '出价:', + 'agentWorld.trading.offerTitlePrefix': '报价:', + 'agentWorld.trading.bidReviewTitlePrefix': '确认出价:', + 'agentWorld.trading.offerReviewTitlePrefix': '确认报价:', + 'agentWorld.trading.spendInsufficient': + '余额不足,无法完成此次支付。请向你的钱包添加资金以继续。', + 'agentWorld.trading.spendBroadcastNote': '你的钱包将对此次支付进行签名并广播至', + 'agentWorld.trading.commitInsufficientWarning': + '如果该承诺被接受,你的余额可能不足以支付。你仍然可以提交——资金仅在被接受时才会转移。', + 'agentWorld.trading.commitSettleNote': '这是一项已签名的承诺——仅在被接受时资金才会转移。', + 'agentWorld.trading.commitReviewSubtitle': '提交前请检查你的承诺。', + 'agentWorld.trading.balanceUnverified': + '我们无法验证你的钱包余额。你仍然可以继续——支付将在提交时进行检查。', + 'agentWorld.trading.amountTooManyDecimals': '该金额的小数位数过多。', + 'agentWorld.trading.amountMustBePositive': '请输入大于零的金额。', + 'agentWorld.trading.amountInvalid': '请输入有效的金额。', }; export default messages;