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
+
+
+
+| 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 @@
+
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();