Initial project setup: Add contracts, API definitions, tests, and documentation

- Add Foundry project configuration (foundry.toml, foundry.lock)
- Add Solidity contracts (TokenFactory138, BridgeVault138, ComplianceRegistry, etc.)
- Add API definitions (OpenAPI, GraphQL, gRPC, AsyncAPI)
- Add comprehensive test suite (unit, integration, fuzz, invariants)
- Add API services (REST, GraphQL, orchestrator, packet service)
- Add documentation (ISO20022 mapping, runbooks, adapter guides)
- Add development tools (RBC tool, Swagger UI, mock server)
- Update OpenZeppelin submodules to v5.0.0
This commit is contained in:
defiQUG
2025-12-12 10:59:41 -08:00
parent 26b5aaf932
commit 651ff4f7eb
281 changed files with 24813 additions and 2 deletions

View File

@@ -0,0 +1,31 @@
{
"name": "@emoney/graphql-api",
"version": "1.0.0",
"description": "GraphQL API server for eMoney Token Factory",
"main": "dist/index.js",
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "ts-node-dev --respawn --transpile-only src/index.ts",
"test": "jest"
},
"dependencies": {
"@apollo/server": "^4.9.5",
"graphql": "^16.8.1",
"graphql-subscriptions": "^2.0.0",
"graphql-ws": "^5.14.2",
"@graphql-tools/schema": "^10.0.0",
"@graphql-tools/load-files": "^6.6.1",
"@graphql-tools/merge": "^9.0.0",
"@emoney/blockchain": "workspace:*",
"@emoney/events": "workspace:*"
},
"devDependencies": {
"@types/node": "^20.10.0",
"typescript": "^5.3.0",
"ts-node-dev": "^2.0.0",
"jest": "^29.7.0",
"@types/jest": "^29.5.11"
}
}

View File

@@ -0,0 +1,82 @@
/**
* GraphQL API Server for eMoney Token Factory
* Implements GraphQL schema with queries, mutations, and subscriptions
*/
import { ApolloServer } from '@apollo/server';
import { expressMiddleware } from '@apollo/server/express4';
import { WebSocketServer } from 'ws';
import { useServer } from 'graphql-ws/lib/use/ws';
import { makeExecutableSchema } from '@graphql-tools/schema';
import { loadFilesSync } from '@graphql-tools/load-files';
import { mergeTypeDefs } from '@graphql-tools/merge';
import express from 'express';
import { readFileSync } from 'fs';
import { join } from 'path';
import { resolvers } from './resolvers';
import { SubscriptionContext, createSubscriptionContext } from './subscriptions/context';
// Load GraphQL schema
const schemaPath = join(__dirname, '../../../packages/graphql/schema.graphql');
const typeDefs = readFileSync(schemaPath, 'utf-8');
// Create executable schema
const schema = makeExecutableSchema({
typeDefs,
resolvers,
});
// Create Apollo Server
const server = new ApolloServer<SubscriptionContext>({
schema,
plugins: [
// WebSocket subscription plugin will be added
],
});
// Express app setup
const app = express();
const PORT = process.env.PORT || 4000;
// Start server
async function startServer() {
await server.start();
// GraphQL endpoint
app.use(
'/graphql',
express.json(),
expressMiddleware(server, {
context: async ({ req }) => {
// TODO: Add auth context
return {
// user: await getUserFromToken(req.headers.authorization),
};
},
})
);
// WebSocket server for subscriptions
const httpServer = app.listen(PORT, () => {
const wsServer = new WebSocketServer({
server: httpServer,
path: '/graphql',
});
useServer(
{
schema,
context: createSubscriptionContext,
},
wsServer
);
console.log(`GraphQL server ready at http://localhost:${PORT}/graphql`);
console.log(`GraphQL subscriptions ready at ws://localhost:${PORT}/graphql`);
});
}
startServer().catch(console.error);
export default app;

View File

@@ -0,0 +1,14 @@
/**
* GraphQL resolvers
*/
import { queryResolvers } from './queries';
import { mutationResolvers } from './mutations';
import { subscriptionResolvers } from './subscriptions';
export const resolvers = {
Query: queryResolvers,
Mutation: mutationResolvers,
Subscription: subscriptionResolvers,
};

View File

@@ -0,0 +1,119 @@
/**
* GraphQL mutation resolvers
* Delegates to REST service layer
*/
// Import services
import { tokenService } from '../../../rest-api/src/services/token-service';
import { lienService } from '../../../rest-api/src/services/lien-service';
import { complianceService } from '../../../rest-api/src/services/compliance-service';
import { mappingService } from '../../../rest-api/src/services/mapping-service';
import { isoService } from '../../../rest-api/src/services/iso-service';
import { triggerService } from '../../../rest-api/src/services/trigger-service';
import { packetService } from '../../../rest-api/src/services/packet-service';
import { bridgeService } from '../../../rest-api/src/services/bridge-service';
interface GraphQLContext {
user?: any;
}
export const mutationResolvers = {
deployToken: async (parent: any, args: { input: any }, context: GraphQLContext) => {
return await tokenService.deployToken(args.input);
},
updateTokenPolicy: async (parent: any, args: { code: string; policy: any }, context: GraphQLContext) => {
return await tokenService.updatePolicy(args.code, args.policy);
},
mintToken: async (parent: any, args: { code: string; input: any }, context: GraphQLContext) => {
return await tokenService.mint(args.code, args.input);
},
burnToken: async (parent: any, args: { code: string; input: any }, context: GraphQLContext) => {
return await tokenService.burn(args.code, args.input);
},
clawbackToken: async (parent: any, args: { code: string; input: any }, context: GraphQLContext) => {
return await tokenService.clawback(args.code, args.input);
},
forceTransferToken: async (parent: any, args: { code: string; input: any }, context: GraphQLContext) => {
return await tokenService.forceTransfer(args.code, args.input);
},
placeLien: async (parent: any, args: { input: any }, context: GraphQLContext) => {
return await lienService.placeLien(args.input);
},
reduceLien: async (parent: any, args: { lienId: string; reduceBy: string }, context: GraphQLContext) => {
return await lienService.reduceLien(args.lienId, args.reduceBy);
},
releaseLien: async (parent: any, args: { lienId: string }, context: GraphQLContext) => {
await lienService.releaseLien(args.lienId);
return { success: true };
},
setCompliance: async (parent: any, args: { refId: string; input: any }, context: GraphQLContext) => {
return await complianceService.setCompliance(args.refId, args.input);
},
setFreeze: async (parent: any, args: { refId: string; frozen: boolean }, context: GraphQLContext) => {
return await complianceService.setFrozen(args.refId, { frozen: args.frozen });
},
linkAccountWallet: async (parent: any, args: { input: any }, context: GraphQLContext) => {
await mappingService.linkAccountWallet(args.input);
return { success: true };
},
unlinkAccountWallet: async (parent: any, args: { input: any }, context: GraphQLContext) => {
await mappingService.unlinkAccountWallet(args.input);
return { success: true };
},
submitInboundMessage: async (parent: any, args: { input: any }, context: GraphQLContext) => {
return await isoService.submitInboundMessage(args.input);
},
submitOutboundMessage: async (parent: any, args: { input: any }, context: GraphQLContext) => {
return await isoService.submitOutboundMessage(args.input);
},
validateAndLockTrigger: async (parent: any, args: { triggerId: string; input?: any }, context: GraphQLContext) => {
return await triggerService.validateAndLock(args.triggerId, args.input || {});
},
markTriggerSubmitted: async (parent: any, args: { triggerId: string }, context: GraphQLContext) => {
return await triggerService.markSubmitted(args.triggerId);
},
confirmTriggerSettled: async (parent: any, args: { triggerId: string }, context: GraphQLContext) => {
return await triggerService.confirmSettled(args.triggerId);
},
confirmTriggerRejected: async (parent: any, args: { triggerId: string; reason?: string }, context: GraphQLContext) => {
return await triggerService.confirmRejected(args.triggerId, args.reason);
},
generatePacket: async (parent: any, args: { input: any }, context: GraphQLContext) => {
return await packetService.generatePacket(args.input);
},
dispatchPacket: async (parent: any, args: { packetId: string; input?: any }, context: GraphQLContext) => {
return await packetService.dispatchPacket({ packetId: args.packetId, ...args.input });
},
acknowledgePacket: async (parent: any, args: { packetId: string; ack: any }, context: GraphQLContext) => {
return await packetService.acknowledgePacket(args.packetId, args.ack);
},
bridgeLock: async (parent: any, args: { input: any }, context: GraphQLContext) => {
return await bridgeService.lock(args.input);
},
bridgeUnlock: async (parent: any, args: { input: any }, context: GraphQLContext) => {
return await bridgeService.unlock(args.input);
},
};

View File

@@ -0,0 +1,183 @@
/**
* GraphQL query resolvers
*/
// Import services (using relative paths since we're in a monorepo)
import { tokenService } from '../../../rest-api/src/services/token-service';
import { lienService } from '../../../rest-api/src/services/lien-service';
import { complianceService } from '../../../rest-api/src/services/compliance-service';
import { mappingService } from '../../../rest-api/src/services/mapping-service';
import { triggerService } from '../../../rest-api/src/services/trigger-service';
import { packetService } from '../../../rest-api/src/services/packet-service';
import { bridgeService } from '../../../rest-api/src/services/bridge-service';
// Type definitions (simplified - in production, use generated types)
interface GraphQLContext {
user?: any;
}
export const queryResolvers = {
token: async (parent: any, args: { code: string }, context: GraphQLContext) => {
return await tokenService.getToken(args.code);
},
tokens: async (parent: any, args: { filters?: any; paging?: any }, context: GraphQLContext) => {
const result = await tokenService.listTokens({
code: args.filters?.code,
issuer: args.filters?.issuer,
limit: args.paging?.limit || 20,
offset: args.paging?.offset || 0,
});
return {
edges: result.tokens.map((token: any) => ({ node: token })),
pageInfo: {
hasNextPage: result.tokens.length === (args.paging?.limit || 20),
hasPreviousPage: (args.paging?.offset || 0) > 0,
},
totalCount: result.total,
};
},
lien: async (parent: any, args: { lienId: string }, context: GraphQLContext) => {
return await lienService.getLien(args.lienId);
},
liens: async (parent: any, args: { filters?: any; paging?: any }, context: GraphQLContext) => {
const result = await lienService.listLiens({
debtor: args.filters?.debtor,
active: args.filters?.active,
limit: args.paging?.limit || 20,
offset: args.paging?.offset || 0,
});
return {
edges: result.liens.map((lien: any) => ({ node: lien })),
pageInfo: {
hasNextPage: result.liens.length === (args.paging?.limit || 20),
hasPreviousPage: (args.paging?.offset || 0) > 0,
},
totalCount: result.total,
};
},
accountLiens: async (parent: any, args: { accountRefId: string }, context: GraphQLContext) => {
return await lienService.getAccountLiens(args.accountRefId);
},
accountEncumbrance: async (parent: any, args: { accountRefId: string }, context: GraphQLContext) => {
return await lienService.getEncumbrance(args.accountRefId);
},
compliance: async (parent: any, args: { refId: string }, context: GraphQLContext) => {
return await complianceService.getProfile(args.refId);
},
accountCompliance: async (parent: any, args: { accountRefId: string }, context: GraphQLContext) => {
return await complianceService.getProfile(args.accountRefId);
},
walletCompliance: async (parent: any, args: { walletRefId: string }, context: GraphQLContext) => {
return await complianceService.getProfile(args.walletRefId);
},
account: async (parent: any, args: { refId: string }, context: GraphQLContext) => {
// In production, fetch from database with nested data
const [liens, compliance, wallets] = await Promise.all([
lienService.getAccountLiens(args.refId),
complianceService.getProfile(args.refId).catch(() => null),
mappingService.getAccountWallets(args.refId),
]);
return {
refId: args.refId,
liens,
compliance,
wallets: wallets.map((w: string) => ({ refId: w })),
};
},
wallet: async (parent: any, args: { refId: string }, context: GraphQLContext) => {
const accounts = await mappingService.getWalletAccounts(args.refId);
return {
refId: args.refId,
accounts: accounts.map((a: string) => ({ refId: a })),
};
},
accountWallets: async (parent: any, args: { accountRefId: string }, context: GraphQLContext) => {
const wallets = await mappingService.getAccountWallets(args.accountRefId);
return wallets.map((w: string) => ({ refId: w }));
},
walletAccounts: async (parent: any, args: { walletRefId: string }, context: GraphQLContext) => {
const accounts = await mappingService.getWalletAccounts(args.walletRefId);
return accounts.map((a: string) => ({ refId: a }));
},
trigger: async (parent: any, args: { triggerId: string }, context: GraphQLContext) => {
const trigger = await triggerService.getTrigger(args.triggerId);
if (!trigger) return null;
// Fetch nested packets
const packetsResult = await packetService.listPackets({ triggerId: args.triggerId });
return {
...trigger,
packets: packetsResult.packets,
};
},
triggers: async (parent: any, args: { filters?: any; paging?: any }, context: GraphQLContext) => {
const result = await triggerService.listTriggers({
rail: args.filters?.rail,
state: args.filters?.state,
accountRef: args.filters?.accountRef,
walletRef: args.filters?.walletRef,
limit: args.paging?.limit || 20,
offset: args.paging?.offset || 0,
});
return {
edges: result.triggers.map((trigger: any) => ({ node: trigger })),
pageInfo: {
hasNextPage: result.triggers.length === (args.paging?.limit || 20),
hasPreviousPage: (args.paging?.offset || 0) > 0,
},
totalCount: result.total,
};
},
packet: async (parent: any, args: { packetId: string }, context: GraphQLContext) => {
return await packetService.getPacket(args.packetId);
},
packets: async (parent: any, args: { filters?: any; paging?: any }, context: GraphQLContext) => {
const result = await packetService.listPackets({
triggerId: args.filters?.triggerId,
status: args.filters?.status,
limit: args.paging?.limit || 20,
offset: args.paging?.offset || 0,
});
return {
edges: result.packets.map((packet: any) => ({ node: packet })),
pageInfo: {
hasNextPage: result.packets.length === (args.paging?.limit || 20),
hasPreviousPage: (args.paging?.offset || 0) > 0,
},
totalCount: result.total,
};
},
bridgeLock: async (parent: any, args: { lockId: string }, context: GraphQLContext) => {
return await bridgeService.getLockStatus(args.lockId);
},
bridgeLocks: async (parent: any, args: { filters?: any; paging?: any }, context: GraphQLContext) => {
// In production, implement list locks
return {
edges: [],
pageInfo: { hasNextPage: false, hasPreviousPage: false },
totalCount: 0,
};
},
bridgeCorridors: async (parent: any, args: any, context: GraphQLContext) => {
const result = await bridgeService.getCorridors();
return result.corridors;
},
};

View File

@@ -0,0 +1,87 @@
/**
* GraphQL subscription resolvers
* Connect to event bus for real-time updates
*/
import { SubscriptionResolvers } from '../generated/graphql-types';
import { eventBusClient } from '@emoney/events';
export const subscriptionResolvers: SubscriptionResolvers = {
onTriggerStateChanged: {
subscribe: async (parent, args, context) => {
// TODO: Subscribe to triggers.state.updated event
return eventBusClient.subscribe(`triggers.state.updated.${args.triggerId}`);
},
},
onTriggerCreated: {
subscribe: async (parent, args, context) => {
// TODO: Subscribe to triggers.created event with filtering
return eventBusClient.subscribe('triggers.created');
},
},
onLienChanged: {
subscribe: async (parent, args, context) => {
// TODO: Subscribe to liens events for specific debtor
return eventBusClient.subscribe(`liens.${args.debtorRefId}`);
},
},
onLienPlaced: {
subscribe: async (parent, args, context) => {
// TODO: Subscribe to liens.placed event
return eventBusClient.subscribe('liens.placed');
},
},
onLienReleased: {
subscribe: async (parent, args, context) => {
// TODO: Subscribe to liens.released event
return eventBusClient.subscribe('liens.released');
},
},
onPacketStatusChanged: {
subscribe: async (parent, args, context) => {
// TODO: Subscribe to packets events for specific packet
return eventBusClient.subscribe(`packets.${args.packetId}`);
},
},
onPacketDispatched: {
subscribe: async (parent, args, context) => {
// TODO: Subscribe to packets.dispatched event
return eventBusClient.subscribe('packets.dispatched');
},
},
onPacketAcknowledged: {
subscribe: async (parent, args, context) => {
// TODO: Subscribe to packets.acknowledged event
return eventBusClient.subscribe('packets.acknowledged');
},
},
onComplianceChanged: {
subscribe: async (parent, args, context) => {
// TODO: Subscribe to compliance.updated event for specific ref
return eventBusClient.subscribe(`compliance.updated.${args.refId}`);
},
},
onFreezeChanged: {
subscribe: async (parent, args, context) => {
// TODO: Subscribe to compliance freeze changes
return eventBusClient.subscribe(`compliance.freeze.${args.refId}`);
},
},
onPolicyUpdated: {
subscribe: async (parent, args, context) => {
// TODO: Subscribe to policy.updated event for specific token
return eventBusClient.subscribe(`policy.updated.${args.token}`);
},
},
};

View File

@@ -0,0 +1,15 @@
/**
* Subscription context for GraphQL WebSocket connections
*/
export interface SubscriptionContext {
// TODO: Add subscription context properties
connectionParams?: any;
}
export function createSubscriptionContext(connectionParams: any): SubscriptionContext {
return {
connectionParams,
};
}

View File

@@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}