diff --git a/.gitleaksignore b/.gitleaksignore
new file mode 100644
index 00000000..7032d8a0
--- /dev/null
+++ b/.gitleaksignore
@@ -0,0 +1,5 @@
+# Public testnet-only keys for the e2e runbook accounts
+scripts/harness/people.testnet.json:generic-api-key:4
+scripts/harness/people.testnet.json:generic-api-key:9
+scripts/harness/people.testnet.json:generic-api-key:14
+scripts/harness/people.testnet.json:generic-api-key:19
diff --git a/E2E-TESTING.md b/E2E-TESTING.md
new file mode 100644
index 00000000..a222dded
--- /dev/null
+++ b/E2E-TESTING.md
@@ -0,0 +1,92 @@
+# E2E testing on testnet
+
+A fresh deploy of `main` (`45e0e23`) on Stellar testnet, with one account per person to run the runbook by hand.
+
+These keys are **testnet only**. They have no real value and are public on purpose. Never use them on mainnet.
+
+- Network: Stellar testnet (`https://soroban-testnet.stellar.org`, passphrase `Test SDF Network ; September 2015`)
+- Deploy: 2026-10-20T19:02:22.970Z (ledger 5299871)
+
+## People
+
+Import the secret into a wallet (for example Freighter, on testnet). Each account has about 10,000 XLM, a USDC trustline and 100,000 USDC, and is on the allowlist, so it can also act as a holder.
+
+| Person | Roles | Public key | Secret key |
+| --- | --- | --- | --- |
+| **Maxi** | governance (admin of the vault, the oracle and the share token)
USDC admin (can mint) | `GD4E4AI7LVHIRX7QXAHQGRPABC6NFWFKHKJ6ZUAQNIGELVVN7MSFJWLF` | `SDHNHV6MEJP26WLCHC6F27XRLJ6H34R33KGKTQFECS7PYVQVB3RW2CE4` |
+| **Pablo** | manager
guardian (vault and oracle) | `GBE43ELIMD3CUR6WMAMTCMI5UWXNFWX3GWJ3RSSIKUGJGTSRHF5X645V` | `SBW54VWPHJTS4O2VVTU4PTV5KNJZR4BXM4QOMFJAKSAC4BQM2GRQ2MDG` |
+| **Luciano** | attester
treasury
custodian | `GCBLIOHMFFYK3GORQL2JIILW2YOGIGCDM6SUKX6RVF4U66E2XTXX42KW` | `SAOAAERXFPPJP4L7EUVECIHLOXIIVALC2CDD7OMIERCKWMQJ3247XUQT` |
+| **Iñaky** | compliance (admin of the identity verifier, compliance authority of the share token)
outsider | `GCFUDBW2N26PBMMAMI3RVTA5O2WOGJO7GCH6E4PNFAVQNRVW2YEOELXE` | `SBD4TKZWYGHJDUS237S53UEJB45QO6WUOXRABUUNUYHLKNMCYTDLCK5G` |
+
+Luciano is both treasury and custodian: `deploy_to_custodian` sends the USDC to Luciano's own account.
+
+## Who signs what
+
+| Action | Signer |
+| --- | --- |
+| `request_deposit`, `request_redeem`, `claim_*`, cancellations | anyone (every person is a holder) |
+| `close_epoch` | Pablo |
+| Attest the NAV on the oracle | Luciano |
+| `fulfill_epoch` | anyone (no permission needed); Iñaky as outsider |
+| `deploy_to_custodian` | Luciano (treasury) |
+| `fund` (return USDC to the vault) | Luciano as custodian; in fact anyone can |
+| Pause / unpause the vault or the oracle | Pablo |
+| Allowlist, freeze, forced transfers | Iñaky |
+| Parameters, roles, custodian, upgrades (timelock), wind-down | Maxi |
+| Mint USDC (`mint` on the USDC contract) | Maxi |
+
+## Initial state
+
+- Epoch #1 **open**, with no requests.
+- No NAV attested on the oracle yet.
+- Vault custodian: Luciano.
+- The vault holds no funds.
+
+## Contracts
+
+| Contract | Address |
+| --- | --- |
+| `compliance` | `CCCA6XDCXBZWWTIDYGLTL5GEHROCZTGYAIRAFA2KBO2JAWOJFU3TMZCB` |
+| `identity_verifier` | `CDMV7DGGLGIWDWI4ERIBZ5YS2FZA3QVZPVUWCAWR5VBLVJMLFYCW6DML` |
+| `share_token` | `CCB23VF4CSI7QNWNDOAAAVOKOLWO5EEQFZIODWEXHS6OB3MMHGFZ3VA6` |
+| `nav_oracle` | `CDDRBLB3GHZGKHGFATZBKFSXC6XRVFLHNPVWHTIE67FVNDYHU2RRRNRO` |
+| `async_vault` | `CAGHFSATIJHNDXQA577APGG6GF2L7Q7KP6H2IUXVW6IT3CJ2OBE3QCEB` |
+| `asset` (USDC SAC) | `CAU7LDCUCLAWSJZS72B7VIPIE7FSN3T63D7MZMSSXTNLKYDYECVGH522` |
+
+USDC: code `USDC`, issuer `GCUYFZRTNQ6WIIN5XBEYWVENKNVCBPK5BRXK4PDBCOGCYA4BZIRGMVIN`. The issuer handed the USDC contract's admin to Maxi, so Maxi is the one who mints.
+
+If your wallet shows two USDC assets, use this issuer's. The other one is from an earlier deploy.
+
+## Harness accounts
+
+The scripts create them. The holders work as extra investors in the runbook.
+
+| Account | Purpose | Public key | Secret key |
+| --- | --- | --- | --- |
+| deployer | Deployed the contracts. No role afterwards. | `GC7UTBTYBYIXKBX3DWURKQQJNDKM35FEBIHJN63NT5OZIJRWFUP3VEC5` | `SBTOLGY2AMHDJSDTZRMSWOVOFN236M56YO57ZIA5WTRA7QCSIJEVWUQF` |
+| issuer | Issuer of the test USDC. No longer the contract's admin; Maxi mints. | `GCUYFZRTNQ6WIIN5XBEYWVENKNVCBPK5BRXK4PDBCOGCYA4BZIRGMVIN` | `SBKDWFFTWAXKKZEIBG3XREEZ7KFHIUGKXNMOKLAJIW44HUUEB46GAYZA` |
+| holder-1 | Holder: 100,000 USDC, trustline and allowlist. | `GB6GENHQQWI3PRD6MVDIRO72VMIJCWENMHRSJU3Z53R372K5ZPHT7ILL` | `SB5RVYBJXKNOWMNBBXGWUHYJRKW75PY7PSX56GHPKXTOB7NW22MXYMHD` |
+| holder-2 | Holder: 100,000 USDC, trustline and allowlist. | `GDJ7VTRMJ5XGALSIJDV4V6PBZRQEZZ6NUQOFUDW6TNYOMZMSL536XNAK` | `SASZ5BFK2W6LOJR4UNVUHPICW4VRDYSATCQQAOF7ISPO3OZSKTTF6NVQ` |
+| holder-3 | Holder: 100,000 USDC, trustline and allowlist. | `GA6PAYUT232UAQKUYJ7QWF27JQWTKOVNRUHAF6B4CI3QT3XFKQAKSTXD` | `SAR27ZG7NEIKNBIC3U2ZFEP2MUKOOLJAPFZJXT4R2ZKIGOM5GO6T3EFX` |
+
+## Redeploy
+
+The roles come from `scripts/harness/people.testnet.json`. If it exists, `deploy` uses those keys for the roles it lists, and new keys for the rest.
+
+```sh
+stellar contract build
+cd scripts/harness
+HARNESS_NETWORK=testnet npm run deploy # new contracts, the same people's keys
+HARNESS_NETWORK=testnet npm run onboard # trustline, USDC and allowlist for each person; custodian
+```
+
+For Maxi to mint, the issuer hands over the USDC contract's admin (the values come from `deployed.testnet.json` and `people.testnet.json`):
+
+```sh
+stellar contract invoke --network testnet --id --source-account -- set_admin --new_admin
+stellar contract invoke --network testnet --id --source-account -- mint --to --amount 1000000000000 # 100,000 USDC (7 decimals)
+```
+
+holder-1..3 need a trustline and the allowlist before the mint.
+
+The contracts change on every deploy, so update this file's contract table afterwards.
diff --git a/README.md b/README.md
index ee24fbdf..8af20bb3 100644
--- a/README.md
+++ b/README.md
@@ -35,12 +35,12 @@ deploy from `environments.toml` → `[staging.contracts]`, which is the record.
| Contract | Address |
| --- | --- |
-| `async_vault` | `CAXGUAAIZOCM6FIQ6PRTQEBBZG62GMN422TNT6N6UMIYY4HGFLJDUFCV` |
-| `nav_oracle` | `CBYVXBRLCKJTH56FVIZFBX7XUIBOLJOROP2DM4NYA4V6FOTTFZO7VJPL` |
-| `share_token` | `CCBANVMWNZXZ46WMV7N7EDGBPW3LIFOXZLILIOTRDTE63H4QXBM7XVYF` |
-| `identity_verifier` | `CBLCGV3YT5AM7ZQNFEIEX3FLA3ZY3XDVW3U2YQO4ZWSJVI47763LLL5T` |
-| `compliance` | `CA45ZSCHMNUMJEF2UDUT4UMCKHHMEG3LYYIB7QBK6LG2NIC7JYZ7XKRA` |
-| `asset` | `CCTSSUV5XYKFEMT44SJBS2GUNBEMMRUQXLK422RAJMPTGLF7WV6ALDKR` |
+| `async_vault` | `CAGHFSATIJHNDXQA577APGG6GF2L7Q7KP6H2IUXVW6IT3CJ2OBE3QCEB` |
+| `nav_oracle` | `CDDRBLB3GHZGKHGFATZBKFSXC6XRVFLHNPVWHTIE67FVNDYHU2RRRNRO` |
+| `share_token` | `CCB23VF4CSI7QNWNDOAAAVOKOLWO5EEQFZIODWEXHS6OB3MMHGFZ3VA6` |
+| `identity_verifier` | `CDMV7DGGLGIWDWI4ERIBZ5YS2FZA3QVZPVUWCAWR5VBLVJMLFYCW6DML` |
+| `compliance` | `CCCA6XDCXBZWWTIDYGLTL5GEHROCZTGYAIRAFA2KBO2JAWOJFU3TMZCB` |
+| `asset` | `CAU7LDCUCLAWSJZS72B7VIPIE7FSN3T63D7MZMSSXTNLKYDYECVGH522` |
## Testnet flow verification
diff --git a/environments.toml b/environments.toml
index 0a689953..60c110ea 100644
--- a/environments.toml
+++ b/environments.toml
@@ -71,12 +71,12 @@ default = true
# underlying Stellar Asset Contract and has no crate here; it still gets a
# generated client, because a SAC carries its own spec.
[staging.contracts]
-async_vault = { id = "CAXGUAAIZOCM6FIQ6PRTQEBBZG62GMN422TNT6N6UMIYY4HGFLJDUFCV" }
-nav_oracle = { id = "CBYVXBRLCKJTH56FVIZFBX7XUIBOLJOROP2DM4NYA4V6FOTTFZO7VJPL" }
-share_token = { id = "CCBANVMWNZXZ46WMV7N7EDGBPW3LIFOXZLILIOTRDTE63H4QXBM7XVYF" }
-identity_verifier = { id = "CBLCGV3YT5AM7ZQNFEIEX3FLA3ZY3XDVW3U2YQO4ZWSJVI47763LLL5T" }
-compliance = { id = "CA45ZSCHMNUMJEF2UDUT4UMCKHHMEG3LYYIB7QBK6LG2NIC7JYZ7XKRA" }
-asset = { id = "CCTSSUV5XYKFEMT44SJBS2GUNBEMMRUQXLK422RAJMPTGLF7WV6ALDKR" }
+async_vault = { id = "CAGHFSATIJHNDXQA577APGG6GF2L7Q7KP6H2IUXVW6IT3CJ2OBE3QCEB" }
+nav_oracle = { id = "CDDRBLB3GHZGKHGFATZBKFSXC6XRVFLHNPVWHTIE67FVNDYHU2RRRNRO" }
+share_token = { id = "CCB23VF4CSI7QNWNDOAAAVOKOLWO5EEQFZIODWEXHS6OB3MMHGFZ3VA6" }
+identity_verifier = { id = "CDMV7DGGLGIWDWI4ERIBZ5YS2FZA3QVZPVUWCAWR5VBLVJMLFYCW6DML" }
+compliance = { id = "CCCA6XDCXBZWWTIDYGLTL5GEHROCZTGYAIRAFA2KBO2JAWOJFU3TMZCB" }
+asset = { id = "CAU7LDCUCLAWSJZS72B7VIPIE7FSN3T63D7MZMSSXTNLKYDYECVGH522" }
### Production environment configuration
# Commented out on purpose. Mainnet needs an external audit and an audited
diff --git a/scripts/harness/deploy.ts b/scripts/harness/deploy.ts
index 8b230e0f..7625e965 100644
--- a/scripts/harness/deploy.ts
+++ b/scripts/harness/deploy.ts
@@ -23,6 +23,7 @@ import {
type Deployment,
} from "./lib/deployment.js"
import { network, type Network } from "./lib/network.js"
+import { assignedKeys, loadPeople } from "./lib/people.js"
const wasmOf = (name: ContractName): string =>
join(REPO_ROOT, "target", "wasm32v1-none", "release", `${name}.wasm`)
@@ -198,9 +199,17 @@ async function main(): Promise {
const server = new rpc.Server(net.rpcUrl, { allowHttp: net.name === "local" })
console.log(`deploying to ${net.name} (${net.rpcUrl})`)
- const accounts = freshAccounts()
- await fund(net, server, Object.values(accounts))
- console.log(`funded ${Object.keys(accounts).length} accounts`)
+ const accounts: Accounts = {
+ ...freshAccounts(),
+ ...assignedKeys(loadPeople(net.name)),
+ }
+ const distinct = [
+ ...new Map(
+ Object.values(accounts).map((key) => [key.publicKey(), key]),
+ ).values(),
+ ]
+ await fund(net, server, distinct)
+ console.log(`funded ${distinct.length} accounts`)
const asset = new Asset("USDC", accounts.issuer.publicKey())
const assetContract = await deployAssetContract(net, asset, accounts.issuer)
diff --git a/scripts/harness/lib/holders.ts b/scripts/harness/lib/holders.ts
new file mode 100644
index 00000000..7d425c4d
--- /dev/null
+++ b/scripts/harness/lib/holders.ts
@@ -0,0 +1,103 @@
+import { type Keypair } from "@stellar/stellar-sdk"
+
+import { pay, trustline } from "./accounts.js"
+import { units } from "./amounts.js"
+import {
+ identityVerifier,
+ send,
+ usdcBalance,
+ type Connected,
+} from "./clients.js"
+
+export function formatAmount(amount: bigint, decimals: number): string {
+ const factor = 10n ** BigInt(decimals)
+ const whole = amount / factor
+ const fraction = amount % factor
+ return `${whole}.${fraction.toString().padStart(decimals, "0").slice(0, 4)}`
+}
+
+async function hasTrustline(c: Connected, address: string): Promise {
+ try {
+ await c.server.getAssetBalance(address, c.asset, c.net.passphrase)
+ return true
+ } catch (error) {
+ if (
+ error instanceof Error &&
+ /Trustline for .* not found for/.test(error.message)
+ ) {
+ return false
+ }
+ throw error
+ }
+}
+
+export async function ensureTrustline(
+ c: Connected,
+ holder: Keypair,
+ name: string,
+): Promise {
+ const exists = await hasTrustline(c, holder.publicKey())
+ if (!exists) {
+ console.log(` + establishing USDC trustline for ${name}...`)
+ await trustline(c.net, c.server, holder, c.asset)
+ console.log(` ✓ established trustline for ${name}`)
+ } else {
+ console.log(` • ${name} trustline verified`)
+ }
+}
+
+export async function ensureFunding(
+ c: Connected,
+ holder: Keypair,
+ name: string,
+ minUsdcWhole: number,
+ topUpUsdcWhole: number,
+ decimals: number,
+): Promise {
+ const minBalance = units(minUsdcWhole, decimals)
+ const current = await usdcBalance(c, holder.publicKey())
+ if (current < minBalance) {
+ console.log(
+ ` + funding ${name} with ${topUpUsdcWhole} USDC (current: ${formatAmount(current, decimals)})...`,
+ )
+ await pay(
+ c.net,
+ c.server,
+ c.keys.issuer,
+ holder.publicKey(),
+ c.asset,
+ topUpUsdcWhole.toString(),
+ )
+ const updated = await usdcBalance(c, holder.publicKey())
+ console.log(
+ ` ✓ funded ${name}, new balance: ${formatAmount(updated, decimals)} USDC`,
+ )
+ } else {
+ console.log(
+ ` • ${name} has sufficient USDC: ${formatAmount(current, decimals)}`,
+ )
+ }
+}
+
+export async function ensureAllowlist(
+ c: Connected,
+ holder: Keypair,
+ name: string,
+): Promise {
+ const verifier = identityVerifier(c, c.keys.compliance)
+ const allowed = (await verifier.is_allowed({ account: holder.publicKey() }))
+ .result
+ if (!allowed) {
+ console.log(` + allowlisting ${name}...`)
+ await send(
+ verifier.allow({
+ account: holder.publicKey(),
+ allowed: true,
+ caller: c.keys.compliance.publicKey(),
+ }),
+ )
+ console.log(` ✓ ${name} allowlisted`)
+ } else {
+ console.log(` • ${name} is already allowlisted`)
+ }
+}
diff --git a/scripts/harness/lib/people.ts b/scripts/harness/lib/people.ts
new file mode 100644
index 00000000..8a19ffcc
--- /dev/null
+++ b/scripts/harness/lib/people.ts
@@ -0,0 +1,48 @@
+import { existsSync, readFileSync } from "node:fs"
+import { join } from "node:path"
+
+import { Keypair } from "@stellar/stellar-sdk"
+
+import { ACCOUNT_NAMES, type AccountName } from "./accounts.js"
+import { PACKAGE_ROOT } from "./deployment.js"
+import { type NetworkName } from "./network.js"
+
+export interface Person {
+ public: string
+ secret: string
+ roles: AccountName[]
+}
+
+export type People = Record
+
+export function peopleFile(network: NetworkName): string {
+ return join(PACKAGE_ROOT, `people.${network}.json`)
+}
+
+export function loadPeople(network: NetworkName): People {
+ const file = peopleFile(network)
+ if (!existsSync(file)) return {}
+ const people = JSON.parse(readFileSync(file, "utf8")) as People
+ const taken = new Map()
+ for (const [name, person] of Object.entries(people)) {
+ for (const role of person.roles) {
+ if (!ACCOUNT_NAMES.includes(role)) {
+ throw new Error(`${name} has unknown role ${role}`)
+ }
+ const holder = taken.get(role)
+ if (holder) throw new Error(`${role} given to both ${holder} and ${name}`)
+ taken.set(role, name)
+ }
+ }
+ return people
+}
+
+export function assignedKeys(
+ people: People,
+): Partial> {
+ return Object.fromEntries(
+ Object.values(people).flatMap((person) =>
+ person.roles.map((role) => [role, Keypair.fromSecret(person.secret)]),
+ ),
+ )
+}
diff --git a/scripts/harness/onboard.ts b/scripts/harness/onboard.ts
new file mode 100644
index 00000000..36fa8fc0
--- /dev/null
+++ b/scripts/harness/onboard.ts
@@ -0,0 +1,46 @@
+import { Keypair } from "@stellar/stellar-sdk"
+
+import { connect, send, shareToken, vault } from "./lib/clients.js"
+import {
+ ensureAllowlist,
+ ensureFunding,
+ ensureTrustline,
+} from "./lib/holders.js"
+import { loadPeople, peopleFile } from "./lib/people.js"
+
+const MIN_USDC = 10_000
+const TOP_UP_USDC = 50_000
+
+async function main(): Promise {
+ const c = connect()
+ const people = loadPeople(c.net.name)
+ if (Object.keys(people).length === 0) {
+ throw new Error(`no people in ${peopleFile(c.net.name)}`)
+ }
+ const decimals = (await shareToken(c, c.keys.outsider).decimals()).result
+
+ for (const [name, person] of Object.entries(people)) {
+ const key = Keypair.fromSecret(person.secret)
+ console.log(`${name} (${person.roles.join(", ")})`)
+ await ensureTrustline(c, key, name)
+ await ensureFunding(c, key, name, MIN_USDC, TOP_UP_USDC, decimals)
+ await ensureAllowlist(c, key, name)
+ }
+
+ const reader = vault(c, c.keys.outsider)
+ const wanted = c.keys.custodian.publicKey()
+ if ((await reader.custodian()).result !== wanted) {
+ await send(
+ vault(c, c.keys.governance).set_custodian({
+ custodian: wanted,
+ caller: c.keys.governance.publicKey(),
+ }),
+ )
+ }
+ console.log(`vault custodian: ${wanted}`)
+}
+
+main().catch((error: unknown) => {
+ console.error(error instanceof Error ? error.message : error)
+ process.exitCode = 1
+})
diff --git a/scripts/harness/package.json b/scripts/harness/package.json
index be8dcf42..606ba33c 100644
--- a/scripts/harness/package.json
+++ b/scripts/harness/package.json
@@ -11,7 +11,8 @@
"typecheck": "tsc --noEmit",
"test:happy-path": "vitest run happy-path.test.ts",
"test:guards": "vitest run guards.test.ts",
- "seed": "tsx seed.ts"
+ "seed": "tsx seed.ts",
+ "onboard": "tsx onboard.ts"
},
"dependencies": {
"@stellar/stellar-sdk": "16.1.0"
diff --git a/scripts/harness/people.testnet.json b/scripts/harness/people.testnet.json
new file mode 100644
index 00000000..2c34810c
--- /dev/null
+++ b/scripts/harness/people.testnet.json
@@ -0,0 +1,22 @@
+{
+ "Maxi": {
+ "public": "GD4E4AI7LVHIRX7QXAHQGRPABC6NFWFKHKJ6ZUAQNIGELVVN7MSFJWLF",
+ "secret": "SDHNHV6MEJP26WLCHC6F27XRLJ6H34R33KGKTQFECS7PYVQVB3RW2CE4",
+ "roles": ["governance"]
+ },
+ "Pablo": {
+ "public": "GBE43ELIMD3CUR6WMAMTCMI5UWXNFWX3GWJ3RSSIKUGJGTSRHF5X645V",
+ "secret": "SBW54VWPHJTS4O2VVTU4PTV5KNJZR4BXM4QOMFJAKSAC4BQM2GRQ2MDG",
+ "roles": ["manager", "guardian"]
+ },
+ "Luciano": {
+ "public": "GCBLIOHMFFYK3GORQL2JIILW2YOGIGCDM6SUKX6RVF4U66E2XTXX42KW",
+ "secret": "SAOAAERXFPPJP4L7EUVECIHLOXIIVALC2CDD7OMIERCKWMQJ3247XUQT",
+ "roles": ["attester", "treasury", "custodian"]
+ },
+ "Iñaky": {
+ "public": "GCFUDBW2N26PBMMAMI3RVTA5O2WOGJO7GCH6E4PNFAVQNRVW2YEOELXE",
+ "secret": "SBD4TKZWYGHJDUS237S53UEJB45QO6WUOXRABUUNUYHLKNMCYTDLCK5G",
+ "roles": ["compliance", "outsider"]
+ }
+}
diff --git a/scripts/harness/seed.ts b/scripts/harness/seed.ts
index a6a546d7..cde25f32 100644
--- a/scripts/harness/seed.ts
+++ b/scripts/harness/seed.ts
@@ -1,10 +1,6 @@
-import { type Keypair } from "@stellar/stellar-sdk"
-
-import { pay, trustline } from "./lib/accounts.js"
import { price, units } from "./lib/amounts.js"
import {
connect,
- identityVerifier,
oracle,
send,
shareToken,
@@ -12,15 +8,14 @@ import {
vault,
type Connected,
} from "./lib/clients.js"
+import {
+ ensureAllowlist,
+ ensureFunding,
+ ensureTrustline,
+ formatAmount,
+} from "./lib/holders.js"
import { attest } from "./lib/oracle.js"
-function formatAmount(amount: bigint, decimals: number): string {
- const factor = 10n ** BigInt(decimals)
- const whole = amount / factor
- const fraction = amount % factor
- return `${whole}.${fraction.toString().padStart(decimals, "0").slice(0, 4)}`
-}
-
function formatWad(wad: bigint): string {
const factor = 10n ** 18n
const whole = wad / factor
@@ -28,92 +23,6 @@ function formatWad(wad: bigint): string {
return `$${whole}.${fraction.toString().padStart(18, "0").slice(0, 4)}`
}
-async function hasTrustline(c: Connected, address: string): Promise {
- try {
- await c.server.getAssetBalance(address, c.asset, c.net.passphrase)
- return true
- } catch (error) {
- if (
- error instanceof Error &&
- /Trustline for .* not found for/.test(error.message)
- ) {
- return false
- }
- throw error
- }
-}
-
-async function ensureTrustline(
- c: Connected,
- holder: Keypair,
- name: string,
-): Promise {
- const exists = await hasTrustline(c, holder.publicKey())
- if (!exists) {
- console.log(` + establishing USDC trustline for ${name}...`)
- await trustline(c.net, c.server, holder, c.asset)
- console.log(` ✓ established trustline for ${name}`)
- } else {
- console.log(` • ${name} trustline verified`)
- }
-}
-
-async function ensureFunding(
- c: Connected,
- holder: Keypair,
- name: string,
- minUsdcWhole: number,
- topUpUsdcWhole: number,
- decimals: number,
-): Promise {
- const minBalance = units(minUsdcWhole, decimals)
- const current = await usdcBalance(c, holder.publicKey())
- if (current < minBalance) {
- console.log(
- ` + funding ${name} with ${topUpUsdcWhole} USDC (current: ${formatAmount(current, decimals)})...`,
- )
- await pay(
- c.net,
- c.server,
- c.keys.issuer,
- holder.publicKey(),
- c.asset,
- topUpUsdcWhole.toString(),
- )
- const updated = await usdcBalance(c, holder.publicKey())
- console.log(
- ` ✓ funded ${name}, new balance: ${formatAmount(updated, decimals)} USDC`,
- )
- } else {
- console.log(
- ` • ${name} has sufficient USDC: ${formatAmount(current, decimals)}`,
- )
- }
-}
-
-async function ensureAllowlist(
- c: Connected,
- holder: Keypair,
- name: string,
-): Promise {
- const verifier = identityVerifier(c, c.keys.compliance)
- const allowed = (await verifier.is_allowed({ account: holder.publicKey() }))
- .result
- if (!allowed) {
- console.log(` + allowlisting ${name}...`)
- await send(
- verifier.allow({
- account: holder.publicKey(),
- allowed: true,
- caller: c.keys.compliance.publicKey(),
- }),
- )
- console.log(` ✓ ${name} allowlisted`)
- } else {
- console.log(` • ${name} is already allowlisted`)
- }
-}
-
async function main(): Promise {
console.log("==================================================")
console.log(" STRATA VAULT SEED SCRIPT (TESTNET) ")
diff --git a/scripts/harness/tsconfig.json b/scripts/harness/tsconfig.json
index df950756..846e5f41 100644
--- a/scripts/harness/tsconfig.json
+++ b/scripts/harness/tsconfig.json
@@ -17,6 +17,7 @@
"deploy.ts",
"build-clients.ts",
"seed.ts",
+ "onboard.ts",
"lib/**/*.ts",
"*.test.ts",
"vitest.config.ts"