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
31 changes: 22 additions & 9 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm audit --audit-level=high

commitlint:
Expand All @@ -52,7 +53,7 @@ jobs:
cache: npm

- name: Install dependencies
run: npm install
run: npm ci

# Lint every commit in the PR from the merge base to HEAD.
- name: Lint commit messages
Expand Down Expand Up @@ -93,20 +94,14 @@ jobs:
steps:
- uses: actions/checkout@v4

# ── Node / npm cache (#130) ──────────────────────────────────────────
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: npm install
- run: npm run lint
- run: npm run typecheck
- run: npm run build
- run: npm test -- --coverage
- run: npm run test:e2e
node-version: 20
cache: npm

- name: Install dependencies
run: npm install
run: npm ci

# ── Prisma ──────────────────────────────────────────────────────────────
- name: Validate Prisma schema
Expand Down Expand Up @@ -135,3 +130,21 @@ jobs:

- name: E2E tests
run: npm run test:e2e

# ── Docker build verification (#131) ──────────────────────────────────────
# Ensures every push that changes application code doesn't silently break
# the Dockerfile. Image publishing to a registry can be wired up separately
# once registry credentials are in place.
docker:
name: Docker – build verification
runs-on: ubuntu-latest
needs: backend
steps:
- uses: actions/checkout@v4

- name: Build Docker image
uses: docker/build-push-action@v6
with:
context: .
push: false
tags: vortex-backend:ci
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,29 @@ flags to override them.

---

## Supported chains

`SupportedChain` in `src/intents/intents.types.ts` lists seven chains.
The table below clarifies which are **live** (real integration exists today)
versus **planned** (schema/token data in place, on-chain settlement pending).

| Chain | Status | Notes |
|-------|--------|-------|
| **Stellar** | ✅ Live | Soroban RPC reads (`/api/v1/chain/*`), signing service, settlement design in progress |
| Ethereum | 🔲 Planned | Token registry populated; on-chain integration not yet implemented |
| Base | 🔲 Planned | Token registry populated; on-chain integration not yet implemented |
| Polygon | 🔲 Planned | Token registry populated; on-chain integration not yet implemented |
| Arbitrum | 🔲 Planned | Token registry populated; on-chain integration not yet implemented |
| Optimism | 🔲 Planned | Token registry populated; on-chain integration not yet implemented |
| Avalanche | 🔲 Planned | Token registry populated; on-chain integration not yet implemented |

> **Contributor note:** EVM chains are accepted in the intent DTO and stored
> in-memory, but no on-chain settlement or bridging logic is wired up yet.
> See [`docs/architecture/onchain-settlement.md`](./docs/architecture/onchain-settlement.md)
> for the target design.

---

## Roadmap

- [x] **Soroban RPC reads** — health/ledger/network/account lookups via `/api/v1/chain/*`
Expand Down
12 changes: 1 addition & 11 deletions src/intents/dto/create-intent.dto.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,9 @@
import { IsIn, IsInt, IsOptional, IsString, Matches, Max, Min, MinLength } from "class-validator";
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { SupportedChain } from "../intents.types";
import { SUPPORTED_CHAINS, SupportedChain } from "../intents.types";
import { IsValidAddress } from "../../common/validators/is-valid-address.validator";
import { IsValidDeadline } from "../../common/validators/deadline.validator";

const SUPPORTED_CHAINS: SupportedChain[] = [
"stellar",
"ethereum",
"base",
"polygon",
"arbitrum",
"optimism",
"avalanche",
];

export class CreateIntentDto {
@ApiProperty({ description: "Stellar address of the user creating the intent" })
@IsString()
Expand Down
25 changes: 17 additions & 8 deletions src/intents/intents.types.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
export type SupportedChain =
| "stellar"
| "ethereum"
| "base"
| "polygon"
| "arbitrum"
| "optimism"
| "avalanche";
/**
* Single source of truth for every chain the protocol recognises.
* `SupportedChain` is derived from this tuple so all three consumers
* (intents.types.ts, create-intent.dto.ts, tokens.data.ts) stay in sync
* automatically — see issue #128.
*/
export const SUPPORTED_CHAINS = [
"stellar",
"ethereum",
"base",
"polygon",
"arbitrum",
"optimism",
"avalanche",
] as const;

export type SupportedChain = (typeof SUPPORTED_CHAINS)[number];

/**
* A single entry in the append-only audit log for an intent.
Expand Down
42 changes: 42 additions & 0 deletions src/intents/supported-chains-sync.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/**
* Regression guard for issue #128.
*
* Ensures that:
* 1. CreateIntentDto's @IsIn validator uses the same canonical list as
* `SupportedChain` (guaranteed structurally after the refactor — this
* test documents the contract).
* 2. Every EVM chain declared in SUPPORTED_CHAINS has a corresponding entry
* in tokens.data.ts so the token registry never silently lags behind.
*/
import { SUPPORTED_CHAINS } from "./intents.types";
import { SUPPORTED_TOKENS } from "../tokens/tokens.data";

describe("SUPPORTED_CHAINS sync guard (#128)", () => {
it("SUPPORTED_CHAINS contains at least 'stellar' and at least one EVM chain", () => {
expect(SUPPORTED_CHAINS).toContain("stellar");
const evmChains = SUPPORTED_CHAINS.filter((c) => c !== "stellar");
expect(evmChains.length).toBeGreaterThan(0);
});

it("every EVM chain in SUPPORTED_CHAINS has token entries in tokens.data.ts", () => {
const evmChains = SUPPORTED_CHAINS.filter((c) => c !== "stellar");
const missingChains: string[] = [];

for (const chain of evmChains) {
const tokens = SUPPORTED_TOKENS[chain];
if (!tokens || tokens.length === 0) {
missingChains.push(chain);
}
}

expect(missingChains).toEqual([]);
});

it("tokens.data.ts has no extra EVM chains that are not listed in SUPPORTED_CHAINS", () => {
const tokenChains = Object.keys(SUPPORTED_TOKENS);
const unrecognised = tokenChains.filter(
(c) => !(SUPPORTED_CHAINS as readonly string[]).includes(c),
);
expect(unrecognised).toEqual([]);
});
});