Skip to content

Commit

Permalink
Merge branch 'main' into lint_and_format
Browse files Browse the repository at this point in the history
  • Loading branch information
ZigaMr authored Jul 29, 2024
2 parents f28f4eb + 0c3600f commit 28e4ab6
Show file tree
Hide file tree
Showing 9 changed files with 191 additions and 113 deletions.
37 changes: 37 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,38 @@
node_modules
.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.


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.

4 changes: 3 additions & 1 deletion backend/contracts/Quiz.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ contract Quiz {
uint256 public constant COUPON_VALID = type(uint256).max - 1;
uint256 public constant COUPON_REMOVED = type(uint256).max - 2;


struct QuizQuestion {
// Question.
string question;
Expand Down Expand Up @@ -41,6 +42,7 @@ contract Quiz {
string private _svgImage;
// NFT contract address
address private _nftAddress;

// List of questions.
QuizQuestion[] private _questions;
// Total number of choices. Used for generating the permutation vector.
Expand Down Expand Up @@ -345,7 +347,7 @@ contract Quiz {

// Invalidate coupon.
_coupons[pc.coupon] = block.number;

// Increase nonce, for gasless tx.
if (msg.sender == _kp.addr) {
_kp.nonce++;
Expand Down
201 changes: 128 additions & 73 deletions backend/hardhat.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,69 @@ 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';

Expand Down Expand Up @@ -61,14 +123,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 +185,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 +195,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 +228,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 +240,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 +255,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 +307,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
2 changes: 1 addition & 1 deletion backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,4 @@
"typechain": "^8.3.2",
"typescript": "^4.9.5"
}
}
}
1 change: 1 addition & 0 deletions backend/test/Quiz.ts
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,7 @@ describe("Quiz", function () {
await ethers.getSigners()
)[1].address
);

expect(payoutCertificate).to.not.equal("0x");

const balance1 = await ethers.provider.getBalance(
Expand Down
Loading

0 comments on commit 28e4ab6

Please sign in to comment.