Skip to content

Commit ee2985b

Browse files
committed
fix: resolve merge conflicts with base
2 parents 8ac4604 + 66fad4d commit ee2985b

63 files changed

Lines changed: 1532 additions & 534 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/ci.yml‎

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,8 @@ jobs:
7979
- run: pnpm format:check
8080
- run: pnpm lint
8181
- run: pnpm typecheck
82-
# The Vercel serverless functions in api/ live outside the pnpm workspace,
83-
# so turbo doesn't cover them — typecheck them explicitly.
82+
# api/ is in the pnpm workspace, but turbo skips it due to missing package
83+
# scripts (tracked separately) — typecheck them explicitly.
8484
- run: pnpm typecheck:api
8585
- name: Verify apps/api-local build output is flat
8686
run: |
@@ -154,7 +154,7 @@ jobs:
154154
# signing is mocked, see apps/web/e2e/fixtures.ts), so it exercises
155155
# the real API and Soroban simulation paths end to end. Tests use page.route()
156156
# to stub live-network-dependent data where it would otherwise be flaky on
157-
# testnet RPC hiccups (see #535). Blocking.
157+
# testnet RPC hiccups (see #535). Not a required check (#1036).
158158
#
159159
# STELLAR_NETWORK must be set explicitly: mainnet is now the default
160160
# (packages/shared/src/constants.ts, #738) since the product is live
@@ -207,10 +207,7 @@ jobs:
207207
working-directory: packages/contracts
208208
run: >-
209209
cargo llvm-cov
210-
--package meridian-vault
211-
--package meridian-blend-adapter
212-
--package meridian-defindex-adapter
213-
--package adapter-common
210+
--workspace
214211
--summary-only
215212
--fail-under-lines 97.5
216213
- name: Build wasm

‎CONTRIBUTING.md‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ npm install -g pnpm@9
125125
| Variable | Required for | Description |
126126
| ----------------------- | ------------ | ---------------------------------------------------------------------------------------------------------------- |
127127
| `PORT` | API | Port for the Fastify API server. Default `3001`. |
128-
| `ALLOWED_ORIGIN` | API | CORS allowed origin. Default `http://localhost:3000`. |
128+
| `ALLOWED_ORIGIN` | API | CORS allowed origin. Default `https://usemeridian.vercel.app`. |
129129
| `STELLAR_NETWORK` | API, SDK | Target network: `testnet` or `mainnet`. Default `mainnet`; `.env.example` sets `testnet` for local dev. |
130130
| `DEFINDEX_VAULT_ID` | API | DeFindex vault contract address (C-address). Leave empty to disable DeFindex; only Blend positions are returned. |
131131
| `VITE_API_URL` | Web | URL the web app uses to reach the API. Default `http://localhost:3001`. |
@@ -159,14 +159,17 @@ meridian/
159159
apps/
160160
web/ -- Vite + React frontend (Freighter wallet, yield dashboard)
161161
api-local/ -- Fastify API, local dev only (yield aggregation, Soroban tx building)
162-
docs/ -- Documentation site
162+
docs/ -- Documentation site (VitePress)
163163
landing/ -- Landing page
164+
strategies/ -- Strategies simulation and visualization dashboard
164165
api/ -- Vercel serverless functions (api/v1/...), the production API
165166
packages/
166167
api-core/ -- Framework-agnostic route handlers shared by both servers
167168
contracts/ -- Soroban smart contracts (Rust)
169+
sdk/ -- Public TypeScript SDK (@meridian/sdk)
168170
shared/ -- Shared utilities used by both api and web
169171
stellar-sdk-helpers/ -- Typed wrappers around @stellar/stellar-sdk
172+
strategies/ -- Strategy engine and simulation library (@meridian/strategies)
170173
```
171174

172175
Changes to `packages/` affect multiple apps. If you modify a shared package, run the full check suite from the repo root.
@@ -379,7 +382,7 @@ pnpm format # Prettier (auto-fix)
379382
pnpm --filter @meridian/stellar-sdk-helpers test
380383

381384
# Run a single test file
382-
pnpm test -- packages/stellar-sdk-helpers/src/__tests__/tx.test.ts
385+
pnpm --filter @meridian/stellar-sdk-helpers exec vitest run src/tx.test.ts
383386

384387
# Soroban contract checks (only needed when modifying packages/contracts)
385388
cargo fmt --all

‎README.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,13 +54,15 @@ meridian/
5454
│ ├── web/ # Vite + React 19 dashboard (TypeScript, Tailwind, Zustand)
5555
│ ├── api-local/ # Fastify REST API (local dev only): builds Soroban txs, aggregates APY
5656
│ ├── docs/ # Internal architecture and operations docs
57-
│ └── landing/ # Marketing landing page
57+
│ ├── landing/ # Marketing landing page
58+
│ └── strategies/ # Vite + React strategies simulation and visualization dashboard
5859
├── api/ # Vercel serverless functions (api/v1/...) — the production API
5960
├── packages/
6061
│ ├── api-core/ # Framework-agnostic route handlers shared by both servers
6162
│ ├── stellar-sdk-helpers/ # Blend & DeFindex client wrappers
6263
│ ├── sdk/ # @meridian/sdk: public TypeScript SDK (ESM + CJS)
6364
│ ├── shared/ # Zod schemas, constants, pure utils
65+
│ ├── strategies/ # Strategy engine and simulation library for Meridian
6466
│ └── contracts/ # Soroban smart contracts (Rust): vault, blend-adapter, defindex-adapter, adapter-common, musdc-token
6567
└── scripts/ # deploy-testnet.sh / deploy-mainnet.sh (fresh stack), redeploy-blend-adapter.sh (swap adapter on a live vault)
6668
```

‎api/__tests__/handlers.test.ts‎

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,7 @@ vi.mock("@meridian/stellar-sdk-helpers", async (importOriginal) => {
193193

194194
import txHandler from "../v1/tx/[action]";
195195
import vaultsHandler from "../v1/vaults/index";
196+
import vaultDetailHandler from "../v1/vaults/[vaultId]";
196197
import positionsHandler from "../v1/positions/[publicKey]";
197198
import keepersHandler from "../v1/keepers/[action]";
198199
import positionHistoryHandler from "../v1/positions/[publicKey]/history";
@@ -216,6 +217,7 @@ import {
216217
MissingTrustlineError,
217218
getPositionHistory,
218219
runPositionSnapshotKeeper,
220+
fetchAllVaults,
219221
} from "@meridian/stellar-sdk-helpers";
220222

221223
// A 56-char Stellar public key shape (only the length is validated).
@@ -631,6 +633,49 @@ describe("GET /api/v1/vaults", () => {
631633
});
632634
});
633635

636+
describe("GET /api/v1/vaults/:vaultId", () => {
637+
it("returns a vault with the same cache header as the list endpoint", async () => {
638+
vi.mocked(fetchAllVaults).mockResolvedValueOnce([
639+
{
640+
id: "meridian-usdc",
641+
protocol: "meridian",
642+
asset: "USDC",
643+
name: "Meridian",
644+
label: "USDC Vault",
645+
apy: 5,
646+
tvl: 1000,
647+
userBalance: 0,
648+
riskLevel: "safe",
649+
},
650+
]);
651+
const res = makeRes();
652+
await vaultDetailHandler(
653+
fakeReq({ method: "GET", query: { vaultId: "meridian-usdc" } }),
654+
res
655+
);
656+
expect(res.statusCode).toBe(200);
657+
658+
// Both vault reads must share one per-network cache policy — a detail
659+
// response cached while the list is fresh (or vice versa) is the #1051 bug.
660+
const listRes = makeRes();
661+
await vaultsHandler(fakeReq({ method: "GET" }), listRes);
662+
expect(res.headers["Cache-Control"]).toBe(
663+
"public, s-maxage=60, stale-while-revalidate=300"
664+
);
665+
expect(res.headers["Cache-Control"]).toBe(listRes.headers["Cache-Control"]);
666+
});
667+
668+
it("leaves error responses uncached", async () => {
669+
const res = makeRes();
670+
await vaultDetailHandler(
671+
fakeReq({ method: "GET", query: { vaultId: "unknown-vault" } }),
672+
res
673+
);
674+
expect(res.statusCode).toBe(404);
675+
expect(res.headers["Cache-Control"]).toBeUndefined();
676+
});
677+
});
678+
634679
describe("GET /api/v1/positions/:publicKey", () => {
635680
it("rejects a malformed public key with 400", async () => {
636681
const res = makeRes();
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { describe, it, expect, vi, afterEach } from "vitest";
2+
import type { VercelRequest, VercelResponse } from "@vercel/node";
3+
4+
vi.mock("@meridian/stellar-sdk-helpers", async (importOriginal) => {
5+
const actual =
6+
await importOriginal<typeof import("@meridian/stellar-sdk-helpers")>();
7+
return {
8+
...actual,
9+
// The header under test doesn't depend on vault contents, and the real
10+
// fetch would reach Stellar RPC / DeFiLlama from a unit test.
11+
fetchAllVaults: vi.fn(async () => [
12+
{
13+
id: "meridian-usdc",
14+
protocol: "meridian",
15+
asset: "USDC",
16+
name: "Meridian",
17+
label: "USDC Vault",
18+
apy: 5,
19+
tvl: 1000,
20+
userBalance: 0,
21+
riskLevel: "safe",
22+
},
23+
]),
24+
};
25+
});
26+
27+
const fakeReq = (obj: object) =>
28+
({ headers: {}, ...obj }) as unknown as VercelRequest;
29+
30+
interface FakeRes {
31+
statusCode: number;
32+
body: unknown;
33+
headers: Record<string, string>;
34+
status(code: number): FakeRes;
35+
json(payload: unknown): FakeRes;
36+
setHeader(key: string, value: string): void;
37+
}
38+
39+
function makeRes(): FakeRes & VercelResponse {
40+
const r: FakeRes = {
41+
statusCode: 200,
42+
body: undefined,
43+
headers: {},
44+
status(code: number) {
45+
r.statusCode = code;
46+
return r;
47+
},
48+
json(payload: unknown) {
49+
r.body = payload;
50+
return r;
51+
},
52+
setHeader(key: string, value: string) {
53+
r.headers[key] = value;
54+
},
55+
};
56+
return r as unknown as FakeRes & VercelResponse;
57+
}
58+
59+
const MAINNET_CACHE_HEADER = "public, s-maxage=60, stale-while-revalidate=300";
60+
61+
// APP_NETWORK is resolved from STELLAR_NETWORK once, at @meridian/shared
62+
// module load, so each network case needs a fresh module graph rather than a
63+
// mutated constant.
64+
const savedNetwork = process.env.STELLAR_NETWORK;
65+
66+
afterEach(() => {
67+
if (savedNetwork === undefined) delete process.env.STELLAR_NETWORK;
68+
else process.env.STELLAR_NETWORK = savedNetwork;
69+
vi.resetModules();
70+
});
71+
72+
describe("vaultReadCacheControl", () => {
73+
it("caches mainnet vault reads at the CDN", async () => {
74+
delete process.env.STELLAR_NETWORK;
75+
vi.resetModules();
76+
const { vaultReadCacheControl } = await import("../_lib/middleware.js");
77+
expect(vaultReadCacheControl()).toBe(MAINNET_CACHE_HEADER);
78+
});
79+
80+
it("serves testnet vault reads uncached", async () => {
81+
process.env.STELLAR_NETWORK = "testnet";
82+
vi.resetModules();
83+
const { vaultReadCacheControl } = await import("../_lib/middleware.js");
84+
expect(vaultReadCacheControl()).toBe("no-store");
85+
});
86+
});
87+
88+
// #1051: the detail endpoint used to answer with
89+
// `public, s-maxage=60, stale-while-revalidate=300` on every network, so a
90+
// testnet deposit was invisible behind the CDN for up to a minute while the
91+
// list endpoint (already no-store on testnet) showed the new balance.
92+
describe("GET /api/v1/vaults/:vaultId on testnet (#1051)", () => {
93+
it("serves a 200 single-vault read with no-store", async () => {
94+
process.env.STELLAR_NETWORK = "testnet";
95+
vi.resetModules();
96+
const { default: handler } = await import("../v1/vaults/[vaultId]");
97+
const res = makeRes();
98+
await handler(
99+
fakeReq({ method: "GET", query: { vaultId: "meridian-usdc" } }),
100+
res
101+
);
102+
expect(res.statusCode).toBe(200);
103+
expect(res.headers["Cache-Control"]).toBe("no-store");
104+
});
105+
});

‎api/_lib/middleware.ts‎

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { createHash, timingSafeEqual } from "node:crypto";
22
import type { VercelRequest, VercelResponse } from "@vercel/node";
3-
import { DEFAULT_ALLOWED_ORIGIN } from "@meridian/shared";
3+
import { APP_NETWORK, DEFAULT_ALLOWED_ORIGIN } from "@meridian/shared";
44
import { Ratelimit } from "@upstash/ratelimit";
55
import { Redis } from "@upstash/redis";
66

@@ -75,6 +75,32 @@ export function applyCors(req: VercelRequest, res: VercelResponse): boolean {
7575
return false;
7676
}
7777

78+
// Cache policy shared by both vault-read endpoints: the list in
79+
// v1/vaults/index.ts and the single-vault read in v1/vaults/[vaultId].ts.
80+
// Both must use the same policy — #1051 happened because the detail endpoint
81+
// cached unconditionally while the list did not, so on testnet the detail
82+
// panel served pre-deposit balances and APY for up to a minute while the list
83+
// beside it was already current. Mainnet APY/TVL move slowly, so a short
84+
// fresh window keeps DeFiLlama call volume low, and stale-while-revalidate
85+
// lets the edge keep serving the last good payload through a transient
86+
// DeFiLlama outage instead of failing the whole dashboard. Testnet reads hit
87+
// the chain on every call (fetchAllVaults bypasses its cache there) so
88+
// deposits/withdrawals show up immediately during testing; an HTTP cache
89+
// layered on top would defeat that, so testnet gets no-store.
90+
const MAINNET_VAULT_CACHE_CONTROL =
91+
"public, s-maxage=60, stale-while-revalidate=300";
92+
const TESTNET_VAULT_CACHE_CONTROL = "no-store";
93+
94+
/**
95+
* Cache-Control for successful (200) vault-read responses: a short CDN cache
96+
* on mainnet, nothing at all on testnet so post-deposit reads stay fresh.
97+
*/
98+
export function vaultReadCacheControl(): string {
99+
return APP_NETWORK.network === "testnet"
100+
? TESTNET_VAULT_CACHE_CONTROL
101+
: MAINNET_VAULT_CACHE_CONTROL;
102+
}
103+
78104
function clientIp(req: VercelRequest): string {
79105
const vercelIp = req.headers["x-vercel-forwarded-for"];
80106
if (typeof vercelIp === "string" && vercelIp)

‎api/v1/vaults/[vaultId].ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ import {
55
applyCors,
66
checkRateLimit,
77
requireGetMethod,
8+
vaultReadCacheControl,
89
} from "../../_lib/middleware.js";
910

10-
const CACHE_CONTROL = "public, s-maxage=60, stale-while-revalidate=300";
1111
const KNOWN_VAULT_IDS = new Set(
1212
[
1313
...Object.values(KNOWN_POOLS.mainnet),
@@ -35,7 +35,7 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
3535
console.error("[vaults] fetch error:", result.error);
3636
}
3737
if (result.status === 200) {
38-
res.setHeader("Cache-Control", CACHE_CONTROL);
38+
res.setHeader("Cache-Control", vaultReadCacheControl());
3939
}
4040
res.status(result.status).json(result.body);
4141
}

‎api/v1/vaults/index.ts‎

Lines changed: 6 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,10 @@
11
import type { VercelRequest, VercelResponse } from "@vercel/node";
22
import { handleGetVaults } from "@meridian/api-core";
3-
import { APP_NETWORK } from "@meridian/shared";
4-
import { applyCors, checkRateLimit } from "../../_lib/middleware.js";
5-
6-
// Cache the aggregated vault list at the Vercel CDN. APY/TVL move slowly on
7-
// mainnet, so a short fresh window keeps DeFiLlama call volume low, and the
8-
// stale-while-revalidate window lets the edge keep serving the last good
9-
// payload through a transient DeFiLlama outage instead of failing the whole
10-
// dashboard. Testnet reads go straight to the chain on every call (see
11-
// fetchAllVaults) specifically so deposits/withdrawals show up immediately
12-
// during testing; caching the HTTP response on top of that would defeat the
13-
// purpose, so testnet gets no-store instead.
14-
const MAINNET_CACHE_CONTROL = "public, s-maxage=60, stale-while-revalidate=300";
15-
const TESTNET_CACHE_CONTROL = "no-store";
3+
import {
4+
applyCors,
5+
checkRateLimit,
6+
vaultReadCacheControl,
7+
} from "../../_lib/middleware.js";
168

179
export default async function handler(req: VercelRequest, res: VercelResponse) {
1810
if (applyCors(req, res)) return;
@@ -30,12 +22,7 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
3022
console.error("[vaults] fetch error:", result.error);
3123
}
3224
if (result.status === 200) {
33-
res.setHeader(
34-
"Cache-Control",
35-
APP_NETWORK.network === "testnet"
36-
? TESTNET_CACHE_CONTROL
37-
: MAINNET_CACHE_CONTROL
38-
);
25+
res.setHeader("Cache-Control", vaultReadCacheControl());
3926
}
4027
res.status(result.status).json(result.body);
4128
}

‎apps/api-local/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"lint": "eslint src",
1111
"test": "vitest run --passWithNoTests",
1212
"coverage": "vitest run --coverage --passWithNoTests",
13-
"typecheck": "tsc --noEmit"
13+
"typecheck": "tsc --noEmit -p tsconfig.typecheck.json"
1414
},
1515
"dependencies": {
1616
"@fastify/cors": "^11.3.0",
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"extends": "./tsconfig.json",
3+
"include": ["src"],
4+
"exclude": []
5+
}

0 commit comments

Comments
 (0)