Skip to content

Commit

Permalink
feat: add mint NFT page
Browse files Browse the repository at this point in the history
  • Loading branch information
sarahschwartz committed Sep 11, 2024
1 parent c8c3e06 commit da63c60
Show file tree
Hide file tree
Showing 12 changed files with 346 additions and 98 deletions.
89 changes: 89 additions & 0 deletions code/webauthn/contracts/contracts/MyNFT.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.20;

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

contract MyNFT is ERC721URIStorage {
uint256 private _tokenId = 0;

string[] zeekColors = [
"#FF5733",
"#FFBD33",
"#FFFF33",
"#33FF57",
"#33FFF6",
"#3380FF",
"#8E33FF",
"#FF33F0",
"#FF33B8",
"#FF3384",
"#FFC133",
"#D4FF33",
"#33FFAC",
"#33FFDA",
"#333EFF",
"#FF5A33",
"#FF33A5",
"#FF3358",
"#FFF633",
"#AFFF33"
];
event NewNFTMinted(address sender, uint256 tokenId);

constructor() ERC721 ("ZeekNFT", "ZEEKS") {}

function pickColor(uint256 tokenId) public view returns (string memory) {
uint256 rand = fakeRandom(string(abi.encodePacked("ZEEK_SWEATER_COLOR", Strings.toString(tokenId))));
rand = rand % zeekColors.length;
return zeekColors[rand];
}

function fakeRandom(string memory input) internal pure returns (uint256) {
return uint256(keccak256(abi.encodePacked(input)));
}

function mintZeek() public {
uint256 newItemId = _tokenId;
string memory color = pickColor(newItemId);
string memory finalSvg = getZeekSVG(color);
string memory json = Base64.encode(
bytes(
string(
abi.encodePacked(
'{"name": "',
abi.encodePacked("Zeek NFT"),
'", "image": "data:image/svg+xml;base64,',
Base64.encode(bytes(finalSvg)),
'"}'
)
)
)
);

bytes memory encoded = abi.encodePacked("data:application/json;base64,", json);
string memory finalTokenUri = string(encoded);

_mint(msg.sender, newItemId);

_setTokenURI(newItemId, finalTokenUri);

_tokenId = _tokenId + 1;

emit NewNFTMinted(msg.sender, newItemId);
}

function getZeekSVG(string memory color) internal pure returns (string memory){
string memory a =
'<svg viewBox="0 0 500 500" xmlns="http://www.w3.org/2000/svg"> <g transform="matrix(1, 0, 0, 1, 1.7300360202789307, 4.777588844299316)"> <path style="stroke: rgb(38, 33, 34); fill: ';
string memory b =
'; stroke-width: 10px;" d="M 64.317 163.963 C 64.317 163.963 59.978 128.007 59.978 127.786 C 59.978 127.564 52.664 91.336 53.195 73.015 C 53.776 52.98 63.985 13.806 63.985 13.806 L 90.85 22.9 C 90.85 22.9 120.743 34.328 129.305 40.744 C 160.276 63.949 163.142 80.444 163.302 80.444 C 163.463 80.444 123.677 105.362 123.677 105.362 L 88.557 135.994 L 64.317 163.963 Z"> <title>Left Ear</title> </path> <path style="stroke: rgb(38, 33, 34); fill: ';
string memory c =
'; stroke-width: 10px; transform-box: fill-box; transform-origin: 50% 50%;" d="M 346.378 12.887 C 346.378 12.887 342.039 48.843 342.039 49.065 C 342.039 49.287 334.725 85.514 335.256 103.835 C 335.838 123.87 346.047 163.045 346.047 163.045 L 372.911 153.95 C 372.911 153.95 402.804 142.522 411.367 136.106 C 442.337 112.901 445.203 96.406 445.364 96.406 C 445.524 96.406 405.738 71.488 405.738 71.488 L 370.618 40.856 L 346.378 12.887 Z" transform="matrix(-1, 0, 0, -1, -0.00004, -0.000042)"> <title>Left Ear</title> </path> <ellipse style="paint-order: stroke; stroke: rgb(38, 33, 34); stroke-width: 18px; fill: rgb(255, 211, 146);" cx="249.094" cy="270.025" rx="222.913" ry="203.58"> <title>Head</title> </ellipse> <path class="cls-19" d="M 218.858 336.829 C 212.799 341.366 207.077 346.444 200.644 350.373 C 185.543 359.614 168.819 363.035 151.212 362.459 C 128.799 360.189 109.399 351.691 93.792 335.135 C 81.703 322.338 74.084 307.136 71.272 290.037 C 66.092 258.346 76.084 231.497 99.781 209.894 C 111.733 199.027 125.921 192.051 141.868 189.65 C 180.298 183.892 214.291 199.569 233.52 234.272 C 235.18 237.288 236.433 238.304 240.19 237.321 C 248.012 235.289 256.036 235.526 264.06 237.965 C 269.716 225.911 277.638 215.753 287.93 207.593 C 304.182 194.761 322.464 188.33 343.489 188.668 C 370.102 189.108 391.907 199.162 408.938 219.24 C 422.512 235.257 429.151 253.844 429.084 275.175 C 428.981 308.524 414.119 333.104 386.524 350.609 C 377.28 356.466 366.954 359.818 356.12 361.511 C 355.205 361.645 354.359 362.086 353.478 362.391 C 340.138 362.661 326.968 362.459 314.138 357.788 C 302.014 353.351 291.383 346.883 282.175 337.946 C 281.158 336.963 280.041 336.117 278.991 335.236 C 271.407 326.603 265.178 317.123 261.217 306.255 C 257.084 294.945 254.95 283.3 255.357 271.212 C 255.392 269.79 254.716 267.625 253.701 267.047 C 249.028 264.474 243.339 267.827 243.645 273.041 C 244.526 288.988 240.935 303.884 233.012 317.598 C 229.256 324.133 224.311 329.987 219.91 336.117 L 219.943 336.117 C 219.605 336.321 219.301 336.558 218.961 336.793 L 218.858 336.829 Z M 201.253 213.384 C 200.948 213.079 200.678 212.808 200.373 212.504 C 200.033 212.097 199.798 211.554 199.357 211.25 C 181.989 199.333 162.521 196.115 142.443 199.974 C 117.119 204.852 98.359 219.544 87.355 243.076 C 78.013 263.017 77.436 283.435 86.001 303.918 C 87.187 306.83 88.033 309.979 89.626 312.654 C 103.81 336.151 124.498 349.865 151.957 352.03 C 174.64 353.791 194.618 346.139 210.633 330.158 C 228.951 311.807 236.534 289.327 232.504 263.424 C 229.425 243.618 220.451 227.095 203.963 215.109 C 203.589 214.805 203.219 214.5 202.845 214.196 L 202.879 214.196 C 202.304 213.925 201.727 213.654 201.152 213.348 L 201.219 213.348 L 201.253 213.384 Z M 387.439 213.348 C 387.065 212.977 386.794 212.469 386.355 212.165 C 369.425 199.907 350.163 196.421 330.151 199.466 C 288.233 205.834 260.471 245.514 266.565 286.109 C 267.582 292.779 270.323 299.213 272.253 305.747 L 272.253 305.68 C 272.525 306.425 272.829 307.136 273.1 307.88 C 276.893 313.872 279.973 320.441 284.545 325.759 C 308.99 354.232 350.296 361.035 382.124 341.096 C 407.514 325.181 420.178 301.822 418.824 271.313 C 418.113 255.4 412.73 241.383 403.419 228.755 C 399.457 223.37 395.701 217.549 388.894 215.109 C 388.387 214.533 387.911 213.96 387.403 213.384 L 387.439 213.348 Z M 257.592 256.685 C 258.1 254.825 258.507 252.625 259.252 250.558 C 260.201 247.884 258.845 246.596 256.61 246.564 C 251.228 246.461 245.846 246.767 239.714 246.902 C 240.73 251.1 241.477 254.149 242.086 256.685 L 257.592 256.685 Z" style="fill: ';
string memory d =
';"> <title>Glasses</title> </path> <path class="cls-20" d="M 257.219 367.402 C 256.951 373.834 260.438 377.321 268.09 377.525 C 274.927 377.695 281.835 377.321 288.64 376.679 C 293.79 376.205 297.377 371.636 297.681 366.151 C 297.951 361.137 299.136 359.749 302.962 359.818 C 306.62 359.886 308.008 361.645 307.804 366.52 C 307.364 377.118 300.119 385.991 289.181 386.836 C 280.92 387.48 272.557 387.345 264.296 386.666 C 260.201 386.328 256.24 383.958 251.669 382.298 C 240.359 389.308 227.02 388.429 213.682 386.905 C 202.065 385.584 195.16 374.378 195.262 365.744 C 195.296 361.782 197.158 359.918 200.644 359.818 C 204.775 359.681 205.454 362.052 205.454 365.506 C 205.419 371.6 210.091 376.611 216.217 376.78 C 223.973 376.982 231.726 376.95 239.479 376.78 C 244.627 376.645 247.232 373.091 246.556 367.434 C 243.815 367.434 241.004 367.503 238.228 367.434 C 232.437 367.266 228.305 364.084 226.749 358.665 C 225.462 354.266 230.336 347.392 235.485 347.256 C 246.183 346.952 256.914 346.952 267.614 347.256 C 272.626 347.392 276.013 352.301 275.808 357.788 C 275.574 363.306 271.509 367.064 265.178 367.434 C 262.671 367.571 260.133 367.434 257.254 367.434 L 257.219 367.402 Z" style="fill: rgb(37, 33, 34);"> <title>Mouth</title> </path> </g> </svg>';

return string(abi.encodePacked(a, color, b, color, c, color, d));
}
}
25 changes: 25 additions & 0 deletions code/webauthn/contracts/deploy/deployMyNFT.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Wallet, Provider } from 'zksync-ethers';
import type { HardhatRuntimeEnvironment } from 'hardhat/types';
import { Deployer } from '@matterlabs/hardhat-zksync-deploy';

// load env file
import dotenv from 'dotenv';
dotenv.config();

const DEPLOYER_PRIVATE_KEY = process.env.WALLET_PRIVATE_KEY || '';

export default async function (hre: HardhatRuntimeEnvironment) {
// @ts-expect-error target config file which can be testnet or local
const provider = new Provider(hre.network.config.url);
const wallet = new Wallet(DEPLOYER_PRIVATE_KEY, provider);
const deployer = new Deployer(hre, wallet);
const artifact = await deployer.loadArtifact('MyNFT');
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const constructorArguments: any[] = [];
const contract = await deployer.deploy(artifact, constructorArguments);
console.log('CONTRACT ADDRESS: ', await contract.getAddress());

const tx = await contract.mintZeek();
await tx.wait();
console.log('DONE MINTING: ');
}
1 change: 0 additions & 1 deletion code/webauthn/contracts/deploy/registerR1Owner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ export default async function (hre: HardhatRuntimeEnvironment) {
gasLimit: BigInt(20000000),
data,
};
console.log(`tx: ${ethTransferTx}`);
const signedTxHash = EIP712Signer.getSignedDigest(ethTransferTx);
console.log(`Signed tx hash: ${signedTxHash}`);
const signature = ethers.concat([ethers.Signature.from(wallet.signingKey.sign(signedTxHash)).serialized]);
Expand Down
1 change: 1 addition & 0 deletions code/webauthn/contracts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"repository": "https://github.com/matter-labs/zksync-hardhat-template.git",
"scripts": {
"deploy": "hardhat deploy-zksync --script deploy.ts",
"deploy:NFT": "hardhat deploy-zksync --script deployMyNFT.ts",
"transfer": "hardhat deploy-zksync --script deployAndTransfer.ts",
"register": "hardhat deploy-zksync --script registerR1Owner.ts",
"compile": "hardhat compile",
Expand Down
2 changes: 1 addition & 1 deletion code/webauthn/frontend/src/components/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export function Layout({ children, isHome }: { children: React.ReactNode; isHome
return (
<div className={inter.className}>
{!isHome && (
<div style={{ marginTop: '2rem' }}>
<div style={{ margin: '2rem 1rem' }}>
<Link
style={buttonStyles}
href="/"
Expand Down
28 changes: 23 additions & 5 deletions code/webauthn/frontend/src/pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import { Layout } from '../components/Layout';
export default function Home() {
return (
<Layout isHome>
<h1 style={{ textAlign: 'center' }}>Sign Txns with WebAuthn Demo</h1>
<div style={{ display: 'flex', justifyContent: 'center' }}>
<h1 style={{ textAlign: 'center', margin: '4rem 1rem' }}>Sign Txns with WebAuthn Demo</h1>
<div style={containerStyles}>
<Link
style={buttonStyles}
href="/register"
Expand All @@ -18,18 +18,36 @@ export default function Home() {
>
Transfer Funds
</Link>
<Link
style={buttonStyles}
href="/mint"
>
Mint NFT
</Link>
</div>
</Layout>
);
}

export const buttonStyles = {
padding: '1rem 2rem',
margin: '1rem',
backgroundColor: '#0621ba',
borderRadius: '5px',
textDecoration: 'none',
cursor: 'pointer',
borderColor: 'transparent',
fontSize: '1rem',
};
fontSize: '1.2rem',
width: '300px',
margin: 'auto',
textAlign: 'center',
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any;

const containerStyles = {
display: 'flex',
alignContent: 'center',
flexDirection: 'column',
gap: '1.5rem',
marginTop: '1rem',
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any;
92 changes: 92 additions & 0 deletions code/webauthn/frontend/src/pages/mint.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { Provider } from 'zksync-ethers';
import { getDataToSign, signAndSend } from '../../utils/sign';
import { getTransaction } from '../../utils/getTransaction';
import React from 'react';
import { Layout } from '../components/Layout';
import { buttonStyles } from '.';
import { containerStyles } from './register';
import { ethers } from 'ethers';
import { authenticate } from '../../utils/webauthn';
import * as NFT_ABI_JSON from '../../../contracts/artifacts-zk/contracts/MyNFT.sol/MyNFT.json';

// Update this with your deployed smart contract account address and NFT contract address
const ACCOUNT_ADDRESS = '0x<YOUR_ACCOUNT_ADDRESS>';
const NFT_CONTRACT_ADDRESS = '0x<YOUR_NFT_CONTRACT_ADDRESS>';

export default function Mint() {
const [mintedSVG, setMintedSVG] = React.useState<string | null>(null);
const provider = new Provider('http://localhost:8011');

async function mint(e: any) {
e.preventDefault();
try {
const contract = new ethers.Contract(NFT_CONTRACT_ADDRESS, NFT_ABI_JSON.abi, provider);
const functionName = 'mintZeek';
const functionArgs: any[] = [];
const data = contract.interface.encodeFunctionData(functionName, functionArgs);
const transferValue = '0';
const tx = await getTransaction(NFT_CONTRACT_ADDRESS, ACCOUNT_ADDRESS, transferValue, data, provider);
const signedTxHash = getDataToSign(tx);
const authResponse = await authenticate(signedTxHash.toString());
const receipt = await signAndSend(provider, tx, authResponse);
console.log('RECEIPT:', receipt);
const foundLog = receipt.logs.find((log: any) => {
try {
const parsedLog = contract.interface.parseLog(log);
if (parsedLog.name === 'NewNFTMinted') {
return true;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (_error) {
// ignore error
}
return false;
});
const dataFromLog = foundLog?.data;
const parsedData = dataFromLog ? contract.interface.decodeEventLog('NewNFTMinted', dataFromLog) : null;
const nftURI = await contract.tokenURI(parsedData?.tokenId);
const prefix = 'data:application/json;base64,';
const base64NFTData = nftURI.slice(prefix.length);
const json = atob(base64NFTData);
const svg = JSON.parse(json).image;
setMintedSVG(svg);
} catch (error) {
console.log('ERROR:', error);
}
}

return (
<Layout>
<div style={{ ...containerStyles, marginBottom: '2rem' }}>
<button
style={buttonStyles}
onClick={mint}
>
Mint a Zeek NFT
</button>
</div>
{mintedSVG && (
<>
<h3 style={{ textAlign: 'center' }}>🎉 You minted a ZEEK NFT! 🎉</h3>
<div
style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
flexDirection: 'column',
maxWidth: '400px',
maxHeight: '400px',
margin: '2rem auto',
}}
>
<img
src={mintedSVG}
alt="Minted NFT"
/>
</div>
</>
)}
</Layout>
);
}
23 changes: 18 additions & 5 deletions code/webauthn/frontend/src/pages/register.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,21 +62,22 @@ export default function Register() {

return (
<Layout>
<h1 style={headerStyles}>Register a New Passkey</h1>
<form style={{ marginTop: '1rem' }}>
<div style={containerStyles}>
<label
style={{ marginRight: '1rem' }}
style={labelStyles}
htmlFor="username"
>
Passkey Name
Passkey Name:
</label>

<input
type="text"
name="username"
id="username"
placeholder="test-zksync-webauthn"
style={{ ...inputStyles, width: '400px', marginBottom: '1rem' }}
style={{ ...inputStyles, width: '300px', marginBottom: '1rem' }}
value={userName}
autoComplete="webauthn register"
onChange={(e) => setUserName(e.target.value)}
Expand Down Expand Up @@ -112,12 +113,24 @@ export default function Register() {

export const containerStyles = {
display: 'flex',
justifyContent: 'center',
};
flexDirection: 'column',
} as React.CSSProperties;

export const inputStyles = {
padding: '0.5rem',
fontSize: '1rem',
border: '1px solid #3557f1',
borderRadius: '0.5rem',
margin: 'auto',
};

export const labelStyles = {
margin: '1rem auto',
fontSize: '1.2rem',
width: '300px',
};

export const headerStyles = {
textAlign: 'center',
marginBottom: '2rem',
} as React.CSSProperties;
Loading

0 comments on commit da63c60

Please sign in to comment.