From 280ac3d8bd00502031db38020c5e4949b0efc6fa Mon Sep 17 00:00:00 2001 From: QSchlegel Date: Thu, 18 Dec 2025 01:05:46 +0100 Subject: [PATCH 1/6] Implement wallet connection checks before creating new wallets - Added user address validation in the new wallet creation process across multiple components to ensure users are connected before proceeding. - Enhanced error messaging to inform users when their wallet is not connected, improving user experience and preventing unnecessary actions. --- .../new-wallet-flow/shared/useWalletFlowState.tsx | 10 +++++++++- .../pages/homepage/wallets/new-wallet/index.tsx | 10 +++++++++- .../info/migration/useMigrationWalletFlowState.tsx | 11 ++++++++++- src/server/api/routers/wallets.ts | 9 ++++++++- 4 files changed, 36 insertions(+), 4 deletions(-) diff --git a/src/components/pages/homepage/wallets/new-wallet-flow/shared/useWalletFlowState.tsx b/src/components/pages/homepage/wallets/new-wallet-flow/shared/useWalletFlowState.tsx index fb39320..c696f77 100644 --- a/src/components/pages/homepage/wallets/new-wallet-flow/shared/useWalletFlowState.tsx +++ b/src/components/pages/homepage/wallets/new-wallet-flow/shared/useWalletFlowState.tsx @@ -449,6 +449,14 @@ export function useWalletFlowState(): WalletFlowState { async function handleCreateNewWallet() { if (router.pathname == "/wallets/new-wallet-flow/save") { + if (!userAddress) { + toast({ + title: "Error", + description: "Please connect your wallet before creating a wallet.", + variant: "destructive", + }); + return; + } setLoading(true); createNewWallet({ name: name, @@ -457,7 +465,7 @@ export function useWalletFlowState(): WalletFlowState { signersDescriptions: signersDescriptions, signersStakeKeys: signersStakeKeys, signersDRepKeys: signersDRepKeys, - ownerAddress: userAddress!, + ownerAddress: userAddress, numRequiredSigners: numRequiredSigners, stakeCredentialHash: stakeKey || undefined, scriptType: nativeScriptType, diff --git a/src/components/pages/homepage/wallets/new-wallet/index.tsx b/src/components/pages/homepage/wallets/new-wallet/index.tsx index ccdd062..5fcf886 100644 --- a/src/components/pages/homepage/wallets/new-wallet/index.tsx +++ b/src/components/pages/homepage/wallets/new-wallet/index.tsx @@ -239,6 +239,14 @@ export default function PageNewWallet() { async function handleCreateNewWallet() { if (router.pathname == "/wallets/new-wallet") { + if (!userAddress) { + toast({ + title: "Error", + description: "Please connect your wallet before creating a wallet.", + variant: "destructive", + }); + return; + } setLoading(true); createNewWallet({ name: name, @@ -247,7 +255,7 @@ export default function PageNewWallet() { signersDescriptions: signersDescriptions, signersStakeKeys: signersStakeKeys, signersDRepKeys: signersDRepKeys, - ownerAddress: userAddress!, + ownerAddress: userAddress, numRequiredSigners: numRequiredSigners, stakeCredentialHash: stakeKey || undefined, scriptType: nativeScriptType, diff --git a/src/components/pages/wallet/info/migration/useMigrationWalletFlowState.tsx b/src/components/pages/wallet/info/migration/useMigrationWalletFlowState.tsx index 5de653c..ff2c65f 100644 --- a/src/components/pages/wallet/info/migration/useMigrationWalletFlowState.tsx +++ b/src/components/pages/wallet/info/migration/useMigrationWalletFlowState.tsx @@ -335,6 +335,15 @@ export function useMigrationWalletFlowState(appWallet: Wallet): MigrationWalletF return; } + if (!userAddress) { + toast({ + title: "Error", + description: "Please connect your wallet before creating a wallet invite link.", + variant: "destructive", + }); + return; + } + setLoading(true); try { const walletData = { @@ -345,7 +354,7 @@ export function useMigrationWalletFlowState(appWallet: Wallet): MigrationWalletF signersStakeKeys: signersStakeKeys, signersDRepKeys: signersDRepKeys, numRequiredSigners: numRequiredSigners, - ownerAddress: userAddress ?? "", + ownerAddress: userAddress, stakeCredentialHash: stakeKey || undefined, scriptType: nativeScriptType || undefined, }; diff --git a/src/server/api/routers/wallets.ts b/src/server/api/routers/wallets.ts index e548f22..f8aaed3 100644 --- a/src/server/api/routers/wallets.ts +++ b/src/server/api/routers/wallets.ts @@ -342,7 +342,14 @@ export const walletRouter = createTRPCRouter({ ) .mutation(async ({ ctx, input }) => { const sessionAddress = requireSessionAddress(ctx); - if (sessionAddress !== input.ownerAddress) { + const sessionWallets: string[] = (ctx as any).sessionWallets ?? []; + + // Allow ownerAddress to be either the sessionAddress or any address in sessionWallets + const isAuthorized = + sessionAddress === input.ownerAddress || + sessionWallets.includes(input.ownerAddress); + + if (!isAuthorized) { throw new TRPCError({ code: "FORBIDDEN", message: "Owner address mismatch" }); } const numRequired = (input.scriptType === "all" || input.scriptType === "any") ? null : input.numRequiredSigners; From 0067f77b980fbbb30663a216b4c23863ed9f2137 Mon Sep 17 00:00:00 2001 From: QSchlegel Date: Thu, 18 Dec 2025 14:46:21 +0100 Subject: [PATCH 2/6] Update environment configuration and enhance documentation - Expanded the .env.example file with detailed environment variable descriptions for local and Docker setups, improving clarity for developers. - Updated README.md to reflect changes in dependencies, installation instructions, and added troubleshooting sections for database connection issues. - Removed outdated VERCEL_DATABASE_SETUP.md to streamline documentation. - Made GITHUB_TOKEN optional in the environment configuration to enhance flexibility. - Cleaned up unused code and improved error handling in GitHub issue creation API. --- .dockerignore | 62 ++++ .env.example | 48 ++- README.md | 319 ++++++++++++------ VERCEL_DATABASE_SETUP.md | 88 ----- docker-compose.dev.yml | 110 ++++++ package-lock.json | 160 --------- .../homepage/wallets/SectionExplanation.tsx | 3 - .../wallets/WalletBalanceSkeleton.tsx | 3 - src/env.js | 2 +- src/pages/api/github/create-issue.ts | 4 + src/utils/query-invalidation.ts | 1 - 11 files changed, 430 insertions(+), 370 deletions(-) create mode 100644 .dockerignore delete mode 100644 VERCEL_DATABASE_SETUP.md create mode 100644 docker-compose.dev.yml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..d493a5c --- /dev/null +++ b/.dockerignore @@ -0,0 +1,62 @@ +# Dependencies +node_modules +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# Next.js +.next +out +dist +build + +# Testing +coverage +.nyc_output +*.test.ts +*.test.tsx +*.spec.ts +*.spec.tsx +__tests__ + +# Environment variables +.env + +# IDE +.vscode +.idea +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Git +.git +.gitignore +.gitattributes + +# Documentation +*.md +!README.md +docs + +# Docker +Dockerfile +.dockerignore +docker-compose*.yml + +# CI/CD +.github +.vercel + +# Misc +.cache +.turbo +*.log +tsconfig.tsbuildinfo + + diff --git a/.env.example b/.env.example index a55b026..ff71a63 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,44 @@ -DATABASE_URL="" -NEXT_PUBLIC_BLOCKFROST_API_KEY_MAINNET="" -NEXT_PUBLIC_BLOCKFROST_API_KEY_PREPROD="" -BLOB_READ_WRITE_TOKEN="" \ No newline at end of file +# Environment Variables Template +# Copy this file to .env.local for local development (non-Docker) +# Or use .env.dev.example / .env.prod.example for Docker Compose setups + +# Database Configuration +# For local development: use localhost +# For Docker Compose: use the service name (e.g., 'postgres') +DATABASE_URL="postgresql://postgres:postgres@localhost:5432/multisig" +DIRECT_URL="postgresql://postgres:postgres@localhost:5432/multisig" + +# Node Environment (development, test, or production) +NODE_ENV="development" + +# JWT Secret (minimum 32 characters required) +# Generate a secure secret: openssl rand -base64 32 +JWT_SECRET="your-jwt-secret-minimum-32-characters-long" + +# Pinata IPFS Configuration +# Get your JWT token from https://app.pinata.cloud/ +PINATA_JWT="your-pinata-jwt-token" + +# GitHub Personal Access Token +# Create one at https://github.com/settings/tokens +# GITHUB_TOKEN="your-github-token" (Optional - for GitHub issue creation) + +# Blockfrost API Keys +# Get your API keys from https://blockfrost.io/ +# These are client-side variables (exposed to browser) +NEXT_PUBLIC_BLOCKFROST_API_KEY_MAINNET="your-blockfrost-mainnet-api-key" +NEXT_PUBLIC_BLOCKFROST_API_KEY_PREPROD="your-blockfrost-preprod-api-key" + +# Optional: Skip environment validation during builds +# Useful for Docker builds where env vars are set at runtime +# SKIP_ENV_VALIDATION=true + +# Optional: NextAuth Configuration (if using NextAuth.js) +# NEXTAUTH_SECRET="your-nextauth-secret" +# NEXTAUTH_URL="http://localhost:3000" + +# Optional: Discord Integration (if using Discord features) +# DISCORD_CLIENT_ID="your-discord-client-id" +# DISCORD_CLIENT_SECRET="your-discord-client-secret" +# DISCORD_BOT_TOKEN="your-discord-bot-token" +# DISCORD_GUILD_ID="your-discord-guild-id" diff --git a/README.md b/README.md index 42db967..efee758 100644 --- a/README.md +++ b/README.md @@ -2,93 +2,71 @@ A comprehensive, enterprise-grade multi-signature wallet solution built on Cardano, designed for teams, DAOs, and organizations to securely manage treasury funds and participate in governance. - -[![Next.js](https://img.shields.io/badge/Next.js-14.2.4-black)](https://nextjs.org/) +[![Next.js](https://img.shields.io/badge/Next.js-16.0.7-black)](https://nextjs.org/) [![TypeScript](https://img.shields.io/badge/TypeScript-5.5.3-blue)](https://www.typescriptlang.org/) -[![Prisma](https://img.shields.io/badge/Prisma-6.4.1-2D3748)](https://www.prisma.io/) -[![Mesh SDK](https://img.shields.io/badge/Mesh%20SDK-1.9.0--beta.18-orange)](https://meshjs.dev/) +[![Prisma](https://img.shields.io/badge/Prisma-6.17.1-2D3748)](https://www.prisma.io/) +[![Mesh SDK](https://img.shields.io/badge/Mesh%20SDK-1.9.0--beta.87-orange)](https://meshjs.dev/) +[![License](https://img.shields.io/github/license/MeshJS/multisig)](LICENSE.md) ## Features ### Multi-Signature Wallet Management -- **Create Multi-Sig Wallets**: Set up wallets with customizable signer requirements -- **Flexible Signing Thresholds**: Choose between "all", "any", or "at least N" signing schemes -- **Signer Management**: Add/remove signers with descriptive labels and verification -- **Wallet Migration**: Seamlessly migrate to new wallet configurations -- **Multi-Asset Support**: Manage ADA and custom tokens in a single wallet - -### Advanced Transaction Management -- **Intuitive Transaction Creation**: User-friendly interface for creating complex transactions -- **UTxO Selection**: Manual or automatic UTxO selection with visual highlighting -- **Recipient Management**: - - Add multiple recipients with different assets - - Quick-add self and signer addresses - - CSV import/export for bulk operations - - ADA Handle resolution support -- **Transaction Descriptions**: Add context for signers with descriptions and metadata -- **Deposit Functionality**: Easy deposits from personal wallets to multi-sig wallets -- **Send All Assets**: Option to send all available assets in one transaction +- Create wallets with customizable signer requirements +- Flexible signing thresholds ("all", "any", or "at least N") +- Signer management with descriptive labels and verification +- Wallet migration to new configurations +- Multi-asset support for ADA and custom tokens + +### Transaction Management +- Intuitive transaction creation interface +- Manual or automatic UTxO selection +- CSV import/export for bulk operations +- ADA Handle resolution support +- Send all assets in one transaction ### Governance & DRep Integration -- **DRep Registration**: Register your team as a Delegated Representative -- **Governance Participation**: View and vote on Cardano governance proposals -- **Team Voting**: Collaborative decision-making for governance actions -- **On-Chain Transparency**: All governance actions recorded on the blockchain +- Register as a Delegated Representative +- View and vote on Cardano governance proposals +- Team collaboration for governance decisions +- On-chain transparency for all actions ### Staking & Delegation -- **Stake Pool Delegation**: Delegate to any Cardano stake pool -- **Reward Management**: Withdraw staking rewards through multi-sig -- **Stake Registration**: Register and deregister stake addresses -- **Multi-Sig Staking**: Secure staking operations requiring multiple signatures - -### Collaboration & Communication -- **Real-Time Chat**: Built-in Nostr-based chat for team communication -- **Discord Integration**: - - Discord notifications for pending transactions - - User verification through Discord - - Avatar integration -- **Signer Verification**: Message signing to verify wallet ownership -- **Transaction Notifications**: Automated alerts for required signatures - -### Developer & API Features -- **RESTful API**: Comprehensive API for wallet operations -- **tRPC Integration**: Type-safe API with React Query -- **Swagger Documentation**: Interactive API documentation -- **JWT Authentication**: Secure token-based authentication -- **Database Management**: PostgreSQL with Prisma ORM -- **File Storage**: Vercel Blob integration for asset storage - -### User Experience -- **Responsive Design**: Mobile-first design with desktop optimization -- **Dark/Light Mode**: Theme switching support -- **Real-Time Updates**: Live transaction status updates -- **Transaction History**: Comprehensive transaction tracking -- **Asset Management**: Detailed asset portfolio view -- **Error Handling**: Robust error handling with user-friendly messages +- Delegate to any Cardano stake pool +- Withdraw staking rewards through multi-sig +- Register and deregister stake addresses +- Secure multi-sig staking operations + +### Collaboration +- Real-time Nostr-based chat +- Discord integration for notifications +- Signer verification via message signing +- Automated transaction alerts + +### Developer Features +- RESTful API with Swagger documentation +- Type-safe tRPC integration +- JWT authentication +- PostgreSQL database with Prisma ORM ## Architecture ### Frontend -- **Next.js 14**: React framework with App Router -- **TypeScript**: Type-safe development -- **Tailwind CSS**: Utility-first styling -- **Framer Motion**: Smooth animations -- **Radix UI**: Accessible component primitives -- **React Hook Form**: Form management -- **Zustand**: State management +- **Next.js 16** - React framework with App Router +- **TypeScript** - Type-safe development +- **Tailwind CSS** - Utility-first styling +- **Radix UI** - Accessible components +- **Zustand** - State management ### Backend -- **tRPC**: End-to-end typesafe APIs -- **Prisma**: Database ORM with PostgreSQL -- **NextAuth.js**: Authentication system -- **JWT**: Token-based authentication -- **CORS**: Cross-origin resource sharing - -### Blockchain Integration -- **Mesh SDK**: Cardano blockchain interaction -- **Native Scripts**: Multi-signature script generation -- **UTxO Management**: Advanced UTxO handling -- **Transaction Building**: Comprehensive transaction construction +- **tRPC** - End-to-end typesafe APIs +- **Prisma** - Database ORM with PostgreSQL +- **NextAuth.js** - Authentication system +- **JWT** - Token-based authentication + +### Blockchain +- **Mesh SDK** - Cardano blockchain interaction +- **Native Scripts** - Multi-signature script generation +- **UTxO Management** - Advanced transaction handling ## Documentation Graph @@ -251,15 +229,25 @@ model Transaction { ## Getting Started ### Prerequisites -- Node.js 18+ -- PostgreSQL database -- Cardano wallet (Nami, Eternl, etc.) -### Installation +- **Node.js 18+** - Required for local development +- **PostgreSQL** - Database (included with Docker Compose setup) +- **Docker & Docker Compose** - Recommended for easy setup +- **Cardano wallet** - Nami, Eternl, or other compatible wallet for testing + +### Development Setup Options + +Choose the setup method that best fits your workflow: + +#### Docker Compose (App Runs Locally, DB in Docker) + +**Best for**: Local development, debugging, IDE integration, faster iteration + +PostgreSQL runs in Docker. The app runs locally for better debugging and IDE support. 1. **Clone the repository** ```bash - git clone https://github.com/your-org/multisig.git + git clone https://github.com/MeshJS/multisig.git cd multisig ``` @@ -270,35 +258,55 @@ model Transaction { 3. **Set up environment variables** ```bash - cp .env.example .env.local + cp .env.example .env ``` - Configure the following variables: - ```env - DATABASE_URL="postgresql://username:password@localhost:5432/multisig" - NEXTAUTH_SECRET="your-secret-key" - NEXTAUTH_URL="http://localhost:3000" - JWT_SECRET="your-jwt-secret" - DISCORD_CLIENT_ID="your-discord-client-id" - DISCORD_CLIENT_SECRET="your-discord-client-secret" - DISCORD_BOT_TOKEN="your-discord-bot-token" - DISCORD_GUILD_ID="your-discord-guild-id" + Edit `.env` and configure: + - `DATABASE_URL` - Use `postgresql://postgres:postgres@localhost:5433/multisig` (localhost, port 5433) + - `JWT_SECRET` - Generate with: `openssl rand -base64 32` + - `PINATA_JWT` - Get from [Pinata Cloud](https://app.pinata.cloud/) + - `NEXT_PUBLIC_BLOCKFROST_API_KEY_MAINNET` - Get from [Blockfrost](https://blockfrost.io/) + - `NEXT_PUBLIC_BLOCKFROST_API_KEY_PREPROD` - Get from [Blockfrost](https://blockfrost.io/) + - `GITHUB_TOKEN` - (Optional) Create at [GitHub Settings](https://github.com/settings/tokens) for GitHub issue creation + +4. **Start PostgreSQL database** + ```bash + docker compose -f docker-compose.dev.yml up -d postgres ``` -4. **Set up the database** +5. **Initialize the database** ```bash - npm run db:push - npm run db:generate + npm run db:update ``` + + This will: + - Format the Prisma schema + - Push schema changes to the database + - Generate the Prisma Client -5. **Start the development server** +6. **Start the development server** ```bash npm run dev ``` -6. **Open your browser** +7. **Open your browser** Navigate to [http://localhost:3000](http://localhost:3000) +**Useful commands:** +```bash +# Start database +docker compose -f docker-compose.dev.yml up -d postgres + +# Stop database +docker compose -f docker-compose.dev.yml down + +# Update database schema (after schema changes) +npm run db:update + +# View database in Prisma Studio +npm run db:studio +``` + ## Usage ### Creating a Multi-Sig Wallet @@ -343,6 +351,8 @@ The application provides comprehensive API documentation through Swagger UI: - `GET /api/v1/lookupMultisigWallet` - Lookup multisig wallet - `POST /api/discord/send-message` - Send Discord notifications +> 💡 **Tip**: The Swagger UI provides interactive API testing. Start the dev server and visit `/api-docs` to explore all available endpoints. + ## Development ### Available Scripts @@ -354,8 +364,10 @@ npm run build # Build for production npm run start # Start production server # Database +npm run db:update # Format schema, push changes, and generate client (recommended) npm run db:push # Push schema changes to database npm run db:generate # Generate Prisma client +npm run db:migrate # Apply migration files npm run db:studio # Open Prisma Studio npm run db:format # Format Prisma schema @@ -364,6 +376,47 @@ npm run lint # Run ESLint npm run type-check # Run TypeScript compiler ``` +### Docker Compose Commands + +**Pure Docker Compose Setup (Option 1):** +```bash +# Start all services (app + database) +docker compose -f docker-compose.dev.yml up --build + +# Start in background +docker compose -f docker-compose.dev.yml up -d --build + +# View logs +docker compose -f docker-compose.dev.yml logs -f + +# View app logs only +docker compose -f docker-compose.dev.yml logs -f app + +# Stop services +docker compose -f docker-compose.dev.yml down + +# Access Prisma Studio +docker compose -f docker-compose.dev.yml exec app npm run db:studio + +# Run commands in app container +docker compose -f docker-compose.dev.yml exec app npm run +``` + +**Local Development Setup (Option 2):** +```bash +# Start PostgreSQL only +docker compose -f docker-compose.dev.yml up -d postgres + +# Stop PostgreSQL +docker compose -f docker-compose.dev.yml down + +# View database logs +docker compose -f docker-compose.dev.yml logs postgres + +# Check database status +docker compose -f docker-compose.dev.yml ps +``` + ### Project Structure ``` @@ -380,34 +433,80 @@ src/ │ └── db.ts # Database connection ├── types/ # TypeScript type definitions └── utils/ # Utility functions + +# Docker Configuration +docker-compose.dev.yml # Development Docker Compose setup +Dockerfile.dev # Development Docker image +docker/ # Docker-related scripts + └── init-db.sh # Database initialization script + +# Environment Templates +.env.example # Environment template (copy to .env) +.env # Your environment variables (create from .env.example, gitignored) ``` ## Security Features -- **Multi-signature Security**: All transactions require multiple signatures -- **Message Signing**: Cryptographic verification of wallet ownership -- **JWT Authentication**: Secure token-based authentication -- **Input Validation**: Comprehensive input sanitization and validation -- **CORS Protection**: Cross-origin request security -- **Nonce System**: Replay attack prevention +- Multi-signature security for all transactions +- Cryptographic message signing for wallet verification +- JWT-based authentication +- Comprehensive input validation +- CORS protection +- Nonce system to prevent replay attacks +- End-to-end TypeScript type safety -## Supported Networks -- **Mainnet**: Production Cardano network -- **Preprod**: Cardano testnet for development and testing +## Troubleshooting +### Database Connection Issues -## License +```bash +# Check if PostgreSQL is running +docker compose -f docker-compose.dev.yml ps -This project is licensed under the Apache License - see the [LICENSE.md](LICENSE.md) file for details. +# View database logs +docker compose -f docker-compose.dev.yml logs postgres -## Support +# Restart PostgreSQL +docker compose -f docker-compose.dev.yml restart postgres +``` + +### Migration Issues + +```bash +# Reset database (⚠️ deletes all data) +npm run db:push -- --force-reset -- **Documentation**: Check our documentation -- **Issues**: Report bugs and request features on GitHub Issues -- **Discord**: Join our Discord community for support at [https://discord.gg/eyTT9k2KZq](https://discord.gg/eyTT9k2KZq) look for ask multisig platform. +# Check migration status +npx prisma migrate status +# Apply pending migrations +npx prisma migrate deploy +``` + +### Environment Variables + +Ensure all required variables are set in `.env`. The app will show helpful error messages if any are missing. + +### Port Conflicts + +If port 3000 is already in use: +```bash +# Use a different port +PORT=3001 npm run dev +``` ---- +## Supported Networks + +- **Mainnet** - Production Cardano network +- **Preprod** - Cardano testnet for development + +## License + +This project is licensed under the Apache License 2.0 - see the [LICENSE.md](LICENSE.md) file for details. + +## Support -**Built for the Cardano ecosystem** +- **Discord**: [Join our community](https://discord.gg/eyTT9k2KZq) - look for "ask multisig platform" channel +- **GitHub Issues**: [Report bugs](https://github.com/MeshJS/multisig/issues) +- **GitHub Discussions**: [Ask questions](https://github.com/MeshJS/multisig/discussions) diff --git a/VERCEL_DATABASE_SETUP.md b/VERCEL_DATABASE_SETUP.md deleted file mode 100644 index 50b592c..0000000 --- a/VERCEL_DATABASE_SETUP.md +++ /dev/null @@ -1,88 +0,0 @@ -# Vercel Database Connection Setup Guide - -## Problem -Your Vercel deployment is losing database connections because the `DATABASE_URL` is incorrectly configured. The error shows Prisma is trying to connect to port 5432 (direct connection) instead of port 6543 (pooled connection). - -## Solution: Configure Supabase Connection Pooling - -### Step 1: Get Your Supabase Connection URLs - -1. Go to your Supabase Dashboard -2. Navigate to **Settings** → **Database** -3. Find the **Connection Pooling** section - -### Step 2: Set Environment Variables in Vercel - -You need to set **two** environment variables in Vercel: - -#### 1. `DATABASE_URL` (for queries - REQUIRED) -- Use the **Connection Pooling** → **Transaction mode** URL -- Format: `postgresql://postgres.[project-ref]:[password]@aws-0-[region].pooler.supabase.com:6543/postgres?pgbouncer=true` -- **Important**: Must use port **6543** (not 5432) -- **Important**: Must include `?pgbouncer=true` parameter - -#### 2. `DIRECT_URL` (for migrations - OPTIONAL but recommended) -- Use the **Connection String** → **URI** (direct connection) -- Format: `postgresql://postgres:[password]@aws-0-[region].pooler.supabase.com:5432/postgres` -- This is used only for migrations (`prisma migrate`) - -### Step 3: Verify Your Configuration - -After setting the environment variables, check your Vercel deployment logs. You should see: - -✅ **Correct configuration:** -- No errors about port 5432 -- Connection pooler URL with port 6543 - -❌ **Wrong configuration (what you likely have now):** -- Error: "DATABASE_URL uses pooler hostname but wrong port (5432)" -- Connection errors: "Can't reach database server" - -### Example Correct URLs - -**DATABASE_URL (pooled - for queries):** -``` -postgresql://postgres.abcdefghijklmnop:[YOUR-PASSWORD]@aws-0-us-east-1.pooler.supabase.com:6543/postgres?pgbouncer=true -``` - -**DIRECT_URL (direct - for migrations):** -``` -postgresql://postgres:[YOUR-PASSWORD]@aws-0-us-east-1.pooler.supabase.com:5432/postgres -``` - -## Why This Matters - -- **Port 6543**: Supabase's connection pooler (PgBouncer) - optimized for serverless -- **Port 5432**: Direct PostgreSQL connection - not suitable for Vercel serverless -- **Connection Pooling**: Prevents connection exhaustion in serverless environments -- **Retry Logic**: The code now includes automatic retry logic for connection failures - -## Additional Improvements Made - -1. ✅ **Connection Retry Logic**: Automatic retry with exponential backoff (3 attempts) -2. ✅ **Connection Health Checks**: Validates connection URL on startup -3. ✅ **Better Error Logging**: Production logs now show connection errors -4. ✅ **Connection Reuse**: Prisma client is reused across serverless invocations - -## Testing - -After updating your Vercel environment variables: - -1. Redeploy your application -2. Check Vercel logs for any connection warnings -3. Test database queries - they should work reliably now -4. Monitor for connection errors in production - -## Troubleshooting - -If you still see connection errors: - -1. **Verify DATABASE_URL format**: Must have `:6543` and `?pgbouncer=true` -2. **Check Supabase Dashboard**: Ensure connection pooling is enabled -3. **Check Vercel Logs**: Look for the validation messages on startup -4. **Test Connection**: Try connecting manually with the pooled URL - -## Need Help? - -Check your Vercel deployment logs for specific error messages. The code now provides detailed warnings about incorrect configuration. - diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..7d9862f --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,110 @@ +services: + postgres: + image: postgres:14-alpine + container_name: multisig-postgres-dev + ports: + - "5433:5432" + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: multisig + volumes: + - postgres-dev-data:/var/lib/postgresql/data + - ./docker/init-db.sh:/docker-entrypoint-initdb.d/init-db.sh:ro + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - multisig-network + + app: + build: + context: . + dockerfile: Dockerfile.dev + container_name: multisig-app-dev + ports: + - "3000:3000" + environment: + - NODE_ENV=development + - DATABASE_URL=postgresql://postgres:postgres@postgres:5432/multisig + - DIRECT_URL=postgresql://postgres:postgres@postgres:5432/multisig + env_file: + - .env + volumes: + - .:/app + - /app/node_modules + - /app/.next + depends_on: + postgres: + condition: service_healthy + networks: + - multisig-network + command: > + sh -c " + echo 'Waiting for PostgreSQL to be ready...' && + until pg_isready -h postgres -p 5432 -U postgres; do + sleep 1 + done && + echo 'PostgreSQL is ready!' && + echo 'Running database migrations...' && + npx prisma migrate deploy || npx prisma db push && + echo 'Starting development server...' && + npm run dev + " + + # Optional: Database initialization service + # Note: For better migration management, use Ansible instead: + # cd ansible && make setup + # or: cd ansible && ansible-playbook playbooks/dev-db-setup.yml + db-init: + image: node:20-alpine + container_name: multisig-db-init-dev + working_dir: /app + environment: + DATABASE_URL: postgresql://postgres:postgres@postgres:5432/multisig + DIRECT_URL: postgresql://postgres:postgres@postgres:5432/multisig + volumes: + - .:/app + - /app/node_modules + - ./docker/ensure-roles.sh:/docker/ensure-roles.sh:ro + - ./docker/fix-crowdfund-rls.sh:/docker/fix-crowdfund-rls.sh:ro + command: > + sh -c " + apk add --no-cache postgresql-client && + echo 'Waiting for PostgreSQL to be ready...' && + until pg_isready -h postgres -p 5432 -U postgres; do + sleep 1 + done && + echo 'PostgreSQL is ready!' && + echo 'Ensuring required roles exist...' && + sh /docker/ensure-roles.sh && + echo 'Installing dependencies...' && + npm ci --silent && + echo 'Running database migrations...' && + if ! npx prisma migrate deploy; then + echo 'Migration failed, attempting to fix Crowdfund table issue...' && + sh /docker/fix-crowdfund-rls.sh || true && + echo 'Retrying migrations...' && + npx prisma migrate deploy + fi && + echo 'Applying post-migration fixes...' && + sh /docker/fix-crowdfund-rls.sh || true && + echo 'Database migrations completed successfully!' + " + depends_on: + postgres: + condition: service_healthy + networks: + - multisig-network + profiles: + - db-init # Only start with: docker compose --profile db-init up + +volumes: + postgres-dev-data: + +networks: + multisig-network: + driver: bridge + diff --git a/package-lock.json b/package-lock.json index 5e78956..c700bcc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -165,95 +165,6 @@ "openapi-types": ">=7" } }, - "node_modules/@auth/core": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@auth/core/-/core-0.34.3.tgz", - "integrity": "sha512-jMjY/S0doZnWYNV90x0jmU3B+UcrsfGYnukxYrRbj0CVvGI/MX3JbHsxSrx2d4mbnXaUsqJmAcDfoQWA6r0lOw==", - "license": "ISC", - "optional": true, - "peer": true, - "dependencies": { - "@panva/hkdf": "^1.1.1", - "@types/cookie": "0.6.0", - "cookie": "0.6.0", - "jose": "^5.1.3", - "oauth4webapi": "^2.10.4", - "preact": "10.11.3", - "preact-render-to-string": "5.2.3" - }, - "peerDependencies": { - "@simplewebauthn/browser": "^9.0.1", - "@simplewebauthn/server": "^9.0.2", - "nodemailer": "^7" - }, - "peerDependenciesMeta": { - "@simplewebauthn/browser": { - "optional": true - }, - "@simplewebauthn/server": { - "optional": true - }, - "nodemailer": { - "optional": true - } - } - }, - "node_modules/@auth/core/node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@auth/core/node_modules/jose": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/jose/-/jose-5.10.0.tgz", - "integrity": "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==", - "license": "MIT", - "optional": true, - "peer": true, - "funding": { - "url": "https://github.com/sponsors/panva" - } - }, - "node_modules/@auth/core/node_modules/preact": { - "version": "10.11.3", - "resolved": "https://registry.npmjs.org/preact/-/preact-10.11.3.tgz", - "integrity": "sha512-eY93IVpod/zG3uMF22Unl8h9KkrcKIRs2EGar8hwLZZDU1lkjph303V9HZBwufh2s736U6VXuhD109LYqPoffg==", - "license": "MIT", - "optional": true, - "peer": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/preact" - } - }, - "node_modules/@auth/core/node_modules/preact-render-to-string": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-5.2.3.tgz", - "integrity": "sha512-aPDxUn5o3GhWdtJtW0svRC2SS/l8D9MAgo2+AWml+BhDImb27ALf04Q2d+AHqUUOc6RdSXFIBVa2gxzgMKgtZA==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "pretty-format": "^3.8.0" - }, - "peerDependencies": { - "preact": ">=10" - } - }, - "node_modules/@auth/core/node_modules/pretty-format": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-3.8.0.tgz", - "integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==", - "license": "MIT", - "optional": true, - "peer": true - }, "node_modules/@auth/prisma-adapter": { "version": "2.11.1", "resolved": "https://registry.npmjs.org/@auth/prisma-adapter/-/prisma-adapter-2.11.1.tgz", @@ -5439,26 +5350,6 @@ "node": ">=10.0.0" } }, - "node_modules/@simplewebauthn/browser": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/@simplewebauthn/browser/-/browser-9.0.1.tgz", - "integrity": "sha512-wD2WpbkaEP4170s13/HUxPcAV5y4ZXaKo1TfNklS5zDefPinIgXOpgz1kpEvobAsaLPa2KeH7AKKX/od1mrBJw==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@simplewebauthn/types": "^9.0.1" - } - }, - "node_modules/@simplewebauthn/types": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/@simplewebauthn/types/-/types-9.0.1.tgz", - "integrity": "sha512-tGSRP1QvsAvsJmnOlRQyw/mvK9gnPtjEc5fg2+m8n+QUa+D7rvrKkOYyfpy42GTs90X3RDOnqJgfHt+qO67/+w==", - "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", - "license": "MIT", - "optional": true, - "peer": true - }, "node_modules/@sinclair/typebox": { "version": "0.34.41", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz", @@ -6443,14 +6334,6 @@ "@types/node": "*" } }, - "node_modules/@types/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", - "license": "MIT", - "optional": true, - "peer": true - }, "node_modules/@types/cors": { "version": "2.8.19", "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", @@ -19548,17 +19431,6 @@ "integrity": "sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==", "license": "MIT" }, - "node_modules/oauth4webapi": { - "version": "2.17.0", - "resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-2.17.0.tgz", - "integrity": "sha512-lbC0Z7uzAFNFyzEYRIC+pkSVvDHJTbEW+dYlSBAlCYDe6RxUkJ26bClhk8ocBZip1wfI9uKTe0fm4Ib4RHn6uQ==", - "license": "MIT", - "optional": true, - "peer": true, - "funding": { - "url": "https://github.com/sponsors/panva" - } - }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -23159,23 +23031,6 @@ "node": ">=8.10.0" } }, - "node_modules/tailwindcss/node_modules/yaml": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", - "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", - "license": "ISC", - "optional": true, - "peer": true, - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - }, - "funding": { - "url": "https://github.com/sponsors/eemeli" - } - }, "node_modules/tar-fs": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", @@ -24469,21 +24324,6 @@ "node": ">=12.20.0" } }, - "node_modules/utf-8-validate": { - "version": "5.0.10", - "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz", - "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "node-gyp-build": "^4.3.0" - }, - "engines": { - "node": ">=6.14.2" - } - }, "node_modules/util": { "version": "0.12.5", "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", diff --git a/src/components/pages/homepage/wallets/SectionExplanation.tsx b/src/components/pages/homepage/wallets/SectionExplanation.tsx index 3aea837..6730639 100644 --- a/src/components/pages/homepage/wallets/SectionExplanation.tsx +++ b/src/components/pages/homepage/wallets/SectionExplanation.tsx @@ -18,6 +18,3 @@ export default function SectionExplanation({ ); } - - - diff --git a/src/components/pages/homepage/wallets/WalletBalanceSkeleton.tsx b/src/components/pages/homepage/wallets/WalletBalanceSkeleton.tsx index 810f071..5655a67 100644 --- a/src/components/pages/homepage/wallets/WalletBalanceSkeleton.tsx +++ b/src/components/pages/homepage/wallets/WalletBalanceSkeleton.tsx @@ -12,6 +12,3 @@ export default function WalletBalanceSkeleton() { /> ); } - - - diff --git a/src/env.js b/src/env.js index e914d13..982df66 100644 --- a/src/env.js +++ b/src/env.js @@ -13,7 +13,7 @@ export const env = createEnv({ .enum(["development", "test", "production"]) .default("development"), PINATA_JWT: z.string(), - GITHUB_TOKEN: z.string(), + GITHUB_TOKEN: z.string().optional(), JWT_SECRET: z.string().min(32), // NEXTAUTH_SECRET: // process.env.NODE_ENV === "production" diff --git a/src/pages/api/github/create-issue.ts b/src/pages/api/github/create-issue.ts index 05f4aa3..dfff404 100644 --- a/src/pages/api/github/create-issue.ts +++ b/src/pages/api/github/create-issue.ts @@ -11,6 +11,10 @@ interface Request extends NextApiRequest { } export default async function handler(req: Request, res: NextApiResponse) { + if (!env.GITHUB_TOKEN) { + return res.status(503).json({ error: "GitHub integration not configured" }); + } + const octokit = new Octokit({ auth: env.GITHUB_TOKEN, }); diff --git a/src/utils/query-invalidation.ts b/src/utils/query-invalidation.ts index 02c10ba..2d356b0 100644 --- a/src/utils/query-invalidation.ts +++ b/src/utils/query-invalidation.ts @@ -116,4 +116,3 @@ export class QueryInvalidation { export function getQueryInvalidation(utils: ReturnType) { return new QueryInvalidation(utils); } - From 8dd635c612a731ba82b8f9292a6f843e2118a36a Mon Sep 17 00:00:00 2001 From: QSchlegel Date: Thu, 18 Dec 2025 15:31:46 +0100 Subject: [PATCH 3/6] Enhance deposit transaction logic and user balance fetching - Implemented direct fetching of user balance from the blockchain, improving accuracy and reliability. - Added fallback logic to retrieve balance from user assets if blockchain data is unavailable. - Enforced ADA-only deposits by restricting asset selection and validating transaction inputs. - Updated UI components to reflect changes in asset handling and ensure consistent user experience. - Introduced an import transaction feature in the CardBalance component for enhanced transaction management. --- .../wallet/new-transaction/deposit/index.tsx | 97 ++--- .../wallet/transactions/card-balance.tsx | 16 + .../import-transaction-dialog.tsx | 128 +++++++ src/server/api/routers/transactions.ts | 357 ++++++++++++++++++ 4 files changed, 550 insertions(+), 48 deletions(-) create mode 100644 src/components/pages/wallet/transactions/import-transaction-dialog.tsx diff --git a/src/components/pages/wallet/new-transaction/deposit/index.tsx b/src/components/pages/wallet/new-transaction/deposit/index.tsx index 9526d56..79618c2 100644 --- a/src/components/pages/wallet/new-transaction/deposit/index.tsx +++ b/src/components/pages/wallet/new-transaction/deposit/index.tsx @@ -24,6 +24,7 @@ import { ToastAction } from "@/components/ui/toast"; import { useToast } from "@/hooks/use-toast"; import { useRouter } from "next/router"; import { cn } from "@/lib/utils"; +import { getBalanceFromUtxos } from "@/utils/getBalance"; export default function PageNewTransaction() { const { connected, wallet } = useWallet(); @@ -42,16 +43,39 @@ export default function PageNewTransaction() { const router = useRouter(); const userAssets = useUserStore((state) => state.userAssets); const userAssetMetadata = useUserStore((state) => state.userAssetMetadata); + const [userBalance, setUserBalance] = useState(0); useEffect(() => { reset(); }, []); - const userBalance = useMemo(() => { - const lovelace = - userAssets.find((asset) => asset.unit === "lovelace")?.quantity || 0; - return Number(lovelace) / Math.pow(10, 6); - }, [userAssets]); + // Fetch user balance directly from blockchain + useEffect(() => { + async function fetchUserBalance() { + if (!userAddress || network === undefined || network === null) { + // Fallback to userAssets if address/network not available + const lovelace = + userAssets.find((asset) => asset.unit === "lovelace")?.quantity || 0; + setUserBalance(Number(lovelace) / Math.pow(10, 6)); + return; + } + + try { + const blockchainProvider = getProvider(network); + const utxos = await blockchainProvider.fetchAddressUTxOs(userAddress); + const balance = getBalanceFromUtxos(utxos); + setUserBalance(balance ?? 0); + } catch (error) { + console.error("Failed to fetch user balance:", error); + // Fallback to userAssets if available + const lovelace = + userAssets.find((asset) => asset.unit === "lovelace")?.quantity || 0; + setUserBalance(Number(lovelace) / Math.pow(10, 6)); + } + } + + fetchUserBalance(); + }, [userAddress, network, userAssets]); function reset() { setMetadata(""); @@ -82,9 +106,11 @@ export default function PageNewTransaction() { > = {}; // reduce assets and amounts to Asset: Amount object + // Only ADA deposits are allowed for (let i = 0; i < assets.length; i++) { const unit = assets[i] ?? ""; - if (unit === "ADA") { + // Only process ADA deposits + if (unit === "ADA" || unit === "lovelace" || unit === "") { if (assetsAmounts.lovelace) { assetsAmounts.lovelace.amount += Number(amounts[i]) ?? 0; } else { @@ -95,19 +121,8 @@ export default function PageNewTransaction() { unit: "lovelace", }; } - } else { - if (assetsAmounts[unit]) { - assetsAmounts[unit].amount += Number(amounts[i]) ?? 0; - } else { - const asset = userWalletAssets.find((asset) => asset.unit === unit); - assetsAmounts[unit] = { - amount: Number(amounts[i]) ?? 0, - assetName: asset?.assetName ?? unit, - decimals: userAssetMetadata[unit]?.decimals ?? 0, - unit: asset?.unit ?? "", - }; - } } + // Ignore any non-ADA assets } return assetsAmounts; }, [amounts, assets, userWalletAssets, userAssetMetadata]); @@ -149,17 +164,17 @@ export default function PageNewTransaction() { for (let i = 0; i < UTxoCount; i++) { if (address && address.startsWith("addr") && address.length > 0) { const rawUnit = assets[i]; - // Default to 'lovelace' if rawUnit is undefined or if it's 'ADA' - const unit = rawUnit - ? rawUnit === "ADA" - ? "lovelace" - : rawUnit - : "lovelace"; - const assetMetadata = userAssetMetadata[unit]; - const multiplier = - unit === "lovelace" - ? 1000000 - : Math.pow(10, assetMetadata?.decimals ?? 0); + // Only allow ADA deposits - enforce lovelace unit + const unit = "lovelace"; + + // Validate that only ADA is being deposited + if (rawUnit && rawUnit !== "ADA" && rawUnit !== "lovelace") { + setError("Only ADA can be deposited. Please select ADA for all UTxOs."); + setLoading(false); + return; + } + + const multiplier = 1000000; // ADA always uses 6 decimals const parsedAmount = parseFloat(amounts[i]!) || 0; const thisAmount = parsedAmount * multiplier; outputs.push({ @@ -242,7 +257,7 @@ export default function PageNewTransaction() { function addNewUTxO() { setUTxoCount(UTxoCount + 1); setAmounts([...amounts, "100"]); - setAssets([...assets, "ADA"]); + setAssets([...assets, "ADA"]); // Only ADA can be deposited } return ( @@ -384,21 +399,6 @@ function UTxORow({ }); }, [userAssets, userAssetMetadata]); - const assetOptions = useMemo(() => { - return ( - <> - {userWalletAssets.map((userWalletAsset) => { - return ( - - ); - })} - - ); - }, [userWalletAssets]); return ( @@ -420,10 +420,11 @@ function UTxORow({ diff --git a/src/components/pages/wallet/transactions/card-balance.tsx b/src/components/pages/wallet/transactions/card-balance.tsx index d92c38a..8724538 100644 --- a/src/components/pages/wallet/transactions/card-balance.tsx +++ b/src/components/pages/wallet/transactions/card-balance.tsx @@ -7,12 +7,15 @@ import type { Wallet } from "@/types/wallet"; import Link from "next/link"; import { useEffect, useState } from "react"; import { getBalanceFromUtxos } from "@/utils/getBalance"; +import { Upload } from "lucide-react"; +import ImportTransactionDialog from "./import-transaction-dialog"; export default function CardBalance({ appWallet }: { appWallet: Wallet }) { const walletsUtxos = useWalletsStore((state) => state.walletsUtxos); const walletAssets = useWalletsStore((state) => state.walletAssets); const utxos = walletsUtxos[appWallet.id]; const [balance, setBalance] = useState(0); + const [importDialogOpen, setImportDialogOpen] = useState(false); useEffect(() => { if(!utxos) return @@ -56,7 +59,20 @@ export default function CardBalance({ appWallet }: { appWallet: Wallet }) { New Transaction + + {/* Suggesting to disable the button if the balance is less than 0, or no previous transactions */} {/*