Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Deploy and Set up quiz in one task #4

Merged
merged 2 commits into from
Jul 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 37 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,38 @@
node_modules
ZigaMr marked this conversation as resolved.
Show resolved Hide resolved
.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
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

ZigaMr marked this conversation as resolved.
Show resolved Hide resolved

Check out other hardhat tasks that will help you manage the quiz:

```shell
Expand Down
7 changes: 0 additions & 7 deletions backend/.gitignore

This file was deleted.

202 changes: 129 additions & 73 deletions backend/hardhat.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");


ZigaMr marked this conversation as resolved.
Show resolved Hide resolved
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';

Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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<questions.length; i++) {
Expand All @@ -139,7 +196,7 @@ task('status')
}
}

// Coupons
// Coupons.
try {
const coupons = await quiz.countCoupons();
console.log(`Coupons Available/All: ${coupons[0]}/${coupons[1]}`)
Expand Down Expand Up @@ -172,7 +229,7 @@ task('addQuestion')
console.log(`Success! Transaction hash: ${receipt!.hash}`);
});

// Add a new question.
// Clear (delete) questions.
task('clearQuestions')
.addPositionalParam('address', 'contract address')
.setAction(async (args, hre) => {
Expand All @@ -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)')
Expand All @@ -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<questions.length; i++) {
const tx = await quiz.addQuestion(questions[i].question, questions[i].choices);
const receipt = await tx.wait();
console.log(`Added question ${questions[i].question}. Transaction hash: ${receipt!.hash}`);
}
});
.addPositionalParam('address', 'contract address')
.addPositionalParam('questionsFile', 'file containing questions in JSON format')
.setAction(async (args, hre) => {
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<coupons.length; i+=20) {
let cs = coupons.slice(i, i+20);
const tx = await quiz.addCoupons(cs);
const receipt = await tx.wait();
console.log(`Added coupons: ${coupons.slice(i, i+20)}. Transaction hash: ${receipt!.hash}`);
}
});
.addPositionalParam('address', 'contract address')
.addPositionalParam('couponsFile', 'file containing coupons, one per line')
.setAction(async (args, hre) => {
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')
Expand All @@ -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.
Expand Down
30 changes: 0 additions & 30 deletions frontend/.gitignore

This file was deleted.

Loading