Build a three-chain vault app
Walk through the Upshift SDK starter, a Vite + React reference app that lists vaults, connects a wallet and deposits and withdraws on EVM, Solana and Stellar. Reads need no API keys; Solana balances and writes need an RPC that allows token-account lookups.
The starter app is the reference integration for @augustdigital/sdk. It runs at starter.upshift.finance against mainnet, so every flow on this page is proven against the real vaults. The public repository is a read-only mirror; the source lives in the Upshift frontend monorepo.
Everything an integrator needs to copy sits in src/integration/. That directory imports only the SDK, wagmi, viem, the Solana wallet adapters and @stellar/freighter-api. Shared reads sit at its top level, and each chain has its own folder with a wallet.tsx and an actions.ts: evm/, solana/ and stellar/. src/components/ is presentation and can be replaced.
1. Construct the SDK once
src/integration/sdk.ts builds the single SDK instance. The settings that matter:
| Setting | Value | Why |
|---|---|---|
appName | your own slug | Tags telemetry per integrator. |
providers | { 1: rpcUrl } | One RPC per EVM chain. The catalog drops vaults on chains with no provider. |
solana | { rpcUrl, network: 'mainnet-beta', commitment: 'confirmed' } | Configure Solana here, not through providers[-1], or commitment stays at finalized and wallet preflight guards trip on stale state. rpcUrl must allow getTokenAccountsByOwner, which free public endpoints reject. |
stellar | { network: 'mainnet' } | The SDK's public Soroban endpoints. |
keys | omitted | Vault reads and writes need no August API key. Solana balances and writes still need a Solana RPC that allows getTokenAccountsByOwner. |
monitoring.curatorAlerts | { enabled: false } on a demo | A failed Stellar redeem on a public host pages the vault's curator. Leave alerts on in a real product. |
The package is CommonJS. Vite exposes its default export differently in dev and build, so the starter unwraps default.default ?? default ?? module and sets build.commonjsOptions.defaultIsModuleExports and define.global in vite.config.ts.
2. Read vaults and group them by chain
src/integration/vaults.ts calls sdk.getVaults() once and groups on the synthetic chain ids: Solana vaults carry -1, Stellar -3, everything else is EVM. src/integration/vault-detail.ts reads one vault with getVault and thirty days of getVaultHistoricalTimeseries in parallel.
Never lowercase a Solana or Stellar address. Both are case-sensitive and the API rejects a lowercased id.
3. Connect a wallet per family
Each chain folder has a wallet.tsx with that chain's provider and hook. src/integration/wallets.tsx stacks the three providers and normalizes them to { family, address, connected, connect, disconnect }.
- EVM: wagmi with the
injectedconnector only. WalletConnect needs a project id, which breaks the zero-key goal. - Solana:
ConnectionProviderandWalletProviderwith Phantom, Solflare and Coinbase. Wallet-Standard wallets appear on their own. - Stellar: Freighter.
requestAccessruns only when the user clicks connect, never on mount, because it re-prompts every load. The address is restored from storage inside an effect.
4. Solana deposit and withdraw
src/integration/solana/actions.ts:
- Resolve the target with
Solana.utils.resolveProgramId(vault.address, { program_id: vault.programId }). The catalog'sprogramIdis required for factory vaults; without it the resolver returns the vault address itself and a v2 vault is targeted as a legacy program. PassvaultAddressonly when it differs from the program. - Call
sdk.solana.setWalletProvider(publicKey, signTransaction)immediately before every write. - Convert the amount with
toNormalizedBn(amount, decimals).rawand pass abigint. - Call
vaultDeposit(programId, idl, publicKey, raw, undefined, vaultAddress). The fifth argument is deprecated and must stayundefined; deleting it shiftsvaultAddressinto its slot. - Size a withdrawal with the share mint's decimals from
fetchUserShareBalanceRaw, not the deposit token's.
A failed balance read is shown as unknown, never as zero. The submit button stays disabled until publicKey and signTransaction are both present.
5. Stellar deposit and withdraw
src/integration/stellar/actions.ts follows build, sign, submit:
sdk.stellar.vaultDeposit({ contractId, amount, senderAddress })returns unsigned XDR.amountis the raw string in the token's smallest unit (7 decimals for USDC).- Freighter signs with the mainnet passphrase and the connected address.
sdk.stellar.submitTransaction(signedXdr)returns the hash.
Before a deposit, the starter asks Horizon whether the account exists. An unfunded account cannot source a transaction, so the form explains how to activate it and re-checks every eight seconds. For the native XLM vault the ledger keeps the reserve and the fee, so Max is capped at the spendable balance; offering the full balance reverts with Error(Contract, #10).
Before a withdrawal, getUserPosition must return decimalsFromFallback: false. Share decimals can be the asset decimals plus a vault offset (13 for earnUSDC), and sizing a redeem with assumed decimals under-redeems by that offset.
If submitTransaction throws an AugustSDKError whose context.resultCode is txBadSeq, the sequence number went stale. The starter rebuilds and resubmits once.
6. EVM deposit, request and claim
src/integration/evm/actions.ts:
sdk.evm.setSigner(walletClient)before every write; the wallet can rotate.sdk.evm.approve(...)as its own visible step, thensdk.evm.vaultDeposit({ target, wallet, amount, wait: true }). The SDK would approve silently insidevaultDeposit; showing the step keeps the user in control.sdk.evm.vaultRequestRedeem({ target, wallet, amount, wait: true })with the amount in shares.sdk.getVaultAvailableRedemptions({ vault, chainId, wallet })lists matured requests. Claim withclaimWithdrawalonevm-2vaults andvaultRedeemwithreceiverIndex: '0'onevm-0andevm-1, chosen byvault.version.
7. Positions and errors
getVaultPositions({ wallet, solanaWallet, stellarWallet }) returns a user's positions across all three families in one call.
src/integration/tx-flow.ts classifies every failure. A wallet decline is reported as declined, not as an error. An AugustTimeoutError means the outcome is unknown: link the explorer rather than saying it failed.
Not in the starter
Documented as hardening for a production integration, left out to keep the example small: Solana retry on Lighthouse guard trips and recovery of a landed signature after a confirmation error; the Stellar XDR sequence guard, a collision-safe Freighter transport for multi-wallet setups, and Soroban error decoding; the Solana withdrawal queue; D'CENT, Para, Ledger and Stellar Wallets Kit.