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

Added NFT minting functionality to Quiz contract, quiz now displays s… #3

Merged
merged 1 commit 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
4 changes: 2 additions & 2 deletions .github/workflows/ci-test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
with:
node-version: 18

- uses: pnpm/action-setup@v2.4.0
- uses: pnpm/action-setup@v4
name: Install pnpm
id: pnpm-install
with:
Expand Down Expand Up @@ -82,7 +82,7 @@ jobs:
with:
node-version: 18

- uses: pnpm/action-setup@v2.4.0
- uses: pnpm/action-setup@v4
name: Install pnpm
id: pnpm-install
with:
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules
.vscode/settings.json
120 changes: 120 additions & 0 deletions backend/contracts/OasisReward.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import "@openzeppelin/contracts/utils/Base64.sol";

/**
* @title OasisReward
* @dev OasisReward contract is an ERC721 contract that allows minting NFT rewards for Oasis Quiz
*/
contract OasisReward is ERC721Enumerable {

mapping(uint256 => string) private _tokenURIs;

/// @notice Mapping from user address to list of owned token IDs
mapping(address => uint256[]) private _ownedTokens;
/// @notice Whitelisted msg.sender accounts
mapping(address => bool) _allowMint;


/// @notice Contract owner
address private _owner;


modifier onlyOwner {
require(msg.sender == _owner, "Only owner can call this function");
_;
}

modifier onlyAllowMint {
require(_allowMint[msg.sender], "Address not allowed");
_;
}

constructor(
string memory name,
string memory symbol
) ERC721(name, symbol) {
_owner = msg.sender;
}


function addAllowMint(address _address) public onlyOwner {
_allowMint[_address] = true;
}

function removeAllowMint(address _address) public onlyOwner {
_allowMint[_address] = false;
}

/**
* @notice Mint new NFT reward
* @param to address to mint the NFT to
* @param base64EncodedSVG base64 encoded SVG image
*/
// Function to mint new tokens
function mint(address to, string calldata base64EncodedSVG) public onlyAllowMint {
require(bytes(base64EncodedSVG).length > 0, "Token URI not set");
uint256 tokenId = totalSupply();
_tokenURIs[tokenId] = base64EncodedSVG;
_safeMint(to, tokenId);
_ownedTokens[to].push(tokenId); // Add the tokenId to the list of owned tokens for the address 'to'
}

/**
*
* @notice Generate a simple SVG image that can be stored on-chain
* @param quizID unique identifier for the quiz contract
*/
function generateComplexSVG(
uint32 quizID
) public view onlyAllowMint returns (string memory) {

// Use tokenId to generate some unique details for the SVG
// For simplicity, let's change the circle's color based on the tokenId's parity
string memory circleColor = quizID % 2 == 0 ? "blue" : "red";

// SVG parts concatenated with dynamic data
string memory svg = string(abi.encodePacked(
"<svg width=\"200\" height=\"200\" xmlns=\"http://www.w3.org/2000/svg\">",
"<circle cx=\"100\" cy=\"100\" r=\"50\" fill=\"", circleColor, "\" />",
"</svg>"
));

// Encode the entire SVG string to Base64
string memory base64EncodedSVG = Base64.encode(bytes(svg));

// Return the data URI for the SVG image
return
string(
abi.encodePacked("data:image/svg+xml;base64,", base64EncodedSVG)
);
}

/**
*
* @notice Returns the SVG base64 encoded string for a tokenID
* @param tokenId unique identifier for the token
*
*/
function tokenURI(
uint256 tokenId
) public view virtual override returns (string memory) {
require(
_exists(tokenId),
"ERC721URIStorage: URI set of nonexistent token"
);
return _tokenURIs[tokenId];
}

/**
*
* @notice Returns the list of token IDs owned by an address
* @param owner address of the owner
*/
function getOwnedTokens(address owner) public view returns (uint256[] memory) {
require(owner != address(0), "Address cannot be the zero address.");
return _ownedTokens[owner];
}
}
51 changes: 46 additions & 5 deletions backend/contracts/Quiz.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ pragma solidity ^0.8.0;

import "@oasisprotocol/sapphire-contracts/contracts/Sapphire.sol";
import {EIP155Signer} from "@oasisprotocol/sapphire-contracts/contracts/EIP155Signer.sol";
import {OasisReward} from "./OasisReward.sol";

contract Quiz {

string constant errInvalidCoupon = "Invalid coupon";
string constant errCouponExists = "Coupon already exists";
string constant errWrongAnswer = "Wrong answer";
Expand Down Expand Up @@ -42,6 +44,12 @@ contract Quiz {
address _owner;
// Encryption key for encrypting payout certificates.
bytes32 _key;
// Quiz ID for generating NFT images
uint32 _quizID;
// Base64 svg image for this quiz instance
string _svgImage;
// NFT contract address
address _nftAddress;
// List of questions.
QuizQuestion[] _questions;
// Total number of choices. Used for generating the permutation vector.
Expand All @@ -54,6 +62,12 @@ contract Quiz {
uint _reward;
// Keypair used for gasless transactions (optional).
EthereumKeypair _kp;

// Flag for making a ROSE payout
bool internal _makePayoutNative = true;
// Flag for making a NFT payout
bool internal _makePayoutNFT = true;


modifier onlyOwner {
require(msg.sender == _owner);
Expand Down Expand Up @@ -243,7 +257,7 @@ contract Quiz {
EIP155Signer.EthTx({
nonce: _kp.nonce,
gasPrice: 100_000_000_000,
gasLimit: 250_000,
gasLimit: 1_000_000,
to: address(this),
value: 0,
data: abi.encodeCall(this.claimReward, encPc),
Expand All @@ -266,19 +280,46 @@ contract Quiz {
// Check coupon validity.
require(_coupons[pc.coupon] == COUPON_VALID, errInvalidCoupon);

// Make a payout.
(bool success, ) = pc.addr.call{value: _reward}("");
require(success, errPayoutFailed);
if (_makePayoutNative) {
// Make a payout.
(bool success, ) = pc.addr.call{value: _reward}("");
require(success, errPayoutFailed);
}

if (_makePayoutNFT) {
// Mint NFT
OasisReward(getNft()).mint(pc.addr, _svgImage);
}

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

// Increase nonce, for gasless tx.
if (msg.sender==_kp.addr) {
_kp.nonce++;
}
}

// Sets the flag for making a ROSE payout and the flag for making a NFT payout
function setPayoutFlags(bool makePayoutNative, bool makePayoutNFT) external onlyOwner {
_makePayoutNative = makePayoutNative;
_makePayoutNFT = makePayoutNFT;
}

// Adds the IDs of the NFT contract to the Quiz contract
function setNft(address nftAddress) external onlyOwner
{
_quizID = uint32(block.number);
_svgImage = OasisReward(nftAddress).generateComplexSVG(_quizID);
_nftAddress = nftAddress;
}

// Function to get the deployed ERC721 contract instance
function getNft() public view returns (address) {
require(_nftAddress != address(0), "NFT contract not deployed");
return _nftAddress;
}

// Reclaims contract funds to given address.
function reclaimFunds(address addr) external onlyOwner {
(bool success, ) = addr.call{value: address(this).balance}("");
Expand Down
37 changes: 36 additions & 1 deletion backend/hardhat.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import 'hardhat-watcher';
import { TASK_COMPILE } from 'hardhat/builtin-tasks/task-names';
import { HardhatUserConfig, task } from 'hardhat/config';
import 'solidity-coverage';
import {ethers} from "hardhat";
require("@nomicfoundation/hardhat-chai-matchers");

const TASK_EXPORT_ABIS = 'export-abis';

Expand Down Expand Up @@ -38,6 +38,23 @@ task(TASK_EXPORT_ABIS, async (_args, hre) => {
);
});

// Deploy the OasisReward contract.
task('deployOasisReward')
.addParam("name", "Name of NFT")
.addParam("symbol", "Symbol for NFT token")
.setAction(async (args, hre) => {
await hre.run('compile');

// For deployment unwrap the provider to enable contract verification.
const uwProvider = new JsonRpcProvider(hre.network.config.url);
const OasisReward = await hre.ethers.getContractFactory('OasisReward', new hre.ethers.Wallet(accounts[0], uwProvider));
const oasisReward = await OasisReward.deploy(args.name, args.symbol);
await oasisReward.waitForDeployment();

console.log(`OasisReward address: ${await oasisReward.getAddress()}`);
return oasisReward;
});

// Unencrypted contract deployment.
task('deploy')
.setAction(async (args, hre) => {
Expand All @@ -53,6 +70,24 @@ task('deploy')
return quiz;
});

// Set the NFT address in the Quiz contract.
task("setNftAddress", "Sets the NFT contract address in the Quiz contract")
.addParam("address", "The address of the deployed Quiz contract")
.addParam("nftaddress", "The address of the NFT contract to set")
.setAction(async (taskArgs, hre) => {

const quiz = await hre.ethers.getContractAt('Quiz', taskArgs.address);
// Call the setNft function with the provided NFT address
const tx = await quiz.setNft(taskArgs.nftaddress);

// Wait for the transaction to be mined
await tx.wait();

console.log(`NFT address set to ${taskArgs.nftaddress} in Quiz contract`);
});



// Get list of valid coupons and spent coupons with the block number.
task('getCoupons')
.addPositionalParam('address', 'contract address')
Expand Down
3 changes: 2 additions & 1 deletion backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"ethers": "^6.10.0"
},
"devDependencies": {
"@nomicfoundation/hardhat-chai-matchers": "^2.0.7",
"@nomicfoundation/hardhat-ethers": "^3.0.5",
"@oasisprotocol/sapphire-contracts": "^0.2.7",
"@oasisprotocol/sapphire-hardhat": "^2.19.4",
Expand All @@ -62,4 +63,4 @@
"typechain": "^8.3.2",
"typescript": "^4.9.5"
}
}
}
Loading