Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
d0eb918
feat: auth token
Dodecahedr0x Mar 30, 2026
b969bc2
feat: auth token in header
Dodecahedr0x Mar 30, 2026
dc6fbb1
feat: validation responses
Dodecahedr0x Mar 30, 2026
873f074
feat: do not cache connection with tokens
Dodecahedr0x Mar 30, 2026
f6b0789
feat: more robust url building
Dodecahedr0x Mar 30, 2026
a3e1363
feat: validate token prefix
Dodecahedr0x Mar 30, 2026
afba3fa
fix: unused import
Dodecahedr0x Mar 30, 2026
ae634d4
fix: update example value
Dodecahedr0x Mar 30, 2026
443c587
feat: remove unused param
Dodecahedr0x Mar 30, 2026
f1b3e6f
fix: prefix
Dodecahedr0x Mar 30, 2026
a277758
chore: docs (#43)
GabrielePicco Mar 30, 2026
085edcd
feat: source program verification (#44)
taco-paco Mar 30, 2026
afbdd40
feat: add simpler transfer from base to delegated account (#38)
GabrielePicco Mar 30, 2026
7954014
chore: fix compilation (#46)
GabrielePicco Mar 30, 2026
17b1a46
chore: charge fee to prevent ata closing and draining of sponsor PDA …
GabrielePicco Mar 30, 2026
17f95b3
chore: refactor (#48)
GabrielePicco Mar 31, 2026
fe8c456
feat: pass user specified encrypted client-ref-id (#50)
GabrielePicco Apr 2, 2026
75fc807
feat: add auto queue topup (#52)
GabrielePicco Apr 6, 2026
ffa5dcb
feat: improve api (#53)
GabrielePicco Apr 7, 2026
9f0ee61
chore: refactor (#54)
GabrielePicco Apr 7, 2026
83178dc
feat: add fees (#55)
GabrielePicco Apr 7, 2026
2264fee
chore: api refactor (#56)
GabrielePicco Apr 7, 2026
d8c2689
chore: api refactor (#57)
GabrielePicco Apr 8, 2026
c5a0b13
feat: mock per
Dodecahedr0x Apr 9, 2026
d8e61b7
Merge branch 'main' into dode/private-balance
Dodecahedr0x Apr 14, 2026
32e55aa
fix: remove unused fn
Dodecahedr0x Apr 14, 2026
398d23d
fix: mock per parsing
Dodecahedr0x Apr 14, 2026
eec1279
feat: mock as parameter
Dodecahedr0x Apr 14, 2026
fecaac7
feat: remove debug log
Dodecahedr0x Apr 14, 2026
ad086a0
feat:
Dodecahedr0x Apr 14, 2026
bc39c47
feat: timeout protection
Dodecahedr0x Apr 14, 2026
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
187 changes: 162 additions & 25 deletions api/src/app.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {

import app from "./app";
import { TOKEN_PROGRAM_ID } from "./lib/solana";
import { MOCK_AUTH_TOKEN, MOCK_CHALLENGE } from "./lib/auth";

const env = {
BASE_RPC_URL: "https://base.rpc.test",
Expand Down Expand Up @@ -87,7 +88,7 @@ function createExecutionContext(): TestExecutionContext {
waitUntil(promise: Promise<unknown>) {
tasks.push(promise);
},
passThroughOnException() {},
passThroughOnException() { },
async drain() {
await Promise.all(tasks);
},
Expand Down Expand Up @@ -137,6 +138,8 @@ describe("app", () => {
expect(json.paths["/.well-known/mcp.json"]).toBeUndefined();
expect(json.paths["/mcp"]?.post?.requestBody?.content?.["application/json"]?.schema).toBeDefined();
expect(json.paths["/v1/spl/private-balance"]).toBeDefined();
expect(json.paths["/v1/spl/challenge"]).toBeDefined();
expect(json.paths["/v1/spl/login"]).toBeDefined();
expect(json.paths["/v1/spl/is-mint-initialized"]).toBeDefined();
expect(json.paths["/v1/spl/initialize-mint"]).toBeDefined();
expect(json.paths["/v1/spl/deposit"]?.post?.responses?.["200"]?.content?.["application/json"]?.example).toMatchObject({
Expand Down Expand Up @@ -325,13 +328,13 @@ describe("app", () => {
const endpoint = (this as Connection & { _rpcEndpoint: string })._rpcEndpoint;
return endpoint.includes("base.devnet.rpc.test")
? {
blockhash: "So11111111111111111111111111111111111111112",
lastValidBlockHeight: 321,
}
blockhash: "So11111111111111111111111111111111111111112",
lastValidBlockHeight: 321,
}
: {
blockhash: "11111111111111111111111111111111",
lastValidBlockHeight: 123,
};
blockhash: "11111111111111111111111111111111",
lastValidBlockHeight: 123,
};
});
vi.spyOn(globalThis, "fetch").mockImplementation(async (input) => {
expect(String(input)).toBe(devnetEnv.EPHEMERAL_DEVNET_RPC_URL);
Expand Down Expand Up @@ -426,13 +429,13 @@ describe("app", () => {
const endpoint = (this as Connection & { _rpcEndpoint: string })._rpcEndpoint;
return endpoint.includes("base")
? {
blockhash: "So11111111111111111111111111111111111111112",
lastValidBlockHeight: 321,
}
blockhash: "So11111111111111111111111111111111111111112",
lastValidBlockHeight: 321,
}
: {
blockhash: "11111111111111111111111111111111",
lastValidBlockHeight: 123,
};
blockhash: "11111111111111111111111111111111",
lastValidBlockHeight: 123,
};
});

const response = await app.request("/v1/spl/initialize-mint", {
Expand Down Expand Up @@ -633,13 +636,13 @@ describe("app", () => {
const endpoint = (this as Connection & { _rpcEndpoint: string })._rpcEndpoint;
return endpoint.includes("ephemeral")
? {
blockhash: "11111111111111111111111111111111",
lastValidBlockHeight: 456,
}
blockhash: "11111111111111111111111111111111",
lastValidBlockHeight: 456,
}
: {
blockhash: "So11111111111111111111111111111111111111112",
lastValidBlockHeight: 123,
};
blockhash: "So11111111111111111111111111111111111111112",
lastValidBlockHeight: 123,
};
});

const response = await app.request("/v1/spl/transfer", {
Expand Down Expand Up @@ -985,7 +988,7 @@ describe("app", () => {
);
const privateResponse = await app.request(
`/v1/spl/private-balance?address=${owner}&mint=So11111111111111111111111111111111111111112`,
{},
{ headers: { authorization: "Bearer 1234567890" } },
env,
);

Expand All @@ -1001,6 +1004,140 @@ describe("app", () => {
expect(privateJson.balance).toBe("9");
});

it("returns base and mock private balances from different RPCs", async () => {
vi.spyOn(Connection.prototype, "getAccountInfo").mockImplementation(async function getAccountInfo(this: Connection & { _rpcEndpoint: string }) {
const endpoint = (this as Connection & { _rpcEndpoint: string })._rpcEndpoint;
return endpoint.includes("ephemeral")
? createAccountInfo(9n)
: createAccountInfo(3n);
});

const baseResponse = await app.request(
`/v1/spl/balance?address=${owner}&mint=So11111111111111111111111111111111111111112`,
{},
env,
);
const privateResponse = await app.request(
`/v1/spl/private-balance?address=${owner}&mint=So11111111111111111111111111111111111111112`,
{ headers: { authorization: "Bearer mock-auth-token" } },
env,
);

expect(baseResponse.status).toBe(200);
expect(privateResponse.status).toBe(200);

const baseJson = await baseResponse.json() as { location: string; balance: string };
const privateJson = await privateResponse.json() as { location: string; balance: string };

expect(baseJson.location).toBe("base");
expect(baseJson.balance).toBe("3");
expect(privateJson.location).toBe("base");
expect(privateJson.balance).toBe("3");
});

it("returns 422 when private balance is requested without authToken", async () => {
const response = await app.request(
`/v1/spl/private-balance?address=${owner}&mint=So11111111111111111111111111111111111111112`,
{},
env,
);
expect(response.status).toBe(422);
});

it("returns a challenge from the ephemeral rollup auth endpoint", async () => {
vi.spyOn(globalThis, "fetch").mockImplementation(async (input) => {
expect(String(input)).toBe(`${env.EPHEMERAL_RPC_URL}/auth/challenge?pubkey=${owner}`);
return new Response(JSON.stringify({ challenge: "challenge-token-abc" }), {
status: 200,
headers: {
"content-type": "application/json",
},
});
});

const response = await app.request(
`/v1/spl/challenge?pubkey=${owner}`,
{},
env,
);

expect(response.status).toBe(200);

const json = await response.json() as { challenge: string };
expect(json.challenge).toBe("challenge-token-abc");
});

it("returns the mock challenge", async () => {
const response = await app.request(
`/v1/spl/challenge?pubkey=${owner}&mock=true`,
{},
env,
);

expect(response.status).toBe(200);

const json = await response.json() as { challenge: string };
expect(json.challenge).toBe(MOCK_CHALLENGE);
});

it("returns a token from the ephemeral rollup login endpoint", async () => {
vi.spyOn(globalThis, "fetch").mockImplementation(async (input, init) => {
expect(String(input)).toBe(`${env.EPHEMERAL_RPC_URL}/auth/login`);
expect(init?.method).toBe("POST");
const body = JSON.parse(init?.body as string) as {
pubkey: string;
challenge: string;
signature: string;
};
expect(body.pubkey).toBe(owner);
expect(body.challenge).toBe("c1");
expect(body.signature).toBe("s1");
return new Response(JSON.stringify({ token: "token-xyz" }), {
status: 200,
headers: {
"content-type": "application/json",
},
});
});

const response = await app.request("/v1/spl/login", {
method: "POST",
headers: {
"content-type": "application/json",
},
body: JSON.stringify({
pubkey: owner,
challenge: "c1",
signature: "s1",
}),
}, env);

expect(response.status).toBe(200);

const json = await response.json() as { token: string };
expect(json.token).toBe("token-xyz");
});

it("returns the mock token", async () => {
const response = await app.request("/v1/spl/login", {
method: "POST",
headers: {
"content-type": "application/json",
},
body: JSON.stringify({
pubkey: owner,
challenge: "c1",
signature: "s1",
mock: true,
}),
}, env);

expect(response.status).toBe(200);

const json = await response.json() as { token: string };
expect(json.token).toBe(MOCK_AUTH_TOKEN);
});

it("redacts RPC URLs from balance error details", async () => {
vi.spyOn(Connection.prototype, "getAccountInfo").mockRejectedValue(
new Error("HTTP status server error (503 Service Unavailable) for url (https://devnet.helius-rpc.com/?api-key=secret-value)"),
Expand Down Expand Up @@ -1065,7 +1202,7 @@ describe("app", () => {

vi.spyOn(Connection.prototype, "getAccountInfo").mockImplementation(async function getAccountInfo(this: Connection & { _rpcEndpoint: string }, address) {
const endpoint = (this as Connection & { _rpcEndpoint: string })._rpcEndpoint;
expect(endpoint).toBe(env.EPHEMERAL_RPC_URL);
expect(endpoint).toBe(env.BASE_RPC_URL);
expect(address.toBase58()).toBe(transferQueue.toBase58());
return createAccountInfo(0n);
});
Expand Down Expand Up @@ -1101,7 +1238,7 @@ describe("app", () => {

vi.spyOn(Connection.prototype, "getAccountInfo").mockImplementation(async function getAccountInfo(this: Connection & { _rpcEndpoint: string }, address) {
const endpoint = (this as Connection & { _rpcEndpoint: string })._rpcEndpoint;
expect(endpoint).toBe(env.EPHEMERAL_RPC_URL);
expect(endpoint).toBe(env.BASE_RPC_URL);
expect(address.toBase58()).toBe(transferQueue.toBase58());
return null;
});
Expand All @@ -1124,7 +1261,7 @@ describe("app", () => {
it("defaults the validator when checking mint initialization", async () => {
const mintInitializationEnv = {
...env,
EPHEMERAL_RPC_URL: "https://ephemeral.mint-init.rpc.test",
BASE_RPC_URL: "https://base.mint-init.rpc.test",
};
const mint = "So11111111111111111111111111111111111111112";
const [transferQueue] = deriveTransferQueue(new PublicKey(mint), new PublicKey(resolvedValidator));
Expand All @@ -1135,7 +1272,7 @@ describe("app", () => {
});
vi.spyOn(Connection.prototype, "getAccountInfo").mockImplementation(async function getAccountInfo(this: Connection & { _rpcEndpoint: string }, address) {
const endpoint = (this as Connection & { _rpcEndpoint: string })._rpcEndpoint;
expect(endpoint).toBe(mintInitializationEnv.EPHEMERAL_RPC_URL);
expect(endpoint).toBe(mintInitializationEnv.BASE_RPC_URL);
expect(address.toBase58()).toBe(transferQueue.toBase58());
return createAccountInfo(0n);
});
Expand Down Expand Up @@ -1304,7 +1441,7 @@ describe("app", () => {
);
const privateResponse = await app.request(
`/v1/spl/private-balance?address=${owner}&mint=So11111111111111111111111111111111111111112&cluster=${encodeURIComponent("https://custom.rpc.test")}`,
{},
{ headers: { authorization: "Bearer 1234567890" } },
env,
);

Expand Down
7 changes: 4 additions & 3 deletions api/src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,17 @@ export type AppEnv = z.infer<typeof envSchema>;
export function getEnv(bindings: AppBindings): AppEnv {
try {
return envSchema.parse(bindings);
}
catch (error) {
} catch (error) {
if (error instanceof z.ZodError) {
throw new ApiError(
500,
"CONFIG_ERROR",
"Missing or invalid worker environment variables",
{
issues: error.issues.map((issue) => ({
path: issue.path.map((segment) => typeof segment === "number" ? segment : String(segment)),
path: issue.path.map((segment) =>
typeof segment === "number" ? segment : String(segment),
),
message: issue.message,
})),
hint: "Create .dev.vars from .dev.vars.example and set BASE_RPC_URL and EPHEMERAL_RPC_URL before running wrangler dev. If you want cluster=devnet, also set BASE_DEVNET_RPC_URL and EPHEMERAL_DEVNET_RPC_URL.",
Expand Down
Loading
Loading