diff --git a/solutions/liquidity-accord/.npmrc b/solutions/liquidity-accord/.npmrc new file mode 100644 index 0000000..d67f374 --- /dev/null +++ b/solutions/liquidity-accord/.npmrc @@ -0,0 +1 @@ +node-linker=hoisted diff --git a/solutions/liquidity-accord/packages/app/.gitignore b/solutions/liquidity-accord/packages/app/.gitignore new file mode 100644 index 0000000..e985853 --- /dev/null +++ b/solutions/liquidity-accord/packages/app/.gitignore @@ -0,0 +1 @@ +.vercel diff --git a/solutions/liquidity-accord/packages/app/src/components/features/attestation-list.tsx b/solutions/liquidity-accord/packages/app/src/components/features/attestation-list.tsx index 01a62df..8d9a5da 100644 --- a/solutions/liquidity-accord/packages/app/src/components/features/attestation-list.tsx +++ b/solutions/liquidity-accord/packages/app/src/components/features/attestation-list.tsx @@ -1,5 +1,6 @@ import type { AttestationResponse } from '@/services/AttestationService'; import { Badge } from '@/components/ui/badge'; +import { CopyableId } from '@/components/ui/copyable-id'; import { Skeleton } from '@/components/ui/skeleton'; interface AttestationListProps { @@ -33,6 +34,7 @@ export function AttestationList({ attestations, loading }: AttestationListProps) + @@ -45,8 +47,11 @@ export function AttestationList({ attestations, loading }: AttestationListProps) {attestations.map((a) => ( - +
ID Engagement Window Spread
- {truncate(a.engagement_public_id, 14)} + + + + {formatDate(a.window_start)} → {formatDate(a.window_end)} diff --git a/solutions/liquidity-accord/packages/app/src/components/features/engagement-list.tsx b/solutions/liquidity-accord/packages/app/src/components/features/engagement-list.tsx index bbae73c..efcbabc 100644 --- a/solutions/liquidity-accord/packages/app/src/components/features/engagement-list.tsx +++ b/solutions/liquidity-accord/packages/app/src/components/features/engagement-list.tsx @@ -1,5 +1,6 @@ import type { EngagementResponse } from '@/services/EngagementService'; import { Badge } from '@/components/ui/badge'; +import { CopyableId } from '@/components/ui/copyable-id'; import { Skeleton } from '@/components/ui/skeleton'; interface EngagementListProps { @@ -34,6 +35,7 @@ export function EngagementList({ engagements, loading, onSelect }: EngagementLis + @@ -49,6 +51,9 @@ export function EngagementList({ engagements, loading, onSelect }: EngagementLis className="border-b border-[var(--border-dark)] last:border-0 cursor-pointer hover:bg-[var(--background-secondary)] transition-colors" onClick={() => onSelect?.(e)} > +
ID Pair Venue Issuer + + {e.pair_symbol} {e.venue} diff --git a/solutions/liquidity-accord/packages/app/src/components/features/mm-profile-list.tsx b/solutions/liquidity-accord/packages/app/src/components/features/mm-profile-list.tsx index 31bcc67..bb381f6 100644 --- a/solutions/liquidity-accord/packages/app/src/components/features/mm-profile-list.tsx +++ b/solutions/liquidity-accord/packages/app/src/components/features/mm-profile-list.tsx @@ -1,5 +1,6 @@ import type { MMProfileResponse } from '@/services/MMProfileService'; import { Badge } from '@/components/ui/badge'; +import { CopyableId } from '@/components/ui/copyable-id'; import { Skeleton } from '@/components/ui/skeleton'; interface MMProfileListProps { @@ -29,6 +30,7 @@ export function MMProfileList({ profiles, loading }: MMProfileListProps) { + @@ -40,6 +42,9 @@ export function MMProfileList({ profiles, loading }: MMProfileListProps) { {profiles.map((p) => ( +
ID Name Operator Tier
+ + {p.name} {truncateAddress(p.operator_address)} diff --git a/solutions/liquidity-accord/packages/app/src/components/ui/copyable-id.tsx b/solutions/liquidity-accord/packages/app/src/components/ui/copyable-id.tsx new file mode 100644 index 0000000..3a0dac7 --- /dev/null +++ b/solutions/liquidity-accord/packages/app/src/components/ui/copyable-id.tsx @@ -0,0 +1,127 @@ +import { useState, type MouseEvent } from 'react'; + +interface CopyableIdProps { + value: string; + max?: number; + className?: string; +} + +function truncate(s: string, max: number): string { + return s.length > max ? `${s.slice(0, max)}…` : s; +} + +async function copyToClipboard(value: string): Promise { + if (navigator.clipboard?.writeText) { + try { + await navigator.clipboard.writeText(value); + return true; + } catch { + // fall through to legacy fallback + } + } + + const ta = document.createElement('textarea'); + ta.value = value; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; + ta.style.top = '0'; + ta.style.left = '0'; + ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.focus(); + ta.select(); + let ok = false; + try { + ok = document.execCommand('copy'); + } catch { + ok = false; + } + document.body.removeChild(ta); + return ok; +} + +export function CopyableId({ value, max = 12, className = '' }: CopyableIdProps) { + const [copied, setCopied] = useState(false); + + const handleClick = async (e: MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + const ok = await copyToClipboard(value); + if (!ok) return; + setCopied(true); + window.setTimeout(() => setCopied(false), 1400); + }; + + return ( + + + + + Copied + + + ); +} + +function CopyIcon({ copied }: { copied: boolean }) { + return ( + + + + + + + + + + ); +} diff --git a/solutions/liquidity-accord/packages/app/src/hooks/use-auth.ts b/solutions/liquidity-accord/packages/app/src/hooks/use-auth.ts index ffb7a61..ba2873f 100644 --- a/solutions/liquidity-accord/packages/app/src/hooks/use-auth.ts +++ b/solutions/liquidity-accord/packages/app/src/hooks/use-auth.ts @@ -23,7 +23,7 @@ async function authenticateWithSiwe(address: string) { const domain = window.location.host; const origin = window.location.origin; - const statement = 'Sign in to ReineiraOS'; + const statement = 'Sign in to Liquidity Accord'; const message = buildSiweMessage(domain, address, statement, origin, nonce); const signature = await useWalletStore.getState().signMessage(message); diff --git a/solutions/liquidity-accord/packages/app/src/routes/index.tsx b/solutions/liquidity-accord/packages/app/src/routes/index.tsx index d9fbfbf..ca4eb6e 100644 --- a/solutions/liquidity-accord/packages/app/src/routes/index.tsx +++ b/solutions/liquidity-accord/packages/app/src/routes/index.tsx @@ -5,8 +5,10 @@ export function WalletAuthPage() {
-

Reineira Platform Modules

-

Build on ReineiraOS

+

Liquidity Accord

+

+ Confidential performance bonds & delisting insurance +

Connect your wallet to get started

diff --git a/solutions/liquidity-accord/packages/app/vercel.json b/solutions/liquidity-accord/packages/app/vercel.json index 0b94be2..4e1fc07 100644 --- a/solutions/liquidity-accord/packages/app/vercel.json +++ b/solutions/liquidity-accord/packages/app/vercel.json @@ -2,7 +2,13 @@ "installCommand": "cd ../.. && pnpm install --frozen-lockfile", "buildCommand": "pnpm run build", "outputDirectory": "dist", - "rewrites": [{ "source": "/((?!assets/).*)", "destination": "/index.html" }], + "rewrites": [ + { + "source": "/api/:path*", + "destination": "https://liquidity-accord-backend.vercel.app/api/:path*" + }, + { "source": "/((?!assets/).*)", "destination": "/index.html" } + ], "headers": [ { "source": "/(.*)", diff --git a/solutions/liquidity-accord/packages/backend/.gitignore b/solutions/liquidity-accord/packages/backend/.gitignore new file mode 100644 index 0000000..c8a7336 --- /dev/null +++ b/solutions/liquidity-accord/packages/backend/.gitignore @@ -0,0 +1,2 @@ +.vercel +.env*.local diff --git a/solutions/liquidity-accord/packages/backend/api/v1/api-credentials/[clientId].ts b/solutions/liquidity-accord/packages/backend/api/v1/api-credentials/[clientId].ts deleted file mode 100644 index 3922aaf..0000000 --- a/solutions/liquidity-accord/packages/backend/api/v1/api-credentials/[clientId].ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { VercelRequest, VercelResponse } from '@vercel/node'; -import { RevokeCredentialsUseCase } from '../../../src/application/use-case/credential/revoke-credentials.use-case.js'; -import { container } from '../../../src/infrastructure/container.js'; -import type { AuthenticatedRequest } from '../../../src/interface/handler-factory.js'; -import { sendResponse } from '../../../src/interface/handler-factory.js'; -import { withAuth } from '../../../src/interface/middleware/with-auth.js'; -import { withCors } from '../../../src/interface/middleware/with-cors.js'; -import { Response } from '../../../src/interface/response.js'; - -const useCase = new RevokeCredentialsUseCase(container.apiCredentialRepo); - -const handler = async (req: VercelRequest, res: VercelResponse): Promise => { - if (req.method !== 'DELETE') { - sendResponse(res, Response.badRequest('Method not allowed')); - return; - } - - try { - const clientId = req.query.clientId as string; - const authReq = req as AuthenticatedRequest; - await useCase.execute(clientId, authReq.authPayload!.userId); - sendResponse(res, Response.noContent()); - } catch (error) { - if (error instanceof Error && 'statusCode' in error) { - sendResponse(res, Response.fromError(error, (error as { statusCode: number }).statusCode)); - return; - } - sendResponse(res, Response.internalServerError()); - } -}; - -export default withCors(withAuth(handler)); diff --git a/solutions/liquidity-accord/packages/backend/api/v1/api-credentials/index.ts b/solutions/liquidity-accord/packages/backend/api/v1/api-credentials/index.ts deleted file mode 100644 index ec7eb39..0000000 --- a/solutions/liquidity-accord/packages/backend/api/v1/api-credentials/index.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { VercelRequest, VercelResponse } from '@vercel/node'; -import { GenerateCredentialsUseCase } from '../../../src/application/use-case/credential/generate-credentials.use-case.js'; -import { GetCredentialsUseCase } from '../../../src/application/use-case/credential/get-credentials.use-case.js'; -import { container } from '../../../src/infrastructure/container.js'; -import { createGetHandler, sendResponse } from '../../../src/interface/handler-factory.js'; -import { withAuth } from '../../../src/interface/middleware/with-auth.js'; -import { withCors } from '../../../src/interface/middleware/with-cors.js'; -import { Response } from '../../../src/interface/response.js'; - -const generateUseCase = new GenerateCredentialsUseCase(container.apiCredentialRepo); -const getUseCase = new GetCredentialsUseCase(container.apiCredentialRepo); - -const postHandler = createGetHandler({ - operationName: 'GenerateCredentials', - execute: async (_req, authPayload) => { - const result = await generateUseCase.execute(authPayload!.userId); - return Response.created(result); - }, -}); - -const getHandler = createGetHandler({ - operationName: 'GetCredentials', - execute: async (_req, authPayload) => { - const result = await getUseCase.execute(authPayload!.userId); - return Response.ok(result); - }, -}); - -const handler = async (req: VercelRequest, res: VercelResponse): Promise => { - if (req.method === 'POST') { - return postHandler(req, res); - } - if (req.method === 'GET') { - return getHandler(req, res); - } - sendResponse(res, Response.badRequest('Method not allowed')); -}; - -export default withCors(withAuth(handler)); diff --git a/solutions/liquidity-accord/packages/backend/api/v1/api-credentials/oauth/token.ts b/solutions/liquidity-accord/packages/backend/api/v1/api-credentials/oauth/token.ts deleted file mode 100644 index c4cba3b..0000000 --- a/solutions/liquidity-accord/packages/backend/api/v1/api-credentials/oauth/token.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { OAuthTokenRequestSchema } from '../../../../src/application/dto/credential/credential.dto.js'; -import { OAuthTokenExchangeUseCase } from '../../../../src/application/use-case/credential/oauth-token-exchange.use-case.js'; -import { container } from '../../../../src/infrastructure/container.js'; -import { createHandler } from '../../../../src/interface/handler-factory.js'; -import { withCors } from '../../../../src/interface/middleware/with-cors.js'; -import { Response } from '../../../../src/interface/response.js'; - -const useCase = new OAuthTokenExchangeUseCase(container.apiCredentialRepo, container.jwtService); - -const handler = createHandler({ - operationName: 'OAuthTokenExchange', - schema: OAuthTokenRequestSchema, - execute: async (dto) => { - const result = await useCase.execute(dto); - return Response.ok(result); - }, -}); - -export default withCors(handler); diff --git a/solutions/liquidity-accord/packages/backend/api/v1/attestations/[publicId].ts b/solutions/liquidity-accord/packages/backend/api/v1/attestations/[publicId].ts deleted file mode 100644 index 6651ad9..0000000 --- a/solutions/liquidity-accord/packages/backend/api/v1/attestations/[publicId].ts +++ /dev/null @@ -1,19 +0,0 @@ -import { GetAttestationByIdUseCase } from '../../../src/application/use-case/attestation/get-attestation-by-id.use-case.js'; -import { container } from '../../../src/infrastructure/container.js'; -import { createGetHandler } from '../../../src/interface/handler-factory.js'; -import { withAuth } from '../../../src/interface/middleware/with-auth.js'; -import { withCors } from '../../../src/interface/middleware/with-cors.js'; -import { Response } from '../../../src/interface/response.js'; - -const useCase = new GetAttestationByIdUseCase(container.attestationRepo); - -const handler = createGetHandler({ - operationName: 'GetAttestationById', - execute: async (req) => { - const publicId = req.query.publicId as string; - const result = await useCase.execute(publicId); - return result ? Response.ok(result) : Response.notFound('Attestation not found'); - }, -}); - -export default withCors(withAuth(handler)); diff --git a/solutions/liquidity-accord/packages/backend/api/v1/balance/index.ts b/solutions/liquidity-accord/packages/backend/api/v1/balance/index.ts deleted file mode 100644 index 9620b03..0000000 --- a/solutions/liquidity-accord/packages/backend/api/v1/balance/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { GetBalanceUseCase } from '../../../src/application/use-case/balance/get-balance.use-case.js'; -import { createGetHandler } from '../../../src/interface/handler-factory.js'; -import { withAuth } from '../../../src/interface/middleware/with-auth.js'; -import { withCors } from '../../../src/interface/middleware/with-cors.js'; -import { Response } from '../../../src/interface/response.js'; - -const useCase = new GetBalanceUseCase(); - -const handler = createGetHandler({ - operationName: 'GetBalance', - execute: async (_req, authPayload) => { - const result = await useCase.execute(authPayload!.walletAddress); - return Response.ok(result); - }, -}); - -export default withCors(withAuth(handler)); diff --git a/solutions/liquidity-accord/packages/backend/api/v1/engagements/[publicId].ts b/solutions/liquidity-accord/packages/backend/api/v1/engagements/[publicId].ts deleted file mode 100644 index 2d308e7..0000000 --- a/solutions/liquidity-accord/packages/backend/api/v1/engagements/[publicId].ts +++ /dev/null @@ -1,19 +0,0 @@ -import { GetEngagementByIdUseCase } from '../../../src/application/use-case/engagement/get-engagement-by-id.use-case.js'; -import { container } from '../../../src/infrastructure/container.js'; -import { createGetHandler } from '../../../src/interface/handler-factory.js'; -import { withAuth } from '../../../src/interface/middleware/with-auth.js'; -import { withCors } from '../../../src/interface/middleware/with-cors.js'; -import { Response } from '../../../src/interface/response.js'; - -const useCase = new GetEngagementByIdUseCase(container.engagementRepo); - -const handler = createGetHandler({ - operationName: 'GetEngagementById', - execute: async (req) => { - const publicId = req.query.publicId as string; - const result = await useCase.execute(publicId); - return result ? Response.ok(result) : Response.notFound('Engagement not found'); - }, -}); - -export default withCors(withAuth(handler)); diff --git a/solutions/liquidity-accord/packages/backend/api/v1/escrows/[publicId].ts b/solutions/liquidity-accord/packages/backend/api/v1/escrows/[publicId].ts deleted file mode 100644 index fee36bb..0000000 --- a/solutions/liquidity-accord/packages/backend/api/v1/escrows/[publicId].ts +++ /dev/null @@ -1,19 +0,0 @@ -import { GetEscrowByIdUseCase } from '../../../src/application/use-case/escrow/get-escrow-by-id.use-case.js'; -import { container } from '../../../src/infrastructure/container.js'; -import { createGetHandler } from '../../../src/interface/handler-factory.js'; -import { withAuth } from '../../../src/interface/middleware/with-auth.js'; -import { withCors } from '../../../src/interface/middleware/with-cors.js'; -import { Response } from '../../../src/interface/response.js'; - -const useCase = new GetEscrowByIdUseCase(container.escrowRepo); - -const handler = createGetHandler({ - operationName: 'GetEscrowById', - execute: async (req) => { - const publicId = req.query.publicId as string; - const result = await useCase.execute(publicId); - return Response.ok(result); - }, -}); - -export default withCors(withAuth(handler)); diff --git a/solutions/liquidity-accord/packages/backend/api/v1/mm-profiles/[publicId].ts b/solutions/liquidity-accord/packages/backend/api/v1/mm-profiles/[publicId].ts deleted file mode 100644 index 1857121..0000000 --- a/solutions/liquidity-accord/packages/backend/api/v1/mm-profiles/[publicId].ts +++ /dev/null @@ -1,19 +0,0 @@ -import { GetMMProfileByIdUseCase } from '../../../src/application/use-case/mm-profile/get-mm-profile-by-id.use-case.js'; -import { container } from '../../../src/infrastructure/container.js'; -import { createGetHandler } from '../../../src/interface/handler-factory.js'; -import { withAuth } from '../../../src/interface/middleware/with-auth.js'; -import { withCors } from '../../../src/interface/middleware/with-cors.js'; -import { Response } from '../../../src/interface/response.js'; - -const useCase = new GetMMProfileByIdUseCase(container.mmProfileRepo); - -const handler = createGetHandler({ - operationName: 'GetMMProfileById', - execute: async (req) => { - const publicId = req.query.publicId as string; - const result = await useCase.execute(publicId); - return result ? Response.ok(result) : Response.notFound('MM profile not found'); - }, -}); - -export default withCors(withAuth(handler)); diff --git a/solutions/liquidity-accord/packages/backend/api/v1/public/escrows/[publicId].ts b/solutions/liquidity-accord/packages/backend/api/v1/public/escrows/[publicId].ts deleted file mode 100644 index 76cc7d2..0000000 --- a/solutions/liquidity-accord/packages/backend/api/v1/public/escrows/[publicId].ts +++ /dev/null @@ -1,18 +0,0 @@ -import { GetPublicEscrowUseCase } from '../../../../src/application/use-case/escrow/get-public-escrow.use-case.js'; -import { container } from '../../../../src/infrastructure/container.js'; -import { createGetHandler } from '../../../../src/interface/handler-factory.js'; -import { withCors } from '../../../../src/interface/middleware/with-cors.js'; -import { Response } from '../../../../src/interface/response.js'; - -const useCase = new GetPublicEscrowUseCase(container.escrowRepo); - -const handler = createGetHandler({ - operationName: 'GetPublicEscrow', - execute: async (req) => { - const publicId = req.query.publicId as string; - const result = await useCase.execute(publicId); - return Response.ok(result); - }, -}); - -export default withCors(handler); diff --git a/solutions/liquidity-accord/packages/backend/api/v1/transactions/withdrawals/report.ts b/solutions/liquidity-accord/packages/backend/api/v1/transactions/withdrawals/report.ts deleted file mode 100644 index cbd9e97..0000000 --- a/solutions/liquidity-accord/packages/backend/api/v1/transactions/withdrawals/report.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { z } from 'zod'; -import { ReportWithdrawalTransactionDtoSchema } from '../../../../src/application/dto/transaction/report-transaction.dto.js'; -import { ReportWithdrawalTransactionUseCase } from '../../../../src/application/use-case/transaction/report-withdrawal-transaction.use-case.js'; -import { container } from '../../../../src/infrastructure/container.js'; -import { createHandler } from '../../../../src/interface/handler-factory.js'; -import { withAuth } from '../../../../src/interface/middleware/with-auth.js'; -import { withCors } from '../../../../src/interface/middleware/with-cors.js'; -import { Response } from '../../../../src/interface/response.js'; - -const useCase = new ReportWithdrawalTransactionUseCase(container.withdrawalRepo); - -const RouteSchema = ReportWithdrawalTransactionDtoSchema.extend({ - withdrawal_public_id: z.string().min(1), -}); - -const handler = createHandler({ - operationName: 'ReportWithdrawalTransaction', - schema: RouteSchema, - execute: async (dto, _req, authPayload) => { - const { withdrawal_public_id, ...reportDto } = dto; - const result = await useCase.execute(reportDto, authPayload!.userId, withdrawal_public_id); - return Response.ok(result); - }, -}); - -export default withCors(withAuth(handler)); diff --git a/solutions/liquidity-accord/packages/backend/api/v1/webhooks/quicknode.ts b/solutions/liquidity-accord/packages/backend/api/v1/webhooks/quicknode.ts deleted file mode 100644 index bdfc599..0000000 --- a/solutions/liquidity-accord/packages/backend/api/v1/webhooks/quicknode.ts +++ /dev/null @@ -1,49 +0,0 @@ -import type { VercelRequest, VercelResponse } from '@vercel/node'; -import { ProcessEscrowEventUseCase } from '../../../src/application/use-case/webhook/process-escrow-event.use-case.js'; -import type { EscrowEventPayload } from '../../../src/application/use-case/webhook/process-escrow-event.use-case.js'; -import { container } from '../../../src/infrastructure/container.js'; -import { sendResponse } from '../../../src/interface/handler-factory.js'; -import { withCors } from '../../../src/interface/middleware/with-cors.js'; -import { Response } from '../../../src/interface/response.js'; - -const useCase = new ProcessEscrowEventUseCase(container.escrowRepo, container.escrowEventRepo); - -const handler = async (req: VercelRequest, res: VercelResponse): Promise => { - if (req.method !== 'POST') { - sendResponse(res, Response.badRequest('Method not allowed')); - return; - } - - try { - const signature = req.headers['x-qn-signature'] as string; - const nonce = req.headers['x-qn-nonce'] as string; - const timestamp = req.headers['x-qn-timestamp'] as string; - - if (!signature || !nonce || !timestamp) { - sendResponse(res, Response.unauthorized('Missing signature headers')); - return; - } - - const rawBody = typeof req.body === 'string' ? req.body : JSON.stringify(req.body); - const verifier = container.getQuickNodeVerifier(); - if (!verifier) { - sendResponse(res, Response.internalServerError('Webhook secret not configured')); - return; - } - - if (!verifier.verify(rawBody, signature, nonce, timestamp)) { - sendResponse(res, Response.unauthorized('Invalid signature')); - return; - } - - const body = typeof req.body === 'string' ? JSON.parse(req.body) : req.body; - const events: EscrowEventPayload[] = Array.isArray(body) ? body : (body.events ?? []); - - await useCase.execute(events); - sendResponse(res, Response.ok({ processed: events.length })); - } catch { - sendResponse(res, Response.internalServerError()); - } -}; - -export default withCors(handler); diff --git a/solutions/liquidity-accord/packages/backend/api/v1/webhooks/relay-callback.ts b/solutions/liquidity-accord/packages/backend/api/v1/webhooks/relay-callback.ts deleted file mode 100644 index 00a4e57..0000000 --- a/solutions/liquidity-accord/packages/backend/api/v1/webhooks/relay-callback.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type { VercelRequest, VercelResponse } from '@vercel/node'; -import { RelayCallbackUseCase } from '../../../src/application/use-case/webhook/relay-callback.use-case.js'; -import { container } from '../../../src/infrastructure/container.js'; -import { sendResponse } from '../../../src/interface/handler-factory.js'; -import { withCors } from '../../../src/interface/middleware/with-cors.js'; -import { Response } from '../../../src/interface/response.js'; - -const useCase = new RelayCallbackUseCase(container.withdrawalRepo); - -const handler = async (req: VercelRequest, res: VercelResponse): Promise => { - if (req.method !== 'POST') { - sendResponse(res, Response.badRequest('Method not allowed')); - return; - } - - try { - const body = typeof req.body === 'string' ? JSON.parse(req.body) : req.body; - await useCase.execute(body); - sendResponse(res, Response.ok({ received: true })); - } catch { - sendResponse(res, Response.internalServerError()); - } -}; - -export default withCors(handler); diff --git a/solutions/liquidity-accord/packages/backend/api/v1/withdrawals/[publicId].ts b/solutions/liquidity-accord/packages/backend/api/v1/withdrawals/[publicId].ts deleted file mode 100644 index e6650e8..0000000 --- a/solutions/liquidity-accord/packages/backend/api/v1/withdrawals/[publicId].ts +++ /dev/null @@ -1,19 +0,0 @@ -import { GetWithdrawalByIdUseCase } from '../../../src/application/use-case/withdrawal/get-withdrawal-by-id.use-case.js'; -import { container } from '../../../src/infrastructure/container.js'; -import { createGetHandler } from '../../../src/interface/handler-factory.js'; -import { withAuth } from '../../../src/interface/middleware/with-auth.js'; -import { withCors } from '../../../src/interface/middleware/with-cors.js'; -import { Response } from '../../../src/interface/response.js'; - -const useCase = new GetWithdrawalByIdUseCase(container.withdrawalRepo); - -const handler = createGetHandler({ - operationName: 'GetWithdrawalById', - execute: async (req, authPayload) => { - const publicId = req.query.publicId as string; - const result = await useCase.execute(publicId, authPayload!.userId); - return Response.ok(result); - }, -}); - -export default withCors(withAuth(handler)); diff --git a/solutions/liquidity-accord/packages/backend/api/v1/withdrawals/[publicId]/bridge-challenge.ts b/solutions/liquidity-accord/packages/backend/api/v1/withdrawals/[publicId]/bridge-challenge.ts deleted file mode 100644 index 002e55e..0000000 --- a/solutions/liquidity-accord/packages/backend/api/v1/withdrawals/[publicId]/bridge-challenge.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { CreateBridgeChallengeUseCase } from '../../../../src/application/use-case/withdrawal/create-bridge-challenge.use-case.js'; -import { container } from '../../../../src/infrastructure/container.js'; -import { createGetHandler } from '../../../../src/interface/handler-factory.js'; -import { withAuth } from '../../../../src/interface/middleware/with-auth.js'; -import { withCors } from '../../../../src/interface/middleware/with-cors.js'; -import { Response } from '../../../../src/interface/response.js'; - -const useCase = new CreateBridgeChallengeUseCase(container.withdrawalRepo); - -const handler = createGetHandler({ - operationName: 'CreateBridgeChallenge', - execute: async (req, authPayload) => { - const publicId = req.query.publicId as string; - const result = await useCase.execute(publicId, authPayload!.userId); - return Response.ok(result); - }, -}); - -export default withCors(withAuth(handler)); diff --git a/solutions/liquidity-accord/packages/backend/api/v1/withdrawals/[publicId]/bridge-readiness.ts b/solutions/liquidity-accord/packages/backend/api/v1/withdrawals/[publicId]/bridge-readiness.ts deleted file mode 100644 index d577ab0..0000000 --- a/solutions/liquidity-accord/packages/backend/api/v1/withdrawals/[publicId]/bridge-readiness.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { CheckBridgeReadinessUseCase } from '../../../../src/application/use-case/withdrawal/check-bridge-readiness.use-case.js'; -import { container } from '../../../../src/infrastructure/container.js'; -import { createGetHandler } from '../../../../src/interface/handler-factory.js'; -import { withAuth } from '../../../../src/interface/middleware/with-auth.js'; -import { withCors } from '../../../../src/interface/middleware/with-cors.js'; -import { Response } from '../../../../src/interface/response.js'; - -const useCase = new CheckBridgeReadinessUseCase(container.withdrawalRepo); - -const handler = createGetHandler({ - operationName: 'CheckBridgeReadiness', - execute: async (req, authPayload) => { - const publicId = req.query.publicId as string; - const result = await useCase.execute(publicId, authPayload!.userId); - return Response.ok(result); - }, -}); - -export default withCors(withAuth(handler)); diff --git a/solutions/liquidity-accord/packages/backend/api/v1/withdrawals/index.ts b/solutions/liquidity-accord/packages/backend/api/v1/withdrawals/index.ts deleted file mode 100644 index ea745de..0000000 --- a/solutions/liquidity-accord/packages/backend/api/v1/withdrawals/index.ts +++ /dev/null @@ -1,48 +0,0 @@ -import type { VercelRequest, VercelResponse } from '@vercel/node'; -import { CreateWithdrawalDtoSchema } from '../../../src/application/dto/withdrawal/create-withdrawal.dto.js'; -import { CreateWithdrawalUseCase } from '../../../src/application/use-case/withdrawal/create-withdrawal.use-case.js'; -import { GetWithdrawalsUseCase } from '../../../src/application/use-case/withdrawal/get-withdrawals.use-case.js'; -import { container } from '../../../src/infrastructure/container.js'; -import { createHandler, createGetHandler, sendResponse } from '../../../src/interface/handler-factory.js'; -import { withAuth } from '../../../src/interface/middleware/with-auth.js'; -import { withCors } from '../../../src/interface/middleware/with-cors.js'; -import { Response } from '../../../src/interface/response.js'; - -const createWithdrawalUseCase = new CreateWithdrawalUseCase(container.escrowRepo, container.withdrawalRepo); -const getWithdrawalsUseCase = new GetWithdrawalsUseCase(container.withdrawalRepo); - -const postHandler = createHandler({ - operationName: 'CreateWithdrawal', - schema: CreateWithdrawalDtoSchema, - execute: async (dto, _req, authPayload) => { - const result = await createWithdrawalUseCase.execute(dto, authPayload!.userId, authPayload!.walletAddress); - return Response.created(result); - }, -}); - -const getHandler = createGetHandler({ - operationName: 'GetWithdrawals', - execute: async (req, authPayload) => { - const limit = req.query.limit ? Number(req.query.limit) : undefined; - const cursor = req.query.continuation_token as string | undefined; - const status = req.query.status as string | undefined; - const result = await getWithdrawalsUseCase.execute(authPayload!.userId, { - limit, - cursor, - status, - }); - return Response.ok(result); - }, -}); - -const handler = async (req: VercelRequest, res: VercelResponse): Promise => { - if (req.method === 'POST') { - return postHandler(req, res); - } - if (req.method === 'GET') { - return getHandler(req, res); - } - sendResponse(res, Response.badRequest('Method not allowed')); -}; - -export default withCors(withAuth(handler)); diff --git a/solutions/liquidity-accord/packages/backend/public/index.html b/solutions/liquidity-accord/packages/backend/public/index.html new file mode 100644 index 0000000..0e68a24 --- /dev/null +++ b/solutions/liquidity-accord/packages/backend/public/index.html @@ -0,0 +1,17 @@ + + + + + Liquidity Accord — Backend + + + + + +

Liquidity Accord — Backend API

+

This is a serverless API. See /api/v1/docs/openapi.json for the OpenAPI spec.

+ + diff --git a/solutions/liquidity-accord/packages/backend/src/application/dto/auth/request-nonce.dto.ts b/solutions/liquidity-accord/packages/backend/src/application/dto/auth/request-nonce.dto.ts index 0b9e26a..4dd5fce 100644 --- a/solutions/liquidity-accord/packages/backend/src/application/dto/auth/request-nonce.dto.ts +++ b/solutions/liquidity-accord/packages/backend/src/application/dto/auth/request-nonce.dto.ts @@ -1,5 +1,5 @@ import { z } from 'zod'; -import { ethAddressSchema } from '@core/validator'; +import { ethAddressSchema } from '../../../core/validator.js'; export const RequestNonceDtoSchema = z.object({ wallet_address: ethAddressSchema, diff --git a/solutions/liquidity-accord/packages/backend/src/infrastructure/container.ts b/solutions/liquidity-accord/packages/backend/src/infrastructure/container.ts index 953dab6..aa0801e 100644 --- a/solutions/liquidity-accord/packages/backend/src/infrastructure/container.ts +++ b/solutions/liquidity-accord/packages/backend/src/infrastructure/container.ts @@ -11,6 +11,11 @@ import { MemoryAttestationRepository, MemoryMMProfileRepository, } from './repository/memory/index.js'; +import { + PostgresNonceRepository, + PostgresUserRepository, + PostgresSessionRepository, +} from './repository/postgres/index.js'; import { JwtService } from './auth/jwt.service.js'; import { NonceService } from './auth/nonce.service.js'; import { SiweVerifier } from './auth/siwe-verifier.js'; @@ -18,9 +23,13 @@ import { FheService } from './fhe/fhe.service.js'; import { QuickNodeVerifier } from './webhook/quicknode-verifier.js'; import { getEnv } from '../core/config.js'; -const nonceRepo = new MemoryNonceRepository(); -const userRepo = new MemoryUserRepository(); -const sessionRepo = new MemorySessionRepository(); +const usePostgres = getEnv().DB_PROVIDER === 'postgres'; + +const nonceRepo = usePostgres ? new PostgresNonceRepository() : new MemoryNonceRepository(); +const userRepo = usePostgres ? new PostgresUserRepository() : new MemoryUserRepository(); +const sessionRepo = usePostgres + ? new PostgresSessionRepository() + : new MemorySessionRepository(); const escrowRepo = new MemoryEscrowRepository(); const withdrawalRepo = new MemoryWithdrawalRepository(); const businessProfileRepo = new MemoryBusinessProfileRepository(); diff --git a/solutions/liquidity-accord/packages/backend/src/infrastructure/repository/postgres/db.ts b/solutions/liquidity-accord/packages/backend/src/infrastructure/repository/postgres/db.ts new file mode 100644 index 0000000..8bb91ed --- /dev/null +++ b/solutions/liquidity-accord/packages/backend/src/infrastructure/repository/postgres/db.ts @@ -0,0 +1,17 @@ +import { neon } from '@neondatabase/serverless'; +import { drizzle } from 'drizzle-orm/neon-http'; +import * as schema from './schema.js'; + +let _db: ReturnType> | null = null; + +export function getDb() { + if (!_db) { + const url = process.env.DATABASE_URL; + if (!url) { + throw new Error('DATABASE_URL is required when DB_PROVIDER=postgres'); + } + const client = neon(url); + _db = drizzle(client, { schema }); + } + return _db; +} diff --git a/solutions/liquidity-accord/packages/backend/src/infrastructure/repository/postgres/index.ts b/solutions/liquidity-accord/packages/backend/src/infrastructure/repository/postgres/index.ts new file mode 100644 index 0000000..541309c --- /dev/null +++ b/solutions/liquidity-accord/packages/backend/src/infrastructure/repository/postgres/index.ts @@ -0,0 +1,3 @@ +export { PostgresNonceRepository } from './postgres-nonce.repository.js'; +export { PostgresUserRepository } from './postgres-user.repository.js'; +export { PostgresSessionRepository } from './postgres-session.repository.js'; diff --git a/solutions/liquidity-accord/packages/backend/src/infrastructure/repository/postgres/postgres-nonce.repository.ts b/solutions/liquidity-accord/packages/backend/src/infrastructure/repository/postgres/postgres-nonce.repository.ts new file mode 100644 index 0000000..b7d89d1 --- /dev/null +++ b/solutions/liquidity-accord/packages/backend/src/infrastructure/repository/postgres/postgres-nonce.repository.ts @@ -0,0 +1,31 @@ +import { and, eq, gt } from 'drizzle-orm'; +import type { INonceRepository } from '../../../domain/nonce/repository/nonce.repository.js'; +import { getDb } from './db.js'; +import { nonces } from './schema.js'; + +export class PostgresNonceRepository implements INonceRepository { + async save(walletAddress: string, nonce: string, ttlSeconds: number): Promise { + const expiresAt = new Date(Date.now() + ttlSeconds * 1000); + await getDb() + .insert(nonces) + .values({ walletAddress, nonce, expiresAt }) + .onConflictDoUpdate({ + target: [nonces.walletAddress, nonces.nonce], + set: { expiresAt }, + }); + } + + async findAndDelete(walletAddress: string, nonce: string): Promise { + const deleted = await getDb() + .delete(nonces) + .where( + and( + eq(nonces.walletAddress, walletAddress), + eq(nonces.nonce, nonce), + gt(nonces.expiresAt, new Date()), + ), + ) + .returning({ nonce: nonces.nonce }); + return deleted.length > 0; + } +} diff --git a/solutions/liquidity-accord/packages/backend/src/infrastructure/repository/postgres/postgres-session.repository.ts b/solutions/liquidity-accord/packages/backend/src/infrastructure/repository/postgres/postgres-session.repository.ts new file mode 100644 index 0000000..115e956 --- /dev/null +++ b/solutions/liquidity-accord/packages/backend/src/infrastructure/repository/postgres/postgres-session.repository.ts @@ -0,0 +1,71 @@ +import { eq } from 'drizzle-orm'; +import type { ISessionRepository } from '../../../domain/auth/repository/session.repository.js'; +import { Session } from '../../../domain/auth/model/session.js'; +import { getDb } from './db.js'; +import { sessions } from './schema.js'; + +type SessionRow = typeof sessions.$inferSelect; + +function toModel(row: SessionRow): Session { + return new Session({ + id: row.id, + userId: row.userId, + refreshToken: row.refreshToken, + expiresAt: row.expiresAt, + createdAt: row.createdAt, + userAgent: row.userAgent ?? undefined, + ipAddress: row.ipAddress ?? undefined, + }); +} + +export class PostgresSessionRepository implements ISessionRepository { + async findById(id: string): Promise { + const rows = await getDb().select().from(sessions).where(eq(sessions.id, id)).limit(1); + return rows[0] ? toModel(rows[0]) : null; + } + + async findByRefreshToken(token: string): Promise { + const rows = await getDb() + .select() + .from(sessions) + .where(eq(sessions.refreshToken, token)) + .limit(1); + return rows[0] ? toModel(rows[0]) : null; + } + + async findByUserId(userId: string): Promise { + const rows = await getDb().select().from(sessions).where(eq(sessions.userId, userId)); + return rows.map(toModel); + } + + async save(session: Session): Promise { + await getDb() + .insert(sessions) + .values({ + id: session.id, + userId: session.userId, + refreshToken: session.refreshToken, + expiresAt: session.expiresAt, + createdAt: session.createdAt, + userAgent: session.userAgent ?? null, + ipAddress: session.ipAddress ?? null, + }) + .onConflictDoUpdate({ + target: sessions.id, + set: { + refreshToken: session.refreshToken, + expiresAt: session.expiresAt, + userAgent: session.userAgent ?? null, + ipAddress: session.ipAddress ?? null, + }, + }); + } + + async delete(id: string): Promise { + await getDb().delete(sessions).where(eq(sessions.id, id)); + } + + async deleteByUserId(userId: string): Promise { + await getDb().delete(sessions).where(eq(sessions.userId, userId)); + } +} diff --git a/solutions/liquidity-accord/packages/backend/src/infrastructure/repository/postgres/postgres-user.repository.ts b/solutions/liquidity-accord/packages/backend/src/infrastructure/repository/postgres/postgres-user.repository.ts new file mode 100644 index 0000000..57e31e6 --- /dev/null +++ b/solutions/liquidity-accord/packages/backend/src/infrastructure/repository/postgres/postgres-user.repository.ts @@ -0,0 +1,53 @@ +import { eq } from 'drizzle-orm'; +import type { IUserRepository } from '../../../domain/auth/repository/user.repository.js'; +import { User, type WalletProvider } from '../../../domain/auth/model/user.js'; +import { getDb } from './db.js'; +import { users } from './schema.js'; + +type UserRow = typeof users.$inferSelect; + +function toModel(row: UserRow): User { + return new User({ + id: row.id, + walletAddress: row.walletAddress, + walletProvider: row.walletProvider as WalletProvider, + email: row.email ?? undefined, + createdAt: row.createdAt, + }); +} + +export class PostgresUserRepository implements IUserRepository { + async findById(id: string): Promise { + const rows = await getDb().select().from(users).where(eq(users.id, id)).limit(1); + return rows[0] ? toModel(rows[0]) : null; + } + + async findByWalletAddress(address: string): Promise { + const rows = await getDb() + .select() + .from(users) + .where(eq(users.walletAddress, address)) + .limit(1); + return rows[0] ? toModel(rows[0]) : null; + } + + async save(user: User): Promise { + await getDb() + .insert(users) + .values({ + id: user.id, + walletAddress: user.walletAddress, + walletProvider: user.walletProvider, + email: user.email ?? null, + createdAt: user.createdAt, + }) + .onConflictDoUpdate({ + target: users.id, + set: { + walletAddress: user.walletAddress, + walletProvider: user.walletProvider, + email: user.email ?? null, + }, + }); + } +} diff --git a/solutions/liquidity-accord/packages/backend/src/infrastructure/repository/postgres/schema.ts b/solutions/liquidity-accord/packages/backend/src/infrastructure/repository/postgres/schema.ts new file mode 100644 index 0000000..dcfaa82 --- /dev/null +++ b/solutions/liquidity-accord/packages/backend/src/infrastructure/repository/postgres/schema.ts @@ -0,0 +1,31 @@ +import { pgTable, text, timestamp, primaryKey } from 'drizzle-orm/pg-core'; + +export const nonces = pgTable( + 'nonces', + { + walletAddress: text('wallet_address').notNull(), + nonce: text('nonce').notNull(), + expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(), + }, + (t) => [primaryKey({ columns: [t.walletAddress, t.nonce] })], +); + +export const users = pgTable('users', { + id: text('id').primaryKey(), + walletAddress: text('wallet_address').notNull().unique(), + walletProvider: text('wallet_provider').notNull(), + email: text('email'), + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), +}); + +export const sessions = pgTable('sessions', { + id: text('id').primaryKey(), + userId: text('user_id') + .notNull() + .references(() => users.id, { onDelete: 'cascade' }), + refreshToken: text('refresh_token').notNull().unique(), + expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(), + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), + userAgent: text('user_agent'), + ipAddress: text('ip_address'), +}); diff --git a/solutions/liquidity-accord/packages/backend/src/interface/middleware/with-cors.ts b/solutions/liquidity-accord/packages/backend/src/interface/middleware/with-cors.ts index f806d98..f3d1730 100644 --- a/solutions/liquidity-accord/packages/backend/src/interface/middleware/with-cors.ts +++ b/solutions/liquidity-accord/packages/backend/src/interface/middleware/with-cors.ts @@ -9,26 +9,34 @@ function resolveOrigin(requestOrigin: string | undefined): string { try { allowedRaw = getEnv().ALLOWED_ORIGINS; } catch { - return '*'; + return requestOrigin ?? '*'; } if (allowedRaw === '*') { - return '*'; + return requestOrigin ?? '*'; } - const allowed = allowedRaw.split(',').map((o) => o.trim()); + const allowed = allowedRaw + .split(/[,\s]+/) + .map((o) => o.trim()) + .filter((o) => o.length > 0); if (requestOrigin && allowed.includes(requestOrigin)) { return requestOrigin; } - return allowed[0] ?? '*'; + return allowed[0] ?? requestOrigin ?? '*'; } function setCorsHeaders(res: VercelResponse, origin: string): void { res.setHeader('Access-Control-Allow-Origin', origin); + res.setHeader('Vary', 'Origin'); + res.setHeader('Access-Control-Allow-Credentials', 'true'); res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); - res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization'); + res.setHeader( + 'Access-Control-Allow-Headers', + 'Content-Type, Authorization, X-Idempotency-Key, X-Wallet-Provider', + ); res.setHeader('Access-Control-Max-Age', '86400'); } diff --git a/solutions/liquidity-accord/packages/backend/vercel.json b/solutions/liquidity-accord/packages/backend/vercel.json index 350643d..92a4cdf 100644 --- a/solutions/liquidity-accord/packages/backend/vercel.json +++ b/solutions/liquidity-accord/packages/backend/vercel.json @@ -1,19 +1,6 @@ { "installCommand": "cd ../.. && pnpm install --frozen-lockfile", "buildCommand": "pnpm run build", - "rewrites": [{ "source": "/api/(.*)", "destination": "/api/$1" }], - "headers": [ - { - "source": "/api/(.*)", - "headers": [ - { "key": "Access-Control-Allow-Credentials", "value": "true" }, - { "key": "Access-Control-Allow-Origin", "value": "*" }, - { "key": "Access-Control-Allow-Methods", "value": "GET,POST,PUT,DELETE,OPTIONS" }, - { - "key": "Access-Control-Allow-Headers", - "value": "Content-Type,Authorization,X-Idempotency-Key,X-Wallet-Provider" - } - ] - } - ] + "outputDirectory": "public", + "rewrites": [{ "source": "/api/(.*)", "destination": "/api/$1" }] } diff --git a/solutions/liquidity-accord/packages/landing/.gitignore b/solutions/liquidity-accord/packages/landing/.gitignore index 3546ba7..6af91c3 100644 --- a/solutions/liquidity-accord/packages/landing/.gitignore +++ b/solutions/liquidity-accord/packages/landing/.gitignore @@ -37,3 +37,4 @@ coverage # Performance testing .lighthouseci +.vercel diff --git a/solutions/liquidity-accord/packages/landing/next-env.d.ts b/solutions/liquidity-accord/packages/landing/next-env.d.ts index c4b7818..9edff1c 100644 --- a/solutions/liquidity-accord/packages/landing/next-env.d.ts +++ b/solutions/liquidity-accord/packages/landing/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -import "./.next/dev/types/routes.d.ts"; +import "./.next/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/solutions/liquidity-accord/packages/landing/next.config.ts b/solutions/liquidity-accord/packages/landing/next.config.ts index 62725e2..1598177 100644 --- a/solutions/liquidity-accord/packages/landing/next.config.ts +++ b/solutions/liquidity-accord/packages/landing/next.config.ts @@ -1,10 +1,17 @@ import type { NextConfig } from 'next' +import { fileURLToPath } from 'node:url' +import { dirname } from 'node:path' + +const projectRoot = dirname(fileURLToPath(import.meta.url)) const nextConfig: NextConfig = { output: 'export', images: { unoptimized: true }, trailingSlash: true, poweredByHeader: false, + turbopack: { + root: projectRoot, + }, } export default nextConfig diff --git a/solutions/liquidity-accord/packages/landing/vercel.json b/solutions/liquidity-accord/packages/landing/vercel.json new file mode 100644 index 0000000..00612a9 --- /dev/null +++ b/solutions/liquidity-accord/packages/landing/vercel.json @@ -0,0 +1,4 @@ +{ + "buildCommand": "pnpm run build", + "outputDirectory": "out" +}