Skip to content
Open
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
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
[submodule "packages/circuit/lib/forge-std"]
path = packages/circuit/lib/forge-std
url = https://github.com/foundry-rs/forge-std
[submodule "vendors/trust402"]
path = vendors/trust402
url = https://github.com/lemmaoracle/trust402.git
19 changes: 19 additions & 0 deletions packages/agent/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Example x402 Agent Environment Variables
# Copy this file to .env and fill in your values

# Private key for signing x402 payments (0x-prefixed hex)
# Must have USDC on Base Sepolia testnet
PRIVATE_KEY=0x...

# Resource server URL (the x402-enabled worker)
RESOURCE_SERVER_URL=http://localhost:8787

# Lemma API credentials (for identity proof generation)
LEMMA_API_BASE=https://api.lemma.frame00.com
LEMMA_API_KEY=your-api-key-here

# Optional: Issuer keys for identity proof generation
ISSUER_SECRET_KEY=...
ISSUER_PUBLIC_KEY=...
ISSUER_MAC=...
HOLDER_KEY=...
7 changes: 7 additions & 0 deletions packages/agent/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@
"test": "vitest run"
},
"dependencies": {
"@lemmaoracle/agent": "^0.0.24-alpha.1",
"@lemmaoracle/sdk": "^0.0.23",
"@trust402/identity": "workspace:*",
"@trust402/protocol": "workspace:*",
"@trust402/roles": "workspace:*",
"@x402/core": "^2.10.0",
"@x402/evm": "^2.10.0",
"@x402/fetch": "^2.10.0",
Expand All @@ -18,10 +23,12 @@
"cli-spinners": "^3.4.0",
"dotenv": "^16.6.1",
"ora": "^9.4.0",
"ramda": "^0.30.0",
"viem": "^2.48.4"
},
"devDependencies": {
"@types/node": "^22.19.17",
"@types/ramda": "^0.30.0",
"tsx": "^4.21.0",
"typescript": "^5.9.3",
"vitest": "^3.2.4"
Expand Down
202 changes: 202 additions & 0 deletions packages/agent/src/kyc/artifact.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
/**
* KYC module tests — Identity artifact creation and verification.
*/

import { describe, it, expect, vi, beforeEach } from "vitest";
import {
buildKycCredential,
type BuildCredentialInput,
} from "./artifact.js";
import type { KycRole, KycScope, KycPermission, KycCredential } from "./types.js";
import type { AgentCredential } from "@lemmaoracle/agent";

// ---------------------------------------------------------------------------
// Test fixtures
// ---------------------------------------------------------------------------

const sampleKycCredential: KycCredential = {
roles: [{ name: "kyc-verified" }, { name: "aml-cleared" }],
scopes: [{ name: "payment:stablecoin" }],
permissions: [{ resource: "stablecoin:transfer", action: "execute" }],
};

const sampleCredentialInput: BuildCredentialInput = {
agentId: "agent-001",
subjectId: "subject-001",
issuerId: "issuer-001",
kyc: sampleKycCredential,
spendLimit: 1000,
currency: "USDC",
validForSeconds: 3600,
};

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

describe("KYC Artifact Builder", () => {
describe("buildKycCredential", () => {
it("should build an AgentCredential from KYC credential input", () => {
const result = buildKycCredential(sampleCredentialInput);

expect(result.schema).toBe("agent-credential-v1");
expect(result.identity.agentId).toBe("agent-001");
expect(result.identity.subjectId).toBe("subject-001");
});

it("should map KYC roles to authority.roles", () => {
const result = buildKycCredential(sampleCredentialInput);

expect(result.authority.roles).toHaveLength(2);
expect(result.authority.roles[0].name).toBe("kyc-verified");
expect(result.authority.roles[1].name).toBe("aml-cleared");
});

it("should map KYC scopes to authority.scopes", () => {
const result = buildKycCredential(sampleCredentialInput);

expect(result.authority.scopes).toHaveLength(1);
expect(result.authority.scopes[0].name).toBe("payment:stablecoin");
});

it("should map KYC permissions to authority.permissions", () => {
const result = buildKycCredential(sampleCredentialInput);

expect(result.authority.permissions).toHaveLength(1);
expect(result.authority.permissions[0].resource).toBe("stablecoin:transfer");
expect(result.authority.permissions[0].action).toBe("execute");
});

it("should include financial data when provided", () => {
const result = buildKycCredential(sampleCredentialInput);

expect(result.financial).toBeDefined();
expect(result.financial?.spendLimit).toBe(1000);
expect(result.financial?.currency).toBe("USDC");
});

it("should not include financial data when not provided", () => {
const input: BuildCredentialInput = {
...sampleCredentialInput,
spendLimit: undefined,
currency: undefined,
};

const result = buildKycCredential(input);
expect(result.financial).toBeUndefined();
});

it("should set lifecycle timestamps correctly", () => {
const before = Math.floor(Date.now() / 1000);
const result = buildKycCredential({
...sampleCredentialInput,
validForSeconds: 3600,
});
const after = Math.floor(Date.now() / 1000);

expect(result.lifecycle.issuedAt).toBeGreaterThanOrEqual(before);
expect(result.lifecycle.issuedAt).toBeLessThanOrEqual(after);
expect(result.lifecycle.expiresAt).toBe(result.lifecycle.issuedAt + 3600);
});

it("should not set expiresAt when validForSeconds is not provided", () => {
const input: BuildCredentialInput = {
...sampleCredentialInput,
validForSeconds: undefined,
};

const result = buildKycCredential(input);
expect(result.lifecycle.expiresAt).toBeUndefined();
});

it("should set revoked to false by default", () => {
const result = buildKycCredential(sampleCredentialInput);
expect(result.lifecycle.revoked).toBe(false);
});

it("should include provenance information", () => {
const result = buildKycCredential(sampleCredentialInput);

expect(result.provenance.issuerId).toBe("issuer-001");
expect(result.provenance.sourceSystem).toBe("trust402-kyc");
});

it("should include controllerId and orgId when provided", () => {
const input: BuildCredentialInput = {
...sampleCredentialInput,
controllerId: "controller-001",
orgId: "org-001",
};

const result = buildKycCredential(input);
expect(result.identity.controllerId).toBe("controller-001");
expect(result.identity.orgId).toBe("org-001");
});

it("should not include controllerId and orgId when not provided", () => {
const result = buildKycCredential(sampleCredentialInput);

expect(result.identity.controllerId).toBeUndefined();
expect(result.identity.orgId).toBeUndefined();
});
});

describe("KYC role types", () => {
it("should accept standard KYC role names", () => {
const roles: ReadonlyArray<KycRole> = [
{ name: "kyc-verified" },
{ name: "aml-cleared" },
{ name: "sanctions-clear" },
{ name: "accredited-investor" },
{ name: "institutional" },
];

const input: BuildCredentialInput = {
...sampleCredentialInput,
kyc: { ...sampleKycCredential, roles },
};

const result = buildKycCredential(input);
expect(result.authority.roles).toHaveLength(5);
});
});

describe("KYC scope types", () => {
it("should accept standard KYC scope names", () => {
const scopes: ReadonlyArray<KycScope> = [
{ name: "payment:stablecoin" },
{ name: "payment:fiat" },
{ name: "payment:crypto" },
{ name: "payment:cross-border" },
];

const input: BuildCredentialInput = {
...sampleCredentialInput,
kyc: { ...sampleKycCredential, scopes },
};

const result = buildKycCredential(input);
expect(result.authority.scopes).toHaveLength(4);
});
});

describe("KYC permission types", () => {
it("should accept standard KYC permission combinations", () => {
const permissions: ReadonlyArray<KycPermission> = [
{ resource: "stablecoin:issue", action: "execute" },
{ resource: "stablecoin:redeem", action: "execute" },
{ resource: "stablecoin:transfer", action: "read" },
{ resource: "fiat:on-ramp", action: "write" },
{ resource: "fiat:off-ramp", action: "execute" },
];

const input: BuildCredentialInput = {
...sampleCredentialInput,
kyc: { ...sampleKycCredential, permissions },
};

const result = buildKycCredential(input);
expect(result.authority.permissions).toHaveLength(5);
});
});
});
Loading