From 94d3d25b4cf472f3ba87d391a1b77969863ddfc1 Mon Sep 17 00:00:00 2001 From: hugo-heer Date: Fri, 26 Jun 2026 10:28:21 +0200 Subject: [PATCH] chore: deployed vault testnet --- .gitignore | 2 +- deployed-vaults.testnet.json | 18 +++ frontend/src/defindex.ts | 44 +++++- frontend/src/views/vault.css | 9 ++ frontend/src/views/vault.ts | 41 ++++++ scripts/check_testnet_infra.ts | 49 +++++++ scripts/deploy_strategy_testnet.ts | 210 +++++++++++++++++++++++++++++ scripts/query_testnet_pool.ts | 72 ++++++++++ scripts/wire_testnet_vaults.ts | 80 +++++++++++ 9 files changed, 523 insertions(+), 2 deletions(-) create mode 100644 deployed-vaults.testnet.json create mode 100644 scripts/check_testnet_infra.ts create mode 100644 scripts/deploy_strategy_testnet.ts create mode 100644 scripts/query_testnet_pool.ts create mode 100644 scripts/wire_testnet_vaults.ts diff --git a/.gitignore b/.gitignore index 4201291..ad30e05 100644 --- a/.gitignore +++ b/.gitignore @@ -14,7 +14,7 @@ src/.DS_Store *.secret .env.local .env*.local - +.env frontend/dist/ frontend/package-lock.json frontend/.env.local diff --git a/deployed-vaults.testnet.json b/deployed-vaults.testnet.json new file mode 100644 index 0000000..833ce4c --- /dev/null +++ b/deployed-vaults.testnet.json @@ -0,0 +1,18 @@ +{ + "USDC": { + "strategy": "CCGM3FT4HKLXGTD5FZYSIWTOPR4REIEMTTC23GU6PHSLBXBADKFQPEKR", + "token": "CDWADWK2AYWWCZOZAHAPAKJDYXAST4VSDAPTIKQZRX7ZLN4YKP5U2G5A" + }, + "CETES": { + "strategy": "CBK3RBS6DTTUTXSCBE3B3WCSQ5XCFPLBIL3AGAZJGNI5PZNBZ66BIGMZ", + "token": "CCUT4XNXJ6H4BFUY7V2QVKLA7UIXH2GAEGCVVTOSQW4M3APHZ3SQTGPE" + }, + "XLM": { + "strategy": "CCCJA2JLLODWPWEYBE6X77SAFY2ZLBHTP33PYLKKZON2LM5OPPNAJ5HB", + "token": "CDDA6LYKAJTUCB4NYS25BOUM7GRVK45ELKTB4KE3557EIHPRIHMELSTD" + }, + "TESOURO": { + "strategy": "CATU5FLSDYXSAXOMXBWFKHPBWW3ZIKESQMR75YR6HUYE2LJJLDKH2QIX", + "token": "CDKEYTBUW6GTHZUWXBLSVCPMGISWHSB4IWETGWUTZMKXICEUGCW4EF7N" + } +} \ No newline at end of file diff --git a/frontend/src/defindex.ts b/frontend/src/defindex.ts index 7b73fc9..69e9066 100644 --- a/frontend/src/defindex.ts +++ b/frontend/src/defindex.ts @@ -98,15 +98,57 @@ const MAINNET_VAULTS: VaultConfig[] = [ }, ]; +// Testnet rehearsal vaults across the 4 reserves of the testnet Blend pool +// (XLM, USDC, CETES, TESOURO — USTRY does not exist on testnet, so TESOURO +// stands in for the 4th vault). vaultId/shareToken are filled post-deploy from +// deployed-vaults.testnet.json (see scripts/wire_testnet_vaults.ts). Config +// mirrors scripts/deploy_strategy_testnet.ts. const TESTNET_VAULTS: VaultConfig[] = [ { - vaultId: "CDOETIUHCETALQMBMYUXGFJFA34KDTV74AMHTWXJLY2XUVNZ23JDLJZA", + vaultId: "CCGM3FT4HKLXGTD5FZYSIWTOPR4REIEMTTC23GU6PHSLBXBADKFQPEKR", + shareToken: "CDWADWK2AYWWCZOZAHAPAKJDYXAST4VSDAPTIKQZRX7ZLN4YKP5U2G5A", // filled post-deploy assetId: "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA", // USDC poolId: "CAPBMXIQTICKWFPWFDJWMAKBXBPJZUKLNONQH3MLPLLBKQ643CYN5PRW", name: "Leveraged USDC (Testnet)", assetSymbol: "USDC", decimals: 7, cFactor: 0.90, + targetLoops: 4, + minHf: 1.05, + }, + { + vaultId: "CBK3RBS6DTTUTXSCBE3B3WCSQ5XCFPLBIL3AGAZJGNI5PZNBZ66BIGMZ", + shareToken: "CCUT4XNXJ6H4BFUY7V2QVKLA7UIXH2GAEGCVVTOSQW4M3APHZ3SQTGPE", + assetId: "CC72F57YTPX76HAA64JQOEGHQAPSADQWSY5DWVBR66JINPFDLNCQYHIC", // CETES + poolId: "CAPBMXIQTICKWFPWFDJWMAKBXBPJZUKLNONQH3MLPLLBKQ643CYN5PRW", + name: "Leveraged CETES (Testnet)", + assetSymbol: "CETES", + decimals: 7, + cFactor: 0.75, + targetLoops: 3, + minHf: 1.05, + }, + { + vaultId: "CCCJA2JLLODWPWEYBE6X77SAFY2ZLBHTP33PYLKKZON2LM5OPPNAJ5HB", + shareToken: "CDDA6LYKAJTUCB4NYS25BOUM7GRVK45ELKTB4KE3557EIHPRIHMELSTD", + assetId: "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC", // XLM (native) + poolId: "CAPBMXIQTICKWFPWFDJWMAKBXBPJZUKLNONQH3MLPLLBKQ643CYN5PRW", + name: "Leveraged XLM (Testnet)", + assetSymbol: "XLM", + decimals: 7, + cFactor: 0.70, + targetLoops: 2, + minHf: 1.10, + }, + { + vaultId: "CATU5FLSDYXSAXOMXBWFKHPBWW3ZIKESQMR75YR6HUYE2LJJLDKH2QIX", + shareToken: "CDKEYTBUW6GTHZUWXBLSVCPMGISWHSB4IWETGWUTZMKXICEUGCW4EF7N", + assetId: "CCKA3OUWLZPX3YT335UNHIFMKSYA37M66VKGD5XZOX4BA4IKTYP4WBEE", // TESOURO + poolId: "CAPBMXIQTICKWFPWFDJWMAKBXBPJZUKLNONQH3MLPLLBKQ643CYN5PRW", + name: "Leveraged TESOURO (Testnet)", + assetSymbol: "TESOURO", + decimals: 7, + cFactor: 0.80, targetLoops: 3, minHf: 1.05, }, diff --git a/frontend/src/views/vault.css b/frontend/src/views/vault.css index 4210c56..da3304c 100644 --- a/frontend/src/views/vault.css +++ b/frontend/src/views/vault.css @@ -94,6 +94,15 @@ } .vault-wallet-hint { font-size: var(--tl-text-xs); color: var(--tl-text-3); margin: 0; } +.vault-dep-preview { + margin-top: calc(-1 * var(--tl-space-1)); + padding: var(--tl-space-2) var(--tl-space-3); + background: var(--tl-input-bg); + border: 1px solid var(--tl-border); + border-radius: var(--tl-radius-sm); +} +.vault-dep-preview__lev { color: var(--tl-text-3); font-weight: 500; } + .vault-divider { border-top: 1px solid var(--tl-border); padding-top: var(--tl-space-3); } /* Receipt token + Aquarius */ diff --git a/frontend/src/views/vault.ts b/frontend/src/views/vault.ts index 1dc4f0d..334b689 100644 --- a/frontend/src/views/vault.ts +++ b/frontend/src/views/vault.ts @@ -299,10 +299,49 @@ function yourPositionCard( disabled: !addr || !ready, onMax: () => { if (vs.userWalletBalance > 0) depField.value = vs.userWalletBalance.toFixed(2); + updateDepPreview(); }, }); const depField = depInput.querySelector("input") as HTMLInputElement; + // ── Projected HF preview (before → after) for the entered deposit amount ── + // The loop levers every deposit at the same ratio, so the marginal HF is the + // deterministic target c·S/(S−1); the aggregate "after" HF blends it with the + // current position. Computed entirely client-side from cFactor + targetLoops + // and the position's underlying collateral/debt — no extra on-chain call. + const targetLeverage = (1 - vault.cFactor ** (vault.targetLoops + 1)) / (1 - vault.cFactor); + const depPreview = el("div", { class: "vault-row vault-dep-preview", style: "display:none" }, []); + function updateDepPreview() { + const amount = Number.parseFloat(depField.value); + if (!ready || !amount || amount <= 0) { + depPreview.style.display = "none"; + return; + } + const cBefore = stats?.collateralValue ?? 0; + const dBefore = stats?.debtValue ?? 0; + const hfBefore = stats?.healthFactor ?? Number.POSITIVE_INFINITY; + const cAfter = cBefore + amount * targetLeverage; + const dAfter = dBefore + amount * (targetLeverage - 1); + const hfAfter = dAfter > 0 ? (vault.cFactor * cAfter) / dAfter : Number.POSITIVE_INFINITY; + const fb = formatHf(hfBefore); + const fa = formatHf(hfAfter); + depPreview.replaceChildren( + el("span", { class: "vault-row__label" }, [ + lbl( + tt("vault.projectedHf", "Projected HF"), + "Health factor of the whole vault before and after your deposit, at the target leverage.", + ), + ]), + el("span", { class: "vault-mono vault-row__value" }, [ + el("span", { class: fb.cls }, [fb.text]), + " → ", + el("span", { class: fa.cls }, [fa.text]), + el("span", { class: "vault-dep-preview__lev" }, [` (~${targetLeverage.toFixed(2)}×)`]), + ]), + ); + depPreview.style.display = ""; + } + const depBtn = Button({ variant: "primary", size: "lg", @@ -311,6 +350,7 @@ function yourPositionCard( children: `${tt("vault.deposit", "Deposit")} ${sym}`, }); on(depBtn, "click", () => void runDeposit()); + on(depField, "input", () => updateDepPreview()); // ── Withdraw ── const wdInput = Input({ @@ -442,6 +482,7 @@ function yourPositionCard( equityRow, shareRow, el("div", { class: "vault-field" }, [el("label", { class: "vault-field__label" }, [`Deposit ${sym}`]), depInput]), + depPreview, depBtn, el("div", { class: "vault-field" }, [el("label", { class: "vault-field__label" }, [`Withdraw ${sym}`]), wdInput]), wdBtn, diff --git a/scripts/check_testnet_infra.ts b/scripts/check_testnet_infra.ts new file mode 100644 index 0000000..2ef80d9 --- /dev/null +++ b/scripts/check_testnet_infra.ts @@ -0,0 +1,49 @@ +/** + * Quick existence/symbol check for the testnet BLND token + Soroswap router used + * by the strategy constructor, so the testnet deploy doesn't surprise us. + * Read-only. Usage: npx tsx scripts/check_testnet_infra.ts + */ +import { + Account, + BASE_FEE, + Contract, + Networks, + rpc as SorobanRpc, + scValToNative, + TransactionBuilder, + xdr, +} from "@stellar/stellar-sdk"; + +const RPC_URL = "https://soroban-testnet.stellar.org"; +const NETWORK = Networks.TESTNET; +const NULL_ACCOUNT = "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF"; +const server = new SorobanRpc.Server(RPC_URL); + +const BLND = "CB22KRA3YZVCNCQI64JQ5WE7UY2VAV7WFLK6A2JN3HEX56T2EDAFO7QF"; +const ROUTER = "CCJUD55AG6W5HAI5LRVNKAE5WDP5XGZBUDS5WNTIVDU7O264UZZE7BRD"; + +async function sim(op: xdr.Operation): Promise { + const acc = new Account(NULL_ACCOUNT, "0"); + const tx = new TransactionBuilder(acc, { fee: BASE_FEE, networkPassphrase: NETWORK }) + .addOperation(op) + .setTimeout(30) + .build(); + const res = await server.simulateTransaction(tx); + if (!SorobanRpc.Api.isSimulationSuccess(res)) return { __error: true }; + return scValToNative(res.result!.retval); +} + +async function main() { + const blndSym = await sim(new Contract(BLND).call("symbol")); + console.log(`BLND (${BLND}): symbol=${JSON.stringify(blndSym)}`); + + // Soroswap router exposes get_pair / router_get_amounts_out etc.; just probe a + // cheap read that exists on the router to confirm the contract is live. + const routerProbe = await sim(new Contract(ROUTER).call("get_factory")); + console.log(`Router (${ROUTER}): get_factory=${JSON.stringify(routerProbe)}`); +} + +main().catch((e) => { + console.error(e); + process.exit(1); +}); diff --git a/scripts/deploy_strategy_testnet.ts b/scripts/deploy_strategy_testnet.ts new file mode 100644 index 0000000..5e24891 --- /dev/null +++ b/scripts/deploy_strategy_testnet.ts @@ -0,0 +1,210 @@ +/** + * Deploy the Turbolong BlendLeverage vaults to Stellar TESTNET — the full-flow + * rehearsal for the mainnet D1 deploy. Mirrors deploy_strategy_mainnet.ts so the + * testnet run exercises the exact same path (install both WASMs → per asset: + * deploy strategy + deploy vault_share token + set_share_token + set_swap_account) + * before any real funds are touched on mainnet. + * + * The testnet Blend pool exposes 4 reserves: XLM (native), USDC, CETES, TESOURO. + * USTRY (a mainnet-only asset) does not exist on testnet, so TESOURO stands in as + * the 4th vault for the rehearsal. The other three mirror the mainnet scope. + * + * Testnet only — no real funds. Use a funded testnet key (Friendbot), e.g.: + * DEPLOY_SECRET_KEY=S... npx tsx scripts/deploy_strategy_testnet.ts + * + * Env: + * DEPLOY_SECRET_KEY S... deployer (pays fees, installs WASM, deploys); REQUIRED + * ADMIN_PUBKEY G... admin; default = deployer + * KEEPER_PUBKEY G... keeper; default = deployer (fine for a testnet rehearsal) + * DRY_RUN=1 simulate the first WASM install only, submit nothing + * + * Pre-req: build both wasms first — + * (cd contracts/strategies/blend_leverage && cargo build --target wasm32v1-none --release) + * (cd contracts/tokens/vault_share && cargo build --target wasm32v1-none --release) + */ +import { + Address, + Contract, + Keypair, + Networks, + Operation, + rpc as SorobanRpc, + TransactionBuilder, + nativeToScVal, + xdr, +} from "@stellar/stellar-sdk"; +import * as fs from "fs"; +import * as path from "path"; +import * as crypto from "crypto"; +import { fileURLToPath } from "url"; + +const here = path.dirname(fileURLToPath(import.meta.url)); + +const RPC_URL = process.env.RPC_URL ?? "https://soroban-testnet.stellar.org"; +const PASSPHRASE = Networks.TESTNET; +const DRY_RUN = process.env.DRY_RUN === "1"; + +const SECRET = process.env.DEPLOY_SECRET_KEY; +if (!SECRET) { + console.error("DEPLOY_SECRET_KEY is required (a funded testnet S... key; use Friendbot)."); + process.exit(1); +} +const keypair = Keypair.fromSecret(SECRET); +const deployer = keypair.publicKey(); +const ADMIN = process.env.ADMIN_PUBKEY ?? deployer; +const KEEPER = process.env.KEEPER_PUBKEY ?? deployer; + +const server = new SorobanRpc.Server(RPC_URL); + +// ── Testnet constants (verified live via scripts/query_testnet_pool.ts + +// scripts/check_testnet_infra.ts) ───────────────────────────────────────── +const POOL = "CAPBMXIQTICKWFPWFDJWMAKBXBPJZUKLNONQH3MLPLLBKQ643CYN5PRW"; // Blend testnet pool +const BLND = "CB22KRA3YZVCNCQI64JQ5WE7UY2VAV7WFLK6A2JN3HEX56T2EDAFO7QF"; // testnet BLND +const ROUTER = "CCJUD55AG6W5HAI5LRVNKAE5WDP5XGZBUDS5WNTIVDU7O264UZZE7BRD"; // Soroswap testnet router + +// Per-asset config. Strategy `c_factor` sits below the pool's live c_factor +// (XLM/USDC/CETES = 0.98, TESOURO = 0.90) to leave an HF buffer. Loops/min_hf/ +// orange_hf mirror the mainnet readiness table where the asset matches. +const REWARD_THRESHOLD = 10_000_000n; // 1 BLND @ 7dp (low, so harvest triggers easily on testnet) +interface AssetCfg { + symbol: string; + asset: string; + cFactor: bigint; // 1e7 + targetLoops: number; + minHf: bigint; // 1e7 + orangeHf: bigint; // 1e7 +} +const ASSETS: AssetCfg[] = [ + { symbol: "USDC", asset: "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA", cFactor: 9_000_000n, targetLoops: 4, minHf: 10_500_000n, orangeHf: 11_500_000n }, + { symbol: "CETES", asset: "CC72F57YTPX76HAA64JQOEGHQAPSADQWSY5DWVBR66JINPFDLNCQYHIC", cFactor: 7_500_000n, targetLoops: 3, minHf: 10_500_000n, orangeHf: 11_500_000n }, + { symbol: "XLM", asset: "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC", cFactor: 7_000_000n, targetLoops: 2, minHf: 11_000_000n, orangeHf: 12_000_000n }, + { symbol: "TESOURO", asset: "CCKA3OUWLZPX3YT335UNHIFMKSYA37M66VKGD5XZOX4BA4IKTYP4WBEE", cFactor: 8_000_000n, targetLoops: 3, minHf: 10_500_000n, orangeHf: 11_500_000n }, +]; + +const STRATEGY_WASM = path.resolve(here, "../contracts/strategies/blend_leverage/target/wasm32v1-none/release/blend_leverage_strategy.wasm"); +const TOKEN_WASM = path.resolve(here, "../contracts/tokens/vault_share/target/wasm32v1-none/release/vault_share_token.wasm"); + +// ── Helpers ──────────────────────────────────────────────────────────────────── + +function addr(a: string): xdr.ScVal { + return a.startsWith("C") ? new Contract(a).address().toScVal() : new Address(a).toScVal(); +} + +async function signSubmit(tx: any, label: string): Promise { + const prepared = await server.prepareTransaction(tx); + if (DRY_RUN) { + console.log(` [dry-run] ${label} prepared (not submitted)`); + throw new Error("DRY_RUN"); + } + prepared.sign(keypair); + const sent = await server.sendTransaction(prepared); + let res = await server.getTransaction(sent.hash); + while (res.status === "NOT_FOUND") { + await new Promise((r) => setTimeout(r, 1500)); + res = await server.getTransaction(sent.hash); + } + if (res.status !== "SUCCESS") { + throw new Error(`${label} failed: ${JSON.stringify(res).slice(0, 500)}`); + } + console.log(` ✓ ${label} tx=${sent.hash}`); + return res; +} + +async function installWasm(wasmPath: string, label: string): Promise { + const wasm = fs.readFileSync(wasmPath); + const acc = await server.getAccount(deployer); + const tx = new TransactionBuilder(acc, { fee: "10000000", networkPassphrase: PASSPHRASE }) + .setTimeout(120) + .addOperation(Operation.uploadContractWasm({ wasm })) + .build(); + const res = await signSubmit(tx, `install ${label} wasm`); + const hash = (res.returnValue as xdr.ScVal).bytes().toString("hex"); + return hash; +} + +async function deploy(wasmHash: string, constructorArgs: xdr.ScVal[], label: string): Promise { + const acc = await server.getAccount(deployer); + const salt = crypto.randomBytes(32); + const op = Operation.createCustomContract({ + wasmHash: Buffer.from(wasmHash, "hex"), + address: new Address(deployer), + salt, + constructorArgs, + }); + const tx = new TransactionBuilder(acc, { fee: "10000000", networkPassphrase: PASSPHRASE }) + .setTimeout(120) + .addOperation(op) + .build(); + const res = await signSubmit(tx, `deploy ${label}`); + return Address.fromScVal(res.returnValue as xdr.ScVal).toString(); +} + +async function invoke(contractId: string, method: string, args: xdr.ScVal[], label: string): Promise { + const acc = await server.getAccount(deployer); + const tx = new TransactionBuilder(acc, { fee: "10000000", networkPassphrase: PASSPHRASE }) + .setTimeout(120) + .addOperation(new Contract(contractId).call(method, ...args)) + .build(); + await signSubmit(tx, label); +} + +// ── Main ─────────────────────────────────────────────────────────────────────── + +async function main() { + console.log(`Turbolong TESTNET deploy ${DRY_RUN ? "(DRY-RUN)" : ""}`); + console.log(` deployer=${deployer} admin=${ADMIN} keeper=${KEEPER}`); + console.log(` pool=${POOL} router=${ROUTER}`); + + const strategyHash = await installWasm(STRATEGY_WASM, "strategy"); + const tokenHash = await installWasm(TOKEN_WASM, "token"); + console.log(` strategy wasm=${strategyHash}\n token wasm=${tokenHash}`); + + const out: Record = {}; + + for (const a of ASSETS) { + console.log(`\n=== ${a.symbol} ===`); + const initArgs = xdr.ScVal.scvVec([ + addr(POOL), // [0] pool + addr(BLND), // [1] blend_token + addr(ROUTER), // [2] router + nativeToScVal(REWARD_THRESHOLD, { type: "i128" }), // [3] reward_threshold + addr(KEEPER), // [4] keeper + nativeToScVal(a.cFactor, { type: "i128" }), // [5] c_factor + nativeToScVal(a.targetLoops, { type: "u32" }), // [6] target_loops + nativeToScVal(a.minHf, { type: "i128" }), // [7] min_hf + nativeToScVal(a.orangeHf, { type: "i128" }), // [8] orange_hf + addr(ADMIN), // [9] admin + ]); + const strategy = await deploy(strategyHash, [addr(a.asset), initArgs], `${a.symbol} strategy`); + console.log(` strategy=${strategy}`); + + const token = await deploy( + tokenHash, + [ + addr(ADMIN), // admin + addr(strategy), // minter = strategy + nativeToScVal(7, { type: "u32" }), // decimals + nativeToScVal(`BlendLeverage ${a.symbol} Share`, { type: "string" }), + nativeToScVal(`blv${a.symbol}`, { type: "string" }), + ], + `${a.symbol} token`, + ); + console.log(` token=${token}`); + + await invoke(strategy, "set_share_token", [addr(token)], `${a.symbol} set_share_token`); + await invoke(strategy, "set_swap_account", [addr(KEEPER)], `${a.symbol} set_swap_account`); + + out[a.symbol] = { strategy, token }; + } + + const file = path.resolve(here, "../deployed-vaults.testnet.json"); + fs.writeFileSync(file, JSON.stringify(out, null, 2)); + console.log(`\nDeployed testnet vaults written to ${file}`); + console.log("Next: wire frontend/src/defindex.ts TESTNET_VAULTS, then deposit→loop→withdraw per asset on testnet."); +} + +main().catch((e) => { + if (e.message === "DRY_RUN") { console.log("dry-run complete (no submissions)."); return; } + console.error(e); + process.exit(1); +}); diff --git a/scripts/query_testnet_pool.ts b/scripts/query_testnet_pool.ts new file mode 100644 index 0000000..afbb875 --- /dev/null +++ b/scripts/query_testnet_pool.ts @@ -0,0 +1,72 @@ +/** + * Query the testnet Blend pool reserves to know which assets are available + * before deploying the 4-asset testnet rehearsal vaults. + * + * Lists the pool's reserve list and, for each reserve, the token symbol/name + + * reserve index + collateral factor. Read-only (simulation), no key needed. + * + * Usage: npx tsx scripts/query_testnet_pool.ts + */ +import { + Account, + Address, + BASE_FEE, + Contract, + Networks, + rpc as SorobanRpc, + scValToNative, + TransactionBuilder, + xdr, +} from "@stellar/stellar-sdk"; + +const RPC_URL = process.env.RPC_URL ?? "https://soroban-testnet.stellar.org"; +const NETWORK = Networks.TESTNET; +const NULL_ACCOUNT = "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF"; +const POOL = process.env.POOL ?? "CAPBMXIQTICKWFPWFDJWMAKBXBPJZUKLNONQH3MLPLLBKQ643CYN5PRW"; + +const server = new SorobanRpc.Server(RPC_URL); +const big = (_: string, v: unknown) => (typeof v === "bigint" ? v.toString() : v); + +async function sim(op: xdr.Operation): Promise { + const acc = new Account(NULL_ACCOUNT, "0"); + const tx = new TransactionBuilder(acc, { fee: BASE_FEE, networkPassphrase: NETWORK }) + .addOperation(op) + .setTimeout(30) + .build(); + const res = await server.simulateTransaction(tx); + if (!SorobanRpc.Api.isSimulationSuccess(res)) { + return { __error: (res as any).error ?? "simulation failed" }; + } + return scValToNative(res.result!.retval); +} + +async function main() { + console.log(`Testnet pool query — ${POOL}\n rpc=${RPC_URL}\n`); + + const pool = new Contract(POOL); + + const reserveList = await sim(pool.call("get_reserve_list")); + if (reserveList?.__error) { + console.error("get_reserve_list failed — pool may not exist (testnet reset?)."); + console.error(JSON.stringify(reserveList.__error, big, 2)); + process.exit(2); + } + console.log(`Reserves (${reserveList.length}):`); + + for (const assetId of reserveList as string[]) { + const token = new Contract(assetId); + const symbol = await sim(token.call("symbol")); + const name = await sim(token.call("name")); + const reserve = await sim(pool.call("get_reserve", new Address(assetId).toScVal())); + const idx = reserve?.config?.index; + const cFactor = reserve?.config?.c_factor; + console.log( + ` - ${String(symbol).padEnd(8)} idx=${idx ?? "?"} c_factor=${cFactor ?? "?"} ${assetId} (${name})`, + ); + } +} + +main().catch((e) => { + console.error(e); + process.exit(1); +}); diff --git a/scripts/wire_testnet_vaults.ts b/scripts/wire_testnet_vaults.ts new file mode 100644 index 0000000..ba52b68 --- /dev/null +++ b/scripts/wire_testnet_vaults.ts @@ -0,0 +1,80 @@ +/** + * Wire the testnet vault IDs into the frontend after the testnet rehearsal deploy. + * + * Reads deployed-vaults.testnet.json (written by deploy_strategy_testnet.ts) and + * fills each TESTNET_VAULTS entry in frontend/src/defindex.ts with its deployed + * strategy address (`vaultId`) and share-token address (`shareToken`), matched by + * `assetSymbol`. Idempotent. + * + * Replacement is scoped to the TESTNET_VAULTS array only, so it never touches the + * MAINNET_VAULTS entries (which share asset symbols like USDC/CETES/XLM). + * + * Usage: npx tsx scripts/wire_testnet_vaults.ts + */ +import * as fs from "fs"; +import * as path from "path"; +import { fileURLToPath } from "url"; + +const here = path.dirname(fileURLToPath(import.meta.url)); +const deployedPath = path.resolve(here, "../deployed-vaults.testnet.json"); +const defindexPath = path.resolve(here, "../frontend/src/defindex.ts"); + +if (!fs.existsSync(deployedPath)) { + console.error(`Missing ${deployedPath}. Run deploy_strategy_testnet.ts first.`); + process.exit(1); +} + +const deployed: Record = JSON.parse( + fs.readFileSync(deployedPath, "utf-8"), +); + +const src = fs.readFileSync(defindexPath, "utf-8"); + +// Isolate the TESTNET_VAULTS array body so edits can't bleed into MAINNET_VAULTS. +const blockRe = /const TESTNET_VAULTS:\s*VaultConfig\[\]\s*=\s*\[([\s\S]*?)\n\];/; +const blockMatch = src.match(blockRe); +if (!blockMatch) { + console.error("Could not locate the TESTNET_VAULTS array in defindex.ts."); + process.exit(1); +} + +let block = blockMatch[1]; +let changed = 0; + +for (const [symbol, ids] of Object.entries(deployed)) { + if (!ids.strategy) { + console.warn(`[${symbol}] no strategy address — skipping`); + continue; + } + const vaultIdRe = new RegExp( + `(vaultId:\\s*")[^"]*("[^}]*?assetSymbol:\\s*"${symbol}")`, + ); + if (!vaultIdRe.test(block)) { + console.warn(`[${symbol}] no TESTNET_VAULTS entry found — skipping`); + continue; + } + block = block.replace(vaultIdRe, `$1${ids.strategy}$2`); + + const hasShareToken = new RegExp( + `vaultId:\\s*"${ids.strategy}"[^}]*?shareToken:\\s*"[^"]*"[^}]*?assetSymbol:\\s*"${symbol}"`, + ).test(block); + if (ids.token) { + if (hasShareToken) { + block = block.replace( + new RegExp(`(shareToken:\\s*")[^"]*("[^}]*?assetSymbol:\\s*"${symbol}")`), + `$1${ids.token}$2`, + ); + } else { + block = block.replace( + new RegExp(`(vaultId:\\s*"${ids.strategy}",)`), + `$1\n shareToken: "${ids.token}",`, + ); + } + } + changed++; + console.log(`[${symbol}] vaultId=${ids.strategy} shareToken=${ids.token}`); +} + +const out = src.replace(blockRe, `const TESTNET_VAULTS: VaultConfig[] = [${block}\n];`); +fs.writeFileSync(defindexPath, out); +console.log(`\nWired ${changed} testnet vault(s) into ${defindexPath}. Run \`npm run build\` in frontend to verify.`);