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

backend: Add genCoupons helper, other cleanups #9

Merged
merged 1 commit into from
Oct 18, 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
66 changes: 40 additions & 26 deletions backend/src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,38 @@ import '@typechain/hardhat';
import 'hardhat-watcher';
import 'solidity-coverage';
import { HardhatEthersSigner, SignerWithAddress } from '@nomicfoundation/hardhat-ethers/signers';
import { ethers } from 'hardhat';

export async function loadYamlConfig(filePath: string): Promise<any> {
try {
const fileContents = await promises.readFile(filePath, 'utf8');
return yaml.load(fileContents);
} catch (error) {
console.error("Error reading YAML file", error);
return {};
const fileContents = await promises.readFile(filePath, 'utf8');
return yaml.load(fileContents);
}

/**
* Generates n unique coupons of length l.
* @param n number of coupons
* @param l length of each coupon
* @returns List of unique coupon strings
*/
export async function genCoupons(n=50, l=6): Promise<string[]> {
// List of allowed characters. Inspired by BASE-58 encoding.
const allowedChars = "abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTVWXYZ23456789";
const coupons = new Set<string>();
for (let i=0; i<n; i++) {
let c: string;
do {
c="";
for (let j = 0; j < l; j++) {
c += allowedChars[Math.floor(Math.random() * allowedChars.length)];
}
} while (coupons.has(c));
coupons.add(c);
}
return Array.from(coupons.values());
}

export async function printAccount(hre: typeof import('hardhat'), addr: string) {
const balance = hre.ethers.formatEther(await hre.ethers.provider.getBalance(addr));
console.log(`Using account ${addr}. Account balance ${balance}`);
}

export async function deployContract(hre: typeof import('hardhat'),
Expand All @@ -40,7 +62,7 @@ export async function addQuestions(quizContract: any, questionsFile: string) {
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}`);
console.log(`Adding question: ${question.question}\n Transaction hash: ${receipt!.hash}`);
}
}

Expand All @@ -50,15 +72,15 @@ export async function addCoupons(quizContract: any, couponsFile: string) {
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}`);
console.log(`Adding coupons: ${chunk}\n Transaction hash: ${receipt!.hash}`);
}
}

export async function setNativeReward(hre: typeof import('hardhat'), quizContract: any, reward: string) {
const { ethers } = hre;
const tx = await quizContract.setPayoutReward(ethers.parseEther(reward));
const receipt = await tx.wait();
console.log(`Set reward to ${reward} ROSE. Transaction hash: ${receipt!.hash}`);
console.log(`Setting reward to ${reward} ROSE\n Transaction hash: ${receipt!.hash}`);
}


Expand All @@ -83,7 +105,7 @@ export async function setNftAddress(hre: typeof import('hardhat'), contract: any
export async function storeNFT(hre: typeof import('hardhat'), quiz: any, jsonMetadata: string) {
console.log(`Storing NFT with metadata: ${jsonMetadata}`);
console.log(`NFT address: ${await quiz.nftAddress()}`);

const nft = await hre.ethers.getContractAt('NftReward', await quiz.nftAddress());
if ((await nft.tokenURIs(hre.ethers.keccak256(hre.ethers.toUtf8Bytes(jsonMetadata)))).length > 0) {
console.log(`NFT already stored. Ignoring.`);
Expand All @@ -94,28 +116,20 @@ export async function storeNFT(hre: typeof import('hardhat'), quiz: any, jsonMet
console.log(`NFT stored.`);
}

export async function fundContract(hre: typeof import('hardhat'), quizContract: any, amount: string) {
export async function fundAccount(hre: typeof import('hardhat'), account: any, amount: string) {
const { ethers } = hre;
const tx = await (await ethers.getSigners())[0].sendTransaction({
to: await quizContract.getAddress(),
to: account,
value: ethers.parseEther(amount),
});
const receipt = await tx.wait();
console.log(`Funded contract with ${amount} ROSE. Transaction hash: ${receipt!.hash}`);
console.log(`Funding account ${account} with ${amount} ROSE\n Transaction hash: ${receipt!.hash}`);
}

export async function fundGaslessAccount(hre: typeof import('hardhat'), gaslessAddress: string, amount: string) {
export async function setGaslessKeyPair(hre: typeof import('hardhat'), quizContract: any, payerSecret: string, nonce: number) {
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}`);
}

export async function setGaslessKeyPair(quizContract: any, payerAddress: string, payerSecret: string, nonce: number) {
const payerAddress = ethers.computeAddress(payerSecret);
const tx = await quizContract.setGaslessKeyPair(payerAddress, payerSecret, nonce);
const receipt = await tx.wait();
console.log(`Set gasless keypair. Transaction hash: ${receipt!.hash}`);
}
console.log(`Setting gasless payer ${payerAddress}\n Transaction hash: ${receipt!.hash}`);
}
71 changes: 40 additions & 31 deletions backend/src/tasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,19 @@ import { HardhatUserConfig, task } from 'hardhat/config';
import 'solidity-coverage';

import {
storeNFT, fundContract, fundGaslessAccount, setGaslessKeyPair,
deployContract, addQuestions, setNativeReward, addCoupons,
addAllowMint, setNftAddress, loadYamlConfig, removeAllowMint
storeNFT,
fundAccount,
setGaslessKeyPair,
deployContract,
addQuestions,
setNativeReward,
addCoupons,
addAllowMint,
setNftAddress,
loadYamlConfig,
removeAllowMint,
genCoupons,
printAccount
} from './helpers';


Expand Down Expand Up @@ -136,7 +146,7 @@ task('status')

console.log(`Reward (in native token): ${hre.ethers.formatEther(await quiz.payoutReward())} ROSE`)
console.log(`Payout Balance: ${hre.ethers.formatEther(await hre.ethers.provider.getBalance(await quiz.getAddress()))} ROSE`)

const gaslessKeyPair = await quiz.getGaslessKeyPair();
console.log("Gasless signer:");
console.log(` Address: ${gaslessKeyPair[0]}`)
Expand Down Expand Up @@ -205,6 +215,17 @@ task('addCoupons')
await addCoupons(quiz, args.couponsFile);
});

// Generate unique coupons suitable for the coupons file.
task('genCoupons')
.addPositionalParam('n', 'number of coupons')
.addPositionalParam('l', 'length of each coupon')
.setAction(async (args, hre) => {
const coupons = await genCoupons(args.n, args.l);
for (let i=0; i<coupons.length; i++) {
console.log(coupons[i]);
}
});

// Fetch image from IPFS.
task('storeNft')
.addPositionalParam('quiz', 'Quiz contract')
Expand Down Expand Up @@ -247,10 +268,9 @@ task('fund')
.addPositionalParam('address', 'contract address')
.addPositionalParam('amount', 'amount in ROSE')
.setAction(async (args, hre) => {
const quiz = await hre.ethers.getContractAt('Quiz', args.address);
await fundContract(hre,
quiz,
args.amount
await fundAccount(hre,
args.address,
args.amount
);
});

Expand All @@ -270,12 +290,11 @@ task('reclaimFunds')
// Set gasless key-pair.
task('setGaslessKeyPair')
.addPositionalParam('address', 'contract address')
.addPositionalParam('payerAddress', 'payer address')
.addPositionalParam('payerSecret', 'payer secret key')
.setAction(async (args, hre) => {
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);
const nonce = await hre.ethers.provider.getTransactionCount(hre.ethers.computeAddress(args.payerSecret));
await setGaslessKeyPair(hre, quiz, args.payerSecret, nonce);
});

// Fetch image from IPFS.
Expand Down Expand Up @@ -303,21 +322,13 @@ task('deployAndSetup')
// Load default configuration from YAML file
// const defaultConfig = await loadYamlConfig(path.resolve(__dirname, args.configFile));
const cfg = await loadYamlConfig(args.configFile);

console.log(`Questions File: ${cfg.questionsFile}`);
console.log(`Coupons File: ${cfg.couponsFile}`);
console.log(`Reward: ${cfg.nativeReward}`);
console.log(`Fund Amount: ${cfg.fundAmount}`);
console.log(`Metadata file path: ${cfg.nftReward.metadataFile}`);
console.log(`Fund gasless amount: ${cfg.fundGaslessAmount}`);
console.log(`Gasless Address: ${cfg.gaslessAddress}`);
console.log(`Gasless Secret: ${cfg.gaslessSecret}`);

const signerAddr = (await hre.ethers.getSigners())[0].address;
await printAccount(hre, signerAddr);

const quiz = await hre.run("deployQuiz");
await hre.run("addQuestions", { address: quiz, questionsFile: cfg.questionsFile })
await hre.run("addCoupons", { address: quiz, couponsFile: cfg.couponsFile })

if (cfg.fundAmount)
{
await hre.run("fund", { address: quiz, amount: cfg.fundAmount });
Expand All @@ -328,27 +339,25 @@ task('deployAndSetup')
await hre.run("setNativeReward", { address: quiz, reward: cfg.nativeReward });
}
// Set gasless key pair if provided
if (cfg.gaslessAddress && cfg.gaslessSecret) {
await hre.run("setGaslessKeyPair", { address: quiz, payerAddress: cfg.gaslessAddress, payerSecret: cfg.gaslessSecret });
if (cfg.fundGaslessAmount)
{
await hre.run("fund", { address: cfg.gaslessAddress, amount: cfg.fundGaslessAmount });
if (cfg.gaslessSecret) {
await hre.run("setGaslessKeyPair", { address: quiz, payerSecret: cfg.gaslessSecret });
if (cfg.fundGaslessAmount) {
await hre.run("fund", { address: hre.ethers.computeAddress(cfg.gaslessSecret), amount: cfg.fundGaslessAmount });
}

}

if (cfg.nftReward) {
let nftReward;
if (cfg.nftReward.address) {
nftReward = await hre.ethers.getContractAt('NftReward', cfg.nftReward.address);
}
else{
} else {
nftReward = await hre.run("deployNftReward", {name: cfg.nftReward.name, symbol: cfg.nftReward.symbol })
}
await hre.run("setNftAddress", { address: quiz, nftAddress: nftReward })
await hre.run("addAllowMint", { rewardContract: nftReward, minterAddress: quiz })
await hre.run("storeNft", {quiz: quiz, jsonFile: cfg.nftReward.metadataFile })

}

await printAccount(hre, signerAddr);
});

9 changes: 4 additions & 5 deletions backend/test-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,11 @@
questionsFile: "test-questions.json" # File containing questions in JSON format
couponsFile: "test-coupons.txt" # File containing coupons, one per line
nativeReward: "2.0" # Reward in ROSE (optional)
# gaslessAddress: "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC" # Gasless address (optional)
# gaslessSecret: "0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a" # Gasless account private key (optional)
# fundGaslessAmount: "10" # Amount of ROSE to send to gasless account (optional)
gaslessSecret: "0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a" # Gasless account private key (optional)
fundGaslessAmount: "10" # Amount of ROSE to send to gasless account (optional)
fundAmount: "5" # Amount in ROSE to fund the contract (optional)
nftReward: # NFT reward (optional)
nftReward: # NFT reward (optional)
address: "" # Address of the existing NFT contract (if exists)
metadataFile: "test/assets/metadata_inline.json" # Metadata File
name: "Quiz" # Name of the NFT collection (for new contract)
symbol: "OAS" # Symbol of the NFT collection (for new contract)
symbol: "OAS" # Symbol of the NFT collection (for new contract)
Loading