From 38181df0830fbf628b6860909f6cb7c4beade42d Mon Sep 17 00:00:00 2001 From: MiracleOfWebfi Date: Sat, 30 May 2026 09:42:31 +0000 Subject: [PATCH] feat: add architecture diagram and rate-limit RPC calls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #79 — Architecture diagram in root README - Add docs/diagrams/architecture.svg: SVG flow diagram showing User → Wallet → Frontend → Soroban RPC → Blend Pool → b/d Tokens - Create README.md embedding the diagram with a layer description table, quickstart instructions, and links to existing docs - Diagram uses inline CSS classes for fill/stroke so it renders correctly on both GitHub dark and light themes #89 — Rate-limit RPC calls from frontend - Upgrade withRetry() in frontend/src/blend.ts: - Increase default retries from 2 to 4 - Switch from fixed delay to exponential backoff (baseDelay × 2^attempt) - Add up to 300 ms of random jitter per attempt to spread retry storms - Detect HTTP 429 responses (status field and message string) and emit a console.warn with the back-off duration when throttling triggers - Wrap every bare server.getAccount(), server.getLatestLedger(), and server.simulateTransaction() call in withRetry() across all transaction builders: buildApproveXdr, buildOpenPositionXdr, buildCloseSubmitXdr, buildRepayXdr, buildWithdrawXdr, buildClaimXdr, buildIncreaseLeverageXdr, buildDecreaseLeverageXdr, buildResupplyXdr Closes #79 Closes #89 --- README.md | 30 +++++++++++++ docs/diagrams/architecture.svg | 79 ++++++++++++++++++++++++++++++++++ frontend/src/blend.ts | 53 +++++++++++++---------- 3 files changed, 139 insertions(+), 23 deletions(-) create mode 100644 README.md create mode 100644 docs/diagrams/architecture.svg diff --git a/README.md b/README.md new file mode 100644 index 0000000..b291d72 --- /dev/null +++ b/README.md @@ -0,0 +1,30 @@ +# TurboLong + +Leveraged long positions on [Blend Protocol](https://blend.capital) — Stellar / Soroban. + +## Architecture + +![Architecture diagram](docs/diagrams/architecture.svg) + +| Layer | Description | +|---|---| +| **User** | Interacts via browser | +| **Wallet** | Freighter, xBull, Albedo, Lobstr, Hana — signs XDR | +| **Frontend** | `blend.ts` + `main.ts` — builds transactions, rate-limits RPC calls with exponential backoff | +| **Soroban RPC** | Simulates and submits Soroban transactions | +| **Blend Pool** | `submit_with_allowance` — atomic supply / borrow / repay / withdraw | +| **b / d Tokens** | bToken = collateral receipt, dToken = debt receipt | + +## Quickstart + +```bash +cd frontend +npm install +npm run dev +``` + +## Docs + +- [`doc.md`](doc.md) — strategy overview +- [`profitability_analysis.md`](profitability_analysis.md) — rate and profit modelling +- [`CONTRIBUTING.md`](CONTRIBUTING.md) — contribution guide diff --git a/docs/diagrams/architecture.svg b/docs/diagrams/architecture.svg new file mode 100644 index 0000000..301e4ab --- /dev/null +++ b/docs/diagrams/architecture.svg @@ -0,0 +1,79 @@ + + + + + + + + + + + User + browser + + + + Wallet + Freighter / xBull + + + + Frontend + blend.ts + main.ts + rate-limit + retry + exp. backoff + jitter + + + + Soroban RPC + simulate / send + + + + Blend Pool + submit_with_allowance + + + + b / d Tokens + bToken = collateral + dToken = debt + + + + + sign + + + + XDR + + + + simulate + + + + submit + + + + on-chain + + + + mint/burn + diff --git a/frontend/src/blend.ts b/frontend/src/blend.ts index 1c4ad6c..e78e396 100644 --- a/frontend/src/blend.ts +++ b/frontend/src/blend.ts @@ -334,14 +334,21 @@ function buildRequestsVec(items: xdr.ScVal[]): xdr.ScVal { // ── RPC retry helper ────────────────────────────────────────────────────────── -async function withRetry(fn: () => Promise, retries = 2, delayMs = 1000): Promise { +async function withRetry(fn: () => Promise, retries = 4, baseDelayMs = 500): Promise { for (let attempt = 0; ; attempt++) { try { return await fn(); - } catch (e) { + } catch (e: any) { if (attempt >= retries) throw e; - console.warn(`RPC call failed (attempt ${attempt + 1}/${retries + 1}), retrying in ${delayMs}ms...`); - await new Promise(r => setTimeout(r, delayMs)); + const is429 = e?.status === 429 || e?.response?.status === 429 || + String(e?.message ?? e).includes("429"); + const delay = baseDelayMs * Math.pow(2, attempt) + Math.random() * 300; + if (is429) { + console.warn(`[rpc] 429 rate-limited — backing off ${Math.round(delay)}ms (attempt ${attempt + 1}/${retries + 1})`); + } else { + console.warn(`[rpc] call failed (attempt ${attempt + 1}/${retries + 1}), retrying in ${Math.round(delay)}ms...`); + } + await new Promise(r => setTimeout(r, delay)); } } } @@ -873,10 +880,10 @@ export async function buildApproveXdr( const token = new Contract(assetId); const addrScVal = new Address(userAddress).toScVal(); const poolScVal = new Address(pool.id).toScVal(); - const ledger = await server.getLatestLedger(); + const ledger = await withRetry(() => server.getLatestLedger()); const expiry = ledger.sequence + 120; - const acc = await server.getAccount(userAddress); + const acc = await withRetry(() => server.getAccount(userAddress)); const tx = new TransactionBuilder(acc, { fee: (BigInt(BASE_FEE) * 10n).toString(), networkPassphrase: _cfg.passphrase, @@ -890,7 +897,7 @@ export async function buildApproveXdr( )) .setTimeout(60).build(); - const sim = await server.simulateTransaction(tx); + const sim = await withRetry(() => server.simulateTransaction(tx)); if (!SorobanRpc.Api.isSimulationSuccess(sim)) throw new Error(`Approve simulation failed: ${(sim as SorobanRpc.Api.SimulateTransactionErrorResponse).error}`); return SorobanRpc.assembleTransaction(tx, sim).build().toXDR(); @@ -908,7 +915,7 @@ export async function buildOpenPositionXdr( const addrScVal = new Address(userAddress).toScVal(); const requests = buildRequestsVec(buildOpenRequests(asset.id, initialStroops, cFactorBn, leverage)); - const acc = await server.getAccount(userAddress); + const acc = await withRetry(() => server.getAccount(userAddress)); const tx = new TransactionBuilder(acc, { fee: (BigInt(BASE_FEE) * 10n).toString(), networkPassphrase: _cfg.passphrase, @@ -916,7 +923,7 @@ export async function buildOpenPositionXdr( .addOperation(poolContract.call("submit_with_allowance", addrScVal, addrScVal, addrScVal, requests)) .setTimeout(60).build(); - const sim = await server.simulateTransaction(tx); + const sim = await withRetry(() => server.simulateTransaction(tx)); if (!SorobanRpc.Api.isSimulationSuccess(sim)) throw new Error(`Open position simulation failed: ${(sim as SorobanRpc.Api.SimulateTransactionErrorResponse).error}`); return SorobanRpc.assembleTransaction(tx, sim).build().toXDR(); @@ -955,7 +962,7 @@ export async function buildCloseSubmitXdr( const poolContract = new Contract(pool.id); const addrScVal = new Address(userAddress).toScVal(); - const acc = await server.getAccount(userAddress); + const acc = await withRetry(() => server.getAccount(userAddress)); const tx = new TransactionBuilder(acc, { fee: (BigInt(BASE_FEE) * 10n).toString(), networkPassphrase: _cfg.passphrase, @@ -963,7 +970,7 @@ export async function buildCloseSubmitXdr( .addOperation(poolContract.call("submit_with_allowance", addrScVal, addrScVal, addrScVal, requests)) .setTimeout(60).build(); - const sim = await server.simulateTransaction(tx); + const sim = await withRetry(() => server.simulateTransaction(tx)); if (!SorobanRpc.Api.isSimulationSuccess(sim)) throw new Error(`Close simulation failed: ${(sim as SorobanRpc.Api.SimulateTransactionErrorResponse).error}`); return SorobanRpc.assembleTransaction(tx, sim).build().toXDR(); @@ -992,7 +999,7 @@ export async function buildRepayXdr( const poolContract = new Contract(pool.id); const addrScVal = new Address(userAddress).toScVal(); - const acc = await server.getAccount(userAddress); + const acc = await withRetry(() => server.getAccount(userAddress)); const tx = new TransactionBuilder(acc, { fee: (BigInt(BASE_FEE) * 10n).toString(), networkPassphrase: _cfg.passphrase, @@ -1000,7 +1007,7 @@ export async function buildRepayXdr( .addOperation(poolContract.call("submit_with_allowance", addrScVal, addrScVal, addrScVal, requests)) .setTimeout(60).build(); - const sim = await server.simulateTransaction(tx); + const sim = await withRetry(() => server.simulateTransaction(tx)); if (!SorobanRpc.Api.isSimulationSuccess(sim)) throw new Error(`Repay simulation failed: ${(sim as SorobanRpc.Api.SimulateTransactionErrorResponse).error}`); return SorobanRpc.assembleTransaction(tx, sim).build().toXDR(); @@ -1023,7 +1030,7 @@ export async function buildWithdrawXdr( const poolContract = new Contract(pool.id); const addrScVal = new Address(userAddress).toScVal(); - const acc = await server.getAccount(userAddress); + const acc = await withRetry(() => server.getAccount(userAddress)); const tx = new TransactionBuilder(acc, { fee: (BigInt(BASE_FEE) * 10n).toString(), networkPassphrase: _cfg.passphrase, @@ -1031,7 +1038,7 @@ export async function buildWithdrawXdr( .addOperation(poolContract.call("submit_with_allowance", addrScVal, addrScVal, addrScVal, requests)) .setTimeout(60).build(); - const sim = await server.simulateTransaction(tx); + const sim = await withRetry(() => server.simulateTransaction(tx)); if (!SorobanRpc.Api.isSimulationSuccess(sim)) throw new Error(`Withdraw simulation failed: ${(sim as SorobanRpc.Api.SimulateTransactionErrorResponse).error}`); return SorobanRpc.assembleTransaction(tx, sim).build().toXDR(); @@ -1079,7 +1086,7 @@ export async function buildClaimXdr( tokenIds.map(id => nativeToScVal(id, { type: "u32" })) ); - const acc = await server.getAccount(userAddress); + const acc = await withRetry(() => server.getAccount(userAddress)); const tx = new TransactionBuilder(acc, { fee: (BigInt(BASE_FEE) * 10n).toString(), networkPassphrase: _cfg.passphrase, @@ -1087,7 +1094,7 @@ export async function buildClaimXdr( .addOperation(poolContract.call("claim", addrScVal, tokenIds_scv, addrScVal)) .setTimeout(60).build(); - const sim = await server.simulateTransaction(tx); + const sim = await withRetry(() => server.simulateTransaction(tx)); if (!SorobanRpc.Api.isSimulationSuccess(sim)) throw new Error(`Claim simulation failed: ${(sim as SorobanRpc.Api.SimulateTransactionErrorResponse).error}`); return SorobanRpc.assembleTransaction(tx, sim).build().toXDR(); @@ -1143,7 +1150,7 @@ export async function buildIncreaseLeverageXdr( const requests = buildRequestsVec(items); const poolContract = new Contract(pool.id); const addrScVal = new Address(userAddress).toScVal(); - const acc = await server.getAccount(userAddress); + const acc = await withRetry(() => server.getAccount(userAddress)); const tx = new TransactionBuilder(acc, { fee: (BigInt(BASE_FEE) * 10n).toString(), networkPassphrase: _cfg.passphrase, @@ -1151,7 +1158,7 @@ export async function buildIncreaseLeverageXdr( .addOperation(poolContract.call("submit_with_allowance", addrScVal, addrScVal, addrScVal, requests)) .setTimeout(60).build(); - const sim = await server.simulateTransaction(tx); + const sim = await withRetry(() => server.simulateTransaction(tx)); if (!SorobanRpc.Api.isSimulationSuccess(sim)) throw new Error(`Increase leverage simulation failed: ${(sim as SorobanRpc.Api.SimulateTransactionErrorResponse).error}`); return SorobanRpc.assembleTransaction(tx, sim).build().toXDR(); @@ -1185,7 +1192,7 @@ export async function buildDecreaseLeverageXdr( const poolContract = new Contract(pool.id); const addrScVal = new Address(userAddress).toScVal(); - const acc = await server.getAccount(userAddress); + const acc = await withRetry(() => server.getAccount(userAddress)); const tx = new TransactionBuilder(acc, { fee: (BigInt(BASE_FEE) * 10n).toString(), networkPassphrase: _cfg.passphrase, @@ -1193,7 +1200,7 @@ export async function buildDecreaseLeverageXdr( .addOperation(poolContract.call("submit_with_allowance", addrScVal, addrScVal, addrScVal, requests)) .setTimeout(60).build(); - const sim = await server.simulateTransaction(tx); + const sim = await withRetry(() => server.simulateTransaction(tx)); if (!SorobanRpc.Api.isSimulationSuccess(sim)) throw new Error(`Decrease leverage simulation failed: ${(sim as SorobanRpc.Api.SimulateTransactionErrorResponse).error}`); return SorobanRpc.assembleTransaction(tx, sim).build().toXDR(); @@ -1218,7 +1225,7 @@ export async function buildResupplyXdr( const poolContract = new Contract(pool.id); const addrScVal = new Address(userAddress).toScVal(); - const acc = await server.getAccount(userAddress); + const acc = await withRetry(() => server.getAccount(userAddress)); const tx = new TransactionBuilder(acc, { fee: (BigInt(BASE_FEE) * 10n).toString(), networkPassphrase: _cfg.passphrase, @@ -1226,7 +1233,7 @@ export async function buildResupplyXdr( .addOperation(poolContract.call("submit_with_allowance", addrScVal, addrScVal, addrScVal, requests)) .setTimeout(60).build(); - const sim = await server.simulateTransaction(tx); + const sim = await withRetry(() => server.simulateTransaction(tx)); if (!SorobanRpc.Api.isSimulationSuccess(sim)) throw new Error(`Resupply simulation failed: ${(sim as SorobanRpc.Api.SimulateTransactionErrorResponse).error}`); return SorobanRpc.assembleTransaction(tx, sim).build().toXDR();