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
12 changes: 12 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
root = true

[*]
indent_style = space
indent_size = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[*.{json,yml,yaml}]
indent_size = 2
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ jobs:
- name: Install dependencies
run: npm install pnpm@latest -g && pnpm install

- name: Lint
run: pnpm lint

- name: Build
run: pnpm run build

Expand Down
15 changes: 15 additions & 0 deletions .oxfmtrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"$schema": "./node_modules/oxfmt/configuration_schema.json",
"printWidth": 120,
"tabWidth": 4,
"semi": true,
"singleQuote": false,
"trailingComma": "all",
"experimentalSortImports": {},
"ignorePatterns": [
"dist",
"coverage",
"*.d.ts",
"packages/types/src/primitives"
]
}
7 changes: 5 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@

## Coding Style & Naming Conventions
- TypeScript strict mode; ES2020+/ESNext modules.
- 2‑space indent, double quotes, named exports when practical.
- 4‑space indent, double quotes, named exports when practical.
- Format touched files before committing: `pnpm oxfmt --write <file>...`.
- Files: kebab‑case; tests end with `.test.ts`; entry files `index.ts`.
- Group imports; use explicit types for public APIs.
- Use `node:crypto` (`randomInt`) instead of `Math.random()` when selecting from security‑relevant sets (e.g. tip accounts, signers).
- Type all external API responses with explicit interfaces; never leave `fetch` JSON as `any`.
- No JSDoc, no code comments; the code should be self-explanatory.
- Pre‑compute expensive objects (e.g. `PublicKey`) at module level when the inputs are static constants.

## Testing Guidelines
Expand All @@ -40,7 +42,7 @@
## Commit & Pull Request Guidelines
- Prefer conventional style where helpful: `feat|fix|chore(scope): message`; keep messages imperative.
- PRs: small, focused; include description, linked issues/PRs, and test notes or screenshots for API responses.
- Must pass `pnpm build` and `pnpm test`; do not commit `dist/`.
- Must pass `pnpm build`, `pnpm test`, and `pnpm lint`; do not commit `dist/`.

## Security & Configuration Tips
- Configure via env vars used by API/providers: `PORT`, `SOLANA_URL`, `SUI_URL`, `TON_URL`.
Expand All @@ -49,5 +51,6 @@
## Agent‑Specific Instructions (all code agents)
- Use `pnpm` workspace filters (`--filter`) and Justfile tasks; avoid changing file layout.
- Keep edits minimal and focused; update adjacent docs/tests when touching APIs or providers.
- Fix any lint issues in files you touch: `pnpm oxlint <file>...`.
- Prefer mocks for external calls; do not add unvetted network dependencies.
- Reflect provider additions/removals in `apps/api/src/index.ts` and docs; exclude unimplemented protocols.
32 changes: 16 additions & 16 deletions apps/api/jest.config.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
transform: {
'^.+\\.tsx?$': [
'ts-jest',
{
useESM: true,
tsconfig: 'tsconfig.json'
},
],
},
extensionsToTreatAsEsm: ['.ts'],
moduleNameMapper: {
'^(\\.{1,2}/.*)\\.js$': '$1',
},
testPathIgnorePatterns: ['/node_modules/', '/dist/'],
preset: "ts-jest",
testEnvironment: "node",
transform: {
"^.+\\.tsx?$": [
"ts-jest",
{
useESM: true,
tsconfig: "tsconfig.json",
},
],
},
extensionsToTreatAsEsm: [".ts"],
moduleNameMapper: {
"^(\\.{1,2}/.*)\\.js$": "$1",
},
testPathIgnorePatterns: ["/node_modules/", "/dist/"],
};
58 changes: 29 additions & 29 deletions apps/api/package.json
Original file line number Diff line number Diff line change
@@ -1,30 +1,30 @@
{
"name": "@gemwallet/api",
"version": "1.0.0",
"private": true,
"description": "Express API application for the monorepo",
"main": "dist/index.js",
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "ts-node-dev --respawn --transpile-only --clear src/index.ts",
"test": "jest --passWithNoTests"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@gemwallet/swapper": "workspace:*",
"@gemwallet/types": "workspace:*",
"dotenv": "17.3.1",
"express": "5.2.1"
},
"devDependencies": {
"@types/express": "5.0.6",
"@types/node": "25.2.3",
"ts-node-dev": "2.0.0"
},
"files": [
"dist"
]
}
"name": "@gemwallet/api",
"version": "1.0.0",
"private": true,
"description": "Express API application for the monorepo",
"keywords": [],
"license": "ISC",
"author": "",
"files": [
"dist"
],
"main": "dist/index.js",
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "ts-node-dev --respawn --transpile-only --clear src/index.ts",
"test": "jest --passWithNoTests"
},
"dependencies": {
"@gemwallet/swapper": "workspace:*",
"@gemwallet/types": "workspace:*",
"dotenv": "17.3.1",
"express": "5.2.1"
},
"devDependencies": {
"@types/express": "5.0.6",
"@types/node": "25.2.3",
"ts-node-dev": "2.0.0"
}
}
64 changes: 32 additions & 32 deletions apps/api/src/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,49 +5,49 @@ type ErrorResponse = { type: string; message: string | object };
export type ProxyErrorResponse = { err: ErrorResponse } | { error: string };

export function errorResponse(err: SwapperError, rawError: unknown, structured: boolean): ProxyErrorResponse {
const rawMessage = extractMessage(rawError);
if (!structured) {
return { error: rawMessage ?? ("message" in err ? err.message : undefined) ?? "Unknown error occurred" };
}
if (hasStringMessage(err)) {
return { err: { type: err.type, message: rawMessage ?? err.message ?? "" } };
}
const { type, ...rest } = err;
return { err: { type, message: rest } };
const rawMessage = extractMessage(rawError);
if (!structured) {
return { error: rawMessage ?? ("message" in err ? err.message : undefined) ?? "Unknown error occurred" };
}
if (hasStringMessage(err)) {
return { err: { type: err.type, message: rawMessage ?? err.message ?? "" } };
}
const { type, ...rest } = err;
return { err: { type, message: rest } };
}

export function httpStatus(err: SwapperError): number {
switch (err.type) {
case "input_amount_error":
case "not_supported_chain":
case "not_supported_asset":
case "invalid_route":
return 400;
case "no_available_provider":
case "no_quote_available":
return 404;
case "compute_quote_error":
case "transaction_error":
default:
return 500;
}
switch (err.type) {
case "input_amount_error":
case "not_supported_chain":
case "not_supported_asset":
case "invalid_route":
return 400;
case "no_available_provider":
case "no_quote_available":
return 404;
case "compute_quote_error":
case "transaction_error":
default:
return 500;
}
}

function extractMessage(error: unknown): string | undefined {
if (error instanceof Error) return error.message;
if (typeof error === "string") return error;
return undefined;
if (error instanceof Error) return error.message;
if (typeof error === "string") return error;
return undefined;
}

function hasStringMessage(err: SwapperError): err is Extract<SwapperError, { message: string }> {
return err.type === "compute_quote_error" || err.type === "transaction_error";
return err.type === "compute_quote_error" || err.type === "transaction_error";
}

export function sendErrorResponse(
res: Response,
swapperError: SwapperError,
rawError: unknown,
objectResponse: boolean
res: Response,
swapperError: SwapperError,
rawError: unknown,
objectResponse: boolean,
) {
res.status(httpStatus(swapperError)).json(errorResponse(swapperError, rawError, objectResponse));
res.status(httpStatus(swapperError)).json(errorResponse(swapperError, rawError, objectResponse));
}
41 changes: 21 additions & 20 deletions apps/api/src/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,30 @@
import { errorResponse, httpStatus } from "./error";
import { SwapperError } from "@gemwallet/types";

import { errorResponse, httpStatus } from "./error";

describe("httpStatus", () => {
it.each([
[400, { type: "input_amount_error", min_amount: "100" }],
[404, { type: "no_quote_available" }],
[500, { type: "compute_quote_error", message: "error" }],
] as const)("returns %i for %s", (expected, err) => {
expect(httpStatus(err as SwapperError)).toBe(expected);
});
it.each([
[400, { type: "input_amount_error", min_amount: "100" }],
[404, { type: "no_quote_available" }],
[500, { type: "compute_quote_error", message: "error" }],
] as const)("returns %i for %s", (expected, err) => {
expect(httpStatus(err as SwapperError)).toBe(expected);
});
});

describe("errorResponse", () => {
it("wraps input_amount_error fields in message object", () => {
const result = errorResponse({ type: "input_amount_error", min_amount: "19620000" }, null, true);
expect(result).toEqual({ err: { type: "input_amount_error", message: { min_amount: "19620000" } } });
});
it("wraps input_amount_error fields in message object", () => {
const result = errorResponse({ type: "input_amount_error", min_amount: "19620000" }, null, true);
expect(result).toEqual({ err: { type: "input_amount_error", message: { min_amount: "19620000" } } });
});

it("uses raw error message for compute_quote_error", () => {
const result = errorResponse({ type: "compute_quote_error", message: "" }, new Error("fail"), true);
expect(result).toEqual({ err: { type: "compute_quote_error", message: "fail" } });
});
it("uses raw error message for compute_quote_error", () => {
const result = errorResponse({ type: "compute_quote_error", message: "" }, new Error("fail"), true);
expect(result).toEqual({ err: { type: "compute_quote_error", message: "fail" } });
});

it("returns plain error when not structured", () => {
const result = errorResponse({ type: "compute_quote_error", message: "" }, new Error("fail"), false);
expect(result).toEqual({ error: "fail" });
});
it("returns plain error when not structured", () => {
const result = errorResponse({ type: "compute_quote_error", message: "" }, new Error("fail"), false);
expect(result).toEqual({ error: "fail" });
});
});
Loading
Loading