Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ VITE_STELLAR_RPC_URL=
# cannot be submitted until this is set. Must match VITE_STELLAR_NETWORK.
VITE_STELLAR_VAULT_CONTRACT_ID=

# Classic USDC issuer backing the vault's Stellar Asset Contract. Testnet
# defaults to GDXDZNSTNORCX4373EGGYS4Q3U3XVZMBSIMMRHG5S5GCCF7JHC7QZWAZ.
# Leave commented to use that default. Mainnet/futurenet require an explicit
# issuer for their vault. Invalid or empty keys block trustline/deposit actions.
# VITE_STELLAR_USDC_ISSUER=GDXDZNSTNORCX4373EGGYS4Q3U3XVZMBSIMMRHG5S5GCCF7JHC7QZWAZ

# Dev server only: set to `true` to show the deposit demo-outcome toggle.
VITE_ENABLE_DEMO_OUTCOME=false

Expand Down
22 changes: 22 additions & 0 deletions docs/Configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,25 @@ in development, because a silently wrong network is worse than a loud warning.
Resolve it through `resolveStellarNetwork()` rather than reading the raw
variable.

### `VITE_STELLAR_USDC_ISSUER`

Classic issuer public key for the `USDC` asset backing the configured vault's
Stellar Asset Contract. On testnet, the default is
`GDXDZNSTNORCX4373EGGYS4Q3U3XVZMBSIMMRHG5S5GCCF7JHC7QZWAZ`, backing token
`CAC7CFZ42L3LTTZCI4C3XI6UF7AHS2PYTNJ5QEG2PSLRGDGMWRQOZOT5`.
Override it only with the issuer used by the vault on the selected network.
Mainnet and futurenet require an explicit issuer; they do not inherit the
testnet issuer. Surrounding whitespace is trimmed. An invalid or explicitly
empty key produces a configuration error before a trustline lookup or signature
request. Validation uses `StrKey.isValidEd25519PublicKey`.

`STELLAR_USDC_ASSET` exports `{ code: "USDC", issuer }`.
`STELLAR_USDC_CONFIGURATION_ERROR` exposes validation failures to the modal.
`STELLAR_HORIZON_URL` follows `STELLAR_NETWORK`: testnet uses
`https://horizon-testnet.stellar.org`, mainnet uses `https://horizon.stellar.org`,
and futurenet uses `https://horizon-futurenet.stellar.org`.
See [Deposit flow](Deposit-Flow.md) for the trustline and funding steps.

### `VITE_DEV_API_PROXY_TARGET`

Where the dev server forwards `/api` requests. Default:
Expand Down Expand Up @@ -124,6 +143,9 @@ relative is what makes that work without a rebuild per environment.
| `STELLAR_NETWORK_PASSPHRASE` | passphrase for `STELLAR_NETWORK`, used to sign deposits |
| `STELLAR_RPC_URL` | `VITE_STELLAR_RPC_URL`, else the network's public RPC (none for mainnet) |
| `STELLAR_VAULT_CONTRACT_ID` | `VITE_STELLAR_VAULT_CONTRACT_ID`, empty by default |
| `STELLAR_USDC_ASSET` | classic USDC code and validated configured issuer |
| `STELLAR_USDC_CONFIGURATION_ERROR` | invalid/missing issuer configuration error, otherwise `null` |
| `STELLAR_HORIZON_URL` | Horizon origin for `STELLAR_NETWORK` |
| `ENABLE_DEMO_OUTCOME` | `VITE_ENABLE_DEMO_OUTCOME === "true"` on the dev server only |
| `EXPLORER_BASE_URL` | stellar.expert transaction URL for `STELLAR_NETWORK` |

Expand Down
169 changes: 118 additions & 51 deletions docs/Deposit-Flow.md
Original file line number Diff line number Diff line change
@@ -1,57 +1,124 @@
# Deposit flow contract

This describes the current **demo** deposit modal on `/billing`. The source of truth is `src/App.tsx`; values shared with the UI come from `src/config/constants.ts`. It is a reference for the wallet integration, not a claim that a wallet transaction or backend deposit exists today.

## Stages and transitions
# Deposit flow

The `/billing` deposit modal signs transactions with Freighter and submits vault
deposits through Stellar RPC. Shared configuration lives in
`src/config/constants.ts`; wallet operations live in `src/services/walletService.ts`.

## Asset and network configuration

`STELLAR_USDC_ASSET` is `{ code: "USDC", issuer }`. The testnet default issuer is
`GDXDZNSTNORCX4373EGGYS4Q3U3XVZMBSIMMRHG5S5GCCF7JHC7QZWAZ`, whose Stellar Asset
Contract is `CAC7CFZ42L3LTTZCI4C3XI6UF7AHS2PYTNJ5QEG2PSLRGDGMWRQOZOT5`.
`VITE_STELLAR_USDC_ISSUER` overrides the issuer and is validated with
`StrKey.isValidEd25519PublicKey`. Mainnet and futurenet require an explicit issuer.
Invalid configuration blocks trustline and deposit actions before Horizon is called.
The configured vault must accept this asset; a public key alone cannot prove that
it matches the vault. See [Configuration](Configuration.md) and `.env.example`.

Freighter must use `STELLAR_NETWORK_PASSPHRASE`. `STELLAR_HORIZON_URL` selects the
same network for classic account lookups and trustline submission. Vault deposits
require `VITE_STELLAR_VAULT_CONTRACT_ID` and the configured Soroban RPC endpoint.

## Trustline step

Opening the modal checks Freighter, connects the selected account, verifies its
network, then calls `hasUsdcTrustline(address)`. It fetches Horizon
`GET /accounts/{address}` and matches **both** `asset_code` and `asset_issuer` in
`balances`. A zero USDC balance still counts as an existing trustline.

| Result | Modal behavior |
| --- | --- |
| `present` | Show **Approve Transaction** for a valid amount. |
| `missing` | Replace approval with **Add USDC trustline** and explain the additional **0.5 XLM base reserve**, plus the transaction fee. |
| `unfunded-account` | Horizon returned 404. Explain XLM funding and offer **Check USDC trustline again**. Show an account-specific Friendbot link only when `STELLAR_NETWORK === "testnet"`. |
| `unknown` | A network/HTTP/invalid-response failure prevents checking. Show a warning and allow approval. |

The trustline step uses native keyboard-operable buttons and links. Its status is
announced through a polite, atomic live region, linked to the action with
`aria-describedby`. Rejected wallet access offers **Connect Freighter** again.

**Add USDC trustline** reconnects the wallet and verifies the network, loads the
classic account sequence, and builds a transaction containing exactly one
`Operation.changeTrust` for `STELLAR_USDC_ASSET`. It has a bounded lifetime and uses
the configured passphrase when signing in Freighter. No payment, mint, deposit,
or other operation is included. Horizon's synchronous `POST /transactions` endpoint
receives the signed XDR and returns after ledger inclusion. Only a response with
`successful: true` counts as success. The modal then rechecks the selected account
before enabling deposit approval.

While adding, controls prevent duplicate submissions or closing the modal. A
rejected signature is a normal cancellation, detected by the existing
`isUserRejection` helper: stay at the trustline step without an error toast.
Other failures remain inline and allow another trustline check before retrying.
A trustline reserves XLM; it does not provide USDC. Minting/faucet logic for USDC
and live wallet balance display are outside this flow.

## Deposit stages and amounts

```mermaid
stateDiagram-v2
[*] --> input: Open deposit modal / reset flow
input --> input: Change amount, select preset or Max
input --> approving: Approve Transaction (valid amount)
approving --> pending: 1.4 s demo timer
pending --> confirmed: 3.6 s demo timer; demoOutcome = confirmed
pending --> failed: 3.6 s demo timer; demoOutcome = failed
failed --> input: Retry deposit
confirmed --> input: Deposit another amount (reset to 50 USDC)
input --> [*]: Close, Cancel, backdrop, or leave /billing
confirmed --> [*]: Close, Cancel, backdrop, or leave /billing
failed --> [*]: Close, Cancel, backdrop, or leave /billing
[*] --> input: Open modal and check trustline
input --> trustline: Missing trustline
trustline --> input: Confirm changeTrust and recheck
trustline --> trustline: Cancel signature or retry
input --> approving: Approve valid amount (present or unknown)
approving --> pending: Stellar RPC accepts transaction and returns hash
pending --> confirmed: RPC confirms success
approving --> failed: Rejection or submission failure
pending --> failed: Failure or confirmation timeout
failed --> input: Retry after definitive failure
confirmed --> input: Deposit another amount
```

`approving` and `pending` are busy states: amount, preset, Max, Close, and Cancel controls are disabled, and the close handler refuses to dismiss the modal. The `pending` timer is scheduled when approval starts, so its 1.4-second delay is measured from the click, not from a wallet callback. The final timer is also measured from that click. The stage strip shows the selected demo outcome as its final stage.

Opening from the billing page, a preset, or `/billing?deposit=true` resets the stage to `input`, clears the transaction hash and captured submission, and preserves the current amount unless a preset was supplied. Closing removes the `deposit` query parameter when present. Leaving `/billing` hides the modal; unmounting the app clears scheduled timers.

**Current wiring caveat:** the billing page's “Open deposit modal” button passes `openDeposit` directly as `onClick`. React therefore supplies its click event as the optional `presetAmount`; that path can put a nonnumeric event string into the amount field. Other callers invoke `openDeposit()` or pass a numeric preset explicitly. The wallet integration should wrap the billing click handler so this demo wiring error is not treated as part of the deposit contract.

| Stage | Status banner heading | Status message and visible result |
| --- | --- | --- |
| `input` | “Enter a deposit amount” until valid, then “Review transaction preview” | “Deposit funds to keep premium calls and AI workflows funded without leaving the dashboard.” The preview is editable; invalid input shows a validation message. |
| `approving` | “Approve in wallet...” | “Approve this USDC deposit in your wallet to continue.” The primary button reads “Approve in wallet...” and is disabled. This is a simulated wait; no wallet prompt is opened. |
| `pending` | “Transaction submitted...” | “Transaction submitted to Stellar. Waiting for confirmation.” A shortened transaction hash, copy action, and testnet explorer link appear. The primary button reads “Transaction submitted...” and is disabled. |
| `confirmed` | “Deposit successful” | “{amount} reached the vault. Your balance is updated and ready for API usage.” The demo adds the captured amount to the captured starting vault balance, rounded to two decimals. A success card shows the new balance. |
| `failed` | “Transaction failed” | “The deposit was not confirmed. Review the details, then retry when your wallet is ready.” A failure card says no funds were added and offers **Retry deposit**. |

## Amount and preview rules

The amount starts at `50` USDC. `src/config/constants.ts` defines `MIN_DEPOSIT = 10`, `PRESET_AMOUNTS = [10, 50, 100, 500]`, and the displayed `NETWORK_FEE = "0.00001 XLM"`. The current wallet balance is an in-memory `1260.5` USDC value in `App.tsx`; it is the upper limit, not a constant exported by config. **Max** fills the input with that balance to two decimal places.

The input handler removes every character except digits and periods. Validation then checks, in order: blank input (“Enter a deposit amount to continue.”), non-finite numeric conversion (“Amount must be a valid number.”), an amount below `MIN_DEPOSIT` (“Minimum deposit is $10.”), and an amount above the available wallet balance (“Amount exceeds available wallet balance.”). The submit button is enabled only when none of these messages applies. The handler repeats this guard and rejects clicks while busy. The implementation does not impose a decimal-place limit or separately reject a malformed string if JavaScript converts it to a finite number; the wallet integration should define stricter amount parsing and USDC precision before submission.

The modal's preview shows deposit amount, starting vault balance, projected vault balance, the displayed XLM fee, and total cost. At approval, the amount and starting vault balance are captured so later display and completion use the submitted values. `src/components/DepositPreview.tsx` is a reusable before/after presentation component that also computes a displayed wallet-after value; the active modal renders its own preview markup. Neither preview authorizes a transfer.

## Retry and reset

On failure, **Retry deposit** restores the submitted amount in the input, selects Custom, sets the stage to `input`, and shows “Review the transaction details and approve again.” It does not itself submit a transaction or credit the vault. The previous hash and captured values remain in state but the hash card is hidden at `input`; the next approval replaces them with a new mock hash and captured amount. Editing the amount or selecting a preset calls the full reset, which clears the hash, submission, copy state, and scheduled timers.

After success, **Deposit another amount** calls the full reset with `50` USDC and the matching preset. It keeps the credited vault balance. The demo outcome selector is independent of these resets, so its previous selection persists until changed on the billing page. Closing and reopening resets the flow state but does not undo an already credited demo balance.

## Current mocks and integration boundary

- `createMockHash()` builds a 64-character string from the clock and `Math.random()`. It is created **before** the approving stage, rather than returned by a wallet or network submission. `EXPLORER_BASE_URL` points to Stellar testnet, but a generated hash is not evidence of an on-chain transaction.
- Two `window.setTimeout` calls advance the flow after 1.4 and 3.6 seconds. They do not observe wallet approval, transaction submission, confirmation depth, or a backend response. Starting another approval or a full reset clears pending timers.
- `demoOutcome` is a billing-page toggle between `confirmed` and `failed`; it alone decides the final branch. Success mutates only the in-memory vault balance. The displayed wallet balance is not debited, and reload loses the demo state.
- The XLM network fee is a display constant, not an estimated or charged fee. Clipboard copy and the explorer link are UI actions only.
The amount starts at 50 USDC. `MIN_DEPOSIT` is 10 and presets are 10, 50, 100, and
500. Input normalizes separators and limits USDC precision to seven decimal places.
Validation rejects blank, nonnumeric, below-minimum, and above-displayed-wallet
amounts. The service independently validates positive amounts and precision.
The displayed wallet and vault balances remain in-memory UI values; this issue
does not add an authoritative balance lookup.

Approval reconnects the wallet and checks its current trustline again before
Soroban simulation, protecting against account changes since the initial check.
A newly missing line or unfunded account returns to the corresponding prerequisite
step. An unknown lookup permits simulation. The service prepares the vault's
`deposit(caller, amount)` invocation, signs via Freighter, submits to RPC, and
polls confirmation. The explorer link uses the hash returned by RPC. Only
confirmed deposits update the displayed vault balance.

Approving, pending, and trustline submission disable amount/preset/Max/Close/Cancel
controls. A definitive deposit failure permits **Retry deposit**, preserving the
amount. Ambiguous submission/confirmation failures require checking the transaction
before another deposit is allowed. **Deposit another amount** resets to 50 USDC.

## Verification and PR recording (#1316)

Service and modal tests mock Horizon fetch and Freighter. They cover existing and
missing trustlines, unfunded accounts, network failure, a confirmed trustline,
signature cancellation, invalid issuer configuration, and mainnet without Friendbot.
The service test decodes the exact XDR passed to mocked `signTransaction` and asserts
one `changeTrust` operation with the configured asset. Run:

```sh
npx tsc -p tsconfig.json --noEmit
npm run build
npx vitest run
npm run test:coverage
```

For a real integration, the wallet/backend boundary must return or report an authoritative transaction identifier and approval/submission result, then track confirmation or failure for that identifier. Only a confirmed deposit should credit a persisted vault balance; a failed, rejected, or timed-out operation must leave it unchanged. The backend must enforce the minimum, available-funds limit, amount precision, and idempotency independently of the UI. The integration must replace demo timers and `demoOutcome` with observed states, handle wallet rejection and network errors deterministically, prevent duplicate submissions while one is in flight, and restore status after reload. Preserve the user-facing stage names and retry/reset behavior unless a deliberate product change updates this contract.
For the required real-wallet recording:

1. Configure testnet and the deployed vault; select a fresh Freighter testnet account.
The vault owner must allowlist it if the deployment restricts callers
(`CallerNotInAllowlist`, contract error 44).
2. Open `/billing?deposit=true`. If unfunded, follow Friendbot and return to check again.
3. Record **Add USDC trustline**, its reserve explanation, Freighter's single
changeTrust approval, ledger confirmation, and the resulting deposit approval.
4. With USDC supplied independently, complete the vault deposit and show its explorer
result. USDC issuance is not part of this change.
5. Include a cancellation demonstration if useful. Attach the recording and actual
command output to a PR linking `Closes #1316`.

Regression proof for row 2: on the original main branch, a wallet without the USDC
trustline can click **Approve Transaction**, reaches simulation, and receives a
generic failure. With this change, the same account is offered the trustline step
before a deposit can be simulated.
Loading