fix(agentworld): confirm-before-spend + balance gate for identity offer/bid (#4039) (#4061)

This commit is contained in:
oxoxDev
2026-06-24 10:10:59 -07:00
committed by GitHub
parent d35de21950
commit f0489cae48
23 changed files with 1158 additions and 118 deletions
+57
View File
@@ -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');
});
});
+51
View File
@@ -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)}`;
}
@@ -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(<AmountCommitDialog {...baseProps()} />);
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(<AmountCommitDialog {...props} />);
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(<AmountCommitDialog {...baseProps()} />);
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(<AmountCommitDialog {...props} />);
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 () => {
@@ -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 (
<ModalShell
@@ -56,35 +133,53 @@ export default function AmountCommitDialog({
<label
className="mb-1 block text-xs text-stone-400 dark:text-neutral-500"
htmlFor="x402-commit-amount">
Amount ({asset})
{t('agentWorld.trading.amountLabel', 'Amount')} ({asset})
</label>
<input
id="x402-commit-amount"
data-testid="commit-amount-input"
className="w-full rounded-md border border-stone-300 dark:border-neutral-700 bg-white dark:bg-neutral-900 px-3 py-2 text-sm text-stone-900 dark:text-neutral-100 outline-none focus:border-primary-500"
inputMode="numeric"
placeholder="0"
inputMode="decimal"
placeholder="0.0"
value={amount}
disabled={busy}
onChange={e => setAmount(e.target.value.replace(/[^0-9]/g, ''))}
onChange={e => setAmount(sanitizeDecimalInput(e.target.value))}
/>
{parsed.errorKey && (
<p className="mt-1 text-xs text-coral-500" data-testid="commit-amount-error">
{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.')}
</p>
)}
</div>
<p className="text-xs text-stone-400 dark:text-neutral-500">
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.'
)}
</p>
<div className="flex justify-end gap-2">
<Button variant="secondary" size="sm" onClick={onCancel} disabled={busy}>
Cancel
{t('agentWorld.trading.cancel', 'Cancel')}
</Button>
<Button
variant="primary"
size="sm"
data-testid="commit-submit"
disabled={!canSubmit}
onClick={() => onSubmit(sanitized)}>
{busy ? busyLabel : submitLabel}
onClick={handleSubmit}>
{busy ? (busyLabel ?? t('agentWorld.trading.submitting', 'Submitting…')) : submitLabel}
</Button>
</div>
</div>
@@ -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<void>;
/** 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<Phase>({ 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 (
<AmountCommitDialog
title={amountTitle}
subtitle={t(
'agentWorld.trading.commitSettleNote',
'A signed commitment — funds move only if it is accepted.'
)}
asset={asset}
decimals={decimals}
submitLabel={t('agentWorld.trading.continue', 'Continue')}
onSubmit={handleAmount}
onCancel={onClose}
/>
);
}
return (
<X402ConfirmDialog
title={reviewTitle}
subtitle={t(
'agentWorld.trading.commitReviewSubtitle',
'Review your commitment before submitting.'
)}
mode="commit"
amount={phase.amountBase}
asset={asset}
network={network}
balance={balance}
walletAddress={walletAddress}
busy={phase.step === 'submitting'}
busyLabel={t('agentWorld.trading.submitting', 'Submitting…')}
confirmLabel={confirmLabel}
onConfirm={handleConfirm}
onCancel={onClose}
/>
);
}
@@ -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(<X402ConfirmDialog {...baseProps()} balance={null} />);
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(<X402ConfirmDialog {...baseProps()} mode="commit" amount="60000000" />);
// 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(<X402ConfirmDialog {...baseProps()} mode="commit" balance={null} amount="60000000" />);
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(<X402ConfirmDialog {...baseProps()} amount="60000000" />);
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(<X402ConfirmDialog {...baseProps()} busy busyLabel="Paying…" />);
const confirm = screen.getByTestId('x402-confirm');
@@ -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 (
<ModalShell
@@ -133,52 +204,86 @@ export default function X402ConfirmDialog({
maxWidthClassName="max-w-sm">
<div className="space-y-4">
<div className="rounded-lg border border-stone-200 dark:border-neutral-800 bg-stone-50 dark:bg-neutral-900/50 p-4 space-y-3">
<Row label="Amount">
<Row label={t('agentWorld.trading.amountLabel', 'Amount')}>
<span
className="font-semibold text-stone-900 dark:text-neutral-100"
data-testid="x402-amount">
{amountDisplay} {assetSymbol}
</span>
</Row>
<Row label="Network">
<Row label={t('agentWorld.trading.networkLabel', 'Network')}>
<span className="text-xs text-stone-500 dark:text-neutral-400">
{friendlyNetwork(network)}
</span>
</Row>
<Row label="Your balance">
<Row label={t('agentWorld.trading.balanceLabel', 'Your balance')}>
<span
className={`font-medium ${
insufficient ? 'text-coral-500' : 'text-stone-700 dark:text-neutral-200'
}`}
data-testid="x402-balance">
{balance ? `${balance.formatted} ${balance.assetSymbol}` : 'Unknown'}
{balance
? `${balance.formatted} ${balance.assetSymbol}`
: t('agentWorld.trading.balanceUnknown', 'Unknown')}
</span>
</Row>
<Row label="Wallet">
<Row label={t('agentWorld.trading.walletLabel', 'Wallet')}>
<span className="font-mono text-xs text-stone-500 dark:text-neutral-400">
{truncateAddress(walletAddress)}
</span>
</Row>
</div>
{insufficient ? (
{hardBlock ? (
<p className="text-xs text-coral-500" data-testid="x402-insufficient">
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.`
)}
</p>
) : 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.
<p className="text-xs text-amber-500" data-testid="x402-commit-warning">
{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.`
)}
</p>
) : 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).
<p className="text-xs text-amber-500" data-testid="x402-balance-unverified">
{t(
'agentWorld.trading.balanceUnverified',
"We couldn't verify your wallet balance. You can still continue — the payment is checked when it is submitted."
)}
</p>
) : mode === 'commit' ? (
<p className="text-xs text-stone-400 dark:text-neutral-500">
{t(
'agentWorld.trading.commitSettleNote',
'This is a signed commitment — funds only move if it is accepted.'
)}
</p>
) : (
<p className="text-xs text-stone-400 dark:text-neutral-500">
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)}.
</p>
)}
<div className="flex justify-end gap-2">
<Button variant="secondary" size="sm" onClick={onCancel} disabled={busy}>
Cancel
{t('agentWorld.trading.cancel', 'Cancel')}
</Button>
{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.
<Button
variant="primary"
size="sm"
@@ -186,7 +291,7 @@ export default function X402ConfirmDialog({
void openUrl(fundingUrl(walletAddress, assetSymbol));
}}
data-testid="x402-add-funds">
Add funds
{t('agentWorld.trading.addFunds', 'Add funds')}
</Button>
) : (
<Button
@@ -195,7 +300,7 @@ export default function X402ConfirmDialog({
onClick={onConfirm}
disabled={confirmDisabled}
data-testid="x402-confirm">
{busy ? busyLabel : 'Confirm & Pay'}
{busy ? busyLabel : (confirmLabel ?? defaultConfirmLabel)}
</Button>
)}
</div>
@@ -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(<IdentitiesSection />);
@@ -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(<IdentitiesSection />);
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(<IdentitiesSection />);
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],
});
+43 -28
View File
@@ -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<void> {
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() {
</div>
)}
{/* Bid / offer amount dialog. */}
{/* Bid / offer commitment flow (amount → review → submit). */}
{commit && (
<AmountCommitDialog
title={
commit.kind === 'bid'
? `Bid on ${commit.listing.name}`
: `Offer for ${commit.listing.name}`
}
subtitle="A signed commitment — funds move only if it is accepted."
<CommitFlow
kind={commit.kind}
name={commit.listing.name}
asset={commit.listing.price.asset}
submitLabel={commit.kind === 'bid' ? 'Place bid' : 'Submit offer'}
busy={commitState.phase === 'busy'}
onCancel={closeCommit}
onSubmit={submitCommit}
decimals={decimalsForAsset(commit.listing.price.asset)}
network={commit.listing.price.network}
submit={performCommit}
onSuccess={handleCommitSuccess}
onError={handleCommitError}
onClose={closeCommit}
/>
)}
+31
View File
@@ -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;
+31
View File
@@ -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;
+32
View File
@@ -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;
+33
View File
@@ -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',
+31
View File
@@ -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;
+32
View File
@@ -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 loffre',
'agentWorld.trading.bidTitlePrefix': 'Enchérir sur',
'agentWorld.trading.offerTitlePrefix': 'Offre pour',
'agentWorld.trading.bidReviewTitlePrefix': 'Confirmer lenchère sur',
'agentWorld.trading.offerReviewTitlePrefix': 'Confirmer loffre 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 lengagement est accepté. Vous pouvez quand même lenvoyer : les fonds ne bougent qu’à lacceptation.',
'agentWorld.trading.commitSettleNote':
'Il sagit dun engagement signé : les fonds ne bougent quen cas dacceptation.',
'agentWorld.trading.commitReviewSubtitle': 'Vérifiez votre engagement avant de lenvoyer.',
'agentWorld.trading.balanceUnverified':
'Nous navons pas pu vérifier le solde de votre portefeuille. Vous pouvez quand même continuer : le paiement est vérifié lors de lenvoi.',
'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;
+32
View File
@@ -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;
+32
View File
@@ -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;
+32
View File
@@ -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 unofferta',
'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 limpegno viene accettato. Puoi comunque inviarlo: i fondi si spostano solo allaccettazione.',
'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 allinvio.',
'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;
+31
View File
@@ -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;
+31
View File
@@ -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;
+32
View File
@@ -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;
+31
View File
@@ -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;
+30
View File
@@ -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;