diff --git a/hardhat.config.ts b/hardhat.config.ts index 0e3fa29..df92294 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -35,13 +35,13 @@ const config: HardhatUserConfig = { networks: { hardhat: { forking: { - url: `https://zenchain-testnet.api.onfinality.io/rpc?apikey=${process.env.RPC_API_KEY}`, + url: `https://zenchain-testnet.api.onfinality.io/public`, }, loggingEnabled: true, chainId: 31337, }, zenchainTestnet: { - url: `https://zenchain-testnet.api.onfinality.io/rpc?apikey=${process.env.RPC_API_KEY}`, + url: `https://zenchain-testnet.api.onfinality.io/public`, chainId: 8408, accounts: [process.env.DEPLOYER_PRIVATE_KEY!], minGasPrice: 1000000000, diff --git a/package.json b/package.json index 84ae3fe..b47142a 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "deploy:localhost": "npx hardhat run scripts/deploy.ts --network localhost ---show-stack-traces", "deploy:testnet": "npx hardhat run scripts/deploy.ts --network zenchainTestnet ---show-stack-traces", "hardhat": "npx hardhat run --network zenchainTestnet --show-stack-traces", + "mint:testnet": "MINT_TO=0x123 MINT_AMOUNT=1000 npx hardhat run scripts/mintTokens.ts --network zenchainTestnet --show-stack-traces", "dev": "concurrently \"npm run node\" \"wait-on http://localhost:8545 && npm run deploy:localhost\"", "reset": "npm run clean && npm run dev" }, diff --git a/scripts/addLiquidity.ts b/scripts/addLiquidity.ts index 00c697b..796d6b6 100644 --- a/scripts/addLiquidity.ts +++ b/scripts/addLiquidity.ts @@ -1,6 +1,6 @@ import hre, { network } from "hardhat"; -import { readDeploymentRecord, addLiquidityToPair } from "./utils"; -import { Address, parseUnits } from "viem"; +import { readDeploymentRecord, addLiquidityToPair, tokenList, getTokenDecimals, getTokenBalance } from "./utils"; +import { Address, formatUnits } from "viem"; async function main() { const [walletClient] = await hre.viem.getWalletClients(); @@ -16,89 +16,96 @@ async function main() { console.log("Deployed Contracts:", deployedContracts); - // 1 ETH = 2750 USDC - console.log("Adding liquidity for ETH/USDC pair..."); - await addLiquidityToPair({ - walletClient, - publicClient, - routerAddress: deployedContracts.router as Address, - tokenA: deployedContracts.tokens.ETH as Address, - tokenB: deployedContracts.tokens.USDC as Address, - amountADesired: parseUnits("1", 18 + 3).toString(), - amountBDesired: parseUnits("2750", 18 + 3).toString(), - mintTokensA: true, // Mint ETH if it's a mock token - mintTokensB: true, // Mint USDC if it's a mock token - }); - - // 1 ETH = 2750 USDT - console.log("Adding liquidity for ETH/USDT pair..."); - await addLiquidityToPair({ - walletClient, - publicClient, - routerAddress: deployedContracts.router as Address, - tokenA: deployedContracts.tokens.ETH as Address, - tokenB: deployedContracts.tokens.USDT as Address, - amountADesired: parseUnits("1", 18 + 6).toString(), - amountBDesired: parseUnits("2750", 18 + 6).toString(), - mintTokensA: true, // Mint ETH if it's a mock token - mintTokensB: true, // Mint USDT if it's a mock token - }); - - // 1 ETH = 6.875 ZTC - console.log("Adding liquidity for ZTC/ETH pair..."); - await addLiquidityToPair({ - walletClient, - publicClient, - routerAddress: deployedContracts.router as Address, - tokenA: deployedContracts.tokens.ZTC as Address, - tokenB: deployedContracts.tokens.ETH as Address, - amountADesired: parseUnits("6.875", 18 + 1).toString(), - amountBDesired: parseUnits("1", 18 + 1).toString(), - mintTokensA: false, - mintTokensB: true, // Mint ETH if it's a mock token - }); - - // 1 ZTC = 400 USDC - console.log("Adding liquidity for ZTC/USDC pair..."); - await addLiquidityToPair({ - walletClient, - publicClient, - routerAddress: deployedContracts.router as Address, - tokenA: deployedContracts.tokens.ZTC as Address, - tokenB: deployedContracts.tokens.USDC as Address, - amountADesired: parseUnits("1", 18 + 1).toString(), - amountBDesired: parseUnits("400", 18 + 1).toString(), - mintTokensA: false, - mintTokensB: true, - }); - - // 1 ZTC = 400 USDT - console.log("Adding liquidity for ZTC/USDT pair..."); - await addLiquidityToPair({ - walletClient, - publicClient, - routerAddress: deployedContracts.router as Address, - tokenA: deployedContracts.tokens.ZTC as Address, - tokenB: deployedContracts.tokens.USDT as Address, - amountADesired: parseUnits("1", 18 + 2).toString(), - amountBDesired: parseUnits("400", 18 + 2).toString(), - mintTokensA: false, - mintTokensB: true, // Mint USDT if it's a mock token - }); - - //1 USDC = 1 USDT - console.log("Adding liquidity for USDC/USDT pair..."); - await addLiquidityToPair({ - walletClient, - publicClient, - routerAddress: deployedContracts.router as Address, - tokenA: deployedContracts.tokens.USDC as Address, - tokenB: deployedContracts.tokens.USDT as Address, - amountADesired: parseUnits("1", 18 + 5).toString(), - amountBDesired: parseUnits("1", 18 + 5).toString(), - mintTokensA: true, // Mint USDC if it's a mock token - mintTokensB: true, // Mint USDT if it's a mock token - }); + // Define USD price map for consistent ratios + const priceUSD: Record = { + ETH: 2750, + USDC: 1, + USDT: 1, + ZTC: 400, + BTC: 55000, + }; + + // Utility to safely extract token symbols from pair key like "ETHUSDC" + const getPairSymbols = (pairKey: string): [keyof typeof deployedContracts.tokens, keyof typeof deployedContracts.tokens] | null => { + for (const a of tokenList) { + if (pairKey.startsWith(a)) { + const b = pairKey.slice(a.length) as keyof typeof deployedContracts.tokens; + if ((tokenList as readonly string[]).includes(b)) { + return [a as keyof typeof deployedContracts.tokens, b]; + } + } + } + return null; + }; + + // Iterate all configured pairs for the network and add liquidity + for (const [pairKey] of Object.entries(deployedContracts.pairs)) { + const syms = getPairSymbols(pairKey); + if (!syms) { + console.warn(`Skipping unknown pair key ${pairKey}`); + continue; + } + const [symA, symB] = syms; + const tokenA = deployedContracts.tokens[symA] as Address; + const tokenB = deployedContracts.tokens[symB] as Address; + if (!tokenA || !tokenB) { + console.warn(`Token addresses missing for pair ${pairKey}: ${symA}=${tokenA}, ${symB}=${tokenB}`); + continue; + } + + // Choose amounts that reflect equal USD value on both sides. + // We target $1000 per side for reasonable sizes, but clamp to available balances for non-mintable tokens (e.g., ZTC). + const usdTargetPerSide = 1000; + const priceA = priceUSD[String(symA)] ?? 1; + const priceB = priceUSD[String(symB)] ?? 1; + + const ztcAddress = deployedContracts.tokens.ZTC as Address; + const mintA = tokenA.toLowerCase() !== ztcAddress.toLowerCase(); + const mintB = tokenB.toLowerCase() !== ztcAddress.toLowerCase(); + + const [decA, decB] = await Promise.all([ + getTokenDecimals({ publicClient, tokenAddress: tokenA }), + getTokenDecimals({ publicClient, tokenAddress: tokenB }), + ]); + const [balAWei, balBWei] = await Promise.all([ + getTokenBalance({ publicClient, tokenAddress: tokenA, owner: owner as Address }), + getTokenBalance({ publicClient, tokenAddress: tokenB, owner: owner as Address }), + ]); + const balAHuman = parseFloat(formatUnits(balAWei, decA)); + const balBHuman = parseFloat(formatUnits(balBWei, decB)); + + const availUsdA = mintA ? Number.POSITIVE_INFINITY : balAHuman * priceA; + const availUsdB = mintB ? Number.POSITIVE_INFINITY : balBHuman * priceB; + const usdPerSideActual = Math.min(usdTargetPerSide, availUsdA, availUsdB); + + if (usdPerSideActual <= 0) { + console.warn(`Skipping ${pairKey}: insufficient non-mintable token balance (mintA=${mintA}, mintB=${mintB}).`); + continue; + } + + const amountA = (usdPerSideActual / priceA).toString(); + const amountB = (usdPerSideActual / priceB).toString(); + + console.log(`\nAdding liquidity for ${pairKey} -> ${symA}/${symB}`); + console.log(`Target USD per side: $${usdTargetPerSide}, using $${usdPerSideActual}. Computed amounts: ${symA}=${amountA}, ${symB}=${amountB}`); + console.log(`Mint flags: ${symA}=${mintA}, ${symB}=${mintB}. Balances: ${symA}=${balAHuman}, ${symB}=${balBHuman}`); + + try { + await addLiquidityToPair({ + walletClient, + publicClient, + routerAddress: deployedContracts.router as Address, + tokenA, + tokenB, + amountA, + amountB, + mintTokensA: mintA, + mintTokensB: mintB, + }); + } catch (e) { + console.error(`Failed to add liquidity for ${pairKey}`, e); + } + } } diff --git a/scripts/mintTokens.ts b/scripts/mintTokens.ts new file mode 100644 index 0000000..d1c4a5e --- /dev/null +++ b/scripts/mintTokens.ts @@ -0,0 +1,67 @@ +import hre, { network } from "hardhat"; +import { Address, parseUnits } from "viem"; +import { readDeploymentRecord, getTokenDecimals, mintMockToken } from "./utils"; + +async function main() { + const [walletClient] = await hre.viem.getWalletClients(); + const publicClient = await hre.viem.getPublicClient(); + const owner = walletClient.account.address; + const networkName = network.name; + + const toEnv = process.env.MINT_TO; + const amountEnv = process.env.MINT_AMOUNT; + + if (!toEnv || !amountEnv) { + console.error("Missing environment variables MINT_TO and/or MINT_AMOUNT."); + console.error("Set them inline in the npm script or your environment, e.g. MINT_TO=0x... MINT_AMOUNT=1000"); + process.exit(1); + } + + const to = toEnv as Address; + const amountHuman = amountEnv; // human-readable amount to mint for each token (e.g., "1000") + + console.log(`Minting ${amountHuman} of each selected token to: ${to}`); + console.log(`Using network: ${networkName}, sender: ${owner}`); + + const deployments = await readDeploymentRecord(networkName); + + const tokenEntries = Object.entries(deployments.tokens) as [keyof typeof deployments.tokens, string][]; + // Skip ZTC (non-mintable by current account) + const mintableTokens = tokenEntries.filter(([sym]) => String(sym).toUpperCase() !== "ZTC"); + + console.log(`Tokens to mint: ${mintableTokens.map(([sym]) => sym).join(", ")}`); + + for (const [sym, tokenAddress] of mintableTokens) { + const tokenAddr = tokenAddress as Address | undefined; + if (!tokenAddr) { + console.warn(`Skipping ${sym}: Address not found in deployments for ${networkName}.`); + continue; + } + + try { + const decimals = await getTokenDecimals({ publicClient, tokenAddress: tokenAddr }); + const amountWei = parseUnits(amountHuman, decimals).toString(); + + console.log(`\nMinting ${amountHuman} ${String(sym)} (decimals: ${decimals}) to ${to} @ ${tokenAddr}`); + await mintMockToken({ + walletClient, + publicClient, + tokenAddress: tokenAddr, + amount: amountWei, + decimals, + forAddress: to, + }); + console.log(`Minted ${amountHuman} ${String(sym)} to ${to}`); + } catch (err) { + console.error(`Failed to mint ${String(sym)} at ${tokenAddr}:`, err); + // Continue with next token + } + } + + console.log("\nMint process completed."); +} + +main().catch((e) => { + console.error(e); + process.exit(1); +}); diff --git a/scripts/utils/index.ts b/scripts/utils/index.ts index 061c8bb..a7bc259 100644 --- a/scripts/utils/index.ts +++ b/scripts/utils/index.ts @@ -1,8 +1,8 @@ import fs from "fs"; -import path, { format } from "path"; +import path from "path"; import hre from "hardhat"; -import { Address, BaseError, ContractFunctionRevertedError, formatUnits, parseUnits, PublicClient } from "viem"; -import { WalletClient } from "@nomicfoundation/hardhat-viem/types"; +import {Address, BaseError, ContractFunctionRevertedError, formatUnits, parseUnits, PublicClient} from "viem"; +import {WalletClient} from "@nomicfoundation/hardhat-viem/types"; export interface CommonParams { walletClient: WalletClient; @@ -114,6 +114,40 @@ export const readDeploymentRecord = async (networkName: string): Promise => { + const IERC20 = await hre.artifacts.readArtifact("IERC20Metadata"); + const decimals = await publicClient.readContract({ + address: tokenAddress, + abi: IERC20.abi, + functionName: "decimals", + args: [], + }); + return Number(decimals); +} + +export const getTokenBalance = async ({ publicClient, tokenAddress, owner }: { publicClient: PublicClient; tokenAddress: Address; owner: Address; }): Promise => { + const IERC20 = await hre.artifacts.readArtifact("IERC20Metadata"); + const balance = await publicClient.readContract({ + address: tokenAddress, + abi: IERC20.abi, + functionName: "balanceOf", + args: [owner], + }); + return Array.isArray(balance) ? (balance[0] as bigint) : (balance as bigint); +} + +export const getTokenAllowance = async ({ publicClient, tokenAddress, owner, spender }: { publicClient: PublicClient; tokenAddress: Address; owner: Address; spender: Address; }): Promise => { + const IERC20 = await hre.artifacts.readArtifact("IERC20Metadata"); + const allowance = await publicClient.readContract({ + address: tokenAddress, + abi: IERC20.abi, + functionName: "allowance", + args: [owner, spender], + }); + return Array.isArray(allowance) ? (allowance[0] as bigint) : (allowance as bigint); +} + export interface MintTokenParms { walletClient: WalletClient; publicClient: PublicClient; @@ -146,6 +180,7 @@ export const mintMockToken = async ({ await publicClient.waitForTransactionReceipt({ hash: mintHash }); } catch (err) { console.error(`Error minting token at address ${tokenAddress}:`, err); + throw err; } } @@ -196,8 +231,8 @@ export interface AddLiquidityParams { routerAddress: Address; tokenA: Address; tokenB: Address; - amountADesired: string; - amountBDesired: string; + amountA: string; // human-readable amount (e.g., "1" for 1 token) + amountB: string; // human-readable amount (e.g., "2750" for 2750 tokens) mintTokensA?: boolean; // Optional, defaults to false mintTokensB?: boolean; // Optional, defaults to false } @@ -208,19 +243,30 @@ export const addLiquidityToPair = async ({ routerAddress, tokenA, tokenB, - amountADesired, - amountBDesired, + amountA, + amountB, mintTokensA = false, // Optional, defaults to false mintTokensB = false, // Optional, defaults to false }: AddLiquidityParams) => { + // Determine token decimals + const [decimalsA, decimalsB] = await Promise.all([ + getTokenDecimals({ publicClient, tokenAddress: tokenA }), + getTokenDecimals({ publicClient, tokenAddress: tokenB }), + ]); + + // Parse desired amounts into token units + const amountAParsed = parseUnits(amountA, decimalsA).toString(); + const amountBParsed = parseUnits(amountB, decimalsB).toString(); + // Mint tokens if they are mock tokens if (mintTokensA) { await mintMockToken({ walletClient, publicClient, tokenAddress: tokenA, - amount: amountADesired, + amount: amountAParsed, + decimals: decimalsA, }); } if (mintTokensB) { @@ -228,7 +274,8 @@ export const addLiquidityToPair = async ({ walletClient, publicClient, tokenAddress: tokenB, - amount: amountBDesired, + amount: amountBParsed, + decimals: decimalsB, }); } @@ -238,19 +285,39 @@ export const addLiquidityToPair = async ({ publicClient, tokenAddress: tokenA, spender: routerAddress, - amount: amountADesired, + amount: amountAParsed, + decimals: decimalsA, }); await approveTokenTransfer({ walletClient, publicClient, tokenAddress: tokenB, spender: routerAddress, - amount: amountBDesired, + amount: amountBParsed, + decimals: decimalsB, }); + // Check balances & allowances + const [balanceA, balanceB, allowanceA, allowanceB] = await Promise.all([ + getTokenBalance({ publicClient, tokenAddress: tokenA, owner: walletClient.account.address }), + getTokenBalance({ publicClient, tokenAddress: tokenB, owner: walletClient.account.address }), + getTokenAllowance({ publicClient, tokenAddress: tokenA, owner: walletClient.account.address, spender: routerAddress }), + getTokenAllowance({ publicClient, tokenAddress: tokenB, owner: walletClient.account.address, spender: routerAddress }), + ]); + + const amountAWei = BigInt(amountAParsed); + const amountBWei = BigInt(amountBParsed); + console.log(`Adding liquidity to pair: ${tokenA} and ${tokenB}`); - console.log(`Amount A Desired: ${formatUnits(BigInt(amountADesired), 18).toString()}`); - console.log(`Amount B Desired: ${formatUnits(BigInt(amountBDesired), 18).toString()}`); + console.log(`Amount A Desired: ${formatUnits(amountAWei, decimalsA).toString()} (decimals: ${decimalsA})`); + console.log(`Amount B Desired: ${formatUnits(amountBWei, decimalsB).toString()} (decimals: ${decimalsB})`); + console.log(`Balance A: ${formatUnits(balanceA, decimalsA)} | Allowance A: ${formatUnits(allowanceA, decimalsA)}`); + console.log(`Balance B: ${formatUnits(balanceB, decimalsB)} | Allowance B: ${formatUnits(allowanceB, decimalsB)}`); + + if (balanceA < amountAWei) throw new Error(`Insufficient balance for tokenA ${tokenA}. Need ${amountAParsed}, have ${balanceA.toString()}`); + if (balanceB < amountBWei) throw new Error(`Insufficient balance for tokenB ${tokenB}. Need ${amountBParsed}, have ${balanceB.toString()}`); + if (allowanceA < amountAWei) throw new Error(`Insufficient allowance for tokenA ${tokenA}. Need ${amountAParsed}, have ${allowanceA.toString()}`); + if (allowanceB < amountBWei) throw new Error(`Insufficient allowance for tokenB ${tokenB}. Need ${amountBParsed}, have ${allowanceB.toString()}`); const IUniswapV2Router02 = await hre.artifacts.readArtifact("IUniswapV2Router02"); @@ -264,8 +331,8 @@ export const addLiquidityToPair = async ({ args: [ tokenA, tokenB, - BigInt(amountADesired), - BigInt(amountBDesired), + amountAWei, + amountBWei, 0n, // Min amount A 0n, // Min amount B walletClient.account.address, // Recipient @@ -273,6 +340,8 @@ export const addLiquidityToPair = async ({ ], account: walletClient.account.address, }); + const receipt = await publicClient.waitForTransactionReceipt({ hash: addLiquidityHash }); + console.log(`Liquidity added successfully. Tx: ${receipt.transactionHash}`); } catch (err) { if (err instanceof BaseError) { const revertError = err.walk(err => err instanceof ContractFunctionRevertedError) @@ -286,11 +355,6 @@ export const addLiquidityToPair = async ({ console.error("Error adding liquidity:", err); throw err; } - - - //await publicClient.waitForTransactionReceipt({ hash: addLiquidityHash }); - console.log("Liquidity added successfully."); - } export interface CreatePairParams { @@ -321,13 +385,12 @@ export const createPair = async ({ const receipt = await publicClient.waitForTransactionReceipt({ hash: createPairHash }); console.log(`Pair created successfully. Transaction Hash: ${receipt.transactionHash}`); - const pairAddress = await publicClient.readContract({ + return await publicClient.readContract({ address: factoryAddress, abi: IUniswapV2Factory.abi, functionName: "getPair", args: [tokenA, tokenB], }); - return pairAddress; } export interface DeployMockTokenParams {