diff --git a/packages/backend/prisma/migrations/20260225080516_add_chain_id_for_account/migration.sql b/packages/backend/prisma/migrations/20260225080516_add_chain_id_for_account/migration.sql new file mode 100644 index 00000000..2d95a9a8 --- /dev/null +++ b/packages/backend/prisma/migrations/20260225080516_add_chain_id_for_account/migration.sql @@ -0,0 +1,5 @@ +-- AlterTable +ALTER TABLE "accounts" ADD COLUMN "chain_id" INTEGER NOT NULL DEFAULT 26514; + +-- AlterTable +ALTER TABLE "votes" ALTER COLUMN "domain_id" DROP DEFAULT; diff --git a/packages/backend/prisma/schema.prisma b/packages/backend/prisma/schema.prisma index 11b347ce..84d1f263 100644 --- a/packages/backend/prisma/schema.prisma +++ b/packages/backend/prisma/schema.prisma @@ -28,6 +28,7 @@ model Account { address String @unique name String threshold Int + chainId Int @default(26514) @map("chain_id") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") signers AccountSigner[] @@ -107,7 +108,7 @@ model Vote { zkVerifyTxHash String? @map("zkverify_tx_hash") aggregationId String? @map("aggregation_id") - domainId Int? @default(175) @map("domain_id") + domainId Int? @map("domain_id") merkleProof String[] @map("merkle_proof") leafCount Int? @map("leaf_count") leafIndex Int? @map("leaf_index") diff --git a/packages/backend/src/account/account.controller.ts b/packages/backend/src/account/account.controller.ts index 82b2eba6..e57a7760 100644 --- a/packages/backend/src/account/account.controller.ts +++ b/packages/backend/src/account/account.controller.ts @@ -15,7 +15,11 @@ import { Patch, UseGuards, } from '@nestjs/common'; -import { CreateAccountDto, UpdateAccountDto } from '@polypay/shared'; +import { + CreateAccountDto, + CreateAccountBatchDto, + UpdateAccountDto, +} from '@polypay/shared'; import { JwtAuthGuard } from '@/auth/guards/jwt-auth.guard'; import { CurrentUser } from '@/auth/decorators/current-user.decorator'; import { AccountMemberGuard } from '@/auth/guards/account-member.guard'; @@ -71,6 +75,35 @@ export class AccountController { return this.accountService.create(dto, user.commitment, dto.userAddress); } + /** + * Batch create multisig accounts on multiple chains + * POST /api/accounts/batch + */ + @Post('batch') + @UseGuards(JwtAuthGuard) + @ApiBearerAuth('JWT-auth') + @ApiOperation({ + summary: 'Batch create multisig accounts on multiple chains', + description: + 'Deploy multiple multisig accounts with the same configuration across different chains in a single request.', + }) + @ApiBody({ + type: CreateAccountBatchDto, + }) + @ApiResponse({ status: 201, description: 'Accounts created successfully' }) + @ApiResponse({ status: 400, description: 'Bad request - Invalid data' }) + @ApiResponse({ status: 401, description: 'Unauthorized - Invalid token' }) + async createBatch( + @CurrentUser() user: User, + @Body() dto: CreateAccountBatchDto, + ) { + return this.accountService.createBatch( + dto, + user.commitment, + dto.userAddress, + ); + } + /** * Get multisig account by address * GET /api/accounts/:address diff --git a/packages/backend/src/account/account.service.ts b/packages/backend/src/account/account.service.ts index 8a2ea369..97c66c52 100644 --- a/packages/backend/src/account/account.service.ts +++ b/packages/backend/src/account/account.service.ts @@ -5,7 +5,11 @@ import { BadRequestException, } from '@nestjs/common'; import { PrismaService } from '@/database/prisma.service'; -import { CreateAccountDto, UpdateAccountDto } from '@polypay/shared'; +import { + CreateAccountDto, + CreateAccountBatchDto, + UpdateAccountDto, +} from '@polypay/shared'; import { RelayerService } from '@/relayer-wallet/relayer-wallet.service'; import { EventsService } from '@/events/events.service'; import { @@ -43,10 +47,11 @@ export class AccountService { ); } - // 3. Deploy contract via relayer + // 3. Deploy contract via relayer on specified chain const { address, txHash } = await this.relayerService.deployAccount( commitments, dto.threshold, + dto.chainId, ); this.logger.log(`Account deployed at ${address}, txHash: ${txHash}`); @@ -80,6 +85,7 @@ export class AccountService { address, name: dto.name, threshold: dto.threshold, + chainId: dto.chainId, }, }); @@ -142,6 +148,172 @@ export class AccountService { return this.findByAddress(address); } + async createBatch( + dto: CreateAccountBatchDto, + creatorCommitment: string, + userAddress?: string, + ) { + // 1. Validate creator is in signers list + const commitments = dto.signers.map((s) => s.commitment); + if (!commitments.includes(creatorCommitment)) { + throw new BadRequestException('Creator must be in signers list'); + } + + // 2. Validate threshold + if (dto.threshold > dto.signers.length) { + throw new BadRequestException( + 'Threshold cannot be greater than number of signers', + ); + } + + if (!dto.chainIds || dto.chainIds.length === 0) { + throw new BadRequestException('At least one chainId is required'); + } + + const uniqueChainIds = Array.from(new Set(dto.chainIds)); + if (uniqueChainIds.length !== dto.chainIds.length) { + throw new BadRequestException('Duplicate chainIds are not allowed'); + } + + // 3. Deploy contracts via relayer for all chains + const deployments: { chainId: number; address: string; txHash: string }[] = + []; + + for (const chainId of uniqueChainIds) { + try { + const { address, txHash } = await this.relayerService.deployAccount( + commitments, + dto.threshold, + chainId, + ); + + this.logger.log( + `Account deployed at ${address} on chain ${chainId}, txHash: ${txHash}`, + ); + + this.analyticsLogger.logCreateAccount(userAddress, address); + + deployments.push({ chainId, address, txHash }); + } catch (error) { + this.logger.error( + `Failed to deploy account on chain ${chainId}: ${error.message}`, + (error as Error).stack, + ); + throw new BadRequestException( + 'Failed to deploy account on one of the selected chains', + ); + } + } + + // 4. Persist all accounts and signers in a single transaction + const createdAccounts = await this.prisma.$transaction(async (prisma) => { + // Upsert all users (update name only if null) + const users = await Promise.all( + dto.signers.map(async (signer) => { + const existing = await prisma.user.findUnique({ + where: { commitment: signer.commitment }, + }); + + if (existing) { + return existing; + } + + return prisma.user.create({ + data: { + commitment: signer.commitment, + }, + }); + }), + ); + + const accounts = []; + + for (const deployment of deployments) { + const newAccount = await prisma.account.create({ + data: { + address: deployment.address, + name: dto.name, + threshold: dto.threshold, + chainId: deployment.chainId, + }, + }); + + await Promise.all( + users.map((user) => { + const signerDto = dto.signers.find( + (s) => s.commitment === user.commitment, + ); + return prisma.accountSigner.create({ + data: { + userId: user.id, + accountId: newAccount.id, + isCreator: user.commitment === creatorCommitment, + displayName: signerDto?.name || null, + }, + }); + }), + ); + + const fullAccount = await prisma.account.findUniqueOrThrow({ + where: { id: newAccount.id }, + include: { + signers: { + include: { + user: true, + }, + }, + }, + }); + + accounts.push(fullAccount); + } + + return accounts; + }); + + // 5. Emit events for each account (excluding creator) + for (const account of createdAccounts) { + const eventData: AccountCreatedEventData = { + accountAddress: account.address, + name: account.name, + threshold: account.threshold, + signerCount: account.signers.length, + createdAt: account.createdAt.toISOString(), + }; + + const otherSigners = account.signers + .map((as) => as.user.commitment) + .filter((commitment) => commitment !== creatorCommitment); + + if (otherSigners.length > 0) { + this.eventsService.emitToCommitments( + otherSigners, + ACCOUNT_CREATED_EVENT, + eventData, + ); + + this.logger.log( + `Emitted ${ACCOUNT_CREATED_EVENT} to ${otherSigners.length} other signers for account ${account.address}`, + ); + } + } + + // Return normalized response objects (similar to findByAddress) + return createdAccounts.map((account) => ({ + id: account.id, + address: account.address, + name: account.name, + threshold: account.threshold, + chainId: account.chainId, + createdAt: account.createdAt, + signers: account.signers.map((as) => ({ + commitment: as.user.commitment, + name: as.displayName, + isCreator: as.isCreator, + })), + })); + } + /** * Get multisig account by address with signers */ @@ -166,6 +338,7 @@ export class AccountService { address: account.address, name: account.name, threshold: account.threshold, + chainId: account.chainId, createdAt: account.createdAt, signers: account.signers.map((as) => ({ commitment: as.user.commitment, @@ -195,6 +368,7 @@ export class AccountService { address: account.address, name: account.name, threshold: account.threshold, + chainId: account.chainId, createdAt: account.createdAt, signers: account.signers.map((as) => ({ commitment: as.user.commitment, diff --git a/packages/backend/src/common/constants/proof.ts b/packages/backend/src/common/constants/proof.ts index 96e5319e..7ec8d80e 100644 --- a/packages/backend/src/common/constants/proof.ts +++ b/packages/backend/src/common/constants/proof.ts @@ -1,2 +1,6 @@ -export const DOMAIN_ID_HORIZEN_TESTNET = 175; -export const DOMAIN_ID_HORIZEN_MAINNET = 3; +export const DOMAIN_ID_BY_CHAIN_ID = { + 2651420: 175, // Horizen testnet + 84532: 2, // Base Sepolia + 26514: 3, // Horizen mainnet + 8453: 2, // Base mainnet +} as const; diff --git a/packages/backend/src/common/utils/proof.ts b/packages/backend/src/common/utils/proof.ts index 0663428c..4cffa340 100644 --- a/packages/backend/src/common/utils/proof.ts +++ b/packages/backend/src/common/utils/proof.ts @@ -1,10 +1,10 @@ -import { NetworkValue } from '@polypay/shared'; -import { - DOMAIN_ID_HORIZEN_MAINNET, - DOMAIN_ID_HORIZEN_TESTNET, -} from '../constants'; +import { DOMAIN_ID_BY_CHAIN_ID } from '../constants'; -export const getDomainId = (): number => { - const isMainnet = process.env.NETWORK === NetworkValue.mainnet; - return isMainnet ? DOMAIN_ID_HORIZEN_MAINNET : DOMAIN_ID_HORIZEN_TESTNET; +export const getDomainId = (chainId: number): number => { + const id = + DOMAIN_ID_BY_CHAIN_ID[chainId as keyof typeof DOMAIN_ID_BY_CHAIN_ID]; + if (id === undefined) { + throw new Error(`Unsupported chainId for domainId: ${chainId}`); + } + return id; }; diff --git a/packages/backend/src/relayer-wallet/relayer-wallet.service.ts b/packages/backend/src/relayer-wallet/relayer-wallet.service.ts index 6eb9f725..16cc8b14 100644 --- a/packages/backend/src/relayer-wallet/relayer-wallet.service.ts +++ b/packages/backend/src/relayer-wallet/relayer-wallet.service.ts @@ -7,24 +7,31 @@ import { } from 'viem'; import { privateKeyToAccount } from 'viem/accounts'; import { - getChain, - getContractConfig, - NetworkType, ZERO_ADDRESS, + getChainById, + getContractConfigByChainId, } from '@polypay/shared'; import { METAMULTISIG_ABI, METAMULTISIG_BYTECODE } from '@polypay/shared'; import { ConfigService } from '@nestjs/config'; import { CONFIG_KEYS } from '@/config/config.keys'; import { waitForReceiptWithRetry } from '@/common/utils/retry'; +type RelayerChainClient = { + chain: any; + publicClient: any; + walletClient: any; + contractConfig: { + zkVerifyAddress: `0x${string}`; + vkHash: string; + poseidonT3Address: `0x${string}` | string; + }; +}; + @Injectable() export class RelayerService { private readonly logger = new Logger(RelayerService.name); - private account; - private publicClient; - private walletClient; - private chain; - private contractConfig; + private readonly account; + private readonly clientsByChainId = new Map(); constructor(private readonly configService: ConfigService) { const privateKey = this.configService.get( @@ -35,58 +42,83 @@ export class RelayerService { throw new Error('RELAYER_WALLET_KEY is not set'); } - // Get network config from env - const network = (this.configService.get(CONFIG_KEYS.APP_NETWORK) || - 'testnet') as NetworkType; - this.chain = getChain(network); - this.contractConfig = getContractConfig(network); - this.account = privateKeyToAccount(privateKey); - this.publicClient = createPublicClient({ - chain: this.chain, - transport: http(), - }); + // Initialize clients for all supported chains + const supportedChainIds = [2651420, 84532, 26514, 8453]; - this.walletClient = createWalletClient({ - account: this.account, - chain: this.chain, - transport: http(), - }); + for (const chainId of supportedChainIds) { + const chain = getChainById(chainId); + const contractConfig = getContractConfigByChainId(chainId); + + const publicClient = createPublicClient({ + chain, + transport: http(), + }); + + const walletClient = createWalletClient({ + account: this.account, + chain, + transport: http(), + }); + + this.clientsByChainId.set(chainId, { + chain, + publicClient, + walletClient, + contractConfig, + }); + } this.logger.log( - `Relayer initialized with address: ${this.account.address}`, + `Relayer initialized with address: ${this.account.address} for chains: ${Array.from( + this.clientsByChainId.keys(), + ).join(', ')}`, ); } + private getChainClient(chainId: number): RelayerChainClient { + const client = this.clientsByChainId.get(chainId); + if (!client) { + throw new Error(`Relayer: unsupported chainId ${chainId}`); + } + return client; + } + /** * Deploy MetaMultiSigWallet contract */ async deployAccount( commitments: string[], threshold: number, + chainId: number, ): Promise<{ address: string; txHash: string }> { + const { chain, walletClient, publicClient, contractConfig } = + this.getChainClient(chainId); + const commitmentsBigInt = commitments.map((c) => BigInt(c)); // Deploy contract - const txHash = await this.walletClient.deployContract({ + const txHash = await walletClient.deployContract({ abi: METAMULTISIG_ABI, bytecode: METAMULTISIG_BYTECODE, args: [ - this.contractConfig.zkVerifyAddress, - this.contractConfig.vkHash, - BigInt(this.chain.id), + contractConfig.zkVerifyAddress, + contractConfig.vkHash, + BigInt(chain.id), commitmentsBigInt, BigInt(threshold), ], account: this.account, - chain: this.chain, + chain, }); - this.logger.log(`Deploy tx sent: ${txHash}`); + this.logger.log( + `Deploy tx sent on chain ${chainId} for relayer ${this.account.address}: ${txHash}`, + ); // Wait for receipt - const receipt = await waitForReceiptWithRetry(this.publicClient, txHash); + const receipt = await waitForReceiptWithRetry(publicClient, txHash); if (!receipt.contractAddress) { throw new Error('Contract deployment failed - no address returned'); @@ -109,6 +141,7 @@ export class RelayerService { to: string, value: string, data: string, + chainId: number, zkProofs: { commitment: string; nullifier: string; @@ -119,8 +152,10 @@ export class RelayerService { index: number; }[], ): Promise<{ txHash: string }> { + const { publicClient, walletClient, chain } = this.getChainClient(chainId); + // 1. Check account ETH balance - const balance = await this.publicClient.getBalance({ + const balance = await publicClient.getBalance({ address: accountAddress as `0x${string}`, }); @@ -245,7 +280,7 @@ export class RelayerService { for (const [tokenAddress, requiredAmount] of Object.entries( erc20Requirements, )) { - const tokenBalance = await this.publicClient.readContract({ + const tokenBalance = await publicClient.readContract({ address: tokenAddress as `0x${string}`, abi: [ { @@ -291,7 +326,7 @@ export class RelayerService { ] as const; // 3. Estimate gas - const gasEstimate = await this.publicClient.estimateContractGas({ + const gasEstimate = await publicClient.estimateContractGas({ address: accountAddress as `0x${string}`, abi: METAMULTISIG_ABI, functionName: 'execute', @@ -302,20 +337,20 @@ export class RelayerService { this.logger.log(`Gas estimate for execute: ${gasEstimate}`); // 4. Execute - const txHash = await this.walletClient.writeContract({ + const txHash = await walletClient.writeContract({ address: accountAddress as `0x${string}`, abi: METAMULTISIG_ABI, functionName: 'execute', args, account: this.account, - chain: this.chain, + chain, gas: gasEstimate + 50000n, }); this.logger.log(`Execute tx sent: ${txHash}`); // 5. Wait for receipt and verify status - const receipt = await waitForReceiptWithRetry(this.publicClient, txHash); + const receipt = await waitForReceiptWithRetry(publicClient, txHash); if (receipt.status === 'reverted') { throw new Error(`Transaction reverted on-chain. TxHash: ${txHash}`); diff --git a/packages/backend/src/transaction/transaction.service.ts b/packages/backend/src/transaction/transaction.service.ts index dfcb4d52..fd8bf04a 100644 --- a/packages/backend/src/transaction/transaction.service.ts +++ b/packages/backend/src/transaction/transaction.service.ts @@ -174,7 +174,7 @@ export class TransactionService { nullifier: dto.nullifier, jobId: proofResult.jobId, proofStatus: 'PENDING', - domainId: getDomainId(), + domainId: getDomainId(account.chainId), zkVerifyTxHash: proofResult.txHash, }, }); @@ -260,9 +260,10 @@ export class TransactionService { dto: ApproveTransactionDto, userCommitment: string, ) { - // 1. Check transaction exists + // 1. Check transaction exists (with account for chainId) const transaction = await this.prisma.transaction.findUnique({ where: { txId }, + include: { account: true }, }); if (!transaction) { @@ -328,7 +329,7 @@ export class TransactionService { nullifier: dto.nullifier, jobId: proofResult.jobId, proofStatus: ProofStatus.PENDING, - domainId: getDomainId(), + domainId: getDomainId(transaction.account.chainId), zkVerifyTxHash: proofResult.txHash, }, }); @@ -410,9 +411,10 @@ export class TransactionService { * Deny transaction */ async deny(txId: number, userCommitment: string, userAddress?: string) { - // 1. Check transaction exists + // 1. Check transaction exists (with account for chainId) const transaction = await this.prisma.transaction.findUnique({ where: { txId }, + include: { account: true }, }); if (!transaction) { @@ -893,6 +895,7 @@ export class TransactionService { // Check transaction exists const transaction = await this.prisma.transaction.findUnique({ where: { txId }, + include: { account: true }, }); if (!transaction) { @@ -917,6 +920,7 @@ export class TransactionService { executionData.to, executionData.value, executionData.data, + transaction.account.chainId, executionData.zkProofs, ); diff --git a/packages/backend/src/user/user.service.ts b/packages/backend/src/user/user.service.ts index f7e286c3..3aed8ea1 100644 --- a/packages/backend/src/user/user.service.ts +++ b/packages/backend/src/user/user.service.ts @@ -81,6 +81,7 @@ export class UserService { address: account.address, name: account.name, threshold: account.threshold, + chainId: account.chainId, createdAt: account.createdAt, updatedAt: account.updatedAt, signers: account.signers.map((signer) => ({ diff --git a/packages/nextjs/contracts/deployedContracts.ts b/packages/nextjs/contracts/deployedContracts.ts index 8cba1916..6b9a414a 100644 --- a/packages/nextjs/contracts/deployedContracts.ts +++ b/packages/nextjs/contracts/deployedContracts.ts @@ -5,724 +5,6 @@ import { GenericContractsDeclaration } from "~~/utils/scaffold-eth/contract"; const deployedContracts = { - 31337: { - MetaMultiSigWallet: { - address: "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0", - abi: [ - { - inputs: [ - { - internalType: "address", - name: "_zkvContract", - type: "address", - }, - { - internalType: "bytes32", - name: "_vkHash", - type: "bytes32", - }, - { - internalType: "uint256", - name: "_chainId", - type: "uint256", - }, - { - internalType: "uint256[]", - name: "_initialCommitments", - type: "uint256[]", - }, - { - internalType: "uint256", - name: "_signaturesRequired", - type: "uint256", - }, - ], - stateMutability: "nonpayable", - type: "constructor", - }, - { - anonymous: false, - inputs: [ - { - indexed: true, - internalType: "address", - name: "sender", - type: "address", - }, - { - indexed: false, - internalType: "uint256", - name: "amount", - type: "uint256", - }, - { - indexed: false, - internalType: "uint256", - name: "balance", - type: "uint256", - }, - ], - name: "Deposit", - type: "event", - }, - { - anonymous: false, - inputs: [ - { - indexed: true, - internalType: "uint256", - name: "commitment", - type: "uint256", - }, - { - indexed: false, - internalType: "bool", - name: "isAdded", - type: "bool", - }, - ], - name: "Owner", - type: "event", - }, - { - anonymous: false, - inputs: [ - { - indexed: true, - internalType: "uint256", - name: "txId", - type: "uint256", - }, - { - indexed: false, - internalType: "uint256", - name: "nullifier", - type: "uint256", - }, - ], - name: "SignatureSubmitted", - type: "event", - }, - { - anonymous: false, - inputs: [ - { - indexed: true, - internalType: "uint256", - name: "txId", - type: "uint256", - }, - { - indexed: false, - internalType: "address", - name: "to", - type: "address", - }, - { - indexed: false, - internalType: "uint256", - name: "value", - type: "uint256", - }, - { - indexed: false, - internalType: "bytes", - name: "data", - type: "bytes", - }, - { - indexed: false, - internalType: "bytes", - name: "result", - type: "bytes", - }, - ], - name: "TransactionExecuted", - type: "event", - }, - { - anonymous: false, - inputs: [ - { - indexed: true, - internalType: "uint256", - name: "txId", - type: "uint256", - }, - { - indexed: false, - internalType: "address", - name: "to", - type: "address", - }, - { - indexed: false, - internalType: "uint256", - name: "value", - type: "uint256", - }, - { - indexed: false, - internalType: "bytes", - name: "data", - type: "bytes", - }, - ], - name: "TransactionProposed", - type: "event", - }, - { - inputs: [], - name: "BN254_PRIME", - outputs: [ - { - internalType: "uint256", - name: "", - type: "uint256", - }, - ], - stateMutability: "view", - type: "function", - }, - { - inputs: [], - name: "MAX_SIGNERS", - outputs: [ - { - internalType: "uint256", - name: "", - type: "uint256", - }, - ], - stateMutability: "view", - type: "function", - }, - { - inputs: [], - name: "PROVING_SYSTEM_ID", - outputs: [ - { - internalType: "bytes32", - name: "", - type: "bytes32", - }, - ], - stateMutability: "view", - type: "function", - }, - { - inputs: [], - name: "VERSION_HASH", - outputs: [ - { - internalType: "bytes32", - name: "", - type: "bytes32", - }, - ], - stateMutability: "view", - type: "function", - }, - { - inputs: [ - { - internalType: "uint256", - name: "newCommitment", - type: "uint256", - }, - { - internalType: "uint256", - name: "newSigRequired", - type: "uint256", - }, - ], - name: "addSigner", - outputs: [], - stateMutability: "nonpayable", - type: "function", - }, - { - inputs: [], - name: "chainId", - outputs: [ - { - internalType: "uint256", - name: "", - type: "uint256", - }, - ], - stateMutability: "view", - type: "function", - }, - { - inputs: [ - { - internalType: "uint256", - name: "", - type: "uint256", - }, - ], - name: "commitments", - outputs: [ - { - internalType: "uint256", - name: "", - type: "uint256", - }, - ], - stateMutability: "view", - type: "function", - }, - { - inputs: [ - { - internalType: "uint256", - name: "txId", - type: "uint256", - }, - ], - name: "executeTransaction", - outputs: [ - { - internalType: "bytes", - name: "", - type: "bytes", - }, - ], - stateMutability: "nonpayable", - type: "function", - }, - { - inputs: [], - name: "getCommitments", - outputs: [ - { - internalType: "uint256[]", - name: "", - type: "uint256[]", - }, - ], - stateMutability: "view", - type: "function", - }, - { - inputs: [ - { - internalType: "uint256", - name: "txId", - type: "uint256", - }, - ], - name: "getPendingTx", - outputs: [ - { - internalType: "address", - name: "to", - type: "address", - }, - { - internalType: "uint256", - name: "value", - type: "uint256", - }, - { - internalType: "bytes", - name: "data", - type: "bytes", - }, - { - internalType: "uint256", - name: "validSignatures", - type: "uint256", - }, - { - internalType: "uint256", - name: "requiredApprovalsWhenExecuted", - type: "uint256", - }, - { - internalType: "bool", - name: "executed", - type: "bool", - }, - ], - stateMutability: "view", - type: "function", - }, - { - inputs: [], - name: "getSignersCount", - outputs: [ - { - internalType: "uint256", - name: "", - type: "uint256", - }, - ], - stateMutability: "view", - type: "function", - }, - { - inputs: [ - { - internalType: "uint256", - name: "_nonce", - type: "uint256", - }, - { - internalType: "address", - name: "to", - type: "address", - }, - { - internalType: "uint256", - name: "value", - type: "uint256", - }, - { - internalType: "bytes", - name: "data", - type: "bytes", - }, - ], - name: "getTransactionHash", - outputs: [ - { - internalType: "bytes32", - name: "", - type: "bytes32", - }, - ], - stateMutability: "view", - type: "function", - }, - { - inputs: [ - { - internalType: "uint256", - name: "txId", - type: "uint256", - }, - ], - name: "getTxHashFromTxid", - outputs: [ - { - internalType: "bytes32", - name: "", - type: "bytes32", - }, - ], - stateMutability: "view", - type: "function", - }, - { - inputs: [], - name: "merkleRoot", - outputs: [ - { - internalType: "uint256", - name: "", - type: "uint256", - }, - ], - stateMutability: "view", - type: "function", - }, - { - inputs: [], - name: "nonce", - outputs: [ - { - internalType: "uint256", - name: "", - type: "uint256", - }, - ], - stateMutability: "view", - type: "function", - }, - { - inputs: [ - { - internalType: "uint256", - name: "", - type: "uint256", - }, - ], - name: "pendingTxs", - outputs: [ - { - internalType: "address", - name: "to", - type: "address", - }, - { - internalType: "uint256", - name: "value", - type: "uint256", - }, - { - internalType: "bytes", - name: "data", - type: "bytes", - }, - { - internalType: "uint256", - name: "requiredApprovalsWhenExecuted", - type: "uint256", - }, - { - internalType: "uint256", - name: "validSignatures", - type: "uint256", - }, - { - internalType: "bool", - name: "executed", - type: "bool", - }, - ], - stateMutability: "view", - type: "function", - }, - { - inputs: [ - { - internalType: "uint256", - name: "a", - type: "uint256", - }, - { - internalType: "uint256", - name: "b", - type: "uint256", - }, - ], - name: "poseidonHash2", - outputs: [ - { - internalType: "uint256", - name: "", - type: "uint256", - }, - ], - stateMutability: "view", - type: "function", - }, - { - inputs: [ - { - internalType: "address", - name: "to", - type: "address", - }, - { - internalType: "uint256", - name: "value", - type: "uint256", - }, - { - internalType: "bytes", - name: "data", - type: "bytes", - }, - { - components: [ - { - internalType: "uint256", - name: "nullifier", - type: "uint256", - }, - { - internalType: "uint256", - name: "aggregationId", - type: "uint256", - }, - { - internalType: "uint256", - name: "domainId", - type: "uint256", - }, - { - internalType: "bytes32[]", - name: "zkMerklePath", - type: "bytes32[]", - }, - { - internalType: "uint256", - name: "leafCount", - type: "uint256", - }, - { - internalType: "uint256", - name: "index", - type: "uint256", - }, - ], - internalType: "struct MetaMultiSigWallet.ZkProof", - name: "proof", - type: "tuple", - }, - ], - name: "proposeTx", - outputs: [ - { - internalType: "uint256", - name: "txId", - type: "uint256", - }, - ], - stateMutability: "nonpayable", - type: "function", - }, - { - inputs: [ - { - internalType: "uint256", - name: "commitment", - type: "uint256", - }, - { - internalType: "uint256", - name: "newSigRequired", - type: "uint256", - }, - ], - name: "removeSigner", - outputs: [], - stateMutability: "nonpayable", - type: "function", - }, - { - inputs: [], - name: "signaturesRequired", - outputs: [ - { - internalType: "uint256", - name: "", - type: "uint256", - }, - ], - stateMutability: "view", - type: "function", - }, - { - inputs: [ - { - internalType: "uint256", - name: "txId", - type: "uint256", - }, - { - components: [ - { - internalType: "uint256", - name: "nullifier", - type: "uint256", - }, - { - internalType: "uint256", - name: "aggregationId", - type: "uint256", - }, - { - internalType: "uint256", - name: "domainId", - type: "uint256", - }, - { - internalType: "bytes32[]", - name: "zkMerklePath", - type: "bytes32[]", - }, - { - internalType: "uint256", - name: "leafCount", - type: "uint256", - }, - { - internalType: "uint256", - name: "index", - type: "uint256", - }, - ], - internalType: "struct MetaMultiSigWallet.ZkProof", - name: "proof", - type: "tuple", - }, - ], - name: "submitSignature", - outputs: [], - stateMutability: "nonpayable", - type: "function", - }, - { - inputs: [ - { - internalType: "uint256", - name: "newSigRequired", - type: "uint256", - }, - ], - name: "updateSignaturesRequired", - outputs: [], - stateMutability: "nonpayable", - type: "function", - }, - { - inputs: [ - { - internalType: "uint256", - name: "", - type: "uint256", - }, - ], - name: "usedNullifiers", - outputs: [ - { - internalType: "bool", - name: "", - type: "bool", - }, - ], - stateMutability: "view", - type: "function", - }, - { - inputs: [], - name: "vkHash", - outputs: [ - { - internalType: "bytes32", - name: "", - type: "bytes32", - }, - ], - stateMutability: "view", - type: "function", - }, - { - inputs: [], - name: "zkvContract", - outputs: [ - { - internalType: "address", - name: "", - type: "address", - }, - ], - stateMutability: "view", - type: "function", - }, - { - stateMutability: "payable", - type: "receive", - }, - ], - inheritedFunctions: {}, - deployedOnBlock: 4, - }, - }, 2651420: { MetaMultiSigWallet: { address: "0x1EaCA128069b2bb1cd476ef66E2701F98cAB148E", diff --git a/packages/nextjs/utils/network-config.ts b/packages/nextjs/utils/network-config.ts index 7a662ea0..c4f41a2e 100644 --- a/packages/nextjs/utils/network-config.ts +++ b/packages/nextjs/utils/network-config.ts @@ -1,5 +1,4 @@ -import { NetworkType, getChain, getContractConfig } from "@polypay/shared"; +import { NetworkType, getChain } from "@polypay/shared"; export const network = (process.env.NEXT_PUBLIC_NETWORK || "testnet") as NetworkType; export const chain = getChain(network); -export const contractConfig = getContractConfig(network); diff --git a/packages/shared/src/chains/baseMainnet.ts b/packages/shared/src/chains/baseMainnet.ts new file mode 100644 index 00000000..e6b451a7 --- /dev/null +++ b/packages/shared/src/chains/baseMainnet.ts @@ -0,0 +1,4 @@ +import { base } from "viem/chains"; + +// Re-export viem's Base mainnet chain definition for consistency with other chains module. +export const baseMainnet = base; diff --git a/packages/shared/src/chains/baseSepolia.ts b/packages/shared/src/chains/baseSepolia.ts new file mode 100644 index 00000000..b2966343 --- /dev/null +++ b/packages/shared/src/chains/baseSepolia.ts @@ -0,0 +1,4 @@ +import { baseSepolia } from "viem/chains"; + +// Re-export viem's Base Sepolia chain definition for consistency with other chains module. +export { baseSepolia }; diff --git a/packages/shared/src/chains/index.ts b/packages/shared/src/chains/index.ts index d2083650..a8966664 100644 --- a/packages/shared/src/chains/index.ts +++ b/packages/shared/src/chains/index.ts @@ -1,7 +1,9 @@ import { horizenTestnet } from "./horizenTestnet"; import { horizenMainnet } from "./horizenMainnet"; +import { baseSepolia } from "./baseSepolia"; +import { baseMainnet } from "./baseMainnet"; -export { horizenTestnet, horizenMainnet }; +export { horizenTestnet, horizenMainnet, baseSepolia, baseMainnet }; export type NetworkType = "testnet" | "mainnet"; export const NetworkValue = { @@ -9,6 +11,23 @@ export const NetworkValue = { testnet: "testnet", }; +// Backward-compatible helper used where only Horizen is needed. export const getChain = (network: NetworkType) => { return network === NetworkValue.mainnet ? horizenMainnet : horizenTestnet; }; + +// New helper: resolve chain by EVM chainId for multi-chain features. +export const getChainById = (chainId: number) => { + switch (chainId) { + case horizenTestnet.id: + return horizenTestnet; + case horizenMainnet.id: + return horizenMainnet; + case baseSepolia.id: + return baseSepolia; + case baseMainnet.id: + return baseMainnet; + default: + throw new Error(`Unsupported chainId: ${chainId}`); + } +}; diff --git a/packages/shared/src/contracts/contracts-config.ts b/packages/shared/src/contracts/contracts-config.ts index e6844f53..0b8f6400 100644 --- a/packages/shared/src/contracts/contracts-config.ts +++ b/packages/shared/src/contracts/contracts-config.ts @@ -1,13 +1,27 @@ -import { NetworkType } from "../chains"; - -export const CONTRACT_CONFIG = { - testnet: { +export const CONTRACT_CONFIG_BY_CHAIN_ID = { + 2651420: { + // Horizen testnet zkVerifyAddress: "0xCC02D0A54F3184dF4c88811E5b9FAb7ff8131e4a", vkHash: "0x80aca2e84f244400a76040aa5c77f9d83ff8409a2bf0d0cde96daffcf0a50e1b", poseidonT3Address: "0x3333333C0A88F9BE4fd23ed0536F9B6c427e3B93", }, - mainnet: { + 84532: { + // Base Sepolia + zkVerifyAddress: "0x0807C544D38aE7729f8798388d89Be6502A1e8A8", + vkHash: + "0x80aca2e84f244400a76040aa5c77f9d83ff8409a2bf0d0cde96daffcf0a50e1b", + poseidonT3Address: "0x3333333C0A88F9BE4fd23ed0536F9B6c427e3B93", + }, + 26514: { + // Horizen mainnet + zkVerifyAddress: "0xCb47A3C3B9Eb2E549a3F2EA4729De28CafbB2b69", + vkHash: + "0x80aca2e84f244400a76040aa5c77f9d83ff8409a2bf0d0cde96daffcf0a50e1b", + poseidonT3Address: "0x3333333C0A88F9BE4fd23ed0536F9B6c427e3B93", + }, + 8453: { + // Base mainnet zkVerifyAddress: "0xCb47A3C3B9Eb2E549a3F2EA4729De28CafbB2b69", vkHash: "0x80aca2e84f244400a76040aa5c77f9d83ff8409a2bf0d0cde96daffcf0a50e1b", @@ -15,6 +29,13 @@ export const CONTRACT_CONFIG = { }, } as const; -export const getContractConfig = (network: NetworkType) => { - return CONTRACT_CONFIG[network]; +export const getContractConfigByChainId = (chainId: number) => { + const config = + CONTRACT_CONFIG_BY_CHAIN_ID[ + chainId as keyof typeof CONTRACT_CONFIG_BY_CHAIN_ID + ]; + if (!config) { + throw new Error(`Unsupported chainId for contract config: ${chainId}`); + } + return config; }; diff --git a/packages/shared/src/dto/account/create-account.dto.ts b/packages/shared/src/dto/account/create-account.dto.ts index f2224fbe..346135aa 100644 --- a/packages/shared/src/dto/account/create-account.dto.ts +++ b/packages/shared/src/dto/account/create-account.dto.ts @@ -23,6 +23,33 @@ export class CreateAccountDto { @ArrayMinSize(1) signers: SignerData[]; + @IsNotEmpty() + @IsNumber() + chainId: number; + + @IsOptional() + @IsString() + userAddress?: string; +} + +export class CreateAccountBatchDto { + @IsNotEmpty() + @IsString() + name: string; + + @IsNotEmpty() + @IsNumber() + @Min(1) + threshold: number; + + @IsArray() + @ArrayMinSize(1) + signers: SignerData[]; + + @IsArray() + @ArrayMinSize(1) + chainIds: number[]; + @IsOptional() @IsString() userAddress?: string;