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
14 changes: 9 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,11 +76,15 @@ apply as the corresponding code lands here.
clone builds the client before the app, or `tsc` cannot resolve the module.
- `app-lib/clients/index.ts` is auto-generated and rewritten on every build or
redeploy. Do not hand-edit it; customize by importing the client under `app/`.
- `app/` never imports the generated barrel — it constructs its own contract
clients at runtime with `contract.Client.from` (`app-lib/contracts.ts`),
bound to addresses `app/src/config/addresses.ts` generates offline. A fresh
clone can build and run `app` with no cargo, no stellar CLI, no scaffold,
and no local network.
- `app/` never imports the generated barrel. `app/`'s `prebuild` runs
`scripts/generate-clients.mjs`, which reads the contract ids and RPC URL of
`ADDRESSES_ENV` (default `staging`) from `environments.toml` and writes
`app-lib/clients/<contract>/` from each contract's on-chain spec, with Node
alone. Those generated types are the only contract types; `app-lib/contracts.ts`
re-exports them and builds the runtime clients with `contract.Client.from`.
A fresh clone builds `app` with no cargo, no stellar CLI and no local network,
but needs RPC access to the selected network at build time. A local scaffold
build writes the same directory from the WASM.
- When a tool must drive `cargo` directly rather than the CLI, set
`SOROBAN_SDK_BUILD_SYSTEM_SUPPORTS_SPEC_SHAKING_V2=1`. The build script fails
without it and its error names neither the variable nor this way out, so
Expand Down
125 changes: 29 additions & 96 deletions app-lib/contracts.ts
Original file line number Diff line number Diff line change
@@ -1,111 +1,44 @@
import {
AssembledTransaction,
Client,
type MethodOptions,
type SignTransaction,
type i128,
type u64,
} from "@stellar/stellar-sdk/contract"
import { network, networkPassphrase, rpcUrl } from "./env"

type Call<T> = (options?: MethodOptions) => Promise<AssembledTransaction<T>>
type CallOf<A, T> = (
args: A,
options?: MethodOptions,
) => Promise<AssembledTransaction<T>>

export type Option<T> = T | null
import type { Client as AssetApi } from "./clients/asset/src/index"
import type {
Client as AsyncVaultApi,
DepositRequest,
EpochInfo,
EpochStatus,
RedeemRequest,
} from "./clients/async_vault/src/index"
import type { Client as IdentityVerifierApi } from "./clients/identity_verifier/src/index"
import type {
Client as NavOracleApi,
NavReport,
OracleState,
} from "./clients/nav_oracle/src/index"
import type { Client as ShareTokenApi } from "./clients/share_token/src/index"

export type {
AssetApi,
AsyncVaultApi,
DepositRequest,
EpochInfo,
EpochStatus,
IdentityVerifierApi,
NavOracleApi,
NavReport,
OracleState,
RedeemRequest,
ShareTokenApi,
}

export interface Signer {
publicKey: string
signTransaction: SignTransaction
}

export interface AsyncVaultApi {
liquid_reserve: Call<i128>
committed: Call<i128>
uncovered: Call<i128>
total_economic_supply: Call<i128>
net_deployed: Call<i128>
governance: Call<Option<string>>
manager: Call<Option<string>>
treasury: Call<Option<string>>
guardian: Call<Option<string>>
custodian: Call<Option<string>>
paused: Call<boolean>
current_epoch: Call<u64>
get_epoch: CallOf<{ epoch_id: u64 }, Option<EpochInfo>>
get_deposit_request: CallOf<
{ epoch_id: u64; controller: string },
Option<DepositRequest>
>
get_redeem_request: CallOf<
{ epoch_id: u64; controller: string },
Option<RedeemRequest>
>
request_deposit: CallOf<{ from: string; amount: i128 }, u64>
cancel_deposit: CallOf<{ from: string; epoch_id: u64 }, i128>
claim_deposit: CallOf<{ caller: string; epoch_id: u64 }, i128>
request_redeem: CallOf<{ from: string; shares: i128 }, u64>
cancel_redeem: CallOf<{ from: string; epoch_id: u64 }, i128>
claim_redeem: CallOf<{ caller: string; epoch_id: u64 }, i128>
}

export type EpochStatus =
| { tag: "Open"; values: void }
| { tag: "Pending"; values: void }
| { tag: "Fulfilled"; values: void }

export interface EpochInfo {
status: EpochStatus
total_deposited: i128
total_shares_redeeming: i128
share_price: i128
closed_at: u64
priceable_at: u64
}

export interface DepositRequest {
amount: i128
claimed: boolean
}

export interface RedeemRequest {
shares: i128
claimed: boolean
}

export type OracleState =
| { tag: "Valid"; values: void }
| { tag: "Stale"; values: void }
| { tag: "Paused"; values: void }

export interface NavReport {
nav_per_share: i128
expires_at: u64
timestamp: u64
}

export interface NavOracleApi {
state: Call<OracleState>
latest: Call<NavReport>
}

export interface IdentityVerifierApi {
is_allowed: CallOf<{ account: string }, boolean>
}

export interface ShareTokenApi {
balance: CallOf<{ account: string }, i128>
symbol: Call<string>
name: Call<string>
}

export interface AssetApi {
symbol: Call<string>
balance: CallOf<{ id: string }, i128>
}

const clientOptions = (contractId: string, signer?: Signer) => ({
contractId,
rpcUrl,
Expand Down
9 changes: 4 additions & 5 deletions app-lib/readContract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import type {
DepositRequest,
EpochInfo,
EpochStatus,
Option,
RedeemRequest,
} from "./contracts"
import { rpcUrl, stellarNetwork } from "./env"
Expand Down Expand Up @@ -127,7 +126,7 @@ function decodeLedgerEntry<T>(
key: xdr.LedgerKey,
decode: (val: xdr.ScVal) => T,
latestLedger: number | null,
): ContractRead<Option<T>> {
): ContractRead<T | null> {
if (!chunkResult.ok) return { kind: "unreadable" }
const entry = chunkResult.byKey.get(key.toXDR("base64"))
if (entry === undefined) return { kind: "value", value: null }
Expand All @@ -154,9 +153,9 @@ const decodeEpochInfo = (val: xdr.ScVal): EpochInfo => {
}

export type EpochRequestsRead = {
epoch: ContractRead<Option<EpochInfo>>
deposit: ContractRead<Option<DepositRequest>>
redeem: ContractRead<Option<RedeemRequest>>
epoch: ContractRead<EpochInfo | null>
deposit: ContractRead<DepositRequest | null>
redeem: ContractRead<RedeemRequest | null>
}

export async function readEpochRequests(
Expand Down
3 changes: 2 additions & 1 deletion app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"dev": "npm start",
"prestart": "node ../scripts/generate-addresses.mjs",
"start": "concurrently --kill-others-on-fail --names stellar,vite -c gray,green --pad-prefix \"stellar scaffold watch --build-clients\" \"vite\"",
"prebuild": "node ../scripts/generate-addresses.mjs",
"prebuild": "node ../scripts/generate-addresses.mjs && node ../scripts/generate-clients.mjs",
"build": "tsc -b && vite build",
"preview": "vite preview",
"lint": "eslint .",
Expand All @@ -26,6 +26,7 @@
"@types/react": "^19.2.15",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.0",
"buffer": "6.0.3",
"concurrently": "^10.0.3",
"eslint": "^10.0.0",
"globals": "^17.6.0",
Expand Down
5 changes: 5 additions & 0 deletions app/src/buffer.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { type Buffer as BufferPackage } from "buffer"

declare global {
type Buffer = BufferPackage
}
15 changes: 7 additions & 8 deletions app/src/hooks/useInvestorRequests.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {
type DepositRequest,
type EpochInfo,
type EpochRequestsRead,
type Option,
type Price,
type RedeemRequest,
networkPassphrase,
Expand Down Expand Up @@ -73,31 +72,31 @@ const fulfilledEpoch: EpochInfo = {
priceable_at: 1_700_003_600n,
}

const depositPresent: ContractRead<Option<DepositRequest>> = {
const depositPresent: ContractRead<DepositRequest | null> = {
kind: "value",
value: { amount: 100_0000000n, claimed: false },
}
const depositClaimed: ContractRead<Option<DepositRequest>> = {
const depositClaimed: ContractRead<DepositRequest | null> = {
kind: "value",
value: { amount: 100_0000000n, claimed: true },
}
const depositAbsent: ContractRead<Option<DepositRequest>> = {
const depositAbsent: ContractRead<DepositRequest | null> = {
kind: "value",
value: null,
}
const depositUnreadable: ContractRead<Option<DepositRequest>> = {
const depositUnreadable: ContractRead<DepositRequest | null> = {
kind: "unreadable",
}
const depositArchived: ContractRead<Option<DepositRequest>> = {
const depositArchived: ContractRead<DepositRequest | null> = {
kind: "archived",
}

const redeemPresent: ContractRead<Option<RedeemRequest>> = {
const redeemPresent: ContractRead<RedeemRequest | null> = {
kind: "value",
value: { shares: 20_0000000n, claimed: false },
}

const redeemAbsent: ContractRead<Option<RedeemRequest>> = {
const redeemAbsent: ContractRead<RedeemRequest | null> = {
kind: "value",
value: null,
}
Expand Down
3 changes: 1 addition & 2 deletions app/src/hooks/useInvestorRequests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {
type DepositRequest,
type EpochInfo,
type EpochStatus,
type Option,
type Price,
type RedeemRequest,
readContract,
Expand Down Expand Up @@ -51,7 +50,7 @@ export type ClassifiedRequest =
| { kind: "archived"; request: ArchivedRequest }

export function classifyRequest(
read: ContractRead<Option<DepositRequest | RedeemRequest>>,
read: ContractRead<DepositRequest | RedeemRequest | null>,
side: RequestSide,
epochId: bigint,
epoch: EpochInfo,
Expand Down
8 changes: 2 additions & 6 deletions app/src/hooks/useVaultAuthorities.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
import {
type ContractRead,
type Option,
readContract,
} from "@stellar-scaffold/app-lib"
import { type ContractRead, readContract } from "@stellar-scaffold/app-lib"
import { useQuery } from "@tanstack/react-query"
import { asyncVault } from "../config/clients"

Expand All @@ -11,7 +7,7 @@ export type AuthorityKey =

export type VaultAuthorities = Record<AuthorityKey, string | null>

const toAddress = (read: ContractRead<Option<string>>): string | null =>
const toAddress = (read: ContractRead<string | null>): string | null =>
read.kind === "value" ? read.value : null

async function fetchVaultAuthorities(): Promise<VaultAuthorities> {
Expand Down
13 changes: 13 additions & 0 deletions app/tsconfig.app.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,17 @@
{
"extends": "@theahaco/ts-config/typescript",
"compilerOptions": {
"paths": {
"@stellar/stellar-sdk": [
"../node_modules/@stellar/stellar-sdk/lib/esm/index.d.ts"
],
"@stellar/stellar-sdk/contract": [
"../node_modules/@stellar/stellar-sdk/lib/esm/contract/index.d.ts"
],
"@stellar/stellar-sdk/rpc": [
"../node_modules/@stellar/stellar-sdk/lib/esm/rpc/index.d.ts"
]
}
},
"include": ["src", "reset.d.ts"]
}
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 4 additions & 20 deletions scripts/generate-addresses.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,36 +4,20 @@
import { mkdirSync, readFileSync, writeFileSync } from "node:fs"
import { dirname, resolve } from "node:path"
import { fileURLToPath } from "node:url"
import { contractIds, selectedEnvironment } from "./lib/environments.mjs"

const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..")
const source = resolve(repoRoot, "environments.toml")
const target = resolve(repoRoot, "app/src/config/addresses.ts")

// Only deployed environments declare their addresses here; a local one gets
// them when it deploys, which is after this runs.
const environment = process.env.ADDRESSES_ENV ?? "staging"
const environment = selectedEnvironment(process.env)

const toml = readFileSync(source, "utf8")
const section = toml.split(`[${environment}.contracts]`)[1]?.split("\n[")[0]
if (section === undefined) {
throw new Error(
`environments.toml has no [${environment}.contracts] section. ` +
`Set ADDRESSES_ENV to an environment it declares.`,
)
}

const addresses = [
...section.matchAll(/^(\w+)\s*=\s*\{[^}]*id\s*=\s*"(C\w+)"/gm),
]
if (addresses.length === 0) {
throw new Error(
`[${environment}.contracts] declares no deployed addresses. ` +
`Deploy first, or set ADDRESSES_ENV to an environment that has them.`,
)
}
const addresses = contractIds(readFileSync(source, "utf8"), environment)

const entries = addresses
.map(([, name, id]) => `\t${name}: "${id}",`)
.map(({ name, id }) => `\t${name}: "${id}",`)
.join("\n")

mkdirSync(dirname(target), { recursive: true })
Expand Down
Loading
Loading