diff --git a/.gitignore b/.gitignore index b36f88a..f3006a6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,38 @@ node_modules -.vscode/settings.json +.openzeppelin/ +artifacts/ +cache/ +coverage* +lib/ +typechain-types/ +abis/ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +.DS_Store +dist +dist-ssr +coverage +*.local + +/cypress/videos/ +/cypress/screenshots/ + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +stats.html diff --git a/README.md b/README.md index 8fe5d3f..62b56cb 100644 --- a/README.md +++ b/README.md @@ -113,6 +113,26 @@ Checklist after deploying a production-ready quiz: npx hardhat getCoupons 0x385cAE1F3afFC50097Ca33f639184f00856928Ff --network sapphire-testnet ``` +### Deploy and setup quiz with a single task + +You can also setup and run the entire quiz in a single task. + +```shell +npx hardhat deployAndSetupQuiz --network sapphire-testnet +``` + +The `deployAndSetupQuiz` task supports several optional parameters: + +1. **`--questions-file`**: A file containing questions in JSON format. Default value is `test-questions.json`. +2. **`--coupons-file`**: A file containing coupons, one per line. Default value is `test-coupons.txt`. +3. **`--reward`**: The reward in ROSE. Default value is `2.0`. +4. **`--gasless-address`**: The payer address for gasless transactions. +5. **`--gasless-secret`**: The payer secret key for gasless transactions. +6. **`--fund-amount`**: The amount in ROSE to fund the contract. Default value is `100`. +7. **`--fund-gasless-amount`**: The amount in ROSE to fund the gasless account. Default value is `10`. +8. **`--contract-address`**: The contract address for status check. + + Check out other hardhat tasks that will help you manage the quiz: ```shell diff --git a/backend/.gitignore b/backend/.gitignore deleted file mode 100644 index 00220ef..0000000 --- a/backend/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -.openzeppelin/ -artifacts/ -cache/ -coverage* -lib/ -typechain-types/ -abis/ diff --git a/backend/hardhat.config.ts b/backend/hardhat.config.ts index 025aea2..07a7a32 100644 --- a/backend/hardhat.config.ts +++ b/backend/hardhat.config.ts @@ -10,7 +10,70 @@ import 'hardhat-watcher'; import { TASK_COMPILE } from 'hardhat/builtin-tasks/task-names'; import { HardhatUserConfig, task } from 'hardhat/config'; import 'solidity-coverage'; -require("@nomicfoundation/hardhat-chai-matchers"); + + +async function deployContract(hre: typeof import('hardhat'), contractName: string, url: string) { + const { ethers } = hre; + const uwProvider = new JsonRpcProvider(url); + const contractFactory = await ethers.getContractFactory(contractName, new hre.ethers.Wallet(accounts[0], uwProvider)); + const contract = await contractFactory.deploy(); + await contract.waitForDeployment(); + console.log(`${contractName} deployed at address: ${await contract.getAddress()}`); + return contract; +} + +async function addQuestions(quizContract: any, questionsFile: string) { + const questions = JSON.parse(await fs.readFile(questionsFile, 'utf8')); + for (const question of questions) { + const tx = await quizContract.addQuestion(question.question, question.choices); + const receipt = await tx.wait(); + console.log(`Added question: ${question.question}. Transaction hash: ${receipt!.hash}`); + } +} + +async function addCoupons(quizContract: any, couponsFile: string) { + const coupons = (await fs.readFile(couponsFile, 'utf8')).split('\n').filter(Boolean); + for (let i = 0; i < coupons.length; i += 20) { + const chunk = coupons.slice(i, i + 20); + const tx = await quizContract.addCoupons(chunk); + const receipt = await tx.wait(); + console.log(`Added coupons: ${chunk}. Transaction hash: ${receipt!.hash}`); + } +} + +async function setReward(hre: typeof import('hardhat'), quizContract: any, reward: string) { + const { ethers } = hre; + const tx = await quizContract.setReward(ethers.parseEther(reward)); + const receipt = await tx.wait(); + console.log(`Set reward to ${reward} ROSE. Transaction hash: ${receipt!.hash}`); +} + +async function fundContract(hre: typeof import('hardhat'), quizContract: any, amount: string) { + const { ethers } = hre; + const tx = await (await ethers.getSigners())[0].sendTransaction({ + to: await quizContract.getAddress(), + value: ethers.parseEther(amount), + }); + const receipt = await tx.wait(); + console.log(`Funded contract with ${amount} ROSE. Transaction hash: ${receipt!.hash}`); +} + +async function fundGaslessAccount(hre: typeof import('hardhat'), gaslessAddress: string, amount: string) { + const { ethers } = hre; + const tx = await (await ethers.getSigners())[0].sendTransaction({ + to: gaslessAddress, + value: ethers.parseEther(amount), + }); + const receipt = await tx.wait(); + console.log(`Funded gasless account with ${amount} ROSE. Transaction hash: ${receipt!.hash}`); +} + +async function setGaslessKeyPair(quizContract: any, payerAddress: string, payerSecret: string, nonce: number) { + const tx = await quizContract.setGaslessKeyPair(payerAddress, payerSecret, nonce); + const receipt = await tx.wait(); + console.log(`Set gasless keypair. Transaction hash: ${receipt!.hash}`); +} + const TASK_EXPORT_ABIS = 'export-abis'; @@ -61,14 +124,8 @@ task('deploy') await hre.run('compile'); // For deployment unwrap the provider to enable contract verification. - const uwProvider = new JsonRpcProvider(hre.network.config.url); - const Quiz = await hre.ethers.getContractFactory('Quiz', new hre.ethers.Wallet(accounts[0], uwProvider)); - const quiz = await Quiz.deploy(); - await quiz.waitForDeployment(); - - console.log(`Quiz address: ${await quiz.getAddress()}`); - return quiz; -}); + const quiz = await deployContract(hre, 'Quiz', hre.network.config.url); + }); // Set the NFT address in the Quiz contract. task("setNftAddress", "Sets the NFT contract address in the Quiz contract") @@ -129,7 +186,7 @@ task('status') console.log(`Status of quiz contract ${args.address}`); const quiz = await hre.ethers.getContractAt('Quiz', args.address); - // Questions + // Questions. const questions = await quiz.getQuestions(""); console.log(`Questions (counting from 0):`); for (let i=0; i { @@ -184,7 +241,7 @@ task('clearQuestions') console.log(`Success! Transaction hash: ${receipt!.hash}`); }); -// Add a new question. +// Update existing question. task('setQuestion') .addPositionalParam('address', 'contract address') .addPositionalParam('number', 'question number (starting from 0)') @@ -199,74 +256,46 @@ task('setQuestion') console.log(`Updated question ${questions[parseInt(args.number)].question}. Transaction hash: ${receipt!.hash}`); }); -// Add a new question. +// Add questions from json file. task('addQuestions') - .addPositionalParam('address', 'contract address') - .addPositionalParam('questionsFile', 'file containing questions in JSON format') - .setAction(async (args, hre) => { - await hre.run('compile'); - - let quiz = await hre.ethers.getContractAt('Quiz', args.address); - const questions = JSON.parse(await fs.readFile(args.questionsFile,'utf8')); - for (var i=0; i { + const quiz = await hre.ethers.getContractAt('Quiz', args.address); + await addQuestions(quiz, args.questionsFile); +}); -// Add a new question. +// Add coupons. task('addCoupons') - .addPositionalParam('address', 'contract address') - .addPositionalParam('couponsFile', 'file containing coupons, one per line') - .setAction(async (args, hre) => { - await hre.run('compile'); - - let quiz = await hre.ethers.getContractAt('Quiz', args.address); - const coupons = (await fs.readFile(args.couponsFile,'utf8')).split("\n"); - // Trim last empty line. - if (coupons[coupons.length-1]=="") { - coupons.pop(); - } - for (var i=0; i { + const quiz = await hre.ethers.getContractAt('Quiz', args.address); + await addCoupons(quiz, args.couponsFile); +}); -// Add a new question. +// Set reward amount in native token. task('setReward') .addPositionalParam('address', 'contract address') .addPositionalParam('reward', 'reward in ROSE') .setAction(async (args, hre) => { - await hre.run('compile'); - - let quiz = await hre.ethers.getContractAt('Quiz', args.address); - const tx = await quiz.setReward(hre.ethers.parseEther(args.reward)); - const receipt = await tx.wait(); - console.log(`Successfully set reward to ${args.reward} ROSE. Transaction hash: ${receipt!.hash}`); + const quiz = await hre.ethers.getContractAt('Quiz', args.address); + await setReward(hre, quiz, args.reward); }); -// Add a new question. +// Send funds from signers account to quiz contract. task('fund') .addPositionalParam('address', 'contract address') - .addPositionalParam('amount', 'reclaim funds to this address') + .addPositionalParam('amount', 'amount in ROSE') .setAction(async (args, hre) => { - await hre.run('compile'); - - let quiz = await hre.ethers.getContractAt('Quiz', args.address); - const tx = await (await hre.ethers.getSigners())[0].sendTransaction({ - from: (await hre.ethers.getSigners())[0].address, - to: await quiz.getAddress(), - value: hre.ethers.parseEther(args.amount), - }); - const receipt = await tx.wait(); - console.log(`Successfully funded ${await quiz.getAddress()} with ${args.amount} ROSE. Transaction hash: ${receipt!.hash}`); + const quiz = await hre.ethers.getContractAt('Quiz', args.address); + await fundContract(hre, + quiz, + args.amount + ); }); -// Add a new question. +// Send funds from quiz contract to specified address. task('reclaimFunds') .addPositionalParam('address', 'contract address') .addPositionalParam('payoutAddress', 'reclaim funds to this address') @@ -279,20 +308,47 @@ task('reclaimFunds') console.log(`Successfully reclaimed funds to ${args.payoutAddress}. Transaction hash: ${receipt!.hash}`); }); -// Add a new question. +// Set gasless key-pair. task('setGaslessKeyPair') .addPositionalParam('address', 'contract address') .addPositionalParam('payerAddress', 'payer address') .addPositionalParam('payerSecret', 'payer secret key') .setAction(async (args, hre) => { - await hre.run('compile'); + const quiz = await hre.ethers.getContractAt('Quiz', args.address); + const nonce = await hre.ethers.provider.getTransactionCount(args.payerAddress); + await setGaslessKeyPair(quiz, args.payerAddress, args.payerSecret, nonce); + }); - let quiz = await hre.ethers.getContractAt('Quiz', args.address); - const nonce = await hre.ethers.provider.getTransactionCount(args.payerAddress); - const tx = await quiz.setGaslessKeyPair(args.payerAddress, args.payerSecret, nonce); - const receipt = await tx.wait(); - console.log(`Successfully set gasless keypair to ${args.payerAddress}, secret ${args.payerSecret} and nonce ${nonce}. Transaction hash: ${receipt!.hash}`); +// Deploy and setup Quiz contract. +task('deployAndSetupQuiz') + .addOptionalParam('questionsFile', 'File containing questions in JSON format', 'test-questions.json') + .addOptionalParam('couponsFile', 'File containing coupons, one per line', 'test-coupons.txt') + .addOptionalParam('reward', 'Reward in ROSE', '2.0') + .addOptionalParam('gaslessAddress', 'Payer address for gasless transactions') + .addOptionalParam('gaslessSecret', 'Payer secret key for gasless transactions') + .addOptionalParam('fundAmount', 'Amount in ROSE to fund the contract', '100') + .addOptionalParam('fundGaslessAmount', 'Amount in ROSE to fund the gasless account', '10') + .addOptionalParam('contractAddress', 'Contract address for status check') + .setAction(async (args, hre) => { + await hre.run('compile'); + const quiz = await deployContract(hre, 'Quiz', hre.network.config.url); + await addQuestions(quiz, args.questionsFile); + await addCoupons(quiz, args.couponsFile); + await setReward(hre, quiz, args.reward); + await fundContract(hre, quiz, args.fundAmount); + const nonce = await hre.ethers.provider.getTransactionCount(args.gaslessAddress); + if (!args.gaslessAddress || !args.gaslessSecret) { + console.log('Provide --gasless-address and --gasless-secret to set gasless keypair.'); + return + } + await setGaslessKeyPair(quiz, args.gaslessAddress, args.gaslessSecret, nonce); + await fundGaslessAccount(hre, args.gaslessAddress, args.fundGaslessAmount); + if (args.contractAddress) { + await hre.run('status', { address: args.contractAddress }); + } else { + await hre.run('status', { address: await quiz.getAddress() }); + } }); // Hardhat Node and sapphire-dev test mnemonic. diff --git a/frontend/.gitignore b/frontend/.gitignore deleted file mode 100644 index f6595cf..0000000 --- a/frontend/.gitignore +++ /dev/null @@ -1,30 +0,0 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* -lerna-debug.log* - -node_modules -.DS_Store -dist -dist-ssr -coverage -*.local - -/cypress/videos/ -/cypress/screenshots/ - -# Editor directories and files -.vscode/* -!.vscode/extensions.json -.idea -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw? - -stats.html