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
79 changes: 79 additions & 0 deletions docs/CONTRACT_VALIDATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# Contract Documentation Validation

This documentation includes a contract specification and validation system to ensure that documented contract methods stay in sync with the actual deployed contracts.

## How it works

1. **Contract specifications** are defined in `lib/contracts.ts` with all method signatures, parameters, auth requirements, and error codes.

2. **Tests** in `__tests__/contracts.test.ts` validate that:
- All documented methods exist in the spec
- All spec methods are documented
- Method signatures (parameter counts) match
- Contract addresses are correct

3. **CI integration**: Run `npm test` to validate all contract documentation before deployment.

## Adding or updating contract methods

When a contract is updated:

1. Update the method signature in `lib/contracts.ts` (add/modify/remove methods)
2. Update the corresponding `.mdx` file with the new documentation
3. Run `npm test` to validate
4. Commit both files together

## Example: Adding a new method

In `lib/contracts.ts`, add to `ASSET_TOKEN_CONTRACT.methods`:

```typescript
{
name: "new_method",
returns: "i128",
params: [
{ name: "admin", type: "Address" },
{ name: "value", type: "i128" },
],
auth: "admin",
errors: ["Unauthorized (3)"],
}
```

Then add a section in `docs/app/docs/contracts/asset-token/page.mdx`:

```mdx
### `new_method`

\`\`\`rust
fn new_method(env: Env, admin: Address, value: i128)
\`\`\`

Description of what the method does...

- **Auth:** `admin`
- **Errors:** `Unauthorized (3)`
```

Run `npm test` to verify they match.

## Contracts

- **Asset Token**: `CBMCWLSQSWUTLUJFCNBHNBSXMUM3XU7NAQ5TSNERW4HA4ZZBYHLG4ECZ`
- **Compliance**: `CBUERYDM7DXTZLLKDBRJKUBPFJ7M4OSUN4T7XKUARU345RLXNAIQD2IU`
- **Registry**: `CBX5SMLTXX6JP4HA5GQIO2V6QM7WCUGL2GZ6D4U773HMRI6RXISKPUR3`
- **Dividend**: `CAR4XY3CEBQWFOL27JEWFW34KXSIZA7RFKDQMEIV7ZU723RWY37I2SYX`

All contracts are deployed on **Testnet** with network passphrase `Test SDF Network ; September 2015`.

## Validation checks

The test suite verifies:

- ✅ Contract addresses match deployed contracts
- ✅ All documented methods exist in the spec
- ✅ All spec methods are documented
- ✅ Method parameter counts match
- ✅ Auth requirements are documented
- ✅ Error codes are up to date
- ✅ Critical methods (transfer, is_allowed, get_all_assets) are present
147 changes: 147 additions & 0 deletions docs/__tests__/contracts.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import {
ASSET_TOKEN_CONTRACT,
COMPLIANCE_CONTRACT,
REGISTRY_CONTRACT,
DIVIDEND_CONTRACT,
validateContractDocumentation,
} from "@/lib/contracts";

/**
* Tests to ensure contract documentation stays in sync with actual contract ABIs.
* When contract methods change, update both the contract spec and these tests.
*/

describe("Asset Token Contract Documentation", () => {
it("should have all expected methods", () => {
const documentedMethods = [
{ name: "initialize", params: 9 },
{ name: "transfer", params: 3 },
{ name: "mint", params: 3 },
{ name: "burn", params: 2 },
{ name: "balance", params: 1 },
{ name: "total_supply", params: 0 },
{ name: "pause", params: 1 },
{ name: "unpause", params: 1 },
{ name: "update_valuation", params: 2 },
{ name: "set_compliance", params: 2 },
{ name: "get_metadata", params: 0 },
];

const result = validateContractDocumentation(ASSET_TOKEN_CONTRACT, documentedMethods);
expect(result.valid).toBe(true);
if (!result.valid) {
console.error("Asset Token mismatches:", result.mismatches);
}
});

it("should match contract at expected address", () => {
expect(ASSET_TOKEN_CONTRACT.address).toBe(
"CBMCWLSQSWUTLUJFCNBHNBSXMUM3XU7NAQ5TSNERW4HA4ZZBYHLG4ECZ",
);
});

it("should have correct method signatures", () => {
const transferMethod = ASSET_TOKEN_CONTRACT.methods.find((m) => m.name === "transfer");
expect(transferMethod).toBeDefined();
expect(transferMethod?.params).toHaveLength(3);
expect(transferMethod?.auth).toBe("from");
});
});

describe("Compliance Contract Documentation", () => {
it("should have all expected methods", () => {
const documentedMethods = [
{ name: "initialize", params: 1 },
{ name: "add_to_allowlist", params: 3 },
{ name: "remove_from_allowlist", params: 2 },
{ name: "is_allowed", params: 1 },
{ name: "set_jurisdiction_restrictions", params: 2 },
{ name: "is_jurisdiction_blocked", params: 1 },
];

const result = validateContractDocumentation(COMPLIANCE_CONTRACT, documentedMethods);
expect(result.valid).toBe(true);
if (!result.valid) {
console.error("Compliance mismatches:", result.mismatches);
}
});

it("should match contract at expected address", () => {
expect(COMPLIANCE_CONTRACT.address).toBe(
"CBUERYDM7DXTZLLKDBRJKUBPFJ7M4OSUN4T7XKUARU345RLXNAIQD2IU",
);
});
});

describe("Registry Contract Documentation", () => {
it("should have all expected methods", () => {
const documentedMethods = [
{ name: "initialize", params: 1 },
{ name: "register_asset", params: 3 },
{ name: "get_all_assets", params: 0 },
{ name: "get_asset", params: 1 },
];

const result = validateContractDocumentation(REGISTRY_CONTRACT, documentedMethods);
expect(result.valid).toBe(true);
if (!result.valid) {
console.error("Registry mismatches:", result.mismatches);
}
});

it("should match contract at expected address", () => {
expect(REGISTRY_CONTRACT.address).toBe(
"CBX5SMLTXX6JP4HA5GQIO2V6QM7WCUGL2GZ6D4U773HMRI6RXISKPUR3",
);
});
});

describe("Dividend Contract Documentation", () => {
it("should have all expected methods", () => {
const documentedMethods = [
{ name: "initialize", params: 1 },
{ name: "create_distribution", params: 4 },
{ name: "claim_distribution", params: 2 },
{ name: "get_distribution", params: 1 },
{ name: "get_distributions_for_asset", params: 1 },
];

const result = validateContractDocumentation(DIVIDEND_CONTRACT, documentedMethods);
expect(result.valid).toBe(true);
if (!result.valid) {
console.error("Dividend mismatches:", result.mismatches);
}
});

it("should match contract at expected address", () => {
expect(DIVIDEND_CONTRACT.address).toBe(
"CAR4XY3CEBQWFOL27JEWFW34KXSIZA7RFKDQMEIV7ZU723RWY37I2SYX",
);
});
});

describe("Contract Specifications", () => {
it("should have valid network configuration", () => {
const contracts = [ASSET_TOKEN_CONTRACT, COMPLIANCE_CONTRACT, REGISTRY_CONTRACT, DIVIDEND_CONTRACT];
contracts.forEach((contract) => {
expect(contract.network).toBe("testnet");
expect(contract.address).toBeTruthy();
expect(contract.methods.length).toBeGreaterThan(0);
});
});

it("should document critical methods", () => {
// Asset token should have transfer gating methods
expect(ASSET_TOKEN_CONTRACT.methods.find((m) => m.name === "transfer")).toBeDefined();
expect(ASSET_TOKEN_CONTRACT.methods.find((m) => m.auth === "from")).toBeDefined();

// Compliance should have allowlist check
expect(COMPLIANCE_CONTRACT.methods.find((m) => m.name === "is_allowed")).toBeDefined();

// Registry should have asset enumeration
expect(REGISTRY_CONTRACT.methods.find((m) => m.name === "get_all_assets")).toBeDefined();

// Dividend should support distributions
expect(DIVIDEND_CONTRACT.methods.find((m) => m.name === "create_distribution")).toBeDefined();
});
});
10 changes: 7 additions & 3 deletions docs/app/docs/api/assets/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ export const metadata = {

# Assets

<CalloutBox variant="note">
Replace <code>$API_BASE_URL</code> in examples with your configured API base URL (see <a href="/docs/api/overview">API Overview</a>).
</CalloutBox>

## List assets

<ApiEndpoint method="GET" path="/assets" description="All tokenized assets with valuation, supply and holder counts." />
Expand All @@ -20,7 +24,7 @@ Returns an array of asset objects.
| `active` | boolean | Filter by active status |

```bash
curl "http://localhost:8080/assets?asset_type=real_estate&active=true"
curl "$API_BASE_URL/assets?asset_type=real_estate&active=true"
```

**Response schema** (per asset):
Expand All @@ -47,7 +51,7 @@ curl "http://localhost:8080/assets?asset_type=real_estate&active=true"
**Example**

```bash
curl http://localhost:8080/assets
curl $API_BASE_URL/assets
```

```json
Expand Down Expand Up @@ -86,7 +90,7 @@ curl http://localhost:8080/assets
**Example**

```bash
curl http://localhost:8080/assets/1
curl $API_BASE_URL/assets/1
```

Returns a single asset object with the schema above.
Expand Down
6 changes: 5 additions & 1 deletion docs/app/docs/api/compliance/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ export const metadata = {

# Compliance

<CalloutBox variant="note">
Replace <code>$API_BASE_URL</code> in examples with your configured API base URL (see <a href="/docs/api/overview">API Overview</a>).
</CalloutBox>

<ApiEndpoint method="GET" path="/assets/:id/compliance" description="Aggregate compliance summary — counts only, no addresses." />

Returns an aggregate view of an asset's KYC allowlist. It deliberately exposes
Expand All @@ -32,7 +36,7 @@ Returns an aggregate view of an asset's KYC allowlist. It deliberately exposes
**Example**

```bash
curl http://localhost:8080/assets/1/compliance
curl $API_BASE_URL/assets/1/compliance
```

```json
Expand Down
6 changes: 5 additions & 1 deletion docs/app/docs/api/dividends/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ export const metadata = {

# Dividends

<CalloutBox variant="note">
Replace <code>$API_BASE_URL</code> in examples with your configured API base URL (see <a href="/docs/api/overview">API Overview</a>).
</CalloutBox>

<ApiEndpoint method="GET" path="/assets/:id/dividends" description="Distribution history for an asset, newest ledger first." />

Returns every dividend distribution created for the asset's token, ordered by
Expand Down Expand Up @@ -34,7 +38,7 @@ creation ledger (newest first).
**Example**

```bash
curl http://localhost:8080/assets/1/dividends
curl $API_BASE_URL/assets/1/dividends
```

```json
Expand Down
6 changes: 5 additions & 1 deletion docs/app/docs/api/holders/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ export const metadata = {

# Holders

<CalloutBox variant="note">
Replace <code>$API_BASE_URL</code> in examples with your configured API base URL (see <a href="/docs/api/overview">API Overview</a>).
</CalloutBox>

<ApiEndpoint method="GET" path="/assets/:id/holders" description="Holder list with balances, sorted by balance descending." />

Returns every address that is on the asset's compliance allowlist **and** holds a
Expand All @@ -29,7 +33,7 @@ the indexer derives this from the allowlist intersected with balances.
**Example**

```bash
curl http://localhost:8080/assets/1/holders
curl $API_BASE_URL/assets/1/holders
```

```json
Expand Down
6 changes: 5 additions & 1 deletion docs/app/docs/api/overview/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ The Stellar RWA API is a **read-only** REST service that indexes all tokenized
asset activity on Stellar and serves it as JSON. It is written in Rust (Axum +
tokio) and holds no keys — it never signs or submits transactions.

Base URL (local): `http://localhost:8080`
**Base URL:** Set via environment variable `NEXT_PUBLIC_API_BASE_URL` (defaults to `http://localhost:8080` for development). Readers should replace examples with their configured URL.

<CalloutBox variant="tip">
An OpenAPI 3.0 specification is available at <code>/openapi.json</code> — use it with Swagger UI, Postman, or to generate SDK clients. See <a href="/docs/integration">Integration Guide</a> for examples.
</CalloutBox>

## How indexing works

Expand Down
4 changes: 4 additions & 0 deletions docs/app/docs/contracts/asset-token/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ export const metadata = {

# Asset Token Contract

<CalloutBox variant="note">
This documentation is validated against the contract specification in <code>docs/lib/contracts.ts</code>. Method signatures must match the spec — if the contract changes, update both the docs and the spec.
</CalloutBox>

A compliant token representing a tokenized real-world asset. Every transfer is
gated by an external compliance contract: **both** the sender and the recipient
must pass `is_allowed` before any balance moves. Minting is gated on the
Expand Down
4 changes: 2 additions & 2 deletions docs/app/docs/getting-started/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,8 @@ docker run -p 8080:8080 --env-file .env stellar-rwa-api
Then query it:

```bash
curl http://localhost:8080/stats
curl http://localhost:8080/assets
curl $API_BASE_URL/stats
curl $API_BASE_URL/assets
```

Run the docs site:
Expand Down
12 changes: 8 additions & 4 deletions docs/app/docs/integration/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,18 @@ Three ways to integrate with the platform, from easiest to most direct:
The [REST API](/docs/api/overview) is the simplest integration surface. It is
read-only and returns plain JSON.

<CalloutBox variant="note">
Replace <code>$API_BASE_URL</code> in the examples below with your configured base URL, or set the environment variable <code>NEXT_PUBLIC_API_BASE_URL</code>.
</CalloutBox>

```bash
# platform stats
curl http://localhost:8080/stats
curl $API_BASE_URL/stats

# all assets, then one asset's holders and dividends
curl http://localhost:8080/assets
curl http://localhost:8080/assets/1/holders
curl http://localhost:8080/assets/1/dividends
curl $API_BASE_URL/assets
curl $API_BASE_URL/assets/1/holders
curl $API_BASE_URL/assets/1/dividends
```

### TypeScript client
Expand Down
Loading