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
6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ AUDIT_REGISTRY_CONTRACT_ID=CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
USAGE_METER_CONTRACT_ID=CBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
PAYMENT_ROUTER_CONTRACT_ID=CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

# Reported by GET /meta so a deployed instance can be identified. Injected by
# CI at build time (e.g. BUILD_SHA=${{ github.sha }}) — the running
# container has no .git directory, so these are never derived at runtime.
# BUILD_SHA=
# BUILD_TIME=

# --- Signing key custody (docs/adr/0001-soroban-signing-key-custody.md) ---
# This service NEVER signs value movement. Settlement is returned to the caller
# as an unsigned envelope. The only key that may exist here is a low-privilege
Expand Down
48 changes: 48 additions & 0 deletions src/meta/version.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import { readFileSync } from "node:fs";
import { getMetaInfo, type MetaConfig } from "./version.js";

const config: MetaConfig = {
stellar: {
network: "testnet",
sorobanRpcUrl: "https://soroban-testnet.stellar.org",
contracts: {
auditRegistry: "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
usageMeter: "CBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB",
paymentRouter: "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC",
},
},
};

test("version is sourced from package.json, not a literal", () => {
const pkg = JSON.parse(readFileSync("package.json", "utf-8")) as { version: string };
const info = getMetaInfo(config, {});
assert.equal(info.version, pkg.version);
});

test("build commit and time default to 'unknown' when not injected", () => {
const info = getMetaInfo(config, {});
assert.equal(info.build.commit, "unknown");
assert.equal(info.build.builtAt, "unknown");
});

test("build commit and time are read from the environment when present", () => {
const info = getMetaInfo(config, {
BUILD_SHA: "abc123",
BUILD_TIME: "2026-08-25T00:00:00.000Z",
});
assert.equal(info.build.commit, "abc123");
assert.equal(info.build.builtAt, "2026-08-25T00:00:00.000Z");
});

test("reports the configured network and contract addresses", () => {
const info = getMetaInfo(config, {});
assert.equal(info.network.stellarNetwork, "testnet");
assert.equal(info.network.contracts.auditRegistry, config.stellar.contracts.auditRegistry);
});

test("description no longer reads as a scaffold", () => {
const info = getMetaInfo(config, {});
assert.ok(!/scaffold/i.test(info.description));
});
85 changes: 85 additions & 0 deletions src/meta/version.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";

const here = dirname(fileURLToPath(import.meta.url));
// src/meta/version.ts compiles to dist/meta/version.js, two levels below
// the repo root where package.json lives.
const packageJsonPath = join(here, "..", "..", "package.json");

interface PackageJson {
name: string;
version: string;
}

let cachedPackageJson: PackageJson | undefined;

function readPackageJson(): PackageJson {
if (!cachedPackageJson) {
cachedPackageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8")) as PackageJson;
}
return cachedPackageJson;
}

export interface MetaInfo {
name: string;
version: string;
description: string;
build: {
/** Commit SHA the running instance was built from. "unknown" outside CI. */
commit: string;
/** ISO timestamp of the build. "unknown" outside CI. */
builtAt: string;
};
network: {
stellarNetwork: string;
sorobanRpcUrl: string;
contracts: {
auditRegistry: string;
usageMeter: string;
paymentRouter: string;
};
};
}

export interface MetaConfig {
stellar: {
network: string;
sorobanRpcUrl: string;
contracts: {
auditRegistry: string;
usageMeter: string;
paymentRouter: string;
};
};
}

/**
* Builds the /meta response. Version comes from package.json rather than a
* literal so it can't drift. Commit SHA and build time come from
* environment variables — the container this runs in has no .git
* directory, so they must be injected at build/deploy time (e.g. a CI step
* exporting BUILD_SHA=${{ github.sha }}), not read via `git rev-parse` at
* runtime.
*/
export function getMetaInfo(config: MetaConfig, env: NodeJS.ProcessEnv = process.env): MetaInfo {
const pkg = readPackageJson();
return {
name: pkg.name,
version: pkg.version,
description: "REST facade for Soroban contracts and indexers.",
build: {
commit: env.BUILD_SHA ?? "unknown",
builtAt: env.BUILD_TIME ?? "unknown",
},
network: {
stellarNetwork: config.stellar.network,
sorobanRpcUrl: config.stellar.sorobanRpcUrl,
contracts: {
auditRegistry: config.stellar.contracts.auditRegistry,
usageMeter: config.stellar.contracts.usageMeter,
paymentRouter: config.stellar.contracts.paymentRouter,
},
},
};
}
8 changes: 3 additions & 5 deletions src/routes/v1/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import type { FastifyPluginAsync } from "fastify";
import { config } from "../../config/env.js";
import { getMetaInfo } from "../../meta/version.js";
import { disputeRoutes } from "../disputes/index.js";

export const v1Routes: FastifyPluginAsync = async (app) => {
app.get("/meta", async () => ({
name: "modeltrace-api",
version: "0.1.0",
description: "REST facade for Soroban contracts and indexers (scaffold).",
}));
app.get("/meta", async () => getMetaInfo(config));

await app.register(disputeRoutes);

Expand Down
Loading