This document describes every validation rule enforced by the CreateVault flow, the balance-aware submission gate, and the three-step form → review → confirm lifecycle.
Defined in src/utils/vaultValidation.ts:
export interface CreateVaultFormValues {
amount: string; // raw numeric string (may include decimals)
deadline: string; // ISO datetime-local string
successAddress: string; // Stellar public key
failureAddress: string; // Stellar public key
}
// Each key is optional; a missing key means the field passed validation.
export type CreateVaultErrors = Partial<Record<keyof CreateVaultFormValues, string>>;All rules are applied by validateCreateVault(values, now?) which returns a
CreateVaultErrors object. An empty object means the form is valid.
| Attribute | Detail |
|---|---|
| Helper | isValidUsdcAmount(amount: string): boolean |
| Regex | ^(?:0|[1-9]\d*)(?:\.\d{1,7})?$ |
| Extra rule | Number(normalized) > 0 — zero is rejected |
| Normalisation | Leading/trailing whitespace is trimmed before testing |
| Error key | errors.amount |
| Error message | 'Enter a positive USDC amount with up to 7 decimal places.' |
Accepted examples:
| Input | Accepted? |
|---|---|
'100' |
✅ |
'0.0000001' |
✅ (7 decimal places) |
'1000.5' |
✅ |
'0' |
❌ — zero is not positive |
'0.00000001' |
❌ — 8 decimal places |
'-10' |
❌ — negative |
'1,000' |
❌ — comma not supported by validator (strip via parseUsdcInput first) |
'' |
❌ — empty |
Input formatting note: The
amountfield inCreateVault.tsxstores a raw string (no commas) viaparseUsdcInput, and only formats it for display viaformatUsdcInput. The raw value is whatvalidateCreateVaultreceives.
| Attribute | Detail |
|---|---|
| Helper | isFutureDeadline(deadline: string, now?: Date): boolean |
| Rule | new Date(deadline).getTime() must be finite and strictly greater than now.getTime(). |
Default now |
new Date() at call time; injectable for testing. |
| Error key | errors.deadline |
| Error message | 'Choose a future deadline.' |
Accepted examples:
| Input | Accepted? |
|---|---|
'2030-01-01T00:00' (future) |
✅ |
'2020-01-01T00:00' (past) |
❌ |
'' |
❌ — Date('') is Invalid Date → NaN |
'not-a-date' |
❌ — not parseable |
Deadline presets in src/utils/deadlinePresets.ts offer quick shortcuts:
| Preset | Days added | Output format |
|---|---|---|
'7d' |
7 | YYYY-MM-DDTHH:mm (local time) |
'30d' |
30 | YYYY-MM-DDTHH:mm |
'90d' |
90 | YYYY-MM-DDTHH:mm |
Clicking a preset calls computeFutureDeadline(days) which returns a
datetime-local string already in the correct format and always in the future.
| Attribute | Detail |
|---|---|
| Helper | isValidStellarAddress(address: string): boolean |
| Regex | ^G[A-Z2-7]{55}$ |
| Normalisation | .trim() before testing |
| Error key | errors.successAddress |
| Error message | 'Enter a valid Stellar public key starting with G.' |
A valid Stellar Ed25519 public key is a 56-character Base32 string starting
with G.
Validated in two sequential passes:
| Pass | Rule | Error message |
|---|---|---|
| 1 | Must satisfy isValidStellarAddress |
'Enter a valid Stellar public key starting with G.' |
| 2 (only if pass 1 succeeds) | Must differ from successAddress (after trimming both) |
'Failure destination must be different from success destination.' |
errors.failureAddress key |
Trigger |
|---|---|
| Invalid address format | Pass 1 fails |
| Same as success address | Pass 2 fails |
| (unset) | Both passes succeed |
| Export | Signature | Purpose |
|---|---|---|
isValidStellarAddress |
(address: string) → boolean |
Tests the Stellar public key regex |
isValidUsdcAmount |
(amount: string) → boolean |
Tests the USDC amount regex and positivity |
isFutureDeadline |
(deadline: string, now?: Date) → boolean |
Tests that the deadline is parseable and in the future |
validateCreateVault |
(values, now?) → CreateVaultErrors |
Runs all field rules; returns an error object |
hasCreateVaultErrors |
(errors: CreateVaultErrors) → boolean |
Returns true when any error key is present |
exceedsBalance |
(amount, balance) → boolean |
Balance gate — see section 4 |
export function exceedsBalance(amount: string, balance: string | null): boolean {
if (balance === null) return false;
const a = Number(amount);
const b = Number(balance);
if (!Number.isFinite(a) || !Number.isFinite(b)) return false;
return a > b;
}| Condition | Returns | UI effect |
|---|---|---|
balance === null (wallet not connected / balance unknown) |
false |
Warning suppressed — unknown balance is non-blocking |
| Either value is not a finite number | false |
Warning suppressed |
amount > balance |
true |
Warning rendered inline below the amount field |
amount <= balance |
false |
No warning |
{balanceStatus === 'success' && exceedsBalance(amount, balance) && (
<p role="status" style={{ color: 'var(--warning)' }}>
Amount exceeds your available USDC balance ({balance}).
</p>
)}The warning is shown only when balanceStatus === 'success' — meaning
Horizon returned a confirmed balance. The warning is advisory, not
blocking: it does not prevent form submission or trigger a validation error.
The user can still proceed to the review step even when the amount exceeds the
balance. Actual contract-level enforcement happens on the Stellar network.
Design intent: treating an unknown balance as non-blocking (
return falsewhenbalance === null) prevents the warning from appearing during wallet connection, on testnet where the balance may be 0, or when the balance fetch is still in progress.
┌──────────────┐ submit (valid) ┌──────────────┐ onConfirm ┌──────────────┐
│ Form step │ ─────────────────► │ Review step │ ────────────► │ Confirmed │
│ showReview │ │ showReview │ │ (logger) │
│ = false │ ◄───────────────── │ = true │ └──────────────┘
└──────────────┘ onBack └──────────────┘
- User fills in
amount,deadline,successAddress,failureAddress, and optionally attaches evidence viaEvidenceUpload. - On submit,
handleSubmitcallsvalidateCreateVaultwith the current field values. - If
hasCreateVaultErrors(nextErrors)istrue:- Errors are stored in state and rendered in the inline error summary
(
role="alert"). - Focus is moved to the first invalid field (in field order:
amount → deadline → successAddress → failureAddress) viaref.current?.focus(). - Navigation to the review step is blocked.
- Errors are stored in state and rendered in the inline error summary
(
- If no errors,
showReviewis set totrue.
Field order for error focus:
const errorFieldOrder = ['amount', 'deadline', 'successAddress', 'failureAddress'];Inline error clearing: Each field's onChange handler clears only its own
error key so errors disappear as the user corrects them without re-running full
validation:
setErrors(current => ({ ...current, amount: undefined }));CreateVaultReview renders a read-only summary of all four required fields plus
optional verifierAddress and milestone (unused by CreateVault.tsx
currently). No additional validation runs in this step. The user can either:
- Back to edit (
onBack) — setsshowReview = false, restoring the form with all values intact and errors cleared. - Confirm Vault (
onConfirm) — callshandleConfirmwhich currently logs the values vialogger.debug. Chain submission is not yet implemented.
handleConfirm is the extension point for the actual Stellar transaction. The
logged payload is:
{ amount, deadline, successAddress, failureAddress, evidenceUrl }- Add the new field to
CreateVaultFormValues(if not already present). - Implement a helper function (e.g.
isValidMilestone) and export it fromvaultValidation.ts. - Call the helper inside
validateCreateVaultand set the appropriate key onerrors. - Add the new field key to
errorFieldOrderandfieldRefsinCreateVault.tsxso focus management covers the new field. - Add an
onChangehandler that clears only the new field's error key. - Pass the new field value to
CreateVaultReviewif it should be shown in the review step. - Update this document with the new field's rule table.
| File | Role |
|---|---|
src/utils/vaultValidation.ts |
All validation helpers and types (CreateVaultFormValues, CreateVaultErrors, validateCreateVault, exceedsBalance) |
src/pages/CreateVault.tsx |
Form, submission handler, balance warning, step state |
src/components/CreateVaultReview.tsx |
Read-only review step and confirm action |
src/utils/deadlinePresets.ts |
DEADLINE_PRESETS, computeFutureDeadline, getPresetLabel |
src/utils/usdcInput.ts |
formatUsdcInput / parseUsdcInput — display formatting separate from validation |
src/context/WalletContext.tsx |
balance and balanceStatus consumed by the balance gate |