mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
This commit is contained in:
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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],
|
||||
});
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user