Skip to content
Merged
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
24 changes: 22 additions & 2 deletions backend/.env.example
Original file line number Diff line number Diff line change
@@ -1,10 +1,30 @@
# Server
PORT=3010
NODE_ENV=development # development | demo | production

# Base chain RPC
# ── Base Chain RPC ───────────────────────────────────────────────
# Single RPC (backward-compatible):
BASE_RPC_URL=https://mainnet.base.org
# Multiple RPCs with failover (comma-separated, priority order):
# BASE_RPC_URLS=https://your-alchemy-base.com,https://base.llamarpc.com,https://mainnet.base.org

# Deployed contract addresses (Base Mainnet)
# ── Avalanche Chain RPC ──────────────────────────────────────────
# AVAX_RPC_URL=https://api.avax.network/ext/bc/C/rpc
# AVAX_RPC_URLS=https://your-alchemy-avax.com,https://api.avax.network/ext/bc/C/rpc,https://avalanche.drpc.org

# ── Deployed Contract Addresses (Base Mainnet) ───────────────────
EXECUTOR_ADDRESS=0x82b000512A19f7B762A23033aEA5AE00aBD0D2bC
AERODROME_ADAPTER_ADDRESS=0x187e499afB2DE75836800ad19147e0cFcd2Dc715
DCA_VAULT_ADDRESS=0x155eC4256cC6f11f3d4C21Af28a2a1CC31f730d1

# ── Avalanche Contract Addresses ─────────────────────────────────
# AVAX_EXECUTOR_ADDRESS=

# ── Demo Mode ────────────────────────────────────────────────────
# Set NODE_ENV=demo or DEMO_MODE=true to use stable RPC endpoints
# and wider timeouts. Recommended for presentations.
# DEMO_MODE=true

# ── CORS ─────────────────────────────────────────────────────────
# Comma-separated allowed origins (defaults to localhost:3000,3010,7777)
# ALLOWED_ORIGINS=http://localhost:3000,http://localhost:7777
58 changes: 58 additions & 0 deletions backend/DEPLOY_CHECKLIST.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Deployment Checklist — Execution Layer

Run through this checklist before every production deployment.

## Pre-Deploy Verification

### 1. Contract Addresses
- [ ] `EXECUTOR_ADDRESS` matches deployed PanoramaExecutor on Base Mainnet
- [ ] `DCA_VAULT_ADDRESS` matches deployed DCAVault on Base Mainnet
- [ ] `AVAX_EXECUTOR_ADDRESS` matches deployed executor on Avalanche (if applicable)
- [ ] Aerodrome protocol addresses in `config/protocols.ts` are correct (Router, Factory, Voter)

### 2. RPC Endpoints
- [ ] `BASE_RPC_URLS` configured with at least 2 endpoints (primary + fallback)
- [ ] `AVAX_RPC_URLS` configured with at least 2 endpoints (if Avalanche is active)
- [ ] Primary RPC endpoint is a paid/stable provider (Alchemy, Infura, QuickNode)
- [ ] Tested: `curl -X POST <rpc_url> -d '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}'` returns a valid block number

### 3. Schema Alignment
- [ ] Frontend (miniapp) is consuming the same response shapes as the backend produces
- [ ] Error codes in `shared/errorCodes.ts` are documented for frontend error mapping
- [ ] No breaking changes in `TransactionBundle` or `PreparedTransaction` types since last deploy

### 4. Rate Limits & Middleware
- [ ] `rateLimiter.ts` settings are appropriate for production traffic
- [ ] `serialize-by-user.ts` queue limits match expected concurrency
- [ ] `execution-timeout.ts` default (15s) is appropriate for production RPCs
- [ ] CORS `ALLOWED_ORIGINS` includes all production frontend domains

### 5. Build & Tests
- [ ] `npm run build` succeeds with zero TypeScript errors
- [ ] `npm test` passes all test suites
- [ ] No `.env` or credentials committed to the repository

## Deploy Steps

1. **Tag the release**: `git tag v<version> && git push origin v<version>`
2. **Build**: `npm run build`
3. **Verify health**: `curl https://<deployment-url>/health` returns `{"status":"ok"}`
4. **Smoke test**: Run the canonical demo flow (see `config/demo.ts`)
- POST `/swap/prepare` (ETH → USDC)
- POST `/staking/prepare-enter` (WETH/USDC volatile)
- GET `/staking/position/:address`

## Post-Deploy Verification

- [ ] Health endpoint returns `{"status":"ok"}`
- [ ] Swagger docs accessible at `/docs` (non-production only)
- [ ] Logs show `execution-service running on port <PORT>`
- [ ] Logs show `[ChainProvider] base: N RPC endpoints configured`
- [ ] Test a swap quote: `POST /swap/quote` returns a valid `amountOut`

## Rollback

If issues are found post-deploy:
1. Revert to previous Docker image / git tag
2. Verify health endpoint
3. Document the issue for next deploy
7 changes: 6 additions & 1 deletion backend/src/__tests__/integration/routes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,12 @@ vi.mock("../../modules/liquid-staking/config/staking-pools", () => ({

// Prevent real chain calls from getContract
vi.mock("../../providers/chain.provider", () => ({
getContract: vi.fn(() => ({ balanceOf: mockBalanceOf })),
getContract: vi.fn(() => ({
balanceOf: mockBalanceOf,
getReserves: vi.fn().mockResolvedValue([1_000_000n, 2_000_000n, 0n]),
totalSupply: vi.fn().mockResolvedValue(10_000n),
token0: vi.fn().mockResolvedValue("0x4200000000000000000000000000000000000006"),
})),
}));

// ── Imports after mocks ───────────────────────────────────────────────────────
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ vi.mock("../../../shared/services/aerodrome.service", () => ({
checkAllowance: vi.fn(),
quoteAddLiquidity: vi.fn(),
withRetry: vi.fn(<T>(fn: () => Promise<T>) => fn()),
withTimeout: vi.fn(<T>(fn: () => Promise<T>) => fn()),
},
}));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,15 @@ vi.mock("../../../shared/services/aerodrome.service", () => ({
},
}));

vi.mock("../../../providers/chain.provider", () => ({
getContract: vi.fn(() => ({
balanceOf: vi.fn().mockResolvedValue(10_000n),
getReserves: vi.fn().mockResolvedValue([1_000_000n, 2_000_000n, 0n]),
totalSupply: vi.fn().mockResolvedValue(10_000n),
token0: vi.fn().mockResolvedValue("0x4200000000000000000000000000000000000006"),
})),
}));

vi.mock("../../../modules/liquid-staking/config/staking-pools", () => ({
getStakingPoolById: vi.fn((id: string) => {
if (id === "weth-usdc-volatile") {
Expand Down Expand Up @@ -82,7 +91,7 @@ describe("executeExitStrategy", () => {
mockWalletBal.mockResolvedValue(0n);
await expect(
executeExitStrategy({ userAddress: USER, poolId: "weth-usdc-volatile", amount: "1000" })
).rejects.toThrow("Insufficient LP balance");
).rejects.toThrow("Have total: 500");
});

it("full exit includes unstake + approve + removeLiquidity steps", async () => {
Expand Down
124 changes: 124 additions & 0 deletions backend/src/config/demo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// ──────────────────────────────────────────────────────────────────
// DEMO ENVIRONMENT CONFIGURATION
//
// Fixed configuration for demo/presentation environments.
// Ensures deterministic behavior by:
// - Using stable, paid RPC endpoints (not free/rate-limited ones)
// - Pinning contract addresses and flow parameters
// - Defining the canonical demo flow for presentations
//
// Usage:
// NODE_ENV=demo npm run dev
//
// The demo config is consumed by chains.ts when NODE_ENV === "demo".
// It can also be used by scripts/tests that need known-good parameters.
// ──────────────────────────────────────────────────────────────────

export const DEMO_CONFIG = {
// ── Chain ──────────────────────────────────────────────────────
chain: "base" as const,
chainId: 8453,

// ── RPC Endpoints (priority order) ─────────────────────────────
// In demo mode, prefer paid/stable endpoints to avoid rate limits
// and flaky responses during presentations.
//
// Override via BASE_RPC_URLS env var (comma-separated).
// These are the recommended defaults when no env var is set:
rpcUrls: [
"https://mainnet.base.org", // Coinbase official — reliable but rate-limited
"https://base.llamarpc.com", // LlamaRPC — generous free tier
"https://base.drpc.org", // dRPC — backup
],

// ── Deployed Contracts ─────────────────────────────────────────
contracts: {
panoramaExecutor: "0x82b000512A19f7B762A23033aEA5AE00aBD0D2bC",
aerodromeAdapter: "0x187e499afB2DE75836800ad19147e0cFcd2Dc715",
dcaVault: "0x155eC4256cC6f11f3d4C21Af28a2a1CC31f730d1",
},

// ── Aerodrome Protocol ─────────────────────────────────────────
aerodrome: {
router: "0xcF77a3Ba9A5CA399B7c97c74d54e5b1Beb874E43",
factory: "0x420DD381b31aEf6683db6B902084cB0FFECe40Da",
voter: "0x16613524e02ad97eDfeF371bC883F2F5d6C480A5",
},

// ── Canonical Demo Flow ────────────────────────────────────────
// This is the sequence used during presentations.
// Each step maps to an API endpoint on the execution-layer.
//
// Flow: Swap ETH → USDC → Add Liquidity WETH/USDC → Stake LP
//
demoFlow: {
description: "Base chain → Aerodrome swap → Liquidity add → Gauge stake",
steps: [
{
name: "Swap ETH → USDC",
endpoint: "POST /swap/prepare",
params: {
tokenIn: "0x0000000000000000000000000000000000000000", // Native ETH
tokenOut: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC
amountIn: "10000000000000000", // 0.01 ETH
},
},
{
name: "Add Liquidity WETH/USDC",
endpoint: "POST /staking/prepare-enter",
params: {
poolId: "weth-usdc-volatile",
amountA: "5000000000000000", // 0.005 WETH
amountB: "15000000", // 15 USDC (approx ratio)
},
},
{
name: "Check Position",
endpoint: "GET /staking/position/:userAddress",
},
{
name: "Claim Rewards",
endpoint: "POST /staking/prepare-claim",
params: {
poolId: "weth-usdc-volatile",
},
},
{
name: "Exit Strategy",
endpoint: "POST /staking/prepare-exit",
params: {
poolId: "weth-usdc-volatile",
},
},
],
},

// ── Demo Tokens ────────────────────────────────────────────────
tokens: {
ETH: { address: "0x0000000000000000000000000000000000000000", decimals: 18 },
WETH: { address: "0x4200000000000000000000000000000000000006", decimals: 18 },
USDC: { address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", decimals: 6 },
AERO: { address: "0x940181a94A35A4569E4529A3CDfB74e38FD98631", decimals: 18 },
},

// ── Demo Pool ──────────────────────────────────────────────────
pool: {
id: "weth-usdc-volatile",
poolAddress: "0xcDAC0d6c6C59727a65F871236188350531885C43",
gaugeAddress: "0x519BBD1Dd8C6A94C46080E24f316c14Ee758C025",
},

// ── Timeouts & Limits ──────────────────────────────────────────
// More generous timeouts for demo to avoid flaky failures.
executionTimeoutMs: 20_000, // 20s vs 15s default
rpcTimeoutMs: 5_000, // 5s vs 3.5s default
slippageBps: 200, // 2% — wider to avoid reverts during live demo
} as const;

/**
* Returns true when the service is running in demo mode.
* Checks NODE_ENV and the DEMO_MODE flag (for docker-compose overrides).
*/
export function isDemoMode(): boolean {
return process.env.NODE_ENV === "demo" || process.env.DEMO_MODE === "true";
}
4 changes: 3 additions & 1 deletion backend/src/config/protocols.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { AppError } from "../shared/errorCodes";

export interface ProtocolConfig {
protocolId: string;
name: string;
Expand Down Expand Up @@ -74,7 +76,7 @@ export function registerProtocol(protocolId: string, config: ProtocolConfig): vo

export function getProtocolConfig(protocolId: string): ProtocolConfig {
const config = PROTOCOL_REGISTRY[protocolId];
if (!config) throw new Error(`Unsupported protocol: ${protocolId}`);
if (!config) throw new AppError("UNSUPPORTED_OPERATION", `Unsupported protocol: ${protocolId}`);
return config;
}

Expand Down
86 changes: 86 additions & 0 deletions backend/src/config/wallet-roles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// ──────────────────────────────────────────────────────────────────
// WALLET SEPARATION MODEL
//
// Documents and enforces the distinct wallet roles in the system.
// Each role has different trust levels and transaction limits.
//
// Architecture:
// ┌──────────────┐ ┌───────────────────┐ ┌──────────────┐
// │ User Wallet │────▶│ PanoramaExecutor │────▶│ UserAdapter │
// │ (signs tx) │ │ (dispatcher) │ │ (per-user) │
// └──────────────┘ └───────────────────┘ └──────────────┘
// │
// ┌────▼────┐
// │ Protocol │
// │ (Aero, │
// │ Benqi) │
// └─────────┘
//
// NON-CUSTODIAL: The backend NEVER holds private keys.
// All transactions are prepared unsigned and signed by the user's wallet.
// ──────────────────────────────────────────────────────────────────

/** Wallet roles in the PanoramaBlock execution system. */
export enum WalletRole {
/** User's own wallet (MetaMask, ThirdWeb in-app). Signs all transactions. */
USER = "USER",

/**
* Per-user BeaconProxy clone created by PanoramaExecutor.
* Holds LP tokens, gauge stakes, and protocol positions on behalf of the user.
* Only the user (via Executor) can interact with their adapter.
*/
USER_ADAPTER = "USER_ADAPTER",

/**
* DCA execution wallet (backend-controlled signer).
* Used ONLY for automated DCA swap execution via DCAVault.
* Has strict per-transaction and per-session limits.
*/
DCA_EXECUTOR = "DCA_EXECUTOR",

/**
* Fee collection treasury (multisig or DAO-controlled).
* Receives protocol fees from adapter operations.
* NOT used in the current execution-layer — reserved for future use.
*/
TREASURY = "TREASURY",
}

// ── Per-Transaction Limits ────────────────────────────────────────
// These apply to the DCA_EXECUTOR role only.
// User-initiated transactions have no backend-enforced limit
// (the user controls their own wallet).

export const EXECUTION_LIMITS = {
/** Max value per single DCA swap execution (in wei). ~0.5 ETH */
MAX_SINGLE_TX_VALUE: BigInt("500000000000000000"),

/** Max cumulative value per DCA session/epoch (in wei). ~5 ETH */
MAX_SESSION_VALUE: BigInt("5000000000000000000"),

/** Max number of DCA executions per session. */
MAX_EXECUTIONS_PER_SESSION: 50,

/** Session duration in milliseconds (1 hour). */
SESSION_DURATION_MS: 60 * 60 * 1000,
} as const;

// ── Audit Log Entry ───────────────────────────────────────────────
// Every DCA execution action should be logged with this shape
// for post-hoc audit trail.

export interface ExecutionAuditEntry {
timestamp: number;
walletRole: WalletRole;
action: string;
protocol: string;
chain: string;
userAddress: string;
adapterAddress?: string;
amountWei: string;
txHash?: string;
orderId?: number;
success: boolean;
error?: string;
}
Loading
Loading