├── .github
└── workflows
│ └── test.yaml
├── .gitignore
├── README.md
├── ape-config.yaml
├── app
├── .eslintrc.json
├── .gitignore
├── .npmrc
├── README.md
├── components
│ ├── About.tsx
│ ├── Commit.tsx
│ ├── Connect.tsx
│ ├── ContractInfo.tsx
│ ├── Mint.tsx
│ ├── Reveal.tsx
│ ├── Success.tsx
│ └── Tokens.tsx
├── config
│ ├── abis
│ │ └── commitReveal.ts
│ └── contracts.ts
├── hooks
│ ├── contracts.ts
│ └── hasMounted.ts
├── next-env.d.ts
├── next.config.js
├── package.json
├── pages
│ ├── _app.tsx
│ └── index.tsx
├── postcss.config.js
├── styles
│ └── globals.css
├── tailwind.config.js
├── tsconfig.json
├── utils
│ └── index.ts
└── yarn.lock
├── contracts
├── Base64.vy
├── CommitReveal.vy
├── CommitRevealMintable.vy
├── Metadata.sol
├── SafeReceiver.vy
└── UnsafeReceiver.vy
├── scripts
├── __init__.py
└── deploy.py
├── svg
└── example.svg
└── tests
├── __init__.py
├── conftest.py
├── test_base64.py
├── test_commit_reveal.py
└── test_metadata.py
/.github/workflows/test.yaml:
--------------------------------------------------------------------------------
1 | on: ["push", "pull_request"]
2 |
3 | name: Test
4 |
5 | jobs:
6 | functional:
7 | runs-on: ubuntu-latest
8 | if: ${{ false }}
9 | steps:
10 | - uses: actions/checkout@v2
11 | - uses: ApeWorX/github-action@v1
12 | - run: ape compile --size
13 | - run: ape test -s
14 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .build/
2 | ape-nft-project/
3 |
4 | # Byte-compiled / optimized / DLL files
5 | __pycache__/
6 | *.py[cod]
7 | *$py.class
8 |
9 | # C extensions
10 | *.so
11 |
12 | # Distribution / packaging
13 | .Python
14 | build/
15 | develop-eggs/
16 | dist/
17 | downloads/
18 | eggs/
19 | .eggs/
20 | lib/
21 | lib64/
22 | parts/
23 | sdist/
24 | var/
25 | wheels/
26 | pip-wheel-metadata/
27 | share/python-wheels/
28 | *.egg-info/
29 | .installed.cfg
30 | *.egg
31 | MANIFEST
32 |
33 | # PyInstaller
34 | # Usually these files are written by a python script from a template
35 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
36 | *.manifest
37 | *.spec
38 |
39 | # Installer logs
40 | pip-log.txt
41 | pip-delete-this-directory.txt
42 |
43 | # Unit test / coverage reports
44 | htmlcov/
45 | .tox/
46 | .nox/
47 | .coverage
48 | .coverage.*
49 | .cache
50 | nosetests.xml
51 | coverage.xml
52 | *.cover
53 | *.py,cover
54 | .hypothesis/
55 | .pytest_cache/
56 |
57 | # Translations
58 | *.mo
59 | *.pot
60 |
61 | # Django stuff:
62 | *.log
63 | local_settings.py
64 | db.sqlite3
65 | db.sqlite3-journal
66 |
67 | # Flask stuff:
68 | instance/
69 | .webassets-cache
70 |
71 | # Scrapy stuff:
72 | .scrapy
73 |
74 | # Sphinx documentation
75 | docs/_build/
76 |
77 | # PyBuilder
78 | target/
79 |
80 | # Jupyter Notebook
81 | .ipynb_checkpoints
82 |
83 | # IPython
84 | profile_default/
85 | ipython_config.py
86 |
87 | # pyenv
88 | .python-version
89 |
90 | # pipenv
91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
94 | # install all needed dependencies.
95 | #Pipfile.lock
96 |
97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
98 | __pypackages__/
99 |
100 | # Celery stuff
101 | celerybeat-schedule
102 | celerybeat.pid
103 |
104 | # SageMath parsed files
105 | *.sage.py
106 |
107 | # Environments
108 | .env
109 | .venv
110 | env/
111 | venv/
112 | ENV/
113 | env.bak/
114 | venv.bak/
115 |
116 | # Spyder project settings
117 | .spyderproject
118 | .spyproject
119 |
120 | # Rope project settings
121 | .ropeproject
122 |
123 | # mkdocs documentation
124 | /site
125 |
126 | # mypy
127 | .mypy_cache/
128 | .dmypy.json
129 | dmypy.json
130 |
131 | # Pyre type checker
132 | .pyre/
133 | .vscode/
134 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Commit/Reveal
2 |
3 | Hashed onchain commitments, revealable NYE 2023.
4 |
--------------------------------------------------------------------------------
/ape-config.yaml:
--------------------------------------------------------------------------------
1 | name: commit-reveal
2 | plugins:
3 | - name: vyper
4 | - name: solidity
5 | - name: alchemy
6 | - name: etherscan
7 | dependencies:
8 | - name: OpenZeppelin
9 | github: OpenZeppelin/openzeppelin-contracts
10 | version: "4.8.0"
11 | solidity:
12 | import_remapping:
13 | - "@openzeppelin=OpenZeppelin/v4.8.0/"
14 |
--------------------------------------------------------------------------------
/app/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals"
3 | }
4 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 | .pnpm-debug.log*
27 |
28 | # local env files
29 | .env.local
30 | .env.development.local
31 | .env.test.local
32 | .env.production.local
33 |
34 | # vercel
35 | .vercel
36 |
37 | # typescript
38 | *.tsbuildinfo
39 |
--------------------------------------------------------------------------------
/app/.npmrc:
--------------------------------------------------------------------------------
1 | strict-peer-dependencies = false
2 |
--------------------------------------------------------------------------------
/app/README.md:
--------------------------------------------------------------------------------
1 | This is a [RainbowKit](https://rainbowkit.com) + [wagmi](https://wagmi.sh) + [Next.js](https://nextjs.org/) project bootstrapped with [`create-rainbowkit`](https://github.com/rainbow-me/rainbowkit/tree/main/packages/create-rainbowkit).
2 |
3 | ## Getting Started
4 |
5 | First, run the development server:
6 |
7 | ```bash
8 | npm run dev
9 | ```
10 |
11 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
12 |
13 | You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file.
14 |
15 | ## Learn More
16 |
17 | To learn more about this stack, take a look at the following resources:
18 |
19 | - [RainbowKit Documentation](https://rainbowkit.com) - Learn how to customize your wallet connection flow.
20 | - [wagmi Documentation](https://wagmi.sh) - Learn how to interact with Ethereum.
21 | - [Next.js Documentation](https://nextjs.org/docs) - Learn how to build a Next.js application.
22 |
23 | You can check out [the RainbowKit GitHub repository](https://github.com/rainbow-me/rainbowkit) - your feedback and contributions are welcome!
24 |
25 | ## Deploy on Vercel
26 |
27 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
28 |
29 | Check out the [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
30 |
--------------------------------------------------------------------------------
/app/components/About.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | function About() {
4 | return (
5 |
6 |
Connect your wallet and select a 2023 commitment to reveal.
7 |
8 | You'll need to enter the exact text of your
9 | commitment message in order to reveal it.
10 |
11 |
21 |
22 | );
23 | }
24 |
25 | export default About;
26 |
--------------------------------------------------------------------------------
/app/components/Commit.tsx:
--------------------------------------------------------------------------------
1 | import { BytesLike, keccak256, toUtf8Bytes } from "ethers";
2 | import React, { useState } from "react";
3 |
4 | interface UploadProps {
5 | onCommitmentChange: (hash: BytesLike) => void;
6 | }
7 |
8 | function Upload({ onCommitmentChange }: UploadProps) {
9 | const [commitment, setCommitment] = useState("");
10 |
11 | function handleChange(event: React.ChangeEvent) {
12 | if (event.target.value.length <= 256) {
13 | setCommitment(event.target.value);
14 | onCommitmentChange(keccak256(toUtf8Bytes(event.target.value)));
15 | }
16 | }
17 |
18 | return (
19 |
20 |
28 |
32 |
{toUtf8Bytes(commitment).length}/256 bytes
33 |
{keccak256(toUtf8Bytes(commitment))}
34 |
35 |
36 | );
37 | }
38 |
39 | export default Upload;
40 |
--------------------------------------------------------------------------------
/app/components/Connect.tsx:
--------------------------------------------------------------------------------
1 | import { ConnectButton } from "@rainbow-me/rainbowkit";
2 |
3 | function Connect() {
4 | return (
5 |
6 |
7 |
8 | );
9 | }
10 |
11 | export default Connect;
12 |
--------------------------------------------------------------------------------
/app/components/ContractInfo.tsx:
--------------------------------------------------------------------------------
1 | import { useNetwork } from "wagmi";
2 |
3 | import { getContract } from "../config/contracts";
4 |
5 | function ContractInfo() {
6 | const { chain } = useNetwork();
7 | const contract = getContract(chain);
8 |
9 | const etherscanURL =
10 | chain && `${chain.blockExplorers?.default.url}/address/${contract}`;
11 |
12 | return (
13 |
27 | );
28 | }
29 |
30 | export default ContractInfo;
31 |
--------------------------------------------------------------------------------
/app/components/Mint.tsx:
--------------------------------------------------------------------------------
1 | import { BigNumberish, BytesLike, Interface } from "ethers";
2 | import React, { useState } from "react";
3 | import {
4 | useAccount,
5 | useContractWrite,
6 | useNetwork,
7 | usePrepareContractWrite,
8 | useWaitForTransaction,
9 | } from "wagmi";
10 | import { commitRevealABI } from "../config/abis/commitReveal";
11 | import { getContract } from "../config/contracts";
12 | import { TransactionReceipt } from "viem";
13 |
14 | interface MintProps {
15 | hash?: BytesLike;
16 | onMintSuccess: (txHash?: string, tokenId?: BigNumberish) => void;
17 | closed: boolean;
18 | }
19 |
20 | interface WagmiError {
21 | reason?: string;
22 | }
23 |
24 | const EMPTY_HASH =
25 | "0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470";
26 |
27 | function Mint({ hash, onMintSuccess, closed }: MintProps) {
28 | const { isConnected } = useAccount();
29 | const { chain } = useNetwork();
30 | const [success, setSuccess] = useState();
31 |
32 | const commitmentHash = hash as `0x${string}`;
33 | const { config } = usePrepareContractWrite({
34 | address: getContract(chain),
35 | abi: commitRevealABI,
36 | functionName: "commit",
37 | args: commitmentHash && [commitmentHash],
38 | });
39 | const {
40 | write: mint,
41 | data: txData,
42 | error: writeError,
43 | } = useContractWrite(config);
44 | const { error: mintError, isLoading: isMinting } = useWaitForTransaction({
45 | hash: txData?.hash,
46 | onSuccess(data) {
47 | setSuccess(true);
48 | onMintSuccess(txData?.hash, parseTokenId(data));
49 | },
50 | });
51 |
52 | const enabled = !closed && isConnected && hash && hash !== EMPTY_HASH;
53 | const error = (mintError || writeError) as WagmiError;
54 |
55 | const parseTokenId = (data: TransactionReceipt) => {
56 | const log = data.logs
57 | .map((log) => {
58 | try {
59 | const contract = new Interface(commitRevealABI);
60 | const parsed = contract.parseLog(log);
61 | return parsed;
62 | } catch {
63 | return;
64 | }
65 | })
66 | .find((log) => log?.name == "Transfer");
67 | return log?.args.tokenId;
68 | };
69 |
70 | const onClick = () => {
71 | if (enabled && !isMinting && !success && mint) {
72 | mint();
73 | }
74 | };
75 |
76 | const getMessage = () => {
77 | if (error) {
78 | const reason = error?.reason;
79 | return reason ? `Error: ${reason}` : "Error";
80 | }
81 | if (success) return "Success!";
82 | if (closed) return "Minting closed";
83 | if (!enabled) return "Type to mint";
84 | return "Mint this commitment";
85 | };
86 |
87 | const message = getMessage();
88 |
89 | return (
90 |
91 |
98 |
99 | );
100 | }
101 |
102 | export default Mint;
103 |
--------------------------------------------------------------------------------
/app/components/Reveal.tsx:
--------------------------------------------------------------------------------
1 | import { keccak256, toUtf8Bytes } from "ethers";
2 | import React, { useState } from "react";
3 | import { Token } from "./Tokens";
4 | import {
5 | useAccount,
6 | useNetwork,
7 | usePrepareContractWrite,
8 | useContractWrite,
9 | useWaitForTransaction,
10 | } from "wagmi";
11 | import { commitRevealABI } from "../config/abis/commitReveal";
12 | import { getContract } from "../config/contracts";
13 | import { getColor } from "../utils";
14 |
15 | interface RevealProps {
16 | token?: Token;
17 | onRevealSuccess: (txHash: string) => void;
18 | }
19 |
20 | interface WagmiError {
21 | reason?: string;
22 | }
23 |
24 | function Reveal({ token, onRevealSuccess }: RevealProps) {
25 | const { isConnected } = useAccount();
26 | const { chain } = useNetwork();
27 | const [success, setSuccess] = useState();
28 | const [commitment, setCommitment] = useState("");
29 |
30 | const { config } = usePrepareContractWrite({
31 | address: getContract(chain),
32 | abi: commitRevealABI,
33 | functionName: "reveal",
34 | args: [BigInt(token?.tokenId || 0), commitment],
35 | });
36 | const {
37 | write: reveal,
38 | data: txData,
39 | error: writeError,
40 | } = useContractWrite(config);
41 | const { error: revealError, isLoading } = useWaitForTransaction({
42 | hash: txData?.hash,
43 | onSuccess() {
44 | setSuccess(true);
45 | onRevealSuccess(txData?.hash ?? "");
46 | },
47 | });
48 |
49 | const hash = keccak256(toUtf8Bytes(commitment));
50 | const matchingHash = hash === token?.commitmentHash;
51 | const alreadyRevealed = token?.commitment !== "";
52 | const canReveal = isConnected && commitment && !alreadyRevealed;
53 | const enabled = canReveal && matchingHash;
54 | const error = (revealError || writeError) as WagmiError;
55 |
56 | function handleChange(event: React.ChangeEvent) {
57 | if (event.target.value.length <= 256) {
58 | setCommitment(event.target.value);
59 | }
60 | }
61 |
62 | const onClick = () => {
63 | if (enabled && !isLoading && !success && reveal) {
64 | reveal();
65 | }
66 | };
67 |
68 | const getMessage = () => {
69 | if (error) {
70 | const reason = error?.reason;
71 | return reason ? `Error: ${reason}` : "Error";
72 | }
73 | if (success) return "Success!";
74 | if (!enabled) return "Type to reveal";
75 | return "Click to reveal";
76 | };
77 |
78 | const message = getMessage();
79 |
80 | if (!isConnected) return ;
81 | return (
82 |
83 | {alreadyRevealed ? (
84 |
90 | ) : (
91 |
92 |
93 |
102 |
106 |
113 | {hash}
114 |
115 |
116 |
117 |
118 |
125 |
126 |
127 | )}
128 |
129 | );
130 | }
131 |
132 | export default Reveal;
133 |
--------------------------------------------------------------------------------
/app/components/Success.tsx:
--------------------------------------------------------------------------------
1 | import { BigNumberish } from "ethers";
2 | import Image from "next/image";
3 | import { useEffect, useState } from "react";
4 | import Confetti from "react-dom-confetti";
5 | import { useNetwork } from "wagmi";
6 |
7 | import { useMetadata } from "../hooks/contracts";
8 |
9 | interface SuccessProps {
10 | txHash?: string;
11 | tokenId?: BigNumberish;
12 | }
13 |
14 | function Success({ txHash, tokenId }: SuccessProps) {
15 | const { chain } = useNetwork();
16 | const { tokenMetadata } = useMetadata(tokenId);
17 | const [active, setActive] = useState(false);
18 |
19 | useEffect(() => {
20 | setActive(true);
21 | }, []);
22 |
23 | const etherscanURL =
24 | chain && `${chain.blockExplorers?.default.url}/tx/${txHash}`;
25 |
26 | return (
27 |
28 |
29 |
30 | {tokenMetadata && (
31 |
38 | )}
39 |
40 | Your commitment was revealed.
41 |
42 | {etherscanURL && (
43 |
44 |
50 | View on Etherscan
51 |
52 |
53 | )}
54 |
55 |
66 |
67 |
68 |
69 |
70 | );
71 | }
72 |
73 | export default Success;
74 |
--------------------------------------------------------------------------------
/app/components/Tokens.tsx:
--------------------------------------------------------------------------------
1 | import { useAccount, useNetwork, usePublicClient } from "wagmi";
2 | import { commitRevealABI } from "../config/abis/commitReveal";
3 | import { getContract } from "../config/contracts";
4 | import { Hex } from "viem";
5 | import { useCallback, useEffect, useState } from "react";
6 | import { getColor } from "../utils";
7 |
8 | interface TokensProps {
9 | onTokenSelected: (token: Token) => void;
10 | }
11 |
12 | export interface Token {
13 | tokenId: number;
14 | tokenURI: string;
15 | commitmentHash: Hex;
16 | commitment: string;
17 | metadata: {
18 | name: string;
19 | description: string;
20 | image: string;
21 | };
22 | }
23 |
24 | function Tokens({ onTokenSelected }: TokensProps) {
25 | const { chain } = useNetwork();
26 | const { address, isConnected } = useAccount();
27 | const contract = getContract(chain);
28 | const publicClient = usePublicClient();
29 |
30 | const [tokens, setTokens] = useState([]);
31 | const [isLoading, setIsLoading] = useState(false);
32 | const [selectedToken, setSelectedToken] = useState();
33 |
34 | const getTokensofOwner = useCallback(async () => {
35 | setIsLoading(true);
36 | const sent = await publicClient.getContractEvents({
37 | address: contract,
38 | abi: commitRevealABI,
39 | eventName: "Transfer",
40 | args: {
41 | sender: address,
42 | },
43 | fromBlock: "earliest",
44 | toBlock: "latest",
45 | });
46 | const received = await publicClient.getContractEvents({
47 | address: contract,
48 | abi: commitRevealABI,
49 | eventName: "Transfer",
50 | args: {
51 | receiver: address,
52 | },
53 | fromBlock: "earliest",
54 | toBlock: "latest",
55 | });
56 | const logs = sent
57 | .concat(received)
58 | .sort(
59 | (a, b) =>
60 | Number(a.blockNumber - b.blockNumber) ||
61 | a.transactionIndex - b.transactionIndex,
62 | );
63 | const owned = new Set();
64 | for (const log of logs) {
65 | const { sender, receiver, tokenId } = log.args;
66 | if (sender === address && tokenId) {
67 | owned.delete(tokenId);
68 | } else if (receiver === address && tokenId) {
69 | owned.add(tokenId);
70 | }
71 | }
72 | let tokenData: Token[] = [];
73 | for (const tokenId of owned) {
74 | const tokenURI = await publicClient.readContract({
75 | address: contract,
76 | abi: commitRevealABI,
77 | functionName: "tokenURI",
78 | args: [tokenId],
79 | });
80 | const commitment = await publicClient.readContract({
81 | address: contract,
82 | abi: commitRevealABI,
83 | functionName: "commitments",
84 | args: [tokenId],
85 | });
86 | const commitmentHash = await publicClient.readContract({
87 | address: contract,
88 | abi: commitRevealABI,
89 | functionName: "commitmentHashes",
90 | args: [tokenId],
91 | });
92 | const metadata = JSON.parse(
93 | Buffer.from(tokenURI.split(",")[1], "base64").toString(),
94 | );
95 | tokenData.push({
96 | tokenId: Number(tokenId),
97 | tokenURI,
98 | commitmentHash,
99 | commitment,
100 | metadata,
101 | });
102 | }
103 | setTokens(tokenData);
104 | setIsLoading(false);
105 | }, [publicClient, contract, address]);
106 |
107 | useEffect(() => {
108 | getTokensofOwner();
109 | }, [getTokensofOwner]);
110 |
111 | const truncate = (hash: Hex) => {
112 | return hash.slice(0, 6) + "…" + hash.slice(-4);
113 | };
114 |
115 | return (
116 |
117 | {isConnected && isLoading &&
Looking up your commitments...
}
118 | {isConnected && !isLoading && (
119 |
120 |
Select a commitment:
121 |
122 | {tokens.map((token) => (
123 |
{
127 | setSelectedToken(token.tokenId);
128 | onTokenSelected(token);
129 | }}
130 | >
131 |
136 |
137 | {truncate(token.commitmentHash)}
138 |
139 |
140 |
141 | ))}
142 |
143 |
144 | )}
145 | {isConnected && !isLoading && tokens.length === 0 && (
146 |
You don't own any Commit/Reveal tokens.
147 | )}
148 |
149 | );
150 | }
151 |
152 | export default Tokens;
153 |
--------------------------------------------------------------------------------
/app/config/abis/commitReveal.ts:
--------------------------------------------------------------------------------
1 | export const commitRevealABI = [
2 | {
3 | name: "Transfer",
4 | inputs: [
5 | { name: "sender", type: "address", indexed: true },
6 | { name: "receiver", type: "address", indexed: true },
7 | { name: "tokenId", type: "uint256", indexed: true },
8 | ],
9 | anonymous: false,
10 | type: "event",
11 | },
12 | {
13 | name: "Approval",
14 | inputs: [
15 | { name: "owner", type: "address", indexed: true },
16 | { name: "approved", type: "address", indexed: true },
17 | { name: "tokenId", type: "uint256", indexed: true },
18 | ],
19 | anonymous: false,
20 | type: "event",
21 | },
22 | {
23 | name: "ApprovalForAll",
24 | inputs: [
25 | { name: "owner", type: "address", indexed: true },
26 | { name: "operator", type: "address", indexed: true },
27 | { name: "isApproved", type: "bool", indexed: false },
28 | ],
29 | anonymous: false,
30 | type: "event",
31 | },
32 | {
33 | stateMutability: "nonpayable",
34 | type: "constructor",
35 | inputs: [
36 | { name: "metadata", type: "address" },
37 | { name: "owner", type: "address" },
38 | ],
39 | outputs: [],
40 | },
41 | {
42 | stateMutability: "pure",
43 | type: "function",
44 | name: "supportsInterface",
45 | inputs: [{ name: "interface_id", type: "bytes4" }],
46 | outputs: [{ name: "", type: "bool" }],
47 | },
48 | {
49 | stateMutability: "pure",
50 | type: "function",
51 | name: "name",
52 | inputs: [],
53 | outputs: [{ name: "", type: "string" }],
54 | },
55 | {
56 | stateMutability: "pure",
57 | type: "function",
58 | name: "symbol",
59 | inputs: [],
60 | outputs: [{ name: "", type: "string" }],
61 | },
62 | {
63 | stateMutability: "view",
64 | type: "function",
65 | name: "contractURI",
66 | inputs: [],
67 | outputs: [{ name: "", type: "string" }],
68 | },
69 | {
70 | stateMutability: "view",
71 | type: "function",
72 | name: "tokenURI",
73 | inputs: [{ name: "tokenId", type: "uint256" }],
74 | outputs: [{ name: "", type: "string" }],
75 | },
76 | {
77 | stateMutability: "view",
78 | type: "function",
79 | name: "ownerOf",
80 | inputs: [{ name: "tokenId", type: "uint256" }],
81 | outputs: [{ name: "", type: "address" }],
82 | },
83 | {
84 | stateMutability: "view",
85 | type: "function",
86 | name: "balanceOf",
87 | inputs: [{ name: "owner", type: "address" }],
88 | outputs: [{ name: "", type: "uint256" }],
89 | },
90 | {
91 | stateMutability: "nonpayable",
92 | type: "function",
93 | name: "commit",
94 | inputs: [{ name: "commitmentHash", type: "bytes32" }],
95 | outputs: [],
96 | },
97 | {
98 | stateMutability: "nonpayable",
99 | type: "function",
100 | name: "reveal",
101 | inputs: [
102 | { name: "tokenId", type: "uint256" },
103 | { name: "commitment", type: "string" },
104 | ],
105 | outputs: [],
106 | },
107 | {
108 | stateMutability: "nonpayable",
109 | type: "function",
110 | name: "approve",
111 | inputs: [
112 | { name: "spender", type: "address" },
113 | { name: "tokenId", type: "uint256" },
114 | ],
115 | outputs: [],
116 | },
117 | {
118 | stateMutability: "nonpayable",
119 | type: "function",
120 | name: "setApprovalForAll",
121 | inputs: [
122 | { name: "operator", type: "address" },
123 | { name: "approved", type: "bool" },
124 | ],
125 | outputs: [],
126 | },
127 | {
128 | stateMutability: "nonpayable",
129 | type: "function",
130 | name: "transferFrom",
131 | inputs: [
132 | { name: "owner", type: "address" },
133 | { name: "receiver", type: "address" },
134 | { name: "tokenId", type: "uint256" },
135 | ],
136 | outputs: [],
137 | },
138 | {
139 | stateMutability: "nonpayable",
140 | type: "function",
141 | name: "safeTransferFrom",
142 | inputs: [
143 | { name: "owner", type: "address" },
144 | { name: "receiver", type: "address" },
145 | { name: "tokenId", type: "uint256" },
146 | ],
147 | outputs: [],
148 | },
149 | {
150 | stateMutability: "nonpayable",
151 | type: "function",
152 | name: "safeTransferFrom",
153 | inputs: [
154 | { name: "owner", type: "address" },
155 | { name: "receiver", type: "address" },
156 | { name: "tokenId", type: "uint256" },
157 | { name: "data", type: "bytes" },
158 | ],
159 | outputs: [],
160 | },
161 | {
162 | stateMutability: "view",
163 | type: "function",
164 | name: "owner",
165 | inputs: [],
166 | outputs: [{ name: "", type: "address" }],
167 | },
168 | {
169 | stateMutability: "view",
170 | type: "function",
171 | name: "commitmentHashes",
172 | inputs: [{ name: "arg0", type: "uint256" }],
173 | outputs: [{ name: "", type: "bytes32" }],
174 | },
175 | {
176 | stateMutability: "view",
177 | type: "function",
178 | name: "commitments",
179 | inputs: [{ name: "arg0", type: "uint256" }],
180 | outputs: [{ name: "", type: "string" }],
181 | },
182 | {
183 | stateMutability: "view",
184 | type: "function",
185 | name: "getApproved",
186 | inputs: [{ name: "arg0", type: "uint256" }],
187 | outputs: [{ name: "", type: "address" }],
188 | },
189 | {
190 | stateMutability: "view",
191 | type: "function",
192 | name: "isApprovedForAll",
193 | inputs: [
194 | { name: "arg0", type: "address" },
195 | { name: "arg1", type: "address" },
196 | ],
197 | outputs: [{ name: "", type: "bool" }],
198 | },
199 | ] as const;
200 |
--------------------------------------------------------------------------------
/app/config/contracts.ts:
--------------------------------------------------------------------------------
1 | import { mainnet } from "wagmi";
2 | import { goerli } from "wagmi/chains";
3 |
4 | import { Chain } from "@rainbow-me/rainbowkit";
5 | import { Hex } from "viem";
6 |
7 | const contracts: { [chainId: number]: Hex } = {
8 | [goerli.id]: "0x5Ea382b48b91778F21D1e605D395579EA3a93638",
9 | [mainnet.id]: "0x71c54ad9f410ed85e81e38af9efc08bac3968665",
10 | };
11 |
12 | export function getContract(currentChain?: Chain): Hex {
13 | return currentChain ? contracts[currentChain.id] : contracts[goerli.id];
14 | }
15 |
--------------------------------------------------------------------------------
/app/hooks/contracts.ts:
--------------------------------------------------------------------------------
1 | import { BigNumberish } from "ethers";
2 | import { useContractRead, useNetwork } from "wagmi";
3 |
4 | import { commitRevealABI } from "../config/abis/commitReveal";
5 | import { getContract } from "../config/contracts";
6 |
7 | export function useMetadata(tokenId?: BigNumberish) {
8 | const { chain } = useNetwork();
9 | const { data, isError, isLoading } = useContractRead({
10 | address: getContract(chain),
11 | abi: commitRevealABI,
12 | functionName: "tokenURI",
13 | args: [BigInt(tokenId ?? 0)],
14 | });
15 | let tokenMetadata;
16 | if (data) {
17 | const json = Buffer.from(data.substring(29), "base64").toString();
18 | tokenMetadata = JSON.parse(json);
19 | }
20 | return { data, tokenMetadata, isError, isLoading };
21 | }
22 |
--------------------------------------------------------------------------------
/app/hooks/hasMounted.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 |
3 | // Pattern for correctly rehydrating the DOM taken from
4 | // https://www.joshwcomeau.com/react/the-perils-of-rehydration/
5 | export const useHasMounted = () => {
6 | const [hasMounted, setHasMounted] = useState(false);
7 | useEffect(() => {
8 | setHasMounted(true);
9 | }, []);
10 | return hasMounted;
11 | };
12 |
--------------------------------------------------------------------------------
/app/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/basic-features/typescript for more information.
6 |
--------------------------------------------------------------------------------
/app/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | reactStrictMode: true,
4 | };
5 |
6 | module.exports = nextConfig;
7 |
--------------------------------------------------------------------------------
/app/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "commit-reveal",
3 | "private": true,
4 | "version": "0.1.0",
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@rainbow-me/rainbowkit": "^1.3.2",
13 | "@types/react-timeago": "^4.1.3",
14 | "@vercel/analytics": "^1.1.1",
15 | "ethers": "^6.9.1",
16 | "next": "^14.0.4",
17 | "react": "^18.2.0",
18 | "react-dom": "^18.2.0",
19 | "react-dom-confetti": "^0.2.0",
20 | "react-timeago": "^7.2.0",
21 | "viem": "^1.21.1",
22 | "wagmi": "^1.4.12"
23 | },
24 | "devDependencies": {
25 | "@types/node": "^17.0.35",
26 | "@types/react": "^18.0.9",
27 | "autoprefixer": "^10.4.13",
28 | "eslint": "^8.56.0",
29 | "eslint-config-next": "^14.0.4",
30 | "postcss": "^8.4.32",
31 | "prettier": "^3.1.1",
32 | "tailwindcss": "^3.4.0",
33 | "typescript": "^5.3.3"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/app/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import "../styles/globals.css";
2 | import "@rainbow-me/rainbowkit/styles.css";
3 |
4 | import { configureChains, createConfig, WagmiConfig } from "wagmi";
5 | import { goerli, mainnet } from "wagmi/chains";
6 | import { alchemyProvider } from "wagmi/providers/alchemy";
7 | import { publicProvider } from "wagmi/providers/public";
8 |
9 | import { getDefaultWallets, RainbowKitProvider } from "@rainbow-me/rainbowkit";
10 | import { Analytics } from "@vercel/analytics/react";
11 |
12 | import type { AppProps } from "next/app";
13 | const { chains, publicClient, webSocketPublicClient } = configureChains(
14 | [
15 | mainnet,
16 | ...(process.env.NEXT_PUBLIC_ENABLE_TESTNETS === "true" ? [goerli] : []),
17 | ],
18 | [
19 | alchemyProvider({
20 | apiKey: process.env.NEXT_PUBLIC_ALCHEMY_API_KEY || "",
21 | }),
22 | publicProvider(),
23 | ],
24 | );
25 |
26 | const { connectors } = getDefaultWallets({
27 | appName: "Commit/Reveal",
28 | projectId: "69239d2d1e43747033bbb9416e84245c",
29 | chains,
30 | });
31 |
32 | const wagmiConfig = createConfig({
33 | autoConnect: true,
34 | connectors,
35 | publicClient,
36 | webSocketPublicClient,
37 | });
38 |
39 | function MyApp({ Component, pageProps }: AppProps) {
40 | return (
41 | <>
42 |
43 |
44 |
45 |
46 |
47 |
48 | >
49 | );
50 | }
51 |
52 | export default MyApp;
53 |
--------------------------------------------------------------------------------
/app/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import Head from "next/head";
2 | import { useState } from "react";
3 |
4 | import About from "../components/About";
5 | import Connect from "../components/Connect";
6 | import ContractInfo from "../components/ContractInfo";
7 | import Success from "../components/Success";
8 | import { useHasMounted } from "../hooks/hasMounted";
9 |
10 | import type { NextPage } from "next";
11 | import Tokens, { Token } from "../components/Tokens";
12 | import Reveal from "../components/Reveal";
13 | const Home: NextPage = () => {
14 | const hasMounted = useHasMounted();
15 | const [token, setToken] = useState();
16 | const [txHash, setTxHash] = useState();
17 | const [success, setSuccess] = useState(false);
18 |
19 | const onRevealSuccess = (txHash: string) => {
20 | setTxHash(txHash);
21 | setSuccess(true);
22 | };
23 |
24 | return (
25 |
26 | {hasMounted && (
27 |
28 |
29 |
Commit/Reveal 2023
30 |
31 |
32 |
33 |
34 |
35 | Commit/Reveal
36 |
37 |
38 | Hashed onchain commitments, revealable NYE 2023.
39 |
40 |
41 |
42 |
43 | {success ? (
44 |
45 | ) : (
46 | <>
47 |
48 | {token && (
49 |
50 | )}
51 |
52 | >
53 | )}
54 |
55 |
56 | )}
57 |
58 | );
59 | };
60 |
61 | export default Home;
62 |
--------------------------------------------------------------------------------
/app/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | };
7 |
--------------------------------------------------------------------------------
/app/styles/globals.css:
--------------------------------------------------------------------------------
1 | @import "tailwindcss/base";
2 | @import "tailwindcss/components";
3 | @import "tailwindcss/utilities";
4 |
--------------------------------------------------------------------------------
/app/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | content: [
4 | "./pages/**/*.{js,ts,jsx,tsx}",
5 | "./components/**/*.{js,ts,jsx,tsx}",
6 | ],
7 | theme: {
8 | extend: {
9 | colors: {
10 | dark: "#151520",
11 | },
12 | },
13 | },
14 | safelist: [
15 | "border-red-500",
16 | "border-orange-500",
17 | "border-yellow-500",
18 | "border-green-500",
19 | "border-sky-500",
20 | "border-violet-500",
21 | "text-red-500",
22 | "text-orange-500",
23 | "text-yellow-500",
24 | "text-green-500",
25 | "text-sky-500",
26 | "text-violet-500",
27 | ],
28 | plugins: [],
29 | };
30 |
--------------------------------------------------------------------------------
/app/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2020",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "forceConsistentCasingInFileNames": true,
9 | "noEmit": true,
10 | "esModuleInterop": true,
11 | "module": "esnext",
12 | "moduleResolution": "node",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "jsx": "preserve",
16 | "incremental": true
17 | },
18 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
19 | "exclude": ["node_modules"]
20 | }
21 |
--------------------------------------------------------------------------------
/app/utils/index.ts:
--------------------------------------------------------------------------------
1 | const colors = ["red", "orange", "yellow", "green", "sky", "violet"];
2 |
3 | export const getColor = (tokenId: number) => {
4 | return colors[tokenId % colors.length];
5 | };
6 |
--------------------------------------------------------------------------------
/contracts/Base64.vy:
--------------------------------------------------------------------------------
1 | # @version 0.3.7
2 |
3 | TABLE: constant(
4 | String[64]
5 | ) = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
6 |
7 |
8 | @view
9 | @external
10 | def encode(data: Bytes[4096]) -> DynArray[String[4], 1366]:
11 | charChunks: DynArray[String[4], 1366] = []
12 |
13 | padBytes: uint256 = 3 - len(data) % 3
14 | padded: Bytes[4098] = empty(Bytes[4098])
15 | if padBytes == 2:
16 | padded = concat(data, b"\x00\x00")
17 | elif padBytes == 1:
18 | padded = concat(data, b"\x00")
19 | else:
20 | padded = data
21 |
22 | i: uint256 = 0
23 | for _ in range(4096):
24 | chunk: uint256 = convert(slice(padded, i, 3), uint256)
25 |
26 | c1: uint256 = shift(chunk, -18) & 63
27 | c2: uint256 = shift(chunk, -12) & 63
28 | c3: uint256 = shift(chunk, -6) & 63
29 | c4: uint256 = chunk & 63
30 |
31 | charChunks.append(
32 | concat(
33 | "",
34 | slice(TABLE, c1, 1),
35 | slice(TABLE, c2, 1),
36 | slice(TABLE, c3, 1),
37 | slice(TABLE, c4, 1),
38 | )
39 | )
40 | i += 3
41 |
42 | if i == len(padded):
43 | break
44 |
45 | if padBytes == 2:
46 | lastChunk: String[2] = slice(charChunks.pop(), 0, 2)
47 | charChunks.append(concat(lastChunk, "=="))
48 | if padBytes == 1:
49 | lastChunk: String[3] = slice(charChunks.pop(), 0, 3)
50 | charChunks.append(concat(lastChunk, "="))
51 |
52 | return charChunks
53 |
--------------------------------------------------------------------------------
/contracts/CommitReveal.vy:
--------------------------------------------------------------------------------
1 | # @version 0.3.7
2 |
3 | from vyper.interfaces import ERC165
4 | from vyper.interfaces import ERC721
5 |
6 | implements: ERC165
7 | implements: ERC721
8 |
9 |
10 | interface Metadata:
11 | def tokenURI(tokenId: uint256) -> String[4096]:
12 | view
13 |
14 | def contractURI() -> String[4096]:
15 | view
16 |
17 |
18 | interface ERC721TokenReceiver:
19 | def onERC721Received(
20 | operator: address, owner: address, tokenId: uint256, data: Bytes[1024]
21 | ) -> bytes4:
22 | view
23 |
24 |
25 | event Transfer:
26 | sender: indexed(address)
27 | receiver: indexed(address)
28 | tokenId: indexed(uint256)
29 |
30 |
31 | event Approval:
32 | owner: indexed(address)
33 | approved: indexed(address)
34 | tokenId: indexed(uint256)
35 |
36 |
37 | event ApprovalForAll:
38 | owner: indexed(address)
39 | operator: indexed(address)
40 | isApproved: bool
41 |
42 |
43 | SUPPORTED_INTERFACE_IDS: constant(bytes4[3]) = [
44 | 0x01FFC9A7, # ERC165
45 | 0x80AC58CD, # ERC721
46 | 0x5B5E139F, # ERC721 Metadata
47 | ]
48 |
49 | # 7 Jan 2023 UTC
50 | COMMIT_PHASE_ENDS: constant(uint256) = 1673136000
51 |
52 | # 31 Dec 2023 UTC
53 | REVEAL_PHASE_BEGINS: constant(uint256) = 1703980800
54 |
55 | NAME: constant(String[13]) = "Commit/Reveal"
56 | SYMBOL: constant(String[8]) = "C/R 2023"
57 |
58 | owner: public(address)
59 | metadata: Metadata
60 |
61 | commitmentHashes: public(HashMap[uint256, bytes32])
62 | commitments: public(HashMap[uint256, String[256]])
63 | _nextId: uint256
64 |
65 | _ownerOf: HashMap[uint256, address]
66 | _balanceOf: HashMap[address, uint256]
67 |
68 | getApproved: public(HashMap[uint256, address])
69 | isApprovedForAll: public(HashMap[address, HashMap[address, bool]])
70 |
71 |
72 | @external
73 | def __init__(metadata: Metadata, owner: address):
74 | self.metadata = metadata
75 | self.owner = owner
76 |
77 |
78 | @pure
79 | @external
80 | def supportsInterface(interface_id: bytes4) -> bool:
81 | return interface_id in SUPPORTED_INTERFACE_IDS
82 |
83 |
84 | @pure
85 | @external
86 | def name() -> String[13]:
87 | return NAME
88 |
89 |
90 | @pure
91 | @external
92 | def symbol() -> String[8]:
93 | return SYMBOL
94 |
95 |
96 | @view
97 | @external
98 | def contractURI() -> String[4096]:
99 | return self.metadata.contractURI()
100 |
101 |
102 | @view
103 | @external
104 | def tokenURI(tokenId: uint256) -> String[4096]:
105 | return self.metadata.tokenURI(tokenId)
106 |
107 |
108 | @view
109 | @external
110 | def ownerOf(tokenId: uint256) -> address:
111 | tokenOwner: address = self._ownerOf[tokenId]
112 | assert tokenOwner != empty(address), "Not minted"
113 | return tokenOwner
114 |
115 |
116 | @view
117 | @external
118 | def balanceOf(owner: address) -> uint256:
119 | assert owner != empty(address), "Zero address"
120 | return self._balanceOf[owner]
121 |
122 |
123 | @external
124 | def commit(commitmentHash: bytes32):
125 | assert block.timestamp < COMMIT_PHASE_ENDS, "Commitments closed"
126 | self._nextId += 1
127 | tokenId: uint256 = self._nextId
128 | self.commitmentHashes[tokenId] = commitmentHash
129 | self._mint(msg.sender, tokenId)
130 |
131 |
132 | @external
133 | def reveal(tokenId: uint256, commitment: String[256]):
134 | assert block.timestamp > REVEAL_PHASE_BEGINS, "Cannot reveal yet"
135 | assert keccak256(commitment) == self.commitmentHashes[tokenId], "Wrong hash"
136 | self.commitments[tokenId] = commitment
137 |
138 |
139 | @external
140 | def approve(spender: address, tokenId: uint256):
141 | tokenOwner: address = self._ownerOf[tokenId]
142 | assert (
143 | msg.sender == tokenOwner or self.isApprovedForAll[tokenOwner][msg.sender]
144 | ), "Not approved"
145 |
146 | self.getApproved[tokenId] = spender
147 |
148 | log Approval(tokenOwner, spender, tokenId)
149 |
150 |
151 | @external
152 | def setApprovalForAll(operator: address, approved: bool):
153 | self.isApprovedForAll[msg.sender][operator] = approved
154 |
155 | log ApprovalForAll(msg.sender, operator, approved)
156 |
157 |
158 | @external
159 | def transferFrom(owner: address, receiver: address, tokenId: uint256):
160 | self._transferFrom(owner, receiver, tokenId)
161 |
162 |
163 | @external
164 | def safeTransferFrom(
165 | owner: address, receiver: address, tokenId: uint256, data: Bytes[1024] = b""
166 | ):
167 | self._transferFrom(owner, receiver, tokenId)
168 | if receiver.is_contract:
169 | returnValue: bytes4 = ERC721TokenReceiver(receiver).onERC721Received(
170 | msg.sender, owner, tokenId, data
171 | )
172 | assert returnValue == method_id(
173 | "onERC721Received(address,address,uint256,bytes)", output_type=bytes4
174 | ), "Unsafe receiver"
175 |
176 |
177 | @internal
178 | def _transferFrom(owner: address, receiver: address, tokenId: uint256):
179 | assert owner == self._ownerOf[tokenId], "Wrong owner"
180 | assert receiver != empty(address), "Invalid receiver"
181 | assert (
182 | msg.sender == owner
183 | or self.isApprovedForAll[owner][msg.sender]
184 | or msg.sender == self.getApproved[tokenId]
185 | ), "Not approved"
186 |
187 | self._balanceOf[owner] -= 1
188 | self._balanceOf[receiver] += 1
189 | self._ownerOf[tokenId] = receiver
190 |
191 | self.getApproved[tokenId] = empty(address)
192 |
193 | log Transfer(owner, receiver, tokenId)
194 |
195 |
196 | @internal
197 | def _mint(receiver: address, tokenId: uint256):
198 | assert receiver != empty(address), "Invalid receiver"
199 | assert self._ownerOf[tokenId] == empty(address), "Already minted"
200 |
201 | self._balanceOf[receiver] += 1
202 | self._ownerOf[tokenId] = receiver
203 |
204 | log Transfer(empty(address), receiver, tokenId)
205 |
--------------------------------------------------------------------------------
/contracts/CommitRevealMintable.vy:
--------------------------------------------------------------------------------
1 | # @version 0.3.7
2 |
3 | from vyper.interfaces import ERC165
4 | from vyper.interfaces import ERC721
5 |
6 | implements: ERC165
7 | implements: ERC721
8 |
9 |
10 | interface Metadata:
11 | def tokenURI(tokenId: uint256) -> String[4096]:
12 | view
13 |
14 | def contractURI() -> String[4096]:
15 | view
16 |
17 |
18 | interface ERC721TokenReceiver:
19 | def onERC721Received(
20 | operator: address, owner: address, tokenId: uint256, data: Bytes[1024]
21 | ) -> bytes4:
22 | view
23 |
24 |
25 | event Transfer:
26 | sender: indexed(address)
27 | receiver: indexed(address)
28 | tokenId: indexed(uint256)
29 |
30 |
31 | event Approval:
32 | owner: indexed(address)
33 | approved: indexed(address)
34 | tokenId: indexed(uint256)
35 |
36 |
37 | event ApprovalForAll:
38 | owner: indexed(address)
39 | operator: indexed(address)
40 | isApproved: bool
41 |
42 |
43 | SUPPORTED_INTERFACE_IDS: constant(bytes4[3]) = [
44 | 0x01FFC9A7, # ERC165
45 | 0x80AC58CD, # ERC721
46 | 0x5B5E139F, # ERC721 Metadata
47 | ]
48 |
49 | # 7 Jan 2023 UTC
50 | COMMIT_PHASE_ENDS: constant(uint256) = 1673136000
51 |
52 | # 31 Dec 2023 UTC
53 | REVEAL_PHASE_BEGINS: constant(uint256) = 1703980800
54 |
55 | NAME: constant(String[13]) = "Commit/Reveal"
56 | SYMBOL: constant(String[8]) = "C/R 2023"
57 |
58 | owner: public(address)
59 | metadata: Metadata
60 |
61 | commitmentHashes: public(HashMap[uint256, bytes32])
62 | commitments: public(HashMap[uint256, String[256]])
63 | _nextId: uint256
64 |
65 | _ownerOf: HashMap[uint256, address]
66 | _balanceOf: HashMap[address, uint256]
67 |
68 | getApproved: public(HashMap[uint256, address])
69 | isApprovedForAll: public(HashMap[address, HashMap[address, bool]])
70 |
71 |
72 | @external
73 | def __init__(metadata: Metadata, owner: address):
74 | self.metadata = metadata
75 | self.owner = owner
76 |
77 |
78 | @pure
79 | @external
80 | def supportsInterface(interface_id: bytes4) -> bool:
81 | return interface_id in SUPPORTED_INTERFACE_IDS
82 |
83 |
84 | @pure
85 | @external
86 | def name() -> String[13]:
87 | return NAME
88 |
89 |
90 | @pure
91 | @external
92 | def symbol() -> String[8]:
93 | return SYMBOL
94 |
95 |
96 | @view
97 | @external
98 | def contractURI() -> String[4096]:
99 | return self.metadata.contractURI()
100 |
101 |
102 | @view
103 | @external
104 | def tokenURI(tokenId: uint256) -> String[4096]:
105 | return self.metadata.tokenURI(tokenId)
106 |
107 |
108 | @view
109 | @external
110 | def ownerOf(tokenId: uint256) -> address:
111 | tokenOwner: address = self._ownerOf[tokenId]
112 | assert tokenOwner != empty(address), "Not minted"
113 | return tokenOwner
114 |
115 |
116 | @view
117 | @external
118 | def balanceOf(owner: address) -> uint256:
119 | assert owner != empty(address), "Zero address"
120 | return self._balanceOf[owner]
121 |
122 |
123 | @external
124 | def mint(receiver: address, tokenId: uint256):
125 | self._mint(receiver, tokenId)
126 |
127 |
128 | @external
129 | def commit(commitmentHash: bytes32):
130 | assert block.timestamp < COMMIT_PHASE_ENDS, "Commitments closed"
131 | self._nextId += 1
132 | tokenId: uint256 = self._nextId
133 | self.commitmentHashes[tokenId] = commitmentHash
134 | self._mint(msg.sender, tokenId)
135 |
136 |
137 | @external
138 | def reveal(tokenId: uint256, commitment: String[256]):
139 | assert block.timestamp > REVEAL_PHASE_BEGINS, "Cannot reveal yet"
140 | assert keccak256(commitment) == self.commitmentHashes[tokenId], "Wrong hash"
141 | self.commitments[tokenId] = commitment
142 |
143 |
144 | @external
145 | def approve(spender: address, tokenId: uint256):
146 | tokenOwner: address = self._ownerOf[tokenId]
147 | assert (
148 | msg.sender == tokenOwner or self.isApprovedForAll[tokenOwner][msg.sender]
149 | ), "Not approved"
150 |
151 | self.getApproved[tokenId] = spender
152 |
153 | log Approval(tokenOwner, spender, tokenId)
154 |
155 |
156 | @external
157 | def setApprovalForAll(operator: address, approved: bool):
158 | self.isApprovedForAll[msg.sender][operator] = approved
159 |
160 | log ApprovalForAll(msg.sender, operator, approved)
161 |
162 |
163 | @external
164 | def transferFrom(owner: address, receiver: address, tokenId: uint256):
165 | self._transferFrom(owner, receiver, tokenId)
166 |
167 |
168 | @external
169 | def safeTransferFrom(
170 | owner: address, receiver: address, tokenId: uint256, data: Bytes[1024] = b""
171 | ):
172 | self._transferFrom(owner, receiver, tokenId)
173 | if receiver.is_contract:
174 | returnValue: bytes4 = ERC721TokenReceiver(receiver).onERC721Received(
175 | msg.sender, owner, tokenId, data
176 | )
177 | assert returnValue == method_id(
178 | "onERC721Received(address,address,uint256,bytes)", output_type=bytes4
179 | ), "Unsafe receiver"
180 |
181 |
182 | @internal
183 | def _transferFrom(owner: address, receiver: address, tokenId: uint256):
184 | assert owner == self._ownerOf[tokenId], "Wrong owner"
185 | assert receiver != empty(address), "Invalid receiver"
186 | assert (
187 | msg.sender == owner
188 | or self.isApprovedForAll[owner][msg.sender]
189 | or msg.sender == self.getApproved[tokenId]
190 | ), "Not approved"
191 |
192 | self._balanceOf[owner] -= 1
193 | self._balanceOf[receiver] += 1
194 | self._ownerOf[tokenId] = receiver
195 |
196 | self.getApproved[tokenId] = empty(address)
197 |
198 | log Transfer(owner, receiver, tokenId)
199 |
200 |
201 | @internal
202 | def _mint(receiver: address, tokenId: uint256):
203 | assert receiver != empty(address), "Invalid receiver"
204 | assert self._ownerOf[tokenId] == empty(address), "Already minted"
205 |
206 | self._balanceOf[receiver] += 1
207 | self._ownerOf[tokenId] = receiver
208 |
209 | log Transfer(empty(address), receiver, tokenId)
210 |
--------------------------------------------------------------------------------
/contracts/Metadata.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity ^0.8.0;
3 |
4 | import "@openzeppelin/access/Ownable.sol";
5 | import "@openzeppelin/utils/Strings.sol";
6 | import "@openzeppelin/utils/Base64.sol";
7 |
8 | interface ICommitReveal {
9 | function commitmentHashes(uint256 tokenID) external view returns (bytes32);
10 |
11 | function commitments(uint256 tokenID) external view returns (string memory);
12 | }
13 |
14 | contract Metadata is Ownable {
15 | using Strings for uint256;
16 |
17 | ICommitReveal public token;
18 |
19 | string[6] colors = [
20 | "ef4444",
21 | "f97316",
22 | "eab308",
23 | "22c55e",
24 | "0ea5e9",
25 | "8b5cf6"
26 | ];
27 |
28 | function contractURI() external view returns (string memory) {
29 | return
30 | _dataURI(
31 | "application/json",
32 | '{"name":"Commit/Reveal 2023","description":"Hashed onchain commitments, revealable December 31, 2023"}'
33 | );
34 | }
35 |
36 | function tokenURI(uint256 tokenId) external view returns (string memory) {
37 | return _dataURI("application/json", tokenJSON(tokenId));
38 | }
39 |
40 | function tokenJSON(uint256 tokenId) public view returns (string memory) {
41 | return
42 | string.concat(
43 | '{"name":"',
44 | _commitmentHash(tokenId),
45 | '","description":"This token represents a hashed commitment that can be revealed December 31, 2023.","image":"',
46 | _dataURI("image/svg+xml", tokenSVG(tokenId)),
47 | '"}'
48 | );
49 | }
50 |
51 | function tokenSVG(uint256 tokenId) public view returns (string memory) {
52 | return
53 | string.concat(
54 | '"
63 | );
64 | }
65 |
66 | function setToken(ICommitReveal _token) external onlyOwner {
67 | if (address(token) == address(0)) token = _token;
68 | }
69 |
70 | function _color(uint256 tokenId) internal view returns (string memory) {
71 | return colors[tokenId % colors.length];
72 | }
73 |
74 | function _commitmentHash(
75 | uint256 tokenId
76 | ) internal view returns (string memory) {
77 | return uint256(token.commitmentHashes(tokenId)).toHexString();
78 | }
79 |
80 | function _commitmentHashes(
81 | uint256 tokenId
82 | ) internal view returns (string memory) {
83 | string memory commitmentHash = _commitmentHash(tokenId);
84 | return
85 | string.concat(
86 | commitmentHash,
87 | " ",
88 | commitmentHash,
89 | " ",
90 | commitmentHash,
91 | " ",
92 | commitmentHash
93 | );
94 | }
95 |
96 | function _commitment(uint256 tokenId) internal view returns (string memory) {
97 | return token.commitments(tokenId);
98 | }
99 |
100 | function _dataURI(
101 | string memory mimeType,
102 | string memory data
103 | ) internal view returns (string memory) {
104 | return
105 | string.concat(
106 | "data:",
107 | mimeType,
108 | ";base64,",
109 | Base64.encode(abi.encodePacked(data))
110 | );
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/contracts/SafeReceiver.vy:
--------------------------------------------------------------------------------
1 | # @version 0.3.7
2 |
3 | implements: ERC721TokenReceiver
4 |
5 |
6 | interface ERC721TokenReceiver:
7 | def onERC721Received(
8 | operator: address, owner: address, tokenId: uint256, data: Bytes[1024]
9 | ) -> bytes4:
10 | view
11 |
12 |
13 | @view
14 | @external
15 | def onERC721Received(
16 | operator: address, owner: address, tokenId: uint256, data: Bytes[1024]
17 | ) -> bytes4:
18 | return method_id(
19 | "onERC721Received(address,address,uint256,bytes)", output_type=bytes4
20 | )
21 |
--------------------------------------------------------------------------------
/contracts/UnsafeReceiver.vy:
--------------------------------------------------------------------------------
1 | # @version 0.3.7
2 |
3 | implements: ERC721TokenReceiver
4 |
5 |
6 | interface ERC721TokenReceiver:
7 | def onERC721Received(
8 | operator: address, owner: address, tokenId: uint256, data: Bytes[1024]
9 | ) -> bytes4:
10 | view
11 |
12 |
13 | @view
14 | @external
15 | def onERC721Received(
16 | operator: address, owner: address, tokenId: uint256, data: Bytes[1024]
17 | ) -> bytes4:
18 | return empty(bytes4)
19 |
--------------------------------------------------------------------------------
/scripts/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/horsefacts/commit-reveal/5426fc17624be18a8fdddc7d87432feef392d8e5/scripts/__init__.py
--------------------------------------------------------------------------------
/scripts/deploy.py:
--------------------------------------------------------------------------------
1 | from ape import project
2 | from ape.cli import get_user_selected_account
3 | import click
4 | from ape.cli import network_option, NetworkBoundCommand, ape_cli_context
5 | from ape.api.networks import LOCAL_NETWORK_NAME
6 |
7 | def main():
8 | account = get_user_selected_account()
9 | metadata = account.deploy(project.Metadata)
10 | cr = account.deploy(project.CommitReveal, metadata)
11 | metadata.setToken(cr)
12 |
13 | @click.command(cls=NetworkBoundCommand)
14 | @ape_cli_context()
15 | @network_option()
16 | def cli(cli_ctx, network):
17 | network = cli_ctx.provider.network.name
18 | if network == LOCAL_NETWORK_NAME or network.endswith("-fork"):
19 | account = cli_ctx.account_manager.test_accounts[0]
20 | else:
21 | account = get_user_selected_account()
22 |
23 | metadata = account.deploy(project.Metadata, publish=True)
24 | cr = account.deploy(project.CommitReveal, metadata, account)
25 | metadata.setToken(cr, sender=account)
26 |
--------------------------------------------------------------------------------
/svg/example.svg:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/horsefacts/commit-reveal/5426fc17624be18a8fdddc7d87432feef392d8e5/tests/__init__.py
--------------------------------------------------------------------------------
/tests/conftest.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 |
4 | @pytest.fixture(scope="session")
5 | def owner(accounts):
6 | return accounts[0]
7 |
8 | @pytest.fixture(scope="session")
9 | def receiver(accounts):
10 | return accounts[1]
11 |
12 | @pytest.fixture(scope="session")
13 | def other(accounts):
14 | return accounts[2]
15 |
16 | @pytest.fixture(scope="session")
17 | def operator(accounts):
18 | return accounts[3]
19 |
20 | @pytest.fixture(scope="session")
21 | def metadata(owner, project):
22 | return owner.deploy(project.Metadata)
23 |
24 | @pytest.fixture(scope="session")
25 | def cr(owner, project, metadata):
26 | return owner.deploy(project.CommitRevealMintable, metadata, owner)
27 |
28 | @pytest.fixture(scope="session")
29 | def unsafe_receiver(owner, project):
30 | return owner.deploy(project.UnsafeReceiver)
31 |
32 | @pytest.fixture(scope="session")
33 | def safe_receiver(owner, project):
34 | return owner.deploy(project.SafeReceiver)
35 |
36 | @pytest.fixture(scope="session")
37 | def b64(owner, project):
38 | return owner.deploy(project.Base64)
39 |
--------------------------------------------------------------------------------
/tests/test_base64.py:
--------------------------------------------------------------------------------
1 | def test_base64(b64):
2 | assert b64.encode("hello world".encode('utf-8')) == ['aGVs', 'bG8g', 'd29y', 'bGQ=']
3 | assert b64.encode("this is a longer string".encode('utf-8')) == ['dGhp', 'cyBp', 'cyBh', 'IGxv', 'bmdl', 'ciBz', 'dHJp', 'bmc=']
4 |
--------------------------------------------------------------------------------
/tests/test_commit_reveal.py:
--------------------------------------------------------------------------------
1 | import ape
2 | import eth_utils
3 |
4 | ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'
5 |
6 | def test_owner_set_on_initialization(cr, owner):
7 | assert cr.owner() == owner
8 |
9 | def test_token_has_name(cr):
10 | assert cr.name() == "Commit/Reveal"
11 |
12 | def test_token_has_symbol(cr):
13 | assert cr.symbol() == "C/R 2023"
14 |
15 | def test_balance_of(cr, owner, receiver):
16 | commitment = eth_utils.keccak("commitment".encode('utf-8'))
17 | cr.commit(commitment, sender=receiver)
18 | assert cr.balanceOf(receiver) == 1
19 |
20 | def test_balance_of_reverts_zero_address(cr):
21 | with ape.reverts("Zero address"):
22 | cr.balanceOf(ZERO_ADDRESS)
23 |
24 | def test_owner_of(cr, owner, receiver):
25 | cr.mint(receiver, 1, sender=owner)
26 | assert cr.ownerOf(1) == receiver
27 |
28 | def test_owner_of_reverts_not_minted(cr):
29 | with ape.reverts("Not minted"):
30 | cr.ownerOf(1)
31 |
32 | def test_mint_reverts_zero_address(cr, owner):
33 | with ape.reverts("Invalid receiver"):
34 | cr.mint(ZERO_ADDRESS, 1, sender=owner)
35 |
36 | def test_mint_reverts_already_minted(cr, owner, receiver):
37 | cr.mint(receiver, 1, sender=owner)
38 | with ape.reverts("Already minted"):
39 | cr.mint(receiver, 1, sender=owner)
40 |
41 | def test_mint_emits_transfer_event(cr, owner, receiver):
42 | tx = cr.mint(receiver, 1, sender=owner)
43 | logs = list(tx.decode_logs(cr.Transfer))
44 | assert len(logs) == 1
45 | assert logs[0].sender == ZERO_ADDRESS
46 | assert logs[0].receiver == receiver
47 | assert logs[0].tokenId == 1
48 |
49 | def test_transfer_reverts_wrong_owner(cr, owner, receiver, other):
50 | cr.mint(receiver, 1, sender=owner)
51 | with ape.reverts("Wrong owner"):
52 | cr.transferFrom(other, other, 1, sender=other)
53 |
54 | def test_transfer_reverts_zero_receiver(cr, owner, receiver, other):
55 | cr.mint(receiver, 1, sender=owner)
56 | with ape.reverts("Invalid receiver"):
57 | cr.transferFrom(receiver, ZERO_ADDRESS, 1, sender=other)
58 |
59 | def test_transfer_reverts_not_approved(cr, owner, receiver, other):
60 | cr.mint(receiver, 1, sender=owner)
61 | with ape.reverts("Not approved"):
62 | cr.transferFrom(receiver, other, 1, sender=other)
63 |
64 | def test_transfer_updates_balances(cr, owner, receiver, other):
65 | cr.mint(receiver, 1, sender=owner)
66 | cr.transferFrom(receiver, other, 1, sender=receiver)
67 | assert cr.balanceOf(receiver) == 0
68 | assert cr.balanceOf(other) == 1
69 |
70 | def test_transfer_updates_ownership(cr, owner, receiver, other):
71 | cr.mint(receiver, 1, sender=owner)
72 | cr.transferFrom(receiver, other, 1, sender=receiver)
73 | assert cr.ownerOf(1) == other
74 |
75 | def test_approved_transfer(cr, owner, receiver, other, operator):
76 | cr.mint(receiver, 1, sender=owner)
77 | cr.approve(operator, 1, sender=receiver)
78 | cr.transferFrom(receiver, other, 1, sender=operator)
79 | assert cr.ownerOf(1) == other
80 |
81 | def test_approved_all_transfer(cr, owner, receiver, other, operator):
82 | cr.mint(receiver, 1, sender=owner)
83 | cr.setApprovalForAll(operator, True, sender=receiver)
84 | cr.transferFrom(receiver, other, 1, sender=operator)
85 | assert cr.ownerOf(1) == other
86 |
87 | def test_transfer_clears_approval(cr, owner, receiver, other, operator):
88 | cr.mint(receiver, 1, sender=owner)
89 | cr.approve(operator, 1, sender=receiver)
90 | assert cr.getApproved(1) == operator
91 | cr.transferFrom(receiver, other, 1, sender=receiver)
92 | assert cr.getApproved(1) == ZERO_ADDRESS
93 |
94 | def test_transfer_emits_transfer_event(cr, owner, receiver, other):
95 | cr.mint(receiver, 1, sender=owner)
96 | tx = cr.transferFrom(receiver, other, 1, sender=receiver)
97 | logs = list(tx.decode_logs(cr.Transfer))
98 | assert len(logs) == 1
99 | assert logs[0].sender == receiver
100 | assert logs[0].receiver == other
101 | assert logs[0].tokenId == 1
102 |
103 | def test_safe_transfer_reverts_unsafe_receiver(cr, owner, receiver, unsafe_receiver):
104 | cr.mint(receiver, 1, sender=owner)
105 | with ape.reverts("Unsafe receiver"):
106 | cr.safeTransferFrom(receiver, unsafe_receiver, 1, sender=receiver)
107 |
108 | def test_safe_transfer_safe_receiver(cr, owner, receiver, safe_receiver):
109 | cr.mint(receiver, 1, sender=owner)
110 | cr.safeTransferFrom(receiver, safe_receiver, 1, sender=receiver)
111 | assert cr.ownerOf(1) == safe_receiver
112 |
113 | def test_set_approval_for_all(cr, owner, operator):
114 | cr.setApprovalForAll(operator, True, sender=owner)
115 | assert cr.isApprovedForAll(owner, operator)
116 |
117 | def test_set_approval_for_all_emits_event(cr, owner, operator):
118 | approve = cr.setApprovalForAll(operator, True, sender=owner)
119 | logs = list(approve.decode_logs(cr.ApprovalForAll))
120 | assert len(logs) == 1
121 | assert logs[0].owner == owner
122 | assert logs[0].operator == operator
123 | assert logs[0].isApproved == True
124 |
125 | revoke = cr.setApprovalForAll(operator, False, sender=owner)
126 | logs = list(revoke.decode_logs(cr.ApprovalForAll))
127 | assert len(logs) == 1
128 | assert logs[0].owner == owner
129 | assert logs[0].operator == operator
130 | assert logs[0].isApproved == False
131 |
132 | def test_approve(cr, owner, receiver, other):
133 | cr.mint(receiver, 1, sender=owner)
134 | cr.approve(other, 1, sender=receiver)
135 | assert cr.getApproved(1) == other
136 |
137 | def test_approve_approved_for_all(cr, owner, receiver, other, operator):
138 | cr.mint(receiver, 1, sender=owner)
139 | cr.setApprovalForAll(operator, True, sender=receiver)
140 | cr.approve(other, 1, sender=operator)
141 | assert cr.getApproved(1) == other
142 |
143 | def test_approve_reverts_unapproved(cr, owner, receiver, other):
144 | cr.mint(receiver, 1, sender=owner)
145 | with ape.reverts("Not approved"):
146 | cr.approve(other, 1, sender=other)
147 |
148 | def test_approve_emits_approval_event(cr, owner, receiver, other):
149 | cr.mint(receiver, 1, sender=owner)
150 | tx = cr.approve(other, 1, sender=receiver)
151 | logs = list(tx.decode_logs(cr.Approval))
152 | assert len(logs) == 1
153 | assert logs[0].owner == receiver
154 | assert logs[0].approved == other
155 | assert logs[0].tokenId == 1
156 |
157 | def test_supported_interfaces(cr):
158 | assert cr.supportsInterface("0x01ffc9a7")
159 | assert cr.supportsInterface("0x80ac58cd")
160 | assert cr.supportsInterface("0x5b5e139f")
161 | assert cr.supportsInterface("0xdeadbeef") == False
162 |
163 | def test_commit_mints_token_to_caller(cr, receiver):
164 | commitment = eth_utils.keccak("commitment".encode('utf-8'))
165 | cr.commit(commitment, sender=receiver)
166 | assert cr.ownerOf(1) == receiver
167 |
168 | def test_commit_stores_commitment(cr, receiver):
169 | commitment = eth_utils.keccak("commitment".encode('utf-8'))
170 | cr.commit(commitment, sender=receiver)
171 | assert cr.commitmentHashes(1) == commitment
172 |
173 | def test_commit_reverts_after_commit_phase(cr, receiver, chain):
174 | chain.pending_timestamp = 1673136000
175 | commitment = eth_utils.keccak("commitment".encode('utf-8'))
176 | with ape.reverts("Commitments closed"):
177 | cr.commit(commitment, sender=receiver)
178 |
179 | def test_reveal(cr, receiver, chain):
180 | commitment = eth_utils.keccak("commitment".encode('utf-8'))
181 | cr.commit(commitment, sender=receiver)
182 | chain.pending_timestamp = 1703980801
183 | cr.reveal(1, "commitment", sender=receiver)
184 | assert cr.commitments(1) == "commitment"
185 |
186 | def test_reveal_long_commitment(cr, receiver, chain):
187 | phrase = "A person I have known for more than ten years, who I consider trustworthy, is convinced the cryptocurrency economy will shortly experience a systemic risk. I don't know anything concrete, but if I were exposed, I would be concerned."
188 | commitment = eth_utils.keccak(phrase.encode('utf-8'))
189 | cr.commit(commitment, sender=receiver)
190 | chain.pending_timestamp = 1703980801
191 | cr.reveal(1, phrase, sender=receiver)
192 | assert cr.commitments(1) == phrase
193 |
194 | def test_reveal_reverts_before_reveal_phase(cr, receiver):
195 | commitment = eth_utils.keccak("commitment".encode('utf-8'))
196 | cr.commit(commitment, sender=receiver)
197 | with ape.reverts("Cannot reveal yet"):
198 | cr.reveal(1, "mismatch", sender=receiver)
199 |
200 | def test_reveal_reverts_wrong_hash(cr, receiver, chain):
201 | commitment = eth_utils.keccak("commitment".encode('utf-8'))
202 | cr.commit(commitment, sender=receiver)
203 | chain.pending_timestamp = 1703980801
204 | with ape.reverts("Wrong hash"):
205 | cr.reveal(1, "mismatch", sender=receiver)
206 |
207 | def test_base64(b64):
208 | assert b64.encode("hello world".encode('utf-8')) == ['aGVs', 'bG8g', 'd29y', 'bGQ=']
209 | assert b64.encode("this is a longer string".encode('utf-8')) == ['dGhp', 'cyBp', 'cyBh', 'IGxv', 'bmdl', 'ciBz', 'dHJp', 'bmc=']
210 |
--------------------------------------------------------------------------------
/tests/test_metadata.py:
--------------------------------------------------------------------------------
1 | import ape
2 | import eth_utils
3 |
4 | ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'
5 |
6 | def test_owner_can_set_token(cr, metadata, owner):
7 | metadata.setToken(cr, sender=owner)
8 | assert metadata.token() == cr
9 |
10 | def test_owner_can_only_set_token_once(cr, metadata, owner):
11 | metadata.setToken(cr, sender=owner)
12 | metadata.setToken(ZERO_ADDRESS, sender=owner)
13 | assert metadata.token() == cr
14 |
15 | def test_token_uri(cr, metadata, owner, receiver, chain):
16 | metadata.setToken(cr, sender=owner)
17 | commitment = eth_utils.keccak("commitment".encode('utf-8'))
18 | cr.commit(commitment, sender=receiver)
19 | chain.pending_timestamp = 1703980801
20 | cr.reveal(1, "commitment", sender=receiver)
21 | uri = metadata.tokenURI(1)
22 | assert uri == 'data:application/json;base64,eyJuYW1lIjoiMHhmODA5NmMzZjNjZmJhZGM5ZjNhMTA4ZDJjNTg2Y2NjODE2YzA1MTA3MTFjOGQ2MjI4YWE0ZmE1MDczMjRkMzBjIiwiZGVzY3JpcHRpb24iOiJUaGlzIHRva2VuIHJlcHJlc2VudHMgYSBoYXNoZWQgY29tbWl0bWVudCB0aGF0IGNhbiBiZSByZXZlYWxlZCBEZWNlbWJlciAzMSwgMjAyMy4iLCJpbWFnZSI6ImRhdGE6aW1hZ2Uvc3ZnK3htbDtiYXNlNjQsUEhOMlp5QjRiV3h1Y3owaWFIUjBjRG92TDNkM2R5NTNNeTV2Y21jdk1qQXdNQzl6ZG1jaUlITjBlV3hsUFNKaVlXTnJaM0p2ZFc1a09pTXhOVEUxTWpBaUlIWnBaWGRDYjNnOUlqQWdNQ0EzTURBZ016QXdJajQ4Y0dGMGFDQnBaRDBpWVNJZ1ptbHNiRDBpSXpFMU1UVXlNQ0lnWkQwaVRURXdJREV3YURZM01HRXlNQ0F4TUNBd0lEQWdNU0F4TUNBeE1IWXlOakJoTWpBZ01UQWdNQ0F3SURFdE1UQWdNVEJJTWpCaE1qQWdNVEFnTUNBd0lERXRNVEF0TVRCV01UQjZJaTgrUEhSbGVIUWdabWxzYkQwaUkyWTVOek14TmlJZ1pHOXRhVzVoYm5RdFltRnpaV3hwYm1VOUltMXBaR1JzWlNJZ1ptOXVkQzFtWVcxcGJIazlJazFsYm14dkxHMXZibTl6Y0dGalpTSWdabTl1ZEMxemFYcGxQU0l4TWlJK1BIUmxlSFJRWVhSb0lHaHlaV1k5SWlOaElqNG1JekUyTUR0RGIyMXRhWFF2VW1WMlpXRnNJREl3TWpNZ0ppTTRNakkyT3p3aFcwTkVRVlJCV3lBd2VHWTRNRGsyWXpObU0yTm1ZbUZrWXpsbU0yRXhNRGhrTW1NMU9EWmpZMk00TVRaak1EVXhNRGN4TVdNNFpEWXlNamhoWVRSbVlUVXdOek15TkdRek1HTWdNSGhtT0RBNU5tTXpaak5qWm1KaFpHTTVaak5oTVRBNFpESmpOVGcyWTJOak9ERTJZekExTVRBM01URmpPR1EyTWpJNFlXRTBabUUxTURjek1qUmtNekJqSURCNFpqZ3dPVFpqTTJZelkyWmlZV1JqT1dZellURXdPR1F5WXpVNE5tTmpZemd4Tm1Nd05URXdOekV4WXpoa05qSXlPR0ZoTkdaaE5UQTNNekkwWkRNd1l5QXdlR1k0TURrMll6Tm1NMk5tWW1Ga1l6bG1NMkV4TURoa01tTTFPRFpqWTJNNE1UWmpNRFV4TURjeE1XTTRaRFl5TWpoaFlUUm1ZVFV3TnpNeU5HUXpNR05kWFQ0OEwzUmxlSFJRWVhSb1Bqd3ZkR1Y0ZEQ0OGNHRjBhQ0JtYVd4c1BTSnlaMkpoS0RBc01Dd3dMREFwSWlCemRISnZhMlU5SWlObU9UY3pNVFlpSUdROUlrMHlNQ0F5TUdnMk5UQmhNVEFnTVRBZ01DQXdJREVnTVRBZ01UQjJNalF3WVRFd0lERXdJREFnTUNBeExURXdJREV3U0RNd1lURXdJREV3SURBZ01DQXhMVEV3TFRFd1ZqSXdlaUl2UGp4bWIzSmxhV2R1VDJKcVpXTjBJSGc5SWpVd0lpQjVQU0l6TUNJZ2QybGtkR2c5SWpZeU1DSWdhR1ZwWjJoMFBTSXlOVEFpUGp4a2FYWWdjM1I1YkdVOUltWnZiblF0Wm1GdGFXeDVPazFsYm14dkxHMXZibTl6Y0dGalpUdGpiMnh2Y2pvalptWm1PMlp2Ym5RdGMybDZaVG95TkhCNE8yUnBjM0JzWVhrNlpteGxlRHRoYkdsbmJpMXBkR1Z0Y3pwalpXNTBaWEk3YW5WemRHbG1lUzFqYjI1MFpXNTBPbU5sYm5SbGNqdG9aV2xuYUhRNk1qUXdjSGdpSUNCNGJXeHVjejBpYUhSMGNEb3ZMM2QzZHk1M015NXZjbWN2TVRrNU9TOTRhSFJ0YkNJK1BIQStZMjl0YldsMGJXVnVkRHd2Y0Q0OEwyUnBkajQ4TDJadmNtVnBaMjVQWW1wbFkzUStQQzl6ZG1jKyJ9'
23 |
24 | def test_json(cr, metadata, owner, receiver, chain):
25 | metadata.setToken(cr, sender=owner)
26 | commitment = eth_utils.keccak("commitment".encode('utf-8'))
27 | cr.commit(commitment, sender=receiver)
28 | chain.pending_timestamp = 1703980801
29 | cr.reveal(1, "commitment", sender=receiver)
30 | json = metadata.tokenJSON(1)
31 | print(json)
32 | assert json == '{"name":"0xf8096c3f3cfbadc9f3a108d2c586ccc816c0510711c8d6228aa4fa507324d30c","description":"This token represents a hashed commitment that can be revealed December 31, 2023.","image":"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHN0eWxlPSJiYWNrZ3JvdW5kOiMxNTE1MjAiIHZpZXdCb3g9IjAgMCA3MDAgMzAwIj48cGF0aCBpZD0iYSIgZmlsbD0iIzE1MTUyMCIgZD0iTTEwIDEwaDY3MGEyMCAxMCAwIDAgMSAxMCAxMHYyNjBhMjAgMTAgMCAwIDEtMTAgMTBIMjBhMjAgMTAgMCAwIDEtMTAtMTBWMTB6Ii8+PHRleHQgZmlsbD0iI2Y5NzMxNiIgZG9taW5hbnQtYmFzZWxpbmU9Im1pZGRsZSIgZm9udC1mYW1pbHk9Ik1lbmxvLG1vbm9zcGFjZSIgZm9udC1zaXplPSIxMiI+PHRleHRQYXRoIGhyZWY9IiNhIj4mIzE2MDtDb21taXQvUmV2ZWFsIDIwMjMgJiM4MjI2OzwhW0NEQVRBWyAweGY4MDk2YzNmM2NmYmFkYzlmM2ExMDhkMmM1ODZjY2M4MTZjMDUxMDcxMWM4ZDYyMjhhYTRmYTUwNzMyNGQzMGMgMHhmODA5NmMzZjNjZmJhZGM5ZjNhMTA4ZDJjNTg2Y2NjODE2YzA1MTA3MTFjOGQ2MjI4YWE0ZmE1MDczMjRkMzBjIDB4ZjgwOTZjM2YzY2ZiYWRjOWYzYTEwOGQyYzU4NmNjYzgxNmMwNTEwNzExYzhkNjIyOGFhNGZhNTA3MzI0ZDMwYyAweGY4MDk2YzNmM2NmYmFkYzlmM2ExMDhkMmM1ODZjY2M4MTZjMDUxMDcxMWM4ZDYyMjhhYTRmYTUwNzMyNGQzMGNdXT48L3RleHRQYXRoPjwvdGV4dD48cGF0aCBmaWxsPSJyZ2JhKDAsMCwwLDApIiBzdHJva2U9IiNmOTczMTYiIGQ9Ik0yMCAyMGg2NTBhMTAgMTAgMCAwIDEgMTAgMTB2MjQwYTEwIDEwIDAgMCAxLTEwIDEwSDMwYTEwIDEwIDAgMCAxLTEwLTEwVjIweiIvPjxmb3JlaWduT2JqZWN0IHg9IjUwIiB5PSIzMCIgd2lkdGg9IjYyMCIgaGVpZ2h0PSIyNTAiPjxkaXYgc3R5bGU9ImZvbnQtZmFtaWx5Ok1lbmxvLG1vbm9zcGFjZTtjb2xvcjojZmZmO2ZvbnQtc2l6ZToyNHB4O2Rpc3BsYXk6ZmxleDthbGlnbi1pdGVtczpjZW50ZXI7anVzdGlmeS1jb250ZW50OmNlbnRlcjtoZWlnaHQ6MjQwcHgiICB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94aHRtbCI+PHA+Y29tbWl0bWVudDwvcD48L2Rpdj48L2ZvcmVpZ25PYmplY3Q+PC9zdmc+"}'
33 |
34 | def test_svg(cr, metadata, owner, receiver, chain):
35 | metadata.setToken(cr, sender=owner)
36 | commitment = eth_utils.keccak("commitment".encode('utf-8'))
37 | cr.commit(commitment, sender=receiver)
38 | chain.pending_timestamp = 1703980801
39 | cr.reveal(1, "commitment", sender=receiver)
40 | svg = metadata.tokenSVG(1)
41 | assert svg == ''
42 |
43 | def test_svg_empty_commitment(cr, metadata, owner, receiver, chain):
44 | metadata.setToken(cr, sender=owner)
45 | commitment = eth_utils.keccak("commitment".encode('utf-8'))
46 | cr.commit(commitment, sender=receiver)
47 | svg = metadata.tokenSVG(1)
48 | assert svg == ''
49 |
--------------------------------------------------------------------------------