Skip to content
Open
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: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,12 +135,16 @@ integration suites against that deployment:
```sh
npm run test:happy-path -w scripts/harness
npm run test:guards -w scripts/harness
CARGO_TARGET_DIR=target/short-wind-down stellar contract build --package async-vault --features short_wind_down
npm run test:wind-down -w scripts/harness
```

Deploy afresh before each run of the suites. The guards leave the vault's
notice at 10 seconds, and lowering it back waits for the timelock, so a second
run against the same deployment can fail wherever it prices an epoch right
after closing it.
after closing it. `test:wind-down` ignores `deployed.local.json`: it deploys
its own throwaway vault set, built from the `short_wind_down` wasm above, so
it can reach an active wind-down without the real seven-day minimum.

### Keeping a deployment alive

Expand Down
11 changes: 11 additions & 0 deletions docs/TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,3 +128,14 @@ fixture wires them as the deploy does and finishes the vault's setup;
| Upgrading replaces the vault's code and migrates once | [`contracts/async-vault/src/test/upgrade.rs`](../contracts/async-vault/src/test/upgrade.rs) (needs `upgrade_wasm`) | `applying_a_wasm_change_replaces_the_code`, `a_migration_refuses_a_second_run` |
| The share token upgrades through the vault's timelock under live balances, an open deposit and an open redemption, and migrates once; called directly, `upgrade` does not exist | [`contracts/async-vault/src/test/upgrade.rs`](../contracts/async-vault/src/test/upgrade.rs) (needs `upgrade_wasm`) | `the_token_upgrades_under_live_balances_and_open_requests`, `the_token_exposes_no_immediate_upgrade` |
| The oracle upgrades through the vault's timelock under a live valuation, keeps its config, roles and vault, and migrates once; called directly, `upgrade` does not exist | [`contracts/nav-oracle/src/test_governance.rs`](../contracts/nav-oracle/src/test_governance.rs) (needs `upgrade_wasm`) | `the_oracle_upgrades_under_a_live_valuation`, `the_oracle_exposes_no_immediate_upgrade` |

## Wind-down on the network

`scripts/harness/wind-down.test.ts` deploys a throwaway vault set on the local
network from a vault built with `--features short_wind_down`. That build drops
the seven-day minimum and carries a `build` meta entry, and is never deployed
elsewhere. The test runs a deposit, a claim, a redeem request, a wind-down, a
round and a claim, then checks that every function in the vault's spec was
called through the generated client. The full wind-down logic is tested in
Rust (`contracts/async-vault/src/test/wind_down.rs`). The testnet run is
spread over seven days in the Tranche 2 release.
161 changes: 103 additions & 58 deletions scripts/harness/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { existsSync } from "node:fs"
import { join } from "node:path"
import { pathToFileURL } from "node:url"

import { Asset, rpc, type Keypair } from "@stellar/stellar-sdk"
import { Asset, Keypair, rpc } from "@stellar/stellar-sdk"
import { basicNodeSigner } from "@stellar/stellar-sdk/contract"

import type * as AsyncVaultModule from "../../app-lib/clients/async_vault/dist/index.js"
Expand All @@ -26,17 +26,28 @@ import {
} from "./lib/deployment.js"
import { network, type Network } from "./lib/network.js"

const wasmOf = (name: ContractName): string =>
join(REPO_ROOT, "target", "wasm32v1-none", "release", `${name}.wasm`)
const RELEASE_DIR = join(REPO_ROOT, "target", "wasm32v1-none", "release")
export const SHORT_WIND_DOWN_DIR = join(
REPO_ROOT,
"target",
"short-wind-down",
"wasm32v1-none",
"release",
)

export const wasmOf = (name: ContractName, dir = RELEASE_DIR): string =>
join(dir, `${name}.wasm`)

const clientDir = (name: ContractName): string =>
join(REPO_ROOT, "app-lib", "clients", name)

const clientEntry = (name: ContractName): string =>
pathToFileURL(join(clientDir(name), "dist", "index.js")).href

function requireWasm(): void {
const missing = CONTRACT_NAMES.filter((name) => !existsSync(wasmOf(name)))
function requireWasm(vaultWasm: string = wasmOf("async_vault")): void {
const missing = CONTRACT_NAMES.filter(
(name) => !existsSync(name === "async_vault" ? vaultWasm : wasmOf(name)),
)
if (missing.length > 0) {
throw new Error(
`no wasm for ${missing.join(", ")}; run \`stellar contract build\` first`,
Expand All @@ -59,65 +70,56 @@ async function deployContracts(
net: Network,
accounts: Accounts,
assetContract: string,
wasms: Record<ContractName, string>,
): Promise<Record<ContractName, string>> {
const pk = (name: keyof Accounts): string => accounts[name].publicKey()
const deployer = accounts.deployer
const governance = pk("governance")

const compliance = await deployContract(net, wasmOf("compliance"), deployer, {
const compliance = await deployContract(net, wasms.compliance, deployer, {
admin: governance,
})
console.log("compliance", compliance)

const identityVerifier = await deployContract(
net,
wasmOf("identity_verifier"),
wasms.identity_verifier,
deployer,
{ admin: pk("compliance") },
)
console.log("identity_verifier", identityVerifier)

const shareToken = await deployContract(
net,
wasmOf("share_token"),
deployer,
{
name: "Strata Vault USDC",
symbol: "bvUSDC",
admin: governance,
compliance_authority: pk("compliance"),
compliance,
identity_verifier: identityVerifier,
},
)
const shareToken = await deployContract(net, wasms.share_token, deployer, {
name: "Strata Vault USDC",
symbol: "bvUSDC",
admin: governance,
compliance_authority: pk("compliance"),
compliance,
identity_verifier: identityVerifier,
})
console.log("share_token", shareToken)

const navOracle = await deployContract(net, wasmOf("nav_oracle"), deployer, {
const navOracle = await deployContract(net, wasms.nav_oracle, deployer, {
admin: governance,
attester: pk("attester"),
guardian: pk("guardian"),
config: JSON.stringify(net.oracle),
})
console.log("nav_oracle", navOracle)

const asyncVault = await deployContract(
net,
wasmOf("async_vault"),
deployer,
{
asset: assetContract,
share_token: shareToken,
oracle: navOracle,
roles: JSON.stringify({
governance,
manager: pk("manager"),
treasury: pk("treasury"),
guardian: pk("guardian"),
compliance: pk("compliance"),
attester: pk("attester"),
}),
},
)
const asyncVault = await deployContract(net, wasms.async_vault, deployer, {
asset: assetContract,
share_token: shareToken,
oracle: navOracle,
roles: JSON.stringify({
governance,
manager: pk("manager"),
treasury: pk("treasury"),
guardian: pk("guardian"),
compliance: pk("compliance"),
attester: pk("attester"),
}),
})
console.log("async_vault", asyncVault)

return {
Expand Down Expand Up @@ -253,12 +255,11 @@ async function finishVaultSetup(
throw new Error(`the vault is not wired: ${broken.join(", ")}`)
}

async function main(): Promise<void> {
requireWasm()
const net = network()
async function freshSet(
net: Network,
wasms: Record<ContractName, string>,
): Promise<Deployment> {
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`)
Expand All @@ -267,17 +268,9 @@ async function main(): Promise<void> {
const assetContract = await deployAssetContract(net, asset, accounts.issuer)
console.log("asset contract", assetContract)

const contracts = await deployContracts(net, accounts, assetContract)

for (const name of CONTRACT_NAMES) {
await generateClient(wasmOf(name), clientDir(name))
}
console.log("clients generated")

await grantAndVerify(net, accounts, contracts)
await finishVaultSetup(net, accounts, contracts)
const contracts = await deployContracts(net, accounts, assetContract, wasms)

const record: Deployment = {
return {
network: net.name,
deployedAt: new Date().toISOString(),
ledger: (await server.getLatestLedger()).sequence,
Expand All @@ -294,14 +287,66 @@ async function main(): Promise<void> {
]),
) as Deployment["accounts"],
}
}

const keysOf = (record: Deployment): Accounts =>
Object.fromEntries(
Object.entries(record.accounts).map(([name, key]) => [
name,
Keypair.fromSecret(key.secret),
]),
) as Accounts

// A full, wired vault set built from a different vault Wasm. It reuses the
// clients the main deploy generated, and nothing is written to disk.
export async function deployThrowaway(
net: Network,
vaultWasm: string,
): Promise<Deployment> {
requireWasm(vaultWasm)
const wasms = Object.fromEntries(
CONTRACT_NAMES.map((name) => [name, wasmOf(name)]),
) as Record<ContractName, string>
wasms.async_vault = vaultWasm
const record = await freshSet(net, wasms)
const accounts = keysOf(record)
await grantAndVerify(net, accounts, record.contracts)
await finishVaultSetup(net, accounts, record.contracts)
return record
}

async function main(): Promise<void> {
requireWasm()
const net = network()
console.log(`deploying to ${net.name} (${net.rpcUrl})`)
const wasms = Object.fromEntries(
CONTRACT_NAMES.map((name) => [name, wasmOf(name)]),
) as Record<ContractName, string>
const record = await freshSet(net, wasms)

for (const name of CONTRACT_NAMES) {
await generateClient(wasmOf(name), clientDir(name))
}
console.log("clients generated")

const accounts = keysOf(record)
await grantAndVerify(net, accounts, record.contracts)
await finishVaultSetup(net, accounts, record.contracts)

saveDeployment(record)
console.log(`wrote deployed.${net.name}.json`)
if (net.name === "testnet") {
console.log("updated environments.toml [staging.contracts]")
}
}

main().catch((error: unknown) => {
console.error(error instanceof Error ? error.message : error)
process.exitCode = 1
})
const invokedDirectly =
process.argv[1] !== undefined &&
import.meta.url === pathToFileURL(process.argv[1]).href

if (invokedDirectly) {
main().catch((error: unknown) => {
console.error(error instanceof Error ? error.message : error)
process.exitCode = 1
})
}
5 changes: 4 additions & 1 deletion scripts/harness/lib/clients.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@ export interface Connected {

export function connect(): Connected {
const net = network()
const deployment = loadDeployment(net.name)
return connectTo(net, loadDeployment(net.name))
}

export function connectTo(net: Network, deployment: Deployment): Connected {
const keys = Object.fromEntries(
ACCOUNT_NAMES.map((name) => [
name,
Expand Down
1 change: 1 addition & 0 deletions scripts/harness/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"typecheck": "tsc --noEmit",
"test:happy-path": "vitest run happy-path.test.ts",
"test:guards": "vitest run guards.test.ts",
"test:wind-down": "vitest run wind-down.test.ts",
"seed": "tsx seed.ts"
},
"dependencies": {
Expand Down
Loading
Loading