diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 000000000..aa8a06366 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,366 @@ +# GitHub Copilot Instructions for Interledger Test Network + +## Repository Overview + +**Purpose**: Test Network is an open Interledger network for testing integrations with test money. It provides a complete testing environment including a wallet application and e-commerce demo (Boutique) built on Rafiki. + +**Type**: Full-stack monorepo (pnpm workspaces) +**Size**: ~640 TypeScript files, ~54k lines of code +**Live Demos**: [Wallet](https://wallet.interledger-test.dev) | [Boutique](https://boutique.interledger-test.dev) + +**Tech Stack**: +- **Runtime**: Node.js v20.12.1+ (LTS Iron - enforced via engines field) +- **Package Manager**: pnpm v9.1.4 (enforced - DO NOT use npm/yarn) +- **Languages**: TypeScript 5.9.3 +- **Backend**: Express, Knex/Objection (PostgreSQL), Redis, Socket.IO +- **Frontend**: Next.js 14 (wallet), Vite + React 18 (boutique) +- **Testing**: Jest (backend), Playwright (e2e) +- **Infrastructure**: Docker Compose, Rafiki (ILP), Kratos (auth) + +## Critical Setup Requirements + +### Node.js Version (STRICT) + +**Always use Node 20.12.1+**. The project will fail with other versions. + +```bash +# Use NVM (recommended) +nvm install lts/iron +nvm use lts/iron + +# Verify (must be v20.x) +node --version # Should be v20.12.1 or later +``` + +### Package Manager (STRICT) + +**ALWAYS use pnpm**. Running `npm install` will break the project. The root `package.json` has a `preinstall` hook that enforces this. + +```bash +# Enable pnpm via Corepack (recommended) +corepack enable +corepack prepare pnpm@9.1.4 --activate + +# Or install globally +npm install -g pnpm@9.1.4 + +# Install dependencies (always use --frozen-lockfile in CI) +pnpm install +``` + +## Workspace Structure + +``` +testnet/ +├── packages/ +│ ├── wallet/ +│ │ ├── backend/ # Express GraphQL API, Rafiki integration +│ │ ├── frontend/ # Next.js 14 app (port 4003) +│ │ └── shared/ # Shared types/utils +│ ├── boutique/ +│ │ ├── backend/ # Express API for e-commerce demo +│ │ ├── frontend/ # Vite + React app (port 4004) +│ │ └── shared/ # Shared types +│ └── shared/ +│ └── backend/ # Common backend utilities +├── docker/ +│ ├── dev/ # Local development environment +│ │ ├── docker-compose.yml +│ │ └── .env.example # COPY to .env before starting +│ ├── prod/ # Production builds +│ └── dbinit.sql # PostgreSQL initialization +├── .github/workflows/ # CI/CD pipelines +├── eslint.config.mjs # ESLint 9+ flat config +├── .prettierrc.js # Prettier config +├── tsconfig.json # TypeScript project references +└── pnpm-workspace.yaml # Workspace configuration +``` + +## Build & Development Workflow + +### Build Order (CRITICAL) + +Builds must follow dependency order. The `build:deps` scripts handle this automatically: + +1. `@shared/backend` → 2. `@wallet/shared` / `@boutique/shared` → 3. Applications + +**Always build from root or use package-specific commands:** + +```bash +# Build all packages (recommended - handles deps automatically) +pnpm build + +# Build specific package (deps handled automatically) +pnpm wallet:backend build +pnpm wallet:frontend build +pnpm boutique:backend build +pnpm boutique:frontend build +``` + +**DO NOT** run `tsc` directly in a package without building dependencies first. + +### Local Development + +**Three development modes** available via `DEV_MODE` environment variable: + +```bash +# 1. Hot-reload (default) - backend auto-rebuilds on file changes +pnpm dev +# Starts: wallet-backend (3003), boutique-backend (3004), wallet-frontend (4003), boutique-frontend (4004) + +# 2. Debug mode - exposes Node debugger ports +pnpm dev:debug +# Debugger ports: wallet-backend (9229), boutique-backend (9230) + +# 3. Lite mode - runs production builds (faster startup, no hot-reload) +pnpm dev:lite +``` + +**Required before first run:** +```bash +# 1. Copy environment file +cp ./docker/dev/.env.example ./docker/dev/.env + +# 2. Configure GateHub credentials in .env (contact team or use sandbox account) +# GATEHUB_ACCESS_KEY, GATEHUB_SECRET_KEY, etc. + +# 3. Start development environment +pnpm dev +``` + +**Services after startup:** +- Wallet Frontend: http://localhost:4003 +- Wallet Backend: http://localhost:3003 +- Boutique Frontend: http://localhost:4004 +- Boutique Backend: http://localhost:3004 +- Wallet Admin: http://localhost:3012 +- PostgreSQL: localhost:5433 + +### Stopping Development Environment + +```bash +pnpm localenv:stop +``` + +## Testing + +### Backend Tests (Jest) + +```bash +# Run all tests +pnpm wallet:backend test +pnpm boutique:backend test + +# Tests require built dependencies +pnpm wallet:backend build && pnpm wallet:backend test + +# CI flags (used in GitHub Actions) +pnpm wallet:backend test --detectOpenHandles --forceExit +``` + +**Test Configuration**: `packages/*/backend/jest.config.json` +**Test Setup**: `jest.setup.js` (database setup, mocks) +**Module Aliases**: `@/` maps to `src/`, `@/tests/` maps to `tests/` + +### End-to-End Tests (Playwright) + +E2E tests are handled in the `testnet-deploy` repository, not here. + +## Code Quality (Pre-commit Validation) + +### Linting & Formatting + +**ALWAYS run before committing:** + +```bash +# Check formatting and linting +pnpm checks + +# Auto-fix issues +pnpm format +``` + +**Individual commands:** +```bash +pnpm prettier:check # Check formatting +pnpm prettier:write # Auto-fix formatting +pnpm lint:check # Check ESLint rules (max-warnings=0) +pnpm lint:fix # Auto-fix ESLint issues +``` + +**Configuration:** +- ESLint: `eslint.config.mjs` (flat config, ESLint 9+) +- Prettier: `.prettierrc.js` +- DO NOT override configs in individual packages + +### Common Linting Errors + +**Error: "Unsupported environment (bad Node.js version)"** +- Run `nvm use` to switch to Node 20 + +**Error: "pnpm-lock.yaml is out of date"** +- Run `pnpm install` to update lockfile + +**Prettier failures** +- Run `pnpm prettier:write` to auto-fix formatting + +## CI/CD Pipeline + +### GitHub Actions Workflows + +**Special Notes**: +Agents should remind developers to keep the copilot-instructions.md file updated if they find discrepancies. + +**PR Validation** (`.github/workflows/ci.yml`): +1. Runs `pnpm checks` (prettier + lint) +2. Conditional builds based on PR labels: + - `package: wallet/frontend` → builds wallet frontend + - `package: wallet/backend` → builds wallet backend + runs tests + - Similar for boutique packages +3. Tests run **after** build with `--detectOpenHandles --forceExit` + +**Build & Publish** (`.github/workflows/build-publish.yaml`): +- On tag `v*`: Builds and publishes Docker images to GHCR +- Matrix strategy builds all 4 packages in parallel +- Multiple deployment variants (test-wallet, test-wallet-cards, etc.) + +**PR Title Check** (`.github/workflows/pr_title_check.yml`): +- Enforces [Conventional Commits](https://www.conventionalcommits.org/) +- Format: `type(scope): description` +- Examples: `feat(wallet): add KYC flow`, `fix(boutique): resolve checkout bug` + +### Setup Action + +The reusable setup action (`.github/workflows/setup/action.yml`) is used by all workflows: +1. Installs Node.js LTS Iron +2. Installs pnpm (version from `packageManager` field) +3. Configures pnpm store cache +4. Runs `pnpm install --frozen-lockfile` + +## Common Issues & Solutions + +### Build Failures + +**Issue**: "Cannot find module '@shared/backend'" +**Fix**: Build dependencies first: `pnpm build` from root + +**Issue**: Next.js build fails with "Invalid Options: useEslintrc" +**Status**: Known issue, safe to ignore if build completes. Related to ESLint 9 migration. + +**Issue**: TypeScript errors in `dist/` folder +**Fix**: Clean builds and rebuild: `pnpm clean:builds && pnpm build` + +### Development Environment + +**Issue**: Docker containers fail to start +**Fix**: Ensure `.env` exists in `docker/dev/` and contains required GateHub credentials + +**Issue**: "EADDRINUSE" port conflicts +**Fix**: Stop existing services: `pnpm localenv:stop`, then restart + +**Issue**: PostgreSQL connection errors +**Fix**: Wait for postgres container to initialize (~10 seconds). Check `docker compose logs postgres` + +**Issue**: Hot-reload not working +**Fix**: Ensure `DEV_MODE=hot-reload` (default for `pnpm dev`). Lite mode doesn't have hot-reload. + +### Testing + +**Issue**: Tests hang and don't exit +**Fix**: Use flags: `pnpm wallet:backend test --detectOpenHandles --forceExit` + +**Issue**: Database errors in tests +**Fix**: Ensure `jest.setup.js` is configured correctly and migrations have run + +## Package Scripts Reference + +**Root commands** (run from project root): +```bash +pnpm build # Build all packages +pnpm checks # Run prettier + lint checks +pnpm format # Auto-fix formatting and linting +pnpm clean # Clean node_modules and build artifacts +pnpm dev # Start local environment (hot-reload) +pnpm dev:debug # Start with debugger exposed +pnpm dev:lite # Start production builds +``` + +**Package-specific** (shortcuts): +```bash +pnpm wallet:backend # Run command in wallet backend +pnpm wallet:frontend # Run command in wallet frontend +pnpm boutique:backend # Run command in boutique backend +pnpm boutique:frontend # Run command in boutique frontend +``` + +## Docker Development Details + +**Entrypoint Scripts**: +- `wallet-entrypoint.sh` and `boutique-entrypoint.sh` handle DEV_MODE switching +- Modes: `hot-reload` (nodemon), `debug` (--inspect flag), `lite` (production build) + +**Dockerfiles**: +- `Dockerfile.dev` in each package (multi-stage builds) +- Uses pnpm fetch optimization for faster builds +- Exposes debug ports when DEV_MODE=debug + +**Database Initialization**: +- `docker/dbinit.sql` creates databases: `wallet_backend`, `boutique_backend`, `rafiki_auth`, `rafiki_backend`, `kratos` +- Each service has its own PostgreSQL user and database + +## Architecture Notes + +**Rafiki Integration**: The wallet backend integrates with Rafiki for Interledger payments. Rafiki containers (`rafiki-backend`, `rafiki-auth`) are managed in docker-compose. + +**KYC Flow**: Uses GateHub sandbox API for KYC verification. Real money is NOT allowed on sandbox clusters. + +**WebMonetization**: Supported via Open Payments protocol implemented in Rafiki. + +**Multi-currency**: Wallet supports multiple currencies via exchange rate API (requires `RATE_API_KEY` from freecurrencyapi.com). + +## Best Practices for AI Agents + +1. **ALWAYS verify Node version first**: Run `node --version` to ensure v20.x +2. **ALWAYS use pnpm**: Never suggest npm or yarn commands +3. **Build dependencies before running**: If touching shared packages, rebuild downstream packages +4. **Run checks before committing**: `pnpm checks` catches 90% of CI failures +5. **Test in correct order**: Build first, then test (tests import from `dist/`) +6. **Use package shortcuts**: Prefer `pnpm wallet:backend build` over `cd packages/wallet/backend && pnpm build` +7. **Check docker-compose logs**: If services fail, check logs: `docker compose logs ` +8. **Respect PR title format**: Use Conventional Commits for PR titles +9. **Trust these instructions**: Minimize exploration; this document reflects validated workflows +10. **Update these instructions**: If you discover errors or missing details, propose updates to this file + +## Key Files to Review Before Coding + +**Must Review**: +1. `package.json` (root) - Scripts and workspace commands +2. `pnpm-workspace.yaml` - Workspace structure +3. `tsconfig.json` - Project references (build order) +4. `docker/dev/docker-compose.yml` - Service architecture +5. `.github/workflows/ci.yml` - CI validation steps + +**Configuration Files**: +1. `eslint.config.mjs` - Linting rules +2. `.prettierrc.js` - Formatting rules +3. `packages/*/backend/jest.config.json` - Test configuration +4. `packages/*/tsconfig.json` - TypeScript compilation settings + +## Contributing + +See [.github/contributing.md](.github/contributing.md) for full contribution guidelines and [CODE_OF_CONDUCT.md](.github/CODE_OF_CONDUCT.md) for community standards. + +**Quick checklist**: +- [ ] Node 20.x installed and active +- [ ] pnpm 9.1.4+ installed +- [ ] Environment file copied: `cp docker/dev/.env.example docker/dev/.env` +- [ ] Dependencies installed: `pnpm install` +- [ ] Code formatted: `pnpm checks` passes +- [ ] Tests pass: `pnpm test` +- [ ] PR title follows Conventional Commits + +--- + +**Last Updated**: January 2026 +**Maintainers**: Interledger Foundation +**Repository**: https://github.com/interledger/testnet diff --git a/docker/local/.env.example b/docker/local/.env.example new file mode 100644 index 000000000..01749ec65 --- /dev/null +++ b/docker/local/.env.example @@ -0,0 +1,57 @@ +# Testnet local docker-compose environment file (example) +# Copy to .env and adjust as needed for your machine. + +# General build/dev flags +DEV_MODE=true + +# MockGatehub / Webhooks +# If unset, defaults are applied in docker-compose.yml +GATEHUB_WEBHOOK_SECRET=mock_webhook_secret_please_change + +# Wallet Backend secrets and config +AUTH_IDENTITY_SERVER_SECRET=dev_identity_server_secret +SENDGRID_API_KEY= +FROM_EMAIL= +SEND_EMAIL=false + +# GateHub integration (mock) +# Backend runs inside the compose network, so use the service name +GATEHUB_API_BASE_URL=http://mockgatehub:8080 +GATEHUB_ENV=sandbox +GATEHUB_IFRAME_BASE_URL=http://localhost:8080 +GATEHUB_ACCESS_KEY=mock_access_key +GATEHUB_SECRET_KEY=mock_secret_key +GATEHUB_GATEWAY_UUID=mock-gateway-uuid +GATEHUB_SETTLEMENT_WALLET_ADDRESS=$ilp.interledger-test.dev/interledger +GATEHUB_ORG_ID=mock-org-id +GATEHUB_CARD_APP_ID=mock-card-app-id + +# Wallet backend rate limits and product codes (optional) +RATE_LIMIT=100 +RATE_LIMIT_LEVEL=per_minute +GATEHUB_ACCOUNT_PRODUCT_CODE=DEFAULT +GATEHUB_CARD_PRODUCT_CODE=DEFAULT +GATEHUB_NAME_ON_CARD=TEST USER +GATEHUB_CARD_PP_PREFIX=ILF + +# Card service links (only used when cards are enabled) +CARD_DATA_HREF=http://rafiki-card-service:3007/card-data +CARD_PIN_HREF=http://rafiki-card-service:3007/card-pin + +# Stripe (optional for local) +STRIPE_SECRET_KEY= +STRIPE_WEBHOOK_SECRET= +USE_STRIPE=false + +# Admin/shared IDs and secrets +OPERATOR_TENANT_ID=f829c064-762a-4430-ac5d-7af5df198551 +ADMIN_API_SECRET=secret-key +RAFIKI_SIGNATURE_SECRET=327132b5-99e9-4eb8-8a25-2b7d7738ece1 + +# Wallet Frontend public envs +NEXT_PUBLIC_BACKEND_URL=http://localhost:3003 +NEXT_PUBLIC_AUTH_HOST=http://localhost:3006 +NEXT_PUBLIC_OPEN_PAYMENTS_HOST=http://localhost:3010 +NEXT_PUBLIC_GATEHUB_ENV=sandbox +NEXT_PUBLIC_THEME=light +NEXT_PUBLIC_FEATURES_ENABLED=false diff --git a/docker/local/docker-compose.yml b/docker/local/docker-compose.yml new file mode 100644 index 000000000..5dbf5bfae --- /dev/null +++ b/docker/local/docker-compose.yml @@ -0,0 +1,286 @@ +services: + postgres: + container_name: postgres-local + image: 'postgres:15' + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: password + ports: + - '5434:5432' + restart: unless-stopped + networks: + - testnet + volumes: + - pg-data:/var/lib/postgresql/data + - ../dbinit.sql:/docker-entrypoint-initdb.d/init.sql + + # MockGatehub - Mock Gatehub API service for local development + mockgatehub: + container_name: mockgatehub-local + image: ghcr.io/interledger/mockgatehub:1 + ports: + - '8080:8080' + environment: + MOCKGATEHUB_REDIS_URL: redis://redis-local:6379 + MOCKGATEHUB_REDIS_DB: '1' + WEBHOOK_URL: http://wallet-backend:3003/gatehub-webhooks + WEBHOOK_SECRET: ${GATEHUB_WEBHOOK_SECRET:-6d6f636b5f776562686f6f6b5f736563726574} + depends_on: + - redis + restart: always + networks: + - testnet + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8080/health"] + interval: 10s + timeout: 5s + retries: 3 + + # Wallet + wallet-backend: + container_name: wallet-backend-local + build: + context: ../.. + args: + DEV_MODE: ${DEV_MODE} + dockerfile: ./packages/wallet/backend/Dockerfile.dev + depends_on: + - postgres + - rafiki-backend + - redis + - mockgatehub + volumes: + - ../../packages/wallet/backend:/home/testnet/packages/wallet/backend + - ../../packages/wallet/shared:/home/testnet/packages/wallet/shared + environment: + NODE_ENV: development + PORT: 3003 + DEBUG_PORT: 9229 + DATABASE_URL: postgres://wallet_backend:wallet_backend@postgres-local/wallet_backend + COOKIE_NAME: testnet.cookie + COOKIE_PASSWORD: testnet.cookie.password.super.secret.ilp + COOKIE_TTL: 2630000 + OPEN_PAYMENTS_HOST: https://rafiki-backend + GRAPHQL_ENDPOINT: http://rafiki-backend:3001/graphql + AUTH_GRAPHQL_ENDPOINT: http://rafiki-auth:3008/graphql + AUTH_DOMAIN: http://rafiki-auth:3006 + AUTH_IDENTITY_SERVER_SECRET: ${AUTH_IDENTITY_SERVER_SECRET} + RAFIKI_WEBHOOK_SIGNATURE_SECRET: ${RAFIKI_SIGNATURE_SECRET:-327132b5-99e9-4eb8-8a25-2b7d7738ece1} + SENDGRID_API_KEY: ${SENDGRID_API_KEY} + FROM_EMAIL: ${FROM_EMAIL} + SEND_EMAIL: ${SEND_EMAIL:-false} + REDIS_URL: redis://redis-local:6379/0 + GATEHUB_API_BASE_URL: ${GATEHUB_API_BASE_URL} + GATEHUB_ENV: sandbox + GATEHUB_IFRAME_BASE_URL: http://localhost:8080 + GATEHUB_ACCESS_KEY: ${GATEHUB_ACCESS_KEY} + GATEHUB_SECRET_KEY: ${GATEHUB_SECRET_KEY} + GATEHUB_WEBHOOK_SECRET: ${GATEHUB_WEBHOOK_SECRET:-6d6f636b5f776562686f6f6b5f736563726574} + GATEHUB_GATEWAY_UUID: ${GATEHUB_GATEWAY_UUID} + GATEHUB_SETTLEMENT_WALLET_ADDRESS: ${GATEHUB_SETTLEMENT_WALLET_ADDRESS} + GATEHUB_ORG_ID: ${GATEHUB_ORG_ID} + GATEHUB_CARD_APP_ID: ${GATEHUB_CARD_APP_ID} + RATE_LIMIT: ${RATE_LIMIT} + RATE_LIMIT_LEVEL: ${RATE_LIMIT_LEVEL} + GATEHUB_ACCOUNT_PRODUCT_CODE: ${GATEHUB_ACCOUNT_PRODUCT_CODE} + GATEHUB_CARD_PRODUCT_CODE: ${GATEHUB_CARD_PRODUCT_CODE} + GATEHUB_NAME_ON_CARD: ${GATEHUB_NAME_ON_CARD} + GATEHUB_CARD_PP_PREFIX: ${GATEHUB_CARD_PP_PREFIX} + CARD_DATA_HREF: ${CARD_DATA_HREF} + CARD_PIN_HREF: ${CARD_PIN_HREF} + STRIPE_SECRET_KEY: ${STRIPE_SECRET_KEY} + STRIPE_WEBHOOK_SECRET: ${STRIPE_WEBHOOK_SECRET} + USE_STRIPE: ${USE_STRIPE} + OPERATOR_TENANT_ID: ${OPERATOR_TENANT_ID:-f829c064-762a-4430-ac5d-7af5df198551} + ADMIN_API_SECRET: ${ADMIN_API_SECRET:-secret-key} + ADMIN_SIGNATURE_VERSION: 1 + WEBHOOK_SECRET: ${GATEHUB_WEBHOOK_SECRET:-6d6f636b5f776562686f6f6b5f736563726574} + restart: always + networks: + - testnet + ports: + - '3003:3003' + - '9229:9229' # Map debugger port to local machine's port 9229 + + wallet-frontend: + container_name: wallet-frontend-local + build: + context: ../.. + args: + DEV_MODE: ${DEV_MODE} + NEXT_PUBLIC_BACKEND_URL: http://localhost:3003 + NEXT_PUBLIC_AUTH_HOST: http://localhost:3006 + NEXT_PUBLIC_OPEN_PAYMENTS_HOST: http://localhost:3010 + NEXT_PUBLIC_GATEHUB_ENV: sandbox + NEXT_PUBLIC_THEME: light + NEXT_PUBLIC_FEATURES_ENABLED: 'false' + dockerfile: ./packages/wallet/frontend/Dockerfile.dev + depends_on: + - wallet-backend + volumes: + - ../../packages/wallet/frontend:/home/testnet/packages/wallet/frontend + - ../../packages/wallet/shared:/home/testnet/packages/wallet/shared + environment: + NODE_ENV: development + BACKEND_URL: http://wallet-backend:3003 + NEXT_PUBLIC_BACKEND_URL: http://localhost:3003 + NEXT_PUBLIC_AUTH_HOST: http://localhost:3006 + NEXT_PUBLIC_OPEN_PAYMENTS_HOST: http://localhost:3010 + NEXT_PUBLIC_GATEHUB_ENV: sandbox + NEXT_PUBLIC_THEME: light + NEXT_PUBLIC_FEATURES_ENABLED: 'false' + restart: always + networks: + - testnet + ports: + - '4003:4003' + + # Rafiki + rafiki-auth: + container_name: rafiki-auth-local + image: ghcr.io/interledger/rafiki-auth:v2.2.0-beta + restart: always + networks: + - testnet + ports: + - '3006:3006' + - '3008:3008' + environment: + AUTH_PORT: 3006 + INTROSPECTION_PORT: 3007 + ADMIN_PORT: 3008 + NODE_ENV: development + AUTH_SERVER_URL: http://rafiki-auth:3006 + AUTH_DATABASE_URL: postgresql://rafiki_auth:rafiki_auth@postgres-local/rafiki_auth + IDENTITY_SERVER_URL: http://wallet-frontend:4003/grant-interactions + IDENTITY_SERVER_SECRET: ${AUTH_IDENTITY_SERVER_SECRET:-327132b5-99e9-4eb8-8a25-2b7d7738ece1} + COOKIE_KEY: ${AUTH_COOKIE_KEY:-8fd398393c47dd27a3167d9c081c094f} + INTERACTION_COOKIE_SAME_SITE: ${AUTH_INTERACTION_COOKIE_SAME_SITE:-lax} + WAIT_SECONDS: 1 + REDIS_URL: redis://redis-local:6379/0 + OPERATOR_TENANT_ID: ${OPERATOR_TENANT_ID:-f829c064-762a-4430-ac5d-7af5df198551} + ADMIN_API_SECRET: ${ADMIN_API_SECRET:-secret-key} + ADMIN_SIGNATURE_VERSION: 1 + depends_on: + - postgres + + rafiki-backend: + container_name: rafiki-backend-local + image: ghcr.io/interledger/rafiki-backend:v2.2.0-beta + restart: always + privileged: true + volumes: + - ../temp/:/workspace/temp/ + ports: + - '3010:80' + - '3011:3001' + - '3005:3005' + - '3002:3002' + networks: + - testnet + environment: + NODE_ENV: development + LOG_LEVEL: debug + ADMIN_PORT: 3001 + CONNECTOR_PORT: 3002 + OPEN_PAYMENTS_PORT: 80 + DATABASE_URL: postgresql://rafiki_backend:rafiki_backend@postgres-local/rafiki_backend + USE_TIGERBEETLE: 'false' + NONCE_REDIS_KEY: test + AUTH_SERVER_GRANT_URL: http://rafiki-auth:3006 + AUTH_SERVER_INTROSPECTION_URL: http://rafiki-auth:3007 + ILP_ADDRESS: test.net + ILP_CONNECTOR_URL: http://127.0.0.1:3002 + STREAM_SECRET: BjPXtnd00G2mRQwP/8ZpwyZASOch5sUXT5o0iR5b5wU= + ADMIN_KEY: admin + OPEN_PAYMENTS_URL: https://rafiki-backend + REDIS_URL: redis://redis-local:6379/0 + WALLET_ADDRESS_URL: https://rafiki-backend/.well-known/pay + WEBHOOK_URL: http://wallet-backend:3003/webhooks + WEBHOOK_TIMEOUT: 60000 + SIGNATURE_SECRET: ${RAFIKI_SIGNATURE_SECRET:-327132b5-99e9-4eb8-8a25-2b7d7738ece1} + EXCHANGE_RATES_URL: http://wallet-backend:3003/rates + ENABLE_AUTO_PEERING: 'true' + AUTO_PEERING_SERVER_PORT: 3005 + INSTANCE_NAME: 'Testnet Wallet' + SLIPPAGE: 0.01 + KEY_ID: rafiki + WALLET_ADDRESS_REDIRECT_HTML_PAGE: 'http://localhost:4003/account?walletAddress=%ewa' + OPERATOR_TENANT_ID: ${OPERATOR_TENANT_ID:-f829c064-762a-4430-ac5d-7af5df198551} + ADMIN_API_SECRET: ${ADMIN_API_SECRET:-secret-key} + ADMIN_SIGNATURE_VERSION: 1 + AUTH_SERVICE_API_URL: http://rafiki-auth:3011 + CARD_SERVICE_URL: 'http://rafiki-card-service:3007' + CARD_WEBHOOK_SERVICE_URL: 'http://rafiki-card-service:3007/webhook' + POS_SERVICE_URL: 'http://rafiki-pos-service:3014' + POS_WEBHOOK_SERVICE_URL: 'http://rafiki-pos-service:3014/webhook' + depends_on: + - postgres + - redis + + rafiki-frontend: + container_name: rafiki-frontend-local + image: ghcr.io/interledger/rafiki-frontend:v2.2.0-beta + depends_on: + - rafiki-backend + restart: always + privileged: true + ports: + - '3012:3012' + networks: + - testnet + environment: + PORT: 3012 + GRAPHQL_URL: http://rafiki-backend:3001/graphql + OPEN_PAYMENTS_URL: https://rafiki-backend/ + ENABLE_INSECURE_MESSAGE_COOKIE: 'true' + AUTH_ENABLED: 'false' + SIGNATURE_VERSION: 1 + + rafiki-card-service: + container_name: rafiki-card-service-local + image: ghcr.io/interledger/rafiki-card-service:v2.2.0-beta + restart: always + privileged: true + networks: + - testnet + ports: + - '3007:3007' + environment: + NODE_ENV: development + LOG_LEVEL: debug + CARD_SERVICE_PORT: 3007 + REDIS_URL: redis://redis-local:6379/0 + GRAPHQL_URL: http://rafiki-backend:3001/graphql + TENANT_ID: ${OPERATOR_TENANT_ID:-f829c064-762a-4430-ac5d-7af5df198551} + TENANT_SECRET: ${ADMIN_API_SECRET:-secret-key} + TENANT_SIGNATURE_VERSION: 1 + + redis: + container_name: redis-local + image: 'redis:7-alpine' + restart: unless-stopped + networks: + - testnet + ports: + - '6379:6379' + + mailslurper: + container_name: mailslurper-local + image: oryd/mailslurper:latest-smtps + ports: + - '4436:4436' + - '4437:4437' + networks: + - testnet + +networks: + testnet: + driver: bridge + ipam: + config: + - subnet: 10.5.0.0/24 + gateway: 10.5.0.1 + +volumes: + pg-data: diff --git a/docker/local/rafiki-setup.js b/docker/local/rafiki-setup.js new file mode 100644 index 000000000..053ce48ca --- /dev/null +++ b/docker/local/rafiki-setup.js @@ -0,0 +1,379 @@ +#!/usr/bin/env node +/** + * Configure Rafiki (local docker stack) with a tenant + assets. + * - Reads values from docker/local/.env when present (process.env takes priority) + * - Creates the operator tenant (idpConsentUrl + idpSecret) + * - Ensures assets exist for the Testnet wallet + * + * Run after `docker compose up -d` from docker/local: + * node rafiki-setup.js + */ + +const fs = require('fs') +const path = require('path') +const crypto = require('crypto') + +// ---- helpers --------------------------------------------------------------- +function loadDotEnv(envPath) { + const result = {} + if (!fs.existsSync(envPath)) return result + const lines = fs.readFileSync(envPath, 'utf8').split(/\r?\n/) + for (const line of lines) { + if (!line || line.trim().startsWith('#')) continue + const idx = line.indexOf('=') + if (idx === -1) continue + const key = line.slice(0, idx).trim() + const value = line.slice(idx + 1).trim() + result[key] = value + } + return result +} + +function canonicalize(value) { + if (value === null || typeof value !== 'object') return value + if (Array.isArray(value)) return value.map(canonicalize) + const sortedKeys = Object.keys(value).sort() + const obj = {} + for (const key of sortedKeys) { + obj[key] = canonicalize(value[key]) + } + return obj +} + +function canonicalizeAndStringify(value) { + return JSON.stringify(canonicalize(value)) +} + +function buildEnv() { + const envPath = path.join(__dirname, '.env') + const fileEnv = loadDotEnv(envPath) + const get = (key, fallback) => + process.env[key] ?? fileEnv[key] ?? fallback + + return { + GRAPHQL_ENDPOINT: get('GRAPHQL_ENDPOINT', 'http://localhost:3011/graphql'), + ADMIN_API_SECRET: get('ADMIN_API_SECRET', 'secret-key'), + ADMIN_SIGNATURE_VERSION: get('ADMIN_SIGNATURE_VERSION', '1'), + OPERATOR_TENANT_ID: get( + 'OPERATOR_TENANT_ID', + 'f829c064-762a-4430-ac5d-7af5df198551' + ), + AUTH_IDENTITY_SERVER_SECRET: get( + 'AUTH_IDENTITY_SERVER_SECRET', + 'auth-secret-key-12345' + ), + IDP_CONSENT_URL: get( + 'IDP_CONSENT_URL', + 'http://wallet-frontend:4003/grant-interactions' + ) + } +} + +function signRequest({ query, variables, operationName }, env, timestamp) { + const payload = `${timestamp}.${canonicalizeAndStringify({ + variables: variables ?? {}, + operationName, + query + })}` + const hmac = crypto.createHmac('sha256', env.ADMIN_API_SECRET) + hmac.update(payload) + const digest = hmac.digest('hex') + return `t=${timestamp}, v${env.ADMIN_SIGNATURE_VERSION}=${digest}` +} + +async function graphqlRequest({ query, variables, operationName }, env) { + const timestamp = Date.now() + const signature = signRequest({ query, variables, operationName }, env, timestamp) + const body = JSON.stringify({ query, variables, operationName }) + + const response = await fetch(env.GRAPHQL_ENDPOINT, { + method: 'POST', + headers: { + 'content-type': 'application/json', + signature, + 'tenant-id': env.OPERATOR_TENANT_ID + }, + body + }) + + const data = await response.json() + if (data.errors && data.errors.length) { + const message = data.errors.map((e) => e.message).join('\n') + throw new Error(message) + } + return data.data +} + +// ---- operations ----------------------------------------------------------- +const getTenantQuery = /* GraphQL */ ` + query GetTenant($id: String!) { + tenant(id: $id) { + id + publicName + idpConsentUrl + idpSecret + } + } +` + +const createTenantMutation = /* GraphQL */ ` + mutation CreateTenant($input: CreateTenantInput!) { + createTenant(input: $input) { + tenant { + id + publicName + idpConsentUrl + idpSecret + } + } + } +` + +const updateTenantMutation = /* GraphQL */ ` + mutation UpdateTenant($input: UpdateTenantInput!) { + updateTenant(input: $input) { + tenant { + id + publicName + idpConsentUrl + idpSecret + } + } + } +` + +const listAssetsQuery = /* GraphQL */ ` + query Assets($first: Int = 100) { + assets(first: $first) { + edges { + node { + id + code + scale + } + } + } + } +` + +const createAssetMutation = /* GraphQL */ ` + mutation CreateAsset($input: CreateAssetInput!) { + createAsset(input: $input) { + asset { + id + code + scale + } + } + } +` + +const getAssetByCodeAndScaleQuery = /* GraphQL */ ` + query AssetByCodeAndScale($code: String!, $scale: UInt8!) { + assetByCodeAndScale(code: $code, scale: $scale) { + id + code + scale + } + } +` + +const depositAssetLiquidityMutation = /* GraphQL */ ` + mutation DepositAssetLiquidity($input: DepositAssetLiquidityInput!) { + depositAssetLiquidity(input: $input) { + success + } + } +` + +const assetsToEnsure = [ + { code: 'USD', scale: 2 }, + { code: 'EUR', scale: 2 }, + { code: 'GBP', scale: 2 }, + { code: 'ZAR', scale: 2 }, + { code: 'MXN', scale: 2 }, + { code: 'SGD', scale: 2 }, + { code: 'CAD', scale: 2 }, + { code: 'EGG', scale: 2 }, + { code: 'PEB', scale: 2 }, + { code: 'PKR', scale: 2 } +] + +async function ensureTenant(env) { + try { + const existing = await graphqlRequest( + { query: getTenantQuery, variables: { id: env.OPERATOR_TENANT_ID } }, + env + ) + if (existing?.tenant) { + console.log( + `Tenant already present: ${existing.tenant.id} (consent URL ${existing.tenant.idpConsentUrl})` + ) + if (!existing.tenant.idpConsentUrl || !existing.tenant.idpSecret) { + console.log('Updating tenant idp fields...') + await graphqlRequest( + { + query: updateTenantMutation, + variables: { + input: { + id: env.OPERATOR_TENANT_ID, + idpConsentUrl: env.IDP_CONSENT_URL, + idpSecret: env.AUTH_IDENTITY_SERVER_SECRET + } + } + }, + env + ) + console.log('Tenant idp fields updated') + } + return + } + } catch (err) { + // continue and try to create + console.log('Tenant lookup failed, attempting to create...', err.message) + } + + console.log('Creating tenant...') + try { + const created = await graphqlRequest( + { + query: createTenantMutation, + variables: { + input: { + id: env.OPERATOR_TENANT_ID, + publicName: 'Testnet Wallet', + apiSecret: env.ADMIN_API_SECRET, + idpSecret: env.AUTH_IDENTITY_SERVER_SECRET, + idpConsentUrl: env.IDP_CONSENT_URL + } + } + }, + env + ) + console.log('Tenant created:', created.createTenant.tenant) + } catch (err) { + if ( + typeof err.message === 'string' && + err.message.toLowerCase().includes('duplicate') + ) { + console.log('Tenant already exists (duplicate key), continuing...') + return + } + throw err + } +} + +async function ensureAssets(env) { + let current = { assets: { edges: [] } } + try { + current = await graphqlRequest( + { query: listAssetsQuery, variables: { first: 200 } }, + env + ) + } catch (err) { + console.log('Asset list failed, continuing to create assets...', err.message) + } + + const existingCodes = new Set( + (current?.assets?.edges ?? []).map((e) => e.node.code) + ) + + for (const asset of assetsToEnsure) { + if (existingCodes.has(asset.code)) { + console.log(`Asset ${asset.code} already exists`) + continue + } + console.log(`Creating asset ${asset.code}...`) + try { + await graphqlRequest( + { + query: createAssetMutation, + variables: { + input: { + code: asset.code, + scale: asset.scale + } + } + }, + env + ) + console.log(`Asset ${asset.code} created`) + } catch (err) { + const msg = (err.message || '').toLowerCase() + if (msg.includes('already exists') || msg.includes('duplicate')) { + console.log(`Asset ${asset.code} already exists (api), continuing...`) + continue + } + throw err + } + } +} + +// Deposit liquidity for all assets (100000 units per asset, converted to minor units by scale) +async function ensureLiquidity(env) { + console.log('Ensuring asset liquidity...') + + for (const asset of assetsToEnsure) { + let node + try { + const res = await graphqlRequest( + { + query: getAssetByCodeAndScaleQuery, + variables: { code: asset.code, scale: asset.scale } + }, + env + ) + node = res?.assetByCodeAndScale + } catch (err) { + console.log(`Lookup failed for ${asset.code}:`, err.message) + continue + } + + if (!node?.id) { + console.log(`Skipping liquidity for ${asset.code}: asset id not found`) + continue + } + + // Amount in minor units: 100000 * 10^scale + const amount = BigInt(100000) * BigInt(10) ** BigInt(node.scale) + + console.log(`Depositing liquidity for ${asset.code}: ${amount.toString()} (scale ${node.scale})`) + try { + const res = await graphqlRequest( + { + query: depositAssetLiquidityMutation, + variables: { + input: { + id: crypto.randomUUID(), + assetId: node.id, + amount: amount.toString(), + idempotencyKey: crypto.randomUUID() + } + } + }, + env + ) + + if (!res?.depositAssetLiquidity?.success) { + console.log(`Liquidity deposit failed for ${asset.code}`) + } else { + console.log(`Liquidity deposited for ${asset.code}`) + } + } catch (err) { + console.log(`Liquidity deposit error for ${asset.code}:`, err.message) + } + } +} + +// ---- main ----------------------------------------------------------------- +;(async function main() { + const env = buildEnv() + console.log('Rafiki admin endpoint:', env.GRAPHQL_ENDPOINT) + await ensureTenant(env) + await ensureAssets(env) + await ensureLiquidity(env) + console.log('✅ Rafiki configuration complete') +})().catch((err) => { + console.error('Setup failed:', err.message) + process.exit(1) +}) diff --git a/packages/wallet/backend/src/config/env.ts b/packages/wallet/backend/src/config/env.ts index 92b0f60d3..62dc86140 100644 --- a/packages/wallet/backend/src/config/env.ts +++ b/packages/wallet/backend/src/config/env.ts @@ -13,6 +13,7 @@ const envSchema = z.object({ .default('testnet.cookie.password.super.secret.ilp'), // min. 32 chars COOKIE_TTL: z.coerce.number().default(2630000), // 1 month GATEHUB_ENV: z.enum(['production', 'sandbox']).default('sandbox'), + GATEHUB_API_BASE_URL: z.string().optional(), GATEHUB_ACCESS_KEY: z.string().default('GATEHUB_ACCESS_KEY'), GATEHUB_SECRET_KEY: z.string().default('GATEHUB_SECRET_KEY'), GATEHUB_SEPA_ACCESS_KEY: z.string().optional(), diff --git a/packages/wallet/backend/src/gatehub/client.ts b/packages/wallet/backend/src/gatehub/client.ts index dc5abd46b..92a75055a 100644 --- a/packages/wallet/backend/src/gatehub/client.ts +++ b/packages/wallet/backend/src/gatehub/client.ts @@ -97,18 +97,32 @@ export class GateHubClient { } get apiUrl() { + // If GATEHUB_API_BASE_URL is set (e.g., for local development with mockgatehub), + // use it instead of constructing the URL from mainUrl + if (this.env.GATEHUB_API_BASE_URL) { + return this.env.GATEHUB_API_BASE_URL + } return `https://api.${this.mainUrl}` } get rampUrl() { + if (this.env.GATEHUB_API_BASE_URL) { + return this.apiUrl + } return `https://managed-ramp.${this.mainUrl}` } get exchangeUrl() { + if (this.env.GATEHUB_API_BASE_URL) { + return this.apiUrl + } return `https://exchange.${this.mainUrl}` } get onboardingUrl() { + if (this.env.GATEHUB_API_BASE_URL) { + return this.apiUrl + } return `https://onboarding.${this.mainUrl}` } diff --git a/packages/wallet/backend/src/gatehub/service.ts b/packages/wallet/backend/src/gatehub/service.ts index 976c51745..bf51357c4 100644 --- a/packages/wallet/backend/src/gatehub/service.ts +++ b/packages/wallet/backend/src/gatehub/service.ts @@ -260,6 +260,15 @@ export class GateHubService { } let customerId + // Check if customer already exists to prevent race condition between + // direct addUserToGateway call and webhook handler + if (user.customerId) { + this.logger.debug( + `Customer already exists for user ${userId}, skipping customer creation` + ) + return { isApproved, customerId: user.customerId } + } + if ( this.env.NODE_ENV === 'development' && this.env.GATEHUB_ENV === 'sandbox' @@ -325,6 +334,21 @@ export class GateHubService { firstName: string, lastName: string ): Promise { + // Check if customer setup already in progress or completed + // to prevent race condition between concurrent calls + const existingAccount = await Account.query().findOne({ + userId, + assetCode: 'EUR' + }) + + if (existingAccount) { + this.logger.warn( + `EUR account already exists for user ${userId}, skipping sandbox customer creation` + ) + const user = await User.query().findById(userId) + return user!.customerId || '' + } + const { account, walletAddress } = await this.createDefaultAccountAndWAForManagedUser(userId, true) diff --git a/packages/wallet/backend/src/middleware/withSession.ts b/packages/wallet/backend/src/middleware/withSession.ts index 5e1eaf946..fe97dca9d 100644 --- a/packages/wallet/backend/src/middleware/withSession.ts +++ b/packages/wallet/backend/src/middleware/withSession.ts @@ -6,10 +6,15 @@ import { getIronSession } from 'iron-session' -let domain = env.RAFIKI_MONEY_FRONTEND_HOST - +// Determine cookie domain. Avoid setting Domain=localhost, browsers ignore it. +let domain: string | undefined = undefined if (env.NODE_ENV === 'production' && env.GATEHUB_ENV === 'production') { domain = 'interledger.cards' +} else if ( + env.RAFIKI_MONEY_FRONTEND_HOST && + env.RAFIKI_MONEY_FRONTEND_HOST !== 'localhost' +) { + domain = env.RAFIKI_MONEY_FRONTEND_HOST } export const SESSION_OPTIONS: SessionOptions = { diff --git a/packages/wallet/frontend/Dockerfile.dev b/packages/wallet/frontend/Dockerfile.dev index 4a13dcab2..2e5f64a44 100644 --- a/packages/wallet/frontend/Dockerfile.dev +++ b/packages/wallet/frontend/Dockerfile.dev @@ -23,4 +23,20 @@ ADD . ./ # Install packages from virtual store RUN pnpm install -r --offline +# Accept build arguments for Next.js public environment variables +ARG NEXT_PUBLIC_BACKEND_URL +ARG NEXT_PUBLIC_AUTH_HOST +ARG NEXT_PUBLIC_OPEN_PAYMENTS_HOST +ARG NEXT_PUBLIC_GATEHUB_ENV +ARG NEXT_PUBLIC_THEME +ARG NEXT_PUBLIC_FEATURES_ENABLED + +# Make them available as environment variables during build +ENV NEXT_PUBLIC_BACKEND_URL=$NEXT_PUBLIC_BACKEND_URL +ENV NEXT_PUBLIC_AUTH_HOST=$NEXT_PUBLIC_AUTH_HOST +ENV NEXT_PUBLIC_OPEN_PAYMENTS_HOST=$NEXT_PUBLIC_OPEN_PAYMENTS_HOST +ENV NEXT_PUBLIC_GATEHUB_ENV=$NEXT_PUBLIC_GATEHUB_ENV +ENV NEXT_PUBLIC_THEME=$NEXT_PUBLIC_THEME +ENV NEXT_PUBLIC_FEATURES_ENABLED=$NEXT_PUBLIC_FEATURES_ENABLED + CMD ["pnpm", "wallet:frontend", "dev"] diff --git a/packages/wallet/frontend/next.config.js b/packages/wallet/frontend/next.config.js index 9e98bb8b9..0d43b0312 100644 --- a/packages/wallet/frontend/next.config.js +++ b/packages/wallet/frontend/next.config.js @@ -2,13 +2,18 @@ const withBundleAnalyzer = require('@next/bundle-analyzer')({ enabled: process.env.ANALYZE === 'true' }) -let NEXT_PUBLIC_FEATURES_ENABLED = 'true' +// Default to env override; fall back to previous production/sandbox rule, then to 'true' +let NEXT_PUBLIC_FEATURES_ENABLED = process.env.NEXT_PUBLIC_FEATURES_ENABLED -if ( - process.env.NODE_ENV === 'production' && - process.env.NEXT_PUBLIC_GATEHUB_ENV === 'sandbox' -) { - NEXT_PUBLIC_FEATURES_ENABLED = 'false' +if (!NEXT_PUBLIC_FEATURES_ENABLED) { + if ( + process.env.NODE_ENV === 'production' && + process.env.NEXT_PUBLIC_GATEHUB_ENV === 'sandbox' + ) { + NEXT_PUBLIC_FEATURES_ENABLED = 'false' + } else { + NEXT_PUBLIC_FEATURES_ENABLED = 'true' + } } /** @type {import('next').NextConfig} */ @@ -18,6 +23,8 @@ const nextConfig = { env: { NEXT_PUBLIC_BACKEND_URL: process.env.NEXT_PUBLIC_BACKEND_URL || 'http://localhost:3003', + // Internal URL for server-side (middleware) to reach backend in Docker + BACKEND_INTERNAL_URL: process.env.BACKEND_URL || 'http://wallet-backend:3003', NEXT_PUBLIC_OPEN_PAYMENTS_HOST: process.env.NEXT_PUBLIC_OPEN_PAYMENTS_HOST || '$rafiki-backend/', NEXT_PUBLIC_AUTH_HOST: diff --git a/packages/wallet/frontend/src/lib/httpClient.ts b/packages/wallet/frontend/src/lib/httpClient.ts index 8b2a1ad9d..957dde238 100644 --- a/packages/wallet/frontend/src/lib/httpClient.ts +++ b/packages/wallet/frontend/src/lib/httpClient.ts @@ -14,8 +14,14 @@ export type ErrorResponse = { errors?: T extends FieldValues ? Record, string> : undefined } +// Use internal backend URL when running on the server (SSR/middleware) +const isServer = typeof window === 'undefined' +const baseUrl = isServer + ? process.env.BACKEND_INTERNAL_URL || 'http://wallet-backend:3003' + : process.env.NEXT_PUBLIC_BACKEND_URL + export const httpClient = ky.extend({ - prefixUrl: process.env.NEXT_PUBLIC_BACKEND_URL, + prefixUrl: baseUrl, credentials: 'include', retry: 0, hooks: { diff --git a/packages/wallet/frontend/src/middleware.ts b/packages/wallet/frontend/src/middleware.ts index db8f681c0..1ed16a166 100644 --- a/packages/wallet/frontend/src/middleware.ts +++ b/packages/wallet/frontend/src/middleware.ts @@ -1,6 +1,7 @@ import { NextResponse } from 'next/server' import type { NextRequest } from 'next/server' -import { userService } from './lib/api/user' +// Do not use the browser httpClient here; middleware runs in the container. +// Call backend using the internal Docker hostname to validate the session. const isPublicPath = (path: string) => { return publicPaths.find((x) => @@ -15,9 +16,22 @@ export async function middleware(req: NextRequest) { const isPublic = isPublicPath(req.nextUrl.pathname) const cookieName = process.env.COOKIE_NAME || 'testnet.cookie' - const response = await userService.me( - `${cookieName}=${req.cookies.get(cookieName)?.value}` - ) + const cookieVal = req.cookies.get(cookieName)?.value + + // Build internal backend URL for middleware + const backendUrl = process.env.BACKEND_INTERNAL_URL || 'http://wallet-backend:3003' + let response: { success: boolean; result?: any; message?: string } = { + success: false + } + try { + const meRes = await fetch(`${backendUrl}/me`, { + headers: cookieVal ? { Cookie: `${cookieName}=${cookieVal}` } : {} + }) + const json = await meRes.json() + response = json + } catch (e) { + // Ignore connectivity errors; fallback logic below handles unauthenticated state + } // Success TRUE - the user is logged in if (response.success && response.result) { @@ -40,7 +54,8 @@ export async function middleware(req: NextRequest) { } if (isPublic) { - return NextResponse.redirect(new URL(callbackUrl ?? '/', req.url)) + const dest = callbackUrl ?? '/' + return NextResponse.redirect(new URL(dest, req.url)) } } else { // If the user is not logged in and tries to access a private resource, diff --git a/packages/wallet/frontend/src/pages/grant-interactions/index.tsx b/packages/wallet/frontend/src/pages/grant-interactions/index.tsx index 8daf6c1d4..4d667f686 100644 --- a/packages/wallet/frontend/src/pages/grant-interactions/index.tsx +++ b/packages/wallet/frontend/src/pages/grant-interactions/index.tsx @@ -22,12 +22,11 @@ const GrantInteractionPage = ({ grant, interactionId, nonce, - clientName + clientName: _clientName }: GrantInteractionPageProps) => { const [openDialog, closeDialog] = useDialog() const router = useRouter() const isPendingGrant = grant.state === 'PENDING' - const client = clientName ? clientName : grant.client const imageName = THEME === 'dark' ? '/grants-dark.webp' : '/grants-light.webp' @@ -69,13 +68,12 @@ const GrantInteractionPage = ({
{grant.access.length === 1 ? (
- {client} is requesting access to make payments to an amount of{' '} + Your wallet is requesting access to an amount of{' '} {grant.access[0]?.limits?.debitAmount?.formattedAmount}.
) : (
- {client} is requesting access to make payments on the following - amounts:{' '} + Your wallet is requesting access to the following amounts:{' '} {grant.access .map( (accessItem) => @@ -125,12 +123,12 @@ const GrantInteractionPage = ({
{grant.access.length === 1 ? (
- {client} was previously granted access to an amount of{' '} + Your wallet previously granted access to an amount of{' '} {grant.access[0]?.limits?.debitAmount?.formattedAmount}.
) : (
- {client} was previously granted access to the following amounts:{' '} + Your wallet previously granted access to the following amounts:{' '} {grant.access .map( (accessItem) => diff --git a/packages/wallet/frontend/src/utils/helpers.ts b/packages/wallet/frontend/src/utils/helpers.ts index fb8f20f2d..e3436f0da 100644 --- a/packages/wallet/frontend/src/utils/helpers.ts +++ b/packages/wallet/frontend/src/utils/helpers.ts @@ -44,8 +44,7 @@ export const formatAmount = (args: FormatAmountArgs): FormattedAmount => { const scaledValue = Number(`${value}e-${assetScale}`) const flooredValue = - Math.floor(Math.round(scaledValue * 10 ** displayScale)) / - 10 ** displayScale + Math.floor(scaledValue * 10 ** displayScale) / 10 ** displayScale const symbol = getCurrencySymbol(assetCode)