245 | Hey, looking for a super-safe experience? 😃
246 | You can interact with the smart-contract directly through {this.state.networkConfig.blockExplorer.name}, without even connecting your wallet to this DAPP! 🚀
247 |
248 | Keep safe! ❤️
249 |
255 | Anyone can generate the proof using any public address in the list, but only the owner of that address will be able to make a successful transaction by using it.
256 |
74 | Total price: {utils.formatEther(this.props.tokenPrice.mul(this.state.mintAmount))} {this.props.networkConfig.symbol}
75 |
76 |
77 |
78 |
79 | {this.state.mintAmount}
80 |
81 |
82 |
83 |
84 | :
85 |
86 | ⏳
87 |
88 | {this.props.isWhitelistMintEnabled ? <>You are not included in the whitelist.> : <>The contract is paused.>}
89 | Please come back during the next sale!
90 |
91 | }
92 | >
93 | );
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/minting-dapp/src/styles/components/general.scss:
--------------------------------------------------------------------------------
1 | body {
2 | @apply p-6;
3 | @apply min-h-screen;
4 |
5 | @apply font-sans;
6 |
7 | // Simple background with color gradient
8 | @apply bg-gradient-to-b from-page-from_bg via-page-from_bg to-page-to_bg;
9 |
10 | // Fullscreen background image example
11 | //background-image: url('../../images/background.jpg');
12 | //@apply bg-center bg-cover bg-fixed;
13 | }
14 |
15 | a, a:link, a:visited {
16 | @apply font-semibold;
17 | @apply text-links-txt;
18 |
19 | &:hover {
20 | @apply underline;
21 | @apply text-links-hover_txt;
22 | }
23 | }
24 |
25 | strong {
26 | @apply font-semibold;
27 | }
28 |
29 | main {
30 | @apply flex flex-col;
31 |
32 | #logo {
33 | @apply m-auto;
34 | @apply w-full;
35 | @apply max-w-md;
36 | }
37 |
38 | span.emoji {
39 | @apply text-2xl;
40 | }
41 |
42 | .error {
43 | @apply flex flex-col;
44 | @apply rounded-lg;
45 | @apply p-3;
46 |
47 | @apply text-error-txt text-sm;
48 | @apply bg-error-bg;
49 | @apply border border-error-border;
50 | @apply shadow;
51 |
52 | &::before {
53 | content: 'Error';
54 |
55 | @apply font-semibold;
56 | @apply text-base;
57 | }
58 |
59 | button {
60 | @apply inline-block;
61 | @apply mt-3 ml-auto;
62 | @apply px-2 py-1;
63 | @apply rounded-md;
64 |
65 | @apply font-semibold;
66 | @apply text-btn_error-txt text-xs;
67 | @apply bg-btn_error-bg;
68 | @apply border-btn_error-border;
69 |
70 | &:hover {
71 | @apply text-btn_error-hover_txt;
72 | @apply bg-btn_error-hover_bg;
73 | @apply border-btn_error-hover_border;
74 | }
75 | }
76 | }
77 |
78 | button {
79 | @apply py-2 px-6;
80 |
81 | @apply rounded-full;
82 |
83 | @apply font-semibold;
84 | @apply text-btn-txt;
85 | @apply bg-btn-bg;
86 | @apply border border-btn-border;
87 | @apply shadow-sm;
88 |
89 | &:hover {
90 | @apply text-btn-hover_txt;
91 | @apply bg-btn-hover_bg;
92 | @apply border-btn-hover_border;
93 | }
94 |
95 | &.primary {
96 | @apply text-btn_primary-txt;
97 | @apply bg-btn_primary-bg;
98 | @apply border-btn_primary-border;
99 |
100 | &:hover {
101 | @apply text-btn_primary-hover_txt;
102 | @apply bg-btn_primary-hover_bg;
103 | @apply border-btn_primary-hover_border;
104 | }
105 |
106 | &:disabled {
107 | @apply opacity-30;
108 |
109 | &:hover {
110 | @apply cursor-not-allowed;
111 | }
112 | }
113 | }
114 | }
115 |
116 | input[type=text] {
117 | @apply py-2 px-4;
118 |
119 | @apply rounded-full;
120 |
121 | @apply font-mono font-semibold;
122 | @apply text-txt_input-txt;
123 | @apply bg-txt_input-bg;
124 | @apply border border-txt_input-border;
125 | @apply shadow-sm;
126 | @apply outline-none;
127 |
128 | &:focus {
129 | @apply text-txt_input-focus_txt;
130 | @apply bg-txt_input-focus_bg;
131 | @apply border-txt_input-focus_border;
132 | }
133 |
134 | &:disabled {
135 | @apply opacity-50;
136 |
137 | &:hover {
138 | @apply cursor-not-allowed;
139 | }
140 | }
141 |
142 | &::placeholder {
143 | @apply text-txt_input-placeholder_txt;
144 | @apply opacity-50;
145 | }
146 | }
147 |
148 | label {
149 | @apply mt-4 mb-1 ml-1;
150 |
151 | @apply font-semibold;
152 | @apply text-label text-sm;
153 | }
154 | }
155 |
--------------------------------------------------------------------------------
/minting-dapp/src/styles/components/minting-dapp.scss:
--------------------------------------------------------------------------------
1 | #minting-dapp {
2 | @apply flex flex-col gap-6;
3 | @apply mt-6 mx-auto;
4 | @apply w-full;
5 | @apply max-w-md;
6 |
7 | .no-wallet {
8 | @apply flex flex-col;
9 | @apply px-4 py-6;
10 | @apply rounded-lg;
11 |
12 | @apply text-popups-txt;
13 | @apply bg-popups-bg;
14 | @apply shadow;
15 |
16 | .use-block-explorer {
17 | &:not(:first-child) {
18 | @apply mt-3;
19 | }
20 |
21 | &:not(:first-child)::before {
22 | content: '';
23 |
24 | @apply block;
25 | @apply mx-auto my-3;
26 | @apply w-12;
27 |
28 | @apply border-t-2 border-popups-internal_border;
29 | }
30 | }
31 |
32 | .merkle-proof-manual-address {
33 | @apply flex flex-col;
34 | @apply mt-4;
35 |
36 | h2 {
37 | @apply font-semibold;
38 | @apply text-titles text-xl text-center;
39 | }
40 |
41 | p {
42 | @apply mt-3;
43 | }
44 |
45 | .feedback-message {
46 | @apply rounded-lg;
47 | @apply mt-4;
48 | @apply p-3;
49 |
50 | @apply text-wl_message-txt text-sm;
51 | @apply bg-wl_message-bg;
52 | }
53 |
54 | input {
55 | @apply rounded-t-lg;
56 | @apply rounded-b-none;
57 | }
58 |
59 | button {
60 | @apply rounded-b-lg;
61 | @apply rounded-t-none;
62 | @apply border-t-0;
63 | }
64 | }
65 | }
66 |
67 | .collection-not-ready {
68 | @apply flex items-center justify-center;
69 | @apply px-6 py-4;
70 | @apply rounded-lg;
71 |
72 | @apply text-popups-txt text-sm;
73 | @apply bg-popups-bg;
74 | @apply shadow;
75 |
76 | .spinner {
77 | @apply inline;
78 | @apply -ml-1 mr-3 h-8 w-8 text-loading_spinner;
79 | @apply animate-spin;
80 | }
81 | }
82 |
83 | .collection-status {
84 | @apply grid sm:grid-cols-2 auto-rows-min;
85 | @apply rounded-lg;
86 |
87 | @apply font-mono;
88 | @apply text-popups-txt text-sm;
89 | @apply bg-popups-bg;
90 | @apply shadow;
91 |
92 | & > * {
93 | @apply flex flex-col items-center;
94 | @apply px-6 py-4;
95 |
96 | .label {
97 | @apply text-xs text-label;
98 | }
99 | }
100 |
101 | .user-address {
102 | @apply sm:col-span-2;
103 | @apply overflow-hidden;
104 |
105 | @apply border-b border-popups-internal_border;
106 |
107 | .address {
108 | @apply w-full;
109 |
110 | @apply font-semibold;
111 | @apply truncate;
112 | @apply text-center;
113 | }
114 | }
115 |
116 | .supply, .current-sale {
117 | .label {
118 | @apply block;
119 |
120 | @apply font-semibold;
121 | }
122 |
123 | &.supply {
124 | @apply border-b sm:border-b-0 sm:border-r border-popups-internal_border;
125 | }
126 | }
127 | }
128 |
129 | .cannot-mint, .not-mainnet, .collection-sold-out {
130 | @apply rounded-lg;
131 | @apply px-6 py-4;
132 |
133 | @apply text-popups-txt text-center;
134 | @apply bg-popups-bg;
135 | @apply shadow;
136 |
137 | &.cannot-mint .emoji {
138 | @apply block;
139 |
140 | @apply text-4xl;
141 | }
142 |
143 | &.not-mainnet {
144 | @apply text-warning-txt;
145 | @apply bg-warning-bg;
146 | @apply border border-warning-border;
147 |
148 |
149 | .small {
150 | @apply block;
151 |
152 | @apply text-sm;
153 | }
154 | }
155 |
156 | &.collection-sold-out {
157 | h2 {
158 | @apply mb-3;
159 |
160 | @apply text-xl;
161 | }
162 | }
163 | }
164 |
165 | .mint-widget {
166 | @apply flex flex-col items-center;
167 | @apply rounded-lg;
168 | @apply overflow-hidden;
169 |
170 | @apply text-popups-txt text-center;
171 | @apply bg-popups-bg;
172 | @apply shadow;
173 |
174 | .preview {
175 | @apply p-8;
176 |
177 | @apply bg-token_preview;
178 |
179 | img {
180 | @apply m-auto;
181 | @apply max-h-52;
182 |
183 | filter: drop-shadow(1px 1px 2px rgba(0, 0, 0, 0.4));
184 | }
185 | }
186 |
187 | .price {
188 | @apply px-6 py-4;
189 | }
190 |
191 | & > * {
192 | @apply w-full;
193 |
194 | &:not(:last-child) {
195 | @apply border-b border-popups-internal_border;
196 | }
197 | }
198 |
199 | .controls {
200 | @apply flex items-stretch;
201 |
202 | & > * {
203 | @apply rounded-none;
204 | @apply border-0;
205 | }
206 |
207 | .decrease, .mint-amount {
208 | @apply border-r border-popups-internal_border;
209 | }
210 |
211 | .mint-amount {
212 | @apply flex items-center justify-center;
213 | @apply w-full;
214 |
215 | @apply font-semibold;
216 | @apply text-label text-lg;
217 | }
218 |
219 | .primary {
220 | @apply border-0;
221 | }
222 | }
223 | }
224 | }
225 |
--------------------------------------------------------------------------------
/minting-dapp/src/styles/main.scss:
--------------------------------------------------------------------------------
1 | @import '~tailwindcss/base';
2 | @import '~tailwindcss/components';
3 | @import '~tailwindcss/utilities';
4 |
5 | @import './components/general.scss';
6 | @import './components/minting-dapp.scss';
7 |
--------------------------------------------------------------------------------
/minting-dapp/tailwind.config.js:
--------------------------------------------------------------------------------
1 | const colors = require('tailwindcss/colors');
2 |
3 | module.exports = {
4 | mode: 'jit',
5 | content: [
6 | './src/**/*.tsx',
7 | './public/index.html',
8 | ],
9 | theme: {
10 | extend: {
11 | colors: {
12 | // General
13 | page: {
14 | from_bg: colors.slate[100],
15 | to_bg: colors.slate[200],
16 | },
17 | titles: colors.indigo[600],
18 | links: {
19 | txt: colors.indigo[600],
20 | hover_txt: colors.indigo[700],
21 | },
22 | loading_spinner: colors.indigo[500],
23 | popups: {
24 | bg: colors.white,
25 | txt: colors.slate[800],
26 | internal_border: colors.slate[200],
27 | },
28 | warning: {
29 | txt: colors.slate[800],
30 | bg: colors.yellow[400],
31 | border: colors.yellow[500],
32 | },
33 | error: {
34 | txt: colors.red[500],
35 | bg: colors.red[50],
36 | border: colors.red[200],
37 | },
38 |
39 | // Inputs
40 | btn: {
41 | txt: colors.slate[800],
42 | bg: colors.white,
43 | border: colors.slate[200],
44 | hover_txt: colors.slate[800],
45 | hover_bg: colors.slate[100],
46 | hover_border: colors.slate[200],
47 | },
48 | btn_primary: {
49 | txt: colors.white,
50 | bg: colors.indigo[500],
51 | border: colors.indigo[500],
52 | hover_txt: colors.white,
53 | hover_bg: colors.indigo[600],
54 | hover_border: colors.indigo[600],
55 | },
56 | btn_error: {
57 | txt: colors.white,
58 | bg: colors.red[500],
59 | border: colors.red[500],
60 | hover_txt: colors.white,
61 | hover_bg: colors.red[600],
62 | hover_border: colors.red[600],
63 | },
64 | label: colors.indigo[600],
65 | txt_input: {
66 | txt: colors.indigo[600],
67 | bg: colors.white,
68 | border: colors.slate[200],
69 | focus_txt: colors.indigo[600],
70 | focus_bg: colors.slate[50],
71 | focus_border: colors.indigo[300],
72 | placeholder_txt: colors.indigo[600],
73 | },
74 |
75 | // Whitelist proof widget
76 | wl_message: {
77 | txt: colors.slate[800],
78 | bg: colors.indigo[100],
79 | },
80 |
81 | // Mint widget
82 | token_preview: colors.indigo[200],
83 | },
84 | },
85 | },
86 | variants: {},
87 | plugins: [],
88 | };
89 |
--------------------------------------------------------------------------------
/minting-dapp/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Basic Options */
4 | // "incremental": true, /* Enable incremental compilation */
5 | "target": "ESNext", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
6 | "module": "ESNext", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
7 | // "lib": [], /* Specify library files to be included in the compilation. */
8 | // "allowJs": true, /* Allow javascript files to be compiled. */
9 | // "checkJs": true, /* Report errors in .js files. */
10 | "jsx": "react-jsx", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
11 | // "declaration": true, /* Generates corresponding '.d.ts' file. */
12 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
13 | // "sourceMap": true, /* Generates corresponding '.map' file. */
14 | // "outFile": "./", /* Concatenate and emit output to single file. */
15 | // "outDir": "./", /* Redirect output structure to the directory. */
16 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
17 | // "composite": true, /* Enable project compilation */
18 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
19 | // "removeComments": true, /* Do not emit comments to output. */
20 | // "noEmit": true, /* Do not emit outputs. */
21 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */
22 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
23 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
24 |
25 | /* Strict Type-Checking Options */
26 | "strict": true, /* Enable all strict type-checking options. */
27 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
28 | // "strictNullChecks": true, /* Enable strict null checks. */
29 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */
30 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
31 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
32 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
33 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
34 |
35 | /* Additional Checks */
36 | // "noUnusedLocals": true, /* Report errors on unused locals. */
37 | // "noUnusedParameters": true, /* Report errors on unused parameters. */
38 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
39 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
40 |
41 | /* Module Resolution Options */
42 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
43 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
44 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
45 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
46 | // "typeRoots": [], /* List of folders to include type definitions from. */
47 | // "types": [], /* Type declaration files to be included in compilation. */
48 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
49 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
50 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
51 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
52 | "resolveJsonModule": true,
53 |
54 | /* Source Map Options */
55 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
56 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
57 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
58 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
59 |
60 | /* Experimental Options */
61 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
62 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
63 | },
64 | "compileOnSave": false,
65 | "exclude": [
66 | "../node_modules"
67 | ],
68 | }
--------------------------------------------------------------------------------
/minting-dapp/webpack.config.js:
--------------------------------------------------------------------------------
1 | const Encore = require('@symfony/webpack-encore');
2 | const webpack = require('webpack');
3 | const NodePolyfillPlugin = require('node-polyfill-webpack-plugin');
4 |
5 | // Manually configure the runtime environment if not already configured yet by the "encore" command.
6 | // It's useful when you use tools that rely on webpack.config.js file.
7 | if (!Encore.isRuntimeEnvironmentConfigured()) {
8 | Encore.configureRuntimeEnvironment(process.env.NODE_ENV || 'dev');
9 | }
10 |
11 | Encore
12 | // directory where compiled assets will be stored
13 | .setOutputPath('public/build/')
14 | // public path used by the web server to access the output path
15 | .setPublicPath('/build')
16 | // only needed for CDN's or sub-directory deploy
17 | //.setManifestKeyPrefix('build/')
18 |
19 | /*
20 | * ENTRY CONFIG
21 | *
22 | * Each entry will result in one JavaScript file (e.g. app.js)
23 | * and one CSS file (e.g. app.css) if your JavaScript imports CSS.
24 | */
25 | .addEntry('main', './src/scripts/main.tsx')
26 |
27 | // copy images
28 | .copyFiles({
29 | from: './src/images',
30 | to: '[path][name].[ext]',
31 | context: './src'
32 | })
33 |
34 | // enables the Symfony UX Stimulus bridge (used in assets/bootstrap.js)
35 | //.enableStimulusBridge('./assets/controllers.json')
36 |
37 | // When enabled, Webpack "splits" your files into smaller pieces for greater optimization.
38 | //.splitEntryChunks()
39 |
40 | // will require an extra script tag for runtime.js
41 | // but, you probably want this, unless you're building a single-page app
42 | //.enableSingleRuntimeChunk()
43 | .disableSingleRuntimeChunk()
44 |
45 | /*
46 | * FEATURE CONFIG
47 | *
48 | * Enable & configure other features below. For a full
49 | * list of features, see:
50 | * https://symfony.com/doc/current/frontend.html#adding-more-features
51 | */
52 | .cleanupOutputBeforeBuild()
53 | .enableBuildNotifications()
54 | .enableSourceMaps(!Encore.isProduction())
55 | // enables hashed filenames (e.g. app.abc123.css)
56 | //.enableVersioning(Encore.isProduction())
57 |
58 | .configureBabel((config) => {
59 | config.plugins.push('@babel/plugin-proposal-class-properties');
60 | })
61 |
62 | // enables @babel/preset-env polyfills
63 | .configureBabelPresetEnv((config) => {
64 | config.useBuiltIns = 'usage';
65 | config.corejs = 3;
66 | })
67 |
68 | // enables Sass/SCSS support
69 | .enableSassLoader()
70 |
71 | // uncomment if you use TypeScript
72 | .enableTypeScriptLoader()
73 |
74 | // enables PostCSS support
75 | .enablePostCssLoader()
76 |
77 | // uncomment if you use React
78 | .enableReactPreset()
79 |
80 | // uncomment to get integrity="..." attributes on your script & link tags
81 | // requires WebpackEncoreBundle 1.4 or higher
82 | //.enableIntegrityHashes(Encore.isProduction())
83 |
84 | // uncomment if you're having problems with a jQuery plugin
85 | //.autoProvidejQuery()
86 |
87 | //.addPlugin(new webpack.ProvidePlugin({
88 | // Buffer: ['buffer', 'Buffer'],
89 | //}))
90 | .addPlugin(new NodePolyfillPlugin())
91 | ;
92 |
93 | module.exports = Encore.getWebpackConfig();
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@hashlips-lab/nft-erc721-collection",
3 | "description": "An all-in-one solution for ERC721 collections.",
4 | "author": "Marco Lipparini ",
5 | "license": "MIT",
6 | "version": "2.4.4",
7 | "repository": {
8 | "type": "git",
9 | "url": "https://github.com/hashlips-lab/nft-erc721-collection.git"
10 | },
11 | "scripts": {
12 | "build-dapp": "cd smart-contract; yarn; yarn compile; cd ../minting-dapp; yarn; yarn build"
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/smart-contract/.env.example:
--------------------------------------------------------------------------------
1 | COLLECTION_URI_PREFIX=ipfs://__CID___/
2 |
3 | NETWORK_TESTNET_URL=https://rinkeby.infura.io/v3/abc123abc123abc123abc123abc123ab
4 | # !!! WARNING !!!
5 | # Please manage your .env files carefully when using them to store your private keys.
6 | # People getting access to you private keys will be able to control your wallet forever.
7 | # Remember you can use the CLI commands even without setting the private keys here, please
8 | # check out the GitHub repo for more information.
9 | NETWORK_TESTNET_PRIVATE_KEY=0xabc123abc123abc123abc123abc123abc123abc123abc123abc123abc123abc1
10 |
11 | NETWORK_MAINNET_URL=https://mainnet.infura.io/v3/abc123abc123abc123abc123abc123ab
12 | # !!! WARNING !!!
13 | # Please manage your .env files carefully when using them to store your private keys.
14 | # People getting access to you private keys will be able to control your wallet forever.
15 | # Remember you can use the CLI commands even without setting the private keys here, please
16 | # check out the GitHub repo for more information.
17 | NETWORK_MAINNET_PRIVATE_KEY=0xabc123abc123abc123abc123abc123abc123abc123abc123abc123abc123abc1
18 |
19 | GAS_REPORTER_COIN_MARKET_CAP_API_KEY=00000000-0000-0000-0000-000000000000
20 |
21 | BLOCK_EXPLORER_API_KEY=ABC123ABC123ABC123ABC123ABC123ABC1
--------------------------------------------------------------------------------
/smart-contract/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | artifacts
3 | cache
4 | coverage
5 |
--------------------------------------------------------------------------------
/smart-contract/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | browser: false,
4 | es2021: true,
5 | mocha: true,
6 | node: true,
7 | },
8 | plugins: ["@typescript-eslint"],
9 | extends: [
10 | "standard",
11 | "plugin:prettier/recommended",
12 | "plugin:node/recommended",
13 | ],
14 | parser: "@typescript-eslint/parser",
15 | parserOptions: {
16 | ecmaVersion: 12,
17 | },
18 | rules: {
19 | "node/no-unsupported-features/es-syntax": [
20 | "error",
21 | { ignores: ["modules"] },
22 | ],
23 | },
24 | };
25 |
--------------------------------------------------------------------------------
/smart-contract/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .env
3 | coverage
4 | coverage.json
5 | typechain
6 |
7 | #Hardhat files
8 | cache
9 | artifacts
10 |
--------------------------------------------------------------------------------
/smart-contract/.npmignore:
--------------------------------------------------------------------------------
1 | hardhat.config.ts
2 | scripts
3 | test
4 |
--------------------------------------------------------------------------------
/smart-contract/.prettierignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | artifacts
3 | cache
4 | coverage*
5 | gasReporterOutput.json
6 |
--------------------------------------------------------------------------------
/smart-contract/.solhint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "solhint:recommended",
3 | "rules": {
4 | "compiler-version": ["error", ">=0.8.9 <0.9.0"],
5 | "func-visibility": ["warn", { "ignoreConstructors": true }]
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/smart-contract/.solhintignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
--------------------------------------------------------------------------------
/smart-contract/config/CollectionConfig.ts:
--------------------------------------------------------------------------------
1 | import CollectionConfigInterface from '../lib/CollectionConfigInterface';
2 | import * as Networks from '../lib/Networks';
3 | import * as Marketplaces from '../lib/Marketplaces';
4 | import whitelistAddresses from './whitelist.json';
5 |
6 | const CollectionConfig: CollectionConfigInterface = {
7 | testnet: Networks.ethereumTestnet,
8 | mainnet: Networks.ethereumMainnet,
9 | // The contract name can be updated using the following command:
10 | // yarn rename-contract NEW_CONTRACT_NAME
11 | // Please DO NOT change it manually!
12 | contractName: 'YourNftToken',
13 | tokenName: 'My NFT Token',
14 | tokenSymbol: 'MNT',
15 | hiddenMetadataUri: 'ipfs://__CID__/hidden.json',
16 | maxSupply: 10000,
17 | whitelistSale: {
18 | price: 0.05,
19 | maxMintAmountPerTx: 1,
20 | },
21 | preSale: {
22 | price: 0.07,
23 | maxMintAmountPerTx: 2,
24 | },
25 | publicSale: {
26 | price: 0.09,
27 | maxMintAmountPerTx: 5,
28 | },
29 | contractAddress: null,
30 | marketplaceIdentifier: 'my-nft-token',
31 | marketplaceConfig: Marketplaces.openSea,
32 | whitelistAddresses,
33 | };
34 |
35 | export default CollectionConfig;
36 |
--------------------------------------------------------------------------------
/smart-contract/config/ContractArguments.ts:
--------------------------------------------------------------------------------
1 | import { utils } from 'ethers';
2 | import CollectionConfig from './CollectionConfig';
3 |
4 | // Update the following array if you change the constructor arguments...
5 | const ContractArguments = [
6 | CollectionConfig.tokenName,
7 | CollectionConfig.tokenSymbol,
8 | utils.parseEther(CollectionConfig.whitelistSale.price.toString()),
9 | CollectionConfig.maxSupply,
10 | CollectionConfig.whitelistSale.maxMintAmountPerTx,
11 | CollectionConfig.hiddenMetadataUri,
12 | ] as const;
13 |
14 | export default ContractArguments;
--------------------------------------------------------------------------------
/smart-contract/config/whitelist.json:
--------------------------------------------------------------------------------
1 | [
2 | "_REPLACE_EVERYTHING_WITH_REAL_ADDRESSES___",
3 | "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
4 | "0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65",
5 | "0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc",
6 | "0x976EA74026E726554dB657fA54763abd0C3a0aa9",
7 | "0x14dC79964da2C08b23698B3D3cc7Ca32193d9955",
8 | "0x23618e81E3f5cdF7f54C3d65f7FBc0aBf5B21E8f",
9 | "0xa0Ee7A142d267C1f36714E4a8F75612F20a79720",
10 | "0xBcd4042DE499D14e55001CcbB24a551F3b954096",
11 | "0x71bE63f3384f5fb98995898A86B02Fb2426c5788",
12 | "0xFABB0ac9d68B0B445fB7357272Ff202C5651694a",
13 | "0x1CBd3b2770909D4e10f157cABC84C7264073C9Ec",
14 | "0xdF3e18d64BC6A983f673Ab319CCaE4f1a57C7097",
15 | "0xcd3B766CCDd6AE721141F452C550Ca635964ce71",
16 | "0x2546BcD3c84621e976D8185a91A922aE77ECEc30",
17 | "0xbDA5747bFD65F08deb54cb465eB87D40e51B197E",
18 | "0xdD2FD4581271e230360230F9337D5c0430Bf44C0",
19 | "0x8626f6940E2eb28930eFb4CeF49B2d1F2C9C1199"
20 | ]
--------------------------------------------------------------------------------
/smart-contract/contracts/YourNftToken.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | pragma solidity >=0.8.9 <0.9.0;
4 |
5 | import 'erc721a/contracts/extensions/ERC721AQueryable.sol';
6 | import '@openzeppelin/contracts/access/Ownable.sol';
7 | import '@openzeppelin/contracts/utils/cryptography/MerkleProof.sol';
8 | import '@openzeppelin/contracts/security/ReentrancyGuard.sol';
9 |
10 | contract YourNftToken is ERC721AQueryable, Ownable, ReentrancyGuard {
11 |
12 | using Strings for uint256;
13 |
14 | bytes32 public merkleRoot;
15 | mapping(address => bool) public whitelistClaimed;
16 |
17 | string public uriPrefix = '';
18 | string public uriSuffix = '.json';
19 | string public hiddenMetadataUri;
20 |
21 | uint256 public cost;
22 | uint256 public maxSupply;
23 | uint256 public maxMintAmountPerTx;
24 |
25 | bool public paused = true;
26 | bool public whitelistMintEnabled = false;
27 | bool public revealed = false;
28 |
29 | constructor(
30 | string memory _tokenName,
31 | string memory _tokenSymbol,
32 | uint256 _cost,
33 | uint256 _maxSupply,
34 | uint256 _maxMintAmountPerTx,
35 | string memory _hiddenMetadataUri
36 | ) ERC721A(_tokenName, _tokenSymbol) {
37 | setCost(_cost);
38 | maxSupply = _maxSupply;
39 | setMaxMintAmountPerTx(_maxMintAmountPerTx);
40 | setHiddenMetadataUri(_hiddenMetadataUri);
41 | }
42 |
43 | modifier mintCompliance(uint256 _mintAmount) {
44 | require(_mintAmount > 0 && _mintAmount <= maxMintAmountPerTx, 'Invalid mint amount!');
45 | require(totalSupply() + _mintAmount <= maxSupply, 'Max supply exceeded!');
46 | _;
47 | }
48 |
49 | modifier mintPriceCompliance(uint256 _mintAmount) {
50 | require(msg.value >= cost * _mintAmount, 'Insufficient funds!');
51 | _;
52 | }
53 |
54 | function whitelistMint(uint256 _mintAmount, bytes32[] calldata _merkleProof) public payable mintCompliance(_mintAmount) mintPriceCompliance(_mintAmount) {
55 | // Verify whitelist requirements
56 | require(whitelistMintEnabled, 'The whitelist sale is not enabled!');
57 | require(!whitelistClaimed[_msgSender()], 'Address already claimed!');
58 | bytes32 leaf = keccak256(abi.encodePacked(_msgSender()));
59 | require(MerkleProof.verify(_merkleProof, merkleRoot, leaf), 'Invalid proof!');
60 |
61 | whitelistClaimed[_msgSender()] = true;
62 | _safeMint(_msgSender(), _mintAmount);
63 | }
64 |
65 | function mint(uint256 _mintAmount) public payable mintCompliance(_mintAmount) mintPriceCompliance(_mintAmount) {
66 | require(!paused, 'The contract is paused!');
67 |
68 | _safeMint(_msgSender(), _mintAmount);
69 | }
70 |
71 | function mintForAddress(uint256 _mintAmount, address _receiver) public mintCompliance(_mintAmount) onlyOwner {
72 | _safeMint(_receiver, _mintAmount);
73 | }
74 |
75 | function _startTokenId() internal view virtual override returns (uint256) {
76 | return 1;
77 | }
78 |
79 | function tokenURI(uint256 _tokenId) public view virtual override returns (string memory) {
80 | require(_exists(_tokenId), 'ERC721Metadata: URI query for nonexistent token');
81 |
82 | if (revealed == false) {
83 | return hiddenMetadataUri;
84 | }
85 |
86 | string memory currentBaseURI = _baseURI();
87 | return bytes(currentBaseURI).length > 0
88 | ? string(abi.encodePacked(currentBaseURI, _tokenId.toString(), uriSuffix))
89 | : '';
90 | }
91 |
92 | function setRevealed(bool _state) public onlyOwner {
93 | revealed = _state;
94 | }
95 |
96 | function setCost(uint256 _cost) public onlyOwner {
97 | cost = _cost;
98 | }
99 |
100 | function setMaxMintAmountPerTx(uint256 _maxMintAmountPerTx) public onlyOwner {
101 | maxMintAmountPerTx = _maxMintAmountPerTx;
102 | }
103 |
104 | function setHiddenMetadataUri(string memory _hiddenMetadataUri) public onlyOwner {
105 | hiddenMetadataUri = _hiddenMetadataUri;
106 | }
107 |
108 | function setUriPrefix(string memory _uriPrefix) public onlyOwner {
109 | uriPrefix = _uriPrefix;
110 | }
111 |
112 | function setUriSuffix(string memory _uriSuffix) public onlyOwner {
113 | uriSuffix = _uriSuffix;
114 | }
115 |
116 | function setPaused(bool _state) public onlyOwner {
117 | paused = _state;
118 | }
119 |
120 | function setMerkleRoot(bytes32 _merkleRoot) public onlyOwner {
121 | merkleRoot = _merkleRoot;
122 | }
123 |
124 | function setWhitelistMintEnabled(bool _state) public onlyOwner {
125 | whitelistMintEnabled = _state;
126 | }
127 |
128 | function withdraw() public onlyOwner nonReentrant {
129 | // This will pay HashLips Lab Team 5% of the initial sale.
130 | // By leaving the following lines as they are you will contribute to the
131 | // development of tools like this and many others.
132 | // =============================================================================
133 | (bool hs, ) = payable(0x146FB9c3b2C13BA88c6945A759EbFa95127486F4).call{value: address(this).balance * 5 / 100}('');
134 | require(hs);
135 | // =============================================================================
136 |
137 | // This will transfer the remaining contract balance to the owner.
138 | // Do not remove this otherwise you will not be able to withdraw the funds.
139 | // =============================================================================
140 | (bool os, ) = payable(owner()).call{value: address(this).balance}('');
141 | require(os);
142 | // =============================================================================
143 | }
144 |
145 | function _baseURI() internal view virtual override returns (string memory) {
146 | return uriPrefix;
147 | }
148 | }
149 |
--------------------------------------------------------------------------------
/smart-contract/hardhat.config.ts:
--------------------------------------------------------------------------------
1 | import fs from 'fs';
2 | import * as dotenv from 'dotenv';
3 | import { HardhatUserConfig, task } from 'hardhat/config';
4 | import { MerkleTree } from 'merkletreejs';
5 | import keccak256 from 'keccak256';
6 | import '@nomiclabs/hardhat-etherscan';
7 | import '@nomiclabs/hardhat-waffle';
8 | import '@typechain/hardhat';
9 | import 'hardhat-gas-reporter';
10 | import 'solidity-coverage';
11 | import CollectionConfig from './config/CollectionConfig';
12 |
13 | dotenv.config();
14 |
15 | /*
16 | * If you have issues with stuck transactions or you simply want to invest in
17 | * higher gas fees in order to make sure your transactions will run smoother
18 | * and faster, then you can update the followind value.
19 | * This value is used by default in any network defined in this project, but
20 | * please make sure to add it manually if you define any custom network.
21 | *
22 | * Example:
23 | * Setting the value to "1.1" will raise the gas values by 10% compared to the
24 | * estimated value.
25 | */
26 | const DEFAULT_GAS_MULTIPLIER: number = 1;
27 |
28 | // This is a sample Hardhat task. To learn how to create your own go to
29 | // https://hardhat.org/guides/create-task.html
30 | task('accounts', 'Prints the list of accounts', async (taskArgs, hre) => {
31 | const accounts = await hre.ethers.getSigners();
32 |
33 | for (const account of accounts) {
34 | console.log(account.address);
35 | }
36 | });
37 |
38 | task('generate-root-hash', 'Generates and prints out the root hash for the current whitelist', async () => {
39 | // Check configuration
40 | if (CollectionConfig.whitelistAddresses.length < 1) {
41 | throw 'The whitelist is empty, please add some addresses to the configuration.';
42 | }
43 |
44 | // Build the Merkle Tree
45 | const leafNodes = CollectionConfig.whitelistAddresses.map(addr => keccak256(addr));
46 | const merkleTree = new MerkleTree(leafNodes, keccak256, { sortPairs: true });
47 | const rootHash = '0x' + merkleTree.getRoot().toString('hex');
48 |
49 | console.log('The Merkle Tree root hash for the current whitelist is: ' + rootHash);
50 | });
51 |
52 | task('generate-proof', 'Generates and prints out the whitelist proof for the given address (compatible with block explorers such as Etherscan)', async (taskArgs: {address: string}) => {
53 | // Check configuration
54 | if (CollectionConfig.whitelistAddresses.length < 1) {
55 | throw 'The whitelist is empty, please add some addresses to the configuration.';
56 | }
57 |
58 | // Build the Merkle Tree
59 | const leafNodes = CollectionConfig.whitelistAddresses.map(addr => keccak256(addr));
60 | const merkleTree = new MerkleTree(leafNodes, keccak256, { sortPairs: true });
61 | const proof = merkleTree.getHexProof(keccak256(taskArgs.address)).toString().replace(/'/g, '').replace(/ /g, '');
62 |
63 | console.log('The whitelist proof for the given address is: ' + proof);
64 | })
65 | .addPositionalParam('address', 'The public address');
66 |
67 | task('rename-contract', 'Renames the smart contract replacing all occurrences in source files', async (taskArgs: {newName: string}, hre) => {
68 | // Validate new name
69 | if (!/^([A-Z][A-Za-z0-9]+)$/.test(taskArgs.newName)) {
70 | throw 'The contract name must be in PascalCase: https://en.wikipedia.org/wiki/Camel_case#Variations_and_synonyms';
71 | }
72 |
73 | const oldContractFile = `${__dirname}/contracts/${CollectionConfig.contractName}.sol`;
74 | const newContractFile = `${__dirname}/contracts/${taskArgs.newName}.sol`;
75 |
76 | if (!fs.existsSync(oldContractFile)) {
77 | throw `Contract file not found: "${oldContractFile}" (did you change the configuration manually?)`;
78 | }
79 |
80 | if (fs.existsSync(newContractFile)) {
81 | throw `A file with that name already exists: "${oldContractFile}"`;
82 | }
83 |
84 | // Replace names in source files
85 | replaceInFile(__dirname + '/../minting-dapp/src/scripts/lib/NftContractType.ts', CollectionConfig.contractName, taskArgs.newName);
86 | replaceInFile(__dirname + '/config/CollectionConfig.ts', CollectionConfig.contractName, taskArgs.newName);
87 | replaceInFile(__dirname + '/lib/NftContractProvider.ts', CollectionConfig.contractName, taskArgs.newName);
88 | replaceInFile(oldContractFile, CollectionConfig.contractName, taskArgs.newName);
89 |
90 | // Rename the contract file
91 | fs.renameSync(oldContractFile, newContractFile);
92 |
93 | console.log(`Contract renamed successfully from "${CollectionConfig.contractName}" to "${taskArgs.newName}"!`);
94 |
95 | // Rebuilding types
96 | await hre.run('typechain');
97 | })
98 | .addPositionalParam('newName', 'The new name');
99 |
100 | // You need to export an object to set up your config
101 | // Go to https://hardhat.org/config/ to learn more
102 |
103 | const config: HardhatUserConfig = {
104 | solidity: {
105 | version: '0.8.9',
106 | settings: {
107 | optimizer: {
108 | enabled: true,
109 | runs: 200,
110 | },
111 | },
112 | },
113 | networks: {
114 | truffle: {
115 | url: 'http://localhost:24012/rpc',
116 | timeout: 60000,
117 | gasMultiplier: DEFAULT_GAS_MULTIPLIER,
118 | },
119 | },
120 | gasReporter: {
121 | enabled: process.env.REPORT_GAS !== undefined,
122 | currency: 'USD',
123 | coinmarketcap: process.env.GAS_REPORTER_COIN_MARKET_CAP_API_KEY,
124 | },
125 | etherscan: {
126 | apiKey: {
127 | // Ethereum
128 | goerli: process.env.BLOCK_EXPLORER_API_KEY,
129 | mainnet: process.env.BLOCK_EXPLORER_API_KEY,
130 | rinkeby: process.env.BLOCK_EXPLORER_API_KEY,
131 |
132 | // Polygon
133 | polygon: process.env.BLOCK_EXPLORER_API_KEY,
134 | polygonMumbai: process.env.BLOCK_EXPLORER_API_KEY,
135 | },
136 | },
137 | };
138 |
139 | // Setup "testnet" network
140 | if (process.env.NETWORK_TESTNET_URL !== undefined) {
141 | config.networks!.testnet = {
142 | url: process.env.NETWORK_TESTNET_URL,
143 | accounts: [process.env.NETWORK_TESTNET_PRIVATE_KEY!],
144 | gasMultiplier: DEFAULT_GAS_MULTIPLIER,
145 | };
146 | }
147 |
148 | // Setup "mainnet" network
149 | if (process.env.NETWORK_MAINNET_URL !== undefined) {
150 | config.networks!.mainnet = {
151 | url: process.env.NETWORK_MAINNET_URL,
152 | accounts: [process.env.NETWORK_MAINNET_PRIVATE_KEY!],
153 | gasMultiplier: DEFAULT_GAS_MULTIPLIER,
154 | };
155 | }
156 |
157 | export default config;
158 |
159 | /**
160 | * Replaces all occurrences of a string in the given file.
161 | */
162 | function replaceInFile(file: string, search: string, replace: string): void
163 | {
164 | const fileContent = fs.readFileSync(file, 'utf8').replace(new RegExp(search, 'g'), replace);
165 |
166 | fs.writeFileSync(file, fileContent, 'utf8');
167 | }
168 |
--------------------------------------------------------------------------------
/smart-contract/lib/CollectionConfigInterface.ts:
--------------------------------------------------------------------------------
1 | import NetworkConfigInterface from '../lib/NetworkConfigInterface';
2 | import MarketplaceConfigInterface from '../lib/MarketplaceConfigInterface';
3 |
4 | interface SaleConfig {
5 | price: number;
6 | maxMintAmountPerTx: number;
7 | };
8 |
9 | export default interface CollectionConfigInterface {
10 | testnet: NetworkConfigInterface;
11 | mainnet: NetworkConfigInterface;
12 | contractName: string;
13 | tokenName: string;
14 | tokenSymbol: string;
15 | hiddenMetadataUri: string;
16 | maxSupply: number;
17 | whitelistSale: SaleConfig;
18 | preSale: SaleConfig;
19 | publicSale: SaleConfig;
20 | contractAddress: string|null;
21 | marketplaceIdentifier: string;
22 | marketplaceConfig: MarketplaceConfigInterface;
23 | whitelistAddresses: string[];
24 | };
25 |
--------------------------------------------------------------------------------
/smart-contract/lib/MarketplaceConfigInterface.ts:
--------------------------------------------------------------------------------
1 | export default interface MarketplaceConfigInterface {
2 | name: string;
3 | generateCollectionUrl: (marketplaceIdentifier: any, isMainnet: boolean) => string;
4 | };
5 |
--------------------------------------------------------------------------------
/smart-contract/lib/Marketplaces.ts:
--------------------------------------------------------------------------------
1 | import MarketplaceConfigInterface from './MarketplaceConfigInterface';
2 |
3 | export const openSea: MarketplaceConfigInterface = {
4 | name: 'OpenSea',
5 | generateCollectionUrl: (marketplaceIdentifier: string, isMainnet: boolean) => 'https://' + (isMainnet ? 'www' : 'testnets') + '.opensea.io/collection/' + marketplaceIdentifier,
6 | }
7 |
--------------------------------------------------------------------------------
/smart-contract/lib/NetworkConfigInterface.ts:
--------------------------------------------------------------------------------
1 | export default interface NetworkConfigInterface {
2 | chainId: number;
3 | symbol: string;
4 | blockExplorer: {
5 | name: string;
6 | generateContractUrl: (contractAddress: string) => string;
7 | generateTransactionUrl: (transactionAddress: string) => string;
8 | };
9 | };
10 |
--------------------------------------------------------------------------------
/smart-contract/lib/Networks.ts:
--------------------------------------------------------------------------------
1 | import NetworkConfigInterface from './NetworkConfigInterface';
2 |
3 | /*
4 | * Local networks
5 | */
6 | export const hardhatLocal: NetworkConfigInterface = {
7 | chainId: 31337,
8 | symbol: 'ETH (test)',
9 | blockExplorer: {
10 | name: 'Block explorer (not available for local chains)',
11 | generateContractUrl: (contractAddress: string) => `#`,
12 | generateTransactionUrl: (transactionAddress: string) => `#`,
13 | },
14 | }
15 |
16 | /*
17 | * Ethereum
18 | */
19 | export const ethereumTestnet: NetworkConfigInterface = {
20 | chainId: 5,
21 | symbol: 'ETH (test)',
22 | blockExplorer: {
23 | name: 'Etherscan (Goerli)',
24 | generateContractUrl: (contractAddress: string) => `https://goerli.etherscan.io/address/${contractAddress}`,
25 | generateTransactionUrl: (transactionAddress: string) => `https://goerli.etherscan.io/tx/${transactionAddress}`,
26 | },
27 | }
28 |
29 | export const ethereumLegacyTestnet: NetworkConfigInterface = {
30 | chainId: 4,
31 | symbol: 'ETH (test)',
32 | blockExplorer: {
33 | name: 'Etherscan (Rinkeby)',
34 | generateContractUrl: (contractAddress: string) => `https://rinkeby.etherscan.io/address/${contractAddress}`,
35 | generateTransactionUrl: (transactionAddress: string) => `https://rinkeby.etherscan.io/tx/${transactionAddress}`,
36 | },
37 | }
38 |
39 | export const ethereumMainnet: NetworkConfigInterface = {
40 | chainId: 1,
41 | symbol: 'ETH',
42 | blockExplorer: {
43 | name: 'Etherscan',
44 | generateContractUrl: (contractAddress: string) => `https://etherscan.io/address/${contractAddress}`,
45 | generateTransactionUrl: (transactionAddress: string) => `https://etherscan.io/tx/${transactionAddress}`,
46 | },
47 | }
48 |
49 | /*
50 | * Polygon
51 | */
52 | export const polygonTestnet: NetworkConfigInterface = {
53 | chainId: 80001,
54 | symbol: 'MATIC (test)',
55 | blockExplorer: {
56 | name: 'Polygonscan (Mumbai)',
57 | generateContractUrl: (contractAddress: string) => `https://mumbai.polygonscan.com/address/${contractAddress}`,
58 | generateTransactionUrl: (transactionAddress: string) => `https://mumbai.polygonscan.com/tx/${transactionAddress}`,
59 | },
60 | }
61 |
62 | export const polygonMainnet: NetworkConfigInterface = {
63 | chainId: 137,
64 | symbol: 'MATIC',
65 | blockExplorer: {
66 | name: 'Polygonscan',
67 | generateContractUrl: (contractAddress: string) => `https://polygonscan.com/address/${contractAddress}`,
68 | generateTransactionUrl: (transactionAddress: string) => `https://polygonscan.com/tx/${transactionAddress}`,
69 | },
70 | }
71 |
--------------------------------------------------------------------------------
/smart-contract/lib/NftContractProvider.ts:
--------------------------------------------------------------------------------
1 | // The name below ("YourNftToken") should match the name of your Solidity contract.
2 | // It can be updated using the following command:
3 | // yarn rename-contract NEW_CONTRACT_NAME
4 | // Please DO NOT change it manually!
5 | import { YourNftToken as ContractType } from '../typechain/index';
6 |
7 | import { ethers } from 'hardhat';
8 | import CollectionConfig from './../config/CollectionConfig';
9 |
10 | export default class NftContractProvider {
11 | public static async getContract(): Promise {
12 | // Check configuration
13 | if (null === CollectionConfig.contractAddress) {
14 | throw '\x1b[31merror\x1b[0m ' + 'Please add the contract address to the configuration before running this command.';
15 | }
16 |
17 | if (await ethers.provider.getCode(CollectionConfig.contractAddress) === '0x') {
18 | throw '\x1b[31merror\x1b[0m ' + `Can't find a contract deployed to the target address: ${CollectionConfig.contractAddress}`;
19 | }
20 |
21 | return await ethers.getContractAt(CollectionConfig.contractName, CollectionConfig.contractAddress) as ContractType;
22 | }
23 | };
24 |
25 | export type NftContractType = ContractType;
26 |
--------------------------------------------------------------------------------
/smart-contract/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@hashlips-lab/nft-erc721-collection-smart-contract",
3 | "version": "0.0.0",
4 | "private": true,
5 | "devDependencies": {
6 | "@nomiclabs/hardhat-ethers": "^2.0.4",
7 | "@nomiclabs/hardhat-etherscan": "^3.0.3",
8 | "@nomiclabs/hardhat-waffle": "^2.0.1",
9 | "@openzeppelin/contracts": "^4.4.2",
10 | "@typechain/ethers-v5": "^7.2.0",
11 | "@typechain/hardhat": "^2.3.1",
12 | "@types/chai": "^4.3.0",
13 | "@types/chai-as-promised": "^7.1.5",
14 | "@types/mocha": "^9.0.0",
15 | "@types/node": "^12.20.41",
16 | "@typescript-eslint/eslint-plugin": "^4.33.0",
17 | "@typescript-eslint/parser": "^4.33.0",
18 | "chai": "^4.3.4",
19 | "chai-as-promised": "^7.1.1",
20 | "dotenv": "^10.0.0",
21 | "erc721a": "^3.0.0",
22 | "eslint": "^7.32.0",
23 | "eslint-config-prettier": "^8.3.0",
24 | "eslint-config-standard": "^16.0.3",
25 | "eslint-plugin-import": "^2.25.4",
26 | "eslint-plugin-node": "^11.1.0",
27 | "eslint-plugin-prettier": "^3.4.1",
28 | "eslint-plugin-promise": "^5.2.0",
29 | "ethereum-waffle": "^3.4.0",
30 | "ethers": "^5.5.3",
31 | "hardhat": "^2.8.2",
32 | "hardhat-gas-reporter": "^1.0.7",
33 | "keccak256": "^1.0.6",
34 | "merkletreejs": "^0.2.27",
35 | "prettier": "^2.5.1",
36 | "prettier-plugin-solidity": "^1.0.0-beta.19",
37 | "solhint": "^3.3.6",
38 | "solidity-coverage": "^0.7.17",
39 | "ts-node": "^10.4.0",
40 | "typechain": "^5.2.0",
41 | "typescript": "^4.5.4"
42 | },
43 | "scripts": {
44 | "accounts": "hardhat accounts",
45 | "rename-contract": "hardhat rename-contract",
46 | "compile": "hardhat compile --force",
47 | "test": "hardhat test",
48 | "test-extended": "EXTENDED_TESTS=1 hardhat test",
49 | "test-gas": "REPORT_GAS=1 hardhat test",
50 | "local-node": "hardhat node",
51 | "root-hash": "hardhat generate-root-hash",
52 | "proof": "hardhat generate-proof",
53 | "deploy": "hardhat run scripts/1_deploy.ts",
54 | "verify": "hardhat verify --constructor-args config/ContractArguments.ts",
55 | "whitelist-open": "hardhat run scripts/2_whitelist_open.ts",
56 | "whitelist-close": "hardhat run scripts/3_whitelist_close.ts",
57 | "presale-open": "hardhat run scripts/4_presale_open.ts",
58 | "presale-close": "hardhat run scripts/5_presale_close.ts",
59 | "public-sale-open": "hardhat run scripts/6_public_sale_open.ts",
60 | "public-sale-close": "hardhat run scripts/7_public_sale_close.ts",
61 | "reveal": "hardhat run scripts/8_reveal.ts"
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/smart-contract/scripts/1_deploy.ts:
--------------------------------------------------------------------------------
1 | import { ethers } from 'hardhat';
2 | import CollectionConfig from '../config/CollectionConfig';
3 | import { NftContractType } from '../lib/NftContractProvider';
4 | import ContractArguments from './../config/ContractArguments';
5 |
6 | async function main() {
7 | // Hardhat always runs the compile task when running scripts with its command
8 | // line interface.
9 | //
10 | // If this script is run directly using `node` you may want to call compile
11 | // manually to make sure everything is compiled
12 | // await hre.run('compile');
13 |
14 | console.log('Deploying contract...');
15 |
16 | // We get the contract to deploy
17 | const Contract = await ethers.getContractFactory(CollectionConfig.contractName);
18 | const contract = await Contract.deploy(...ContractArguments) as NftContractType;
19 |
20 | await contract.deployed();
21 |
22 | console.log('Contract deployed to:', contract.address);
23 | }
24 |
25 | // We recommend this pattern to be able to use async/await everywhere
26 | // and properly handle errors.
27 | main().catch((error) => {
28 | console.error(error);
29 | process.exitCode = 1;
30 | });
31 |
--------------------------------------------------------------------------------
/smart-contract/scripts/2_whitelist_open.ts:
--------------------------------------------------------------------------------
1 | import { utils } from 'ethers';
2 | import { MerkleTree } from 'merkletreejs';
3 | import keccak256 from 'keccak256';
4 | import CollectionConfig from './../config/CollectionConfig';
5 | import NftContractProvider from '../lib/NftContractProvider';
6 |
7 | async function main() {
8 | // Check configuration
9 | if (CollectionConfig.whitelistAddresses.length < 1) {
10 | throw '\x1b[31merror\x1b[0m ' + 'The whitelist is empty, please add some addresses to the configuration.';
11 | }
12 |
13 | // Build the Merkle Tree
14 | const leafNodes = CollectionConfig.whitelistAddresses.map(addr => keccak256(addr));
15 | const merkleTree = new MerkleTree(leafNodes, keccak256, { sortPairs: true });
16 | const rootHash = '0x' + merkleTree.getRoot().toString('hex');
17 |
18 | // Attach to deployed contract
19 | const contract = await NftContractProvider.getContract();
20 |
21 | // Update sale price (if needed)
22 | const whitelistPrice = utils.parseEther(CollectionConfig.whitelistSale.price.toString());
23 | if (!await (await contract.cost()).eq(whitelistPrice)) {
24 | console.log(`Updating the token price to ${CollectionConfig.whitelistSale.price} ${CollectionConfig.mainnet.symbol}...`);
25 |
26 | await (await contract.setCost(whitelistPrice)).wait();
27 | }
28 |
29 | // Update max amount per TX (if needed)
30 | if (!await (await contract.maxMintAmountPerTx()).eq(CollectionConfig.whitelistSale.maxMintAmountPerTx)) {
31 | console.log(`Updating the max mint amount per TX to ${CollectionConfig.whitelistSale.maxMintAmountPerTx}...`);
32 |
33 | await (await contract.setMaxMintAmountPerTx(CollectionConfig.whitelistSale.maxMintAmountPerTx)).wait();
34 | }
35 |
36 | // Update root hash (if changed)
37 | if ((await contract.merkleRoot()) !== rootHash) {
38 | console.log(`Updating the root hash to: ${rootHash}`);
39 |
40 | await (await contract.setMerkleRoot(rootHash)).wait();
41 | }
42 |
43 | // Enable whitelist sale (if needed)
44 | if (!await contract.whitelistMintEnabled()) {
45 | console.log('Enabling whitelist sale...');
46 |
47 | await (await contract.setWhitelistMintEnabled(true)).wait();
48 | }
49 |
50 | console.log('Whitelist sale has been enabled!');
51 | }
52 |
53 | // We recommend this pattern to be able to use async/await everywhere
54 | // and properly handle errors.
55 | main().catch((error) => {
56 | console.error(error);
57 | process.exitCode = 1;
58 | });
59 |
--------------------------------------------------------------------------------
/smart-contract/scripts/3_whitelist_close.ts:
--------------------------------------------------------------------------------
1 | import NftContractProvider from '../lib/NftContractProvider';
2 |
3 | async function main() {
4 | // Attach to deployed contract
5 | const contract = await NftContractProvider.getContract();
6 |
7 | // Disable whitelist sale (if needed)
8 | if (await contract.whitelistMintEnabled()) {
9 | console.log('Disabling whitelist sale...');
10 |
11 | await (await contract.setWhitelistMintEnabled(false)).wait();
12 | }
13 |
14 | console.log('Whitelist sale has been disabled!');
15 | }
16 |
17 | // We recommend this pattern to be able to use async/await everywhere
18 | // and properly handle errors.
19 | main().catch((error) => {
20 | console.error(error);
21 | process.exitCode = 1;
22 | });
23 |
--------------------------------------------------------------------------------
/smart-contract/scripts/4_presale_open.ts:
--------------------------------------------------------------------------------
1 | import { utils } from 'ethers';
2 | import CollectionConfig from './../config/CollectionConfig';
3 | import NftContractProvider from '../lib/NftContractProvider';
4 |
5 | async function main() {
6 | // Attach to deployed contract
7 | const contract = await NftContractProvider.getContract();
8 |
9 | if (await contract.whitelistMintEnabled()) {
10 | throw '\x1b[31merror\x1b[0m ' + 'Please close the whitelist sale before opening a pre-sale.';
11 | }
12 |
13 | // Update sale price (if needed)
14 | const preSalePrice = utils.parseEther(CollectionConfig.preSale.price.toString());
15 | if (!await (await contract.cost()).eq(preSalePrice)) {
16 | console.log(`Updating the token price to ${CollectionConfig.preSale.price} ${CollectionConfig.mainnet.symbol}...`);
17 |
18 | await (await contract.setCost(preSalePrice)).wait();
19 | }
20 |
21 | // Update max amount per TX (if needed)
22 | if (!await (await contract.maxMintAmountPerTx()).eq(CollectionConfig.preSale.maxMintAmountPerTx)) {
23 | console.log(`Updating the max mint amount per TX to ${CollectionConfig.preSale.maxMintAmountPerTx}...`);
24 |
25 | await (await contract.setMaxMintAmountPerTx(CollectionConfig.preSale.maxMintAmountPerTx)).wait();
26 | }
27 |
28 | // Unpause the contract (if needed)
29 | if (await contract.paused()) {
30 | console.log('Unpausing the contract...');
31 |
32 | await (await contract.setPaused(false)).wait();
33 | }
34 |
35 | console.log('Pre-sale is now open!');
36 | }
37 |
38 | // We recommend this pattern to be able to use async/await everywhere
39 | // and properly handle errors.
40 | main().catch((error) => {
41 | console.error(error);
42 | process.exitCode = 1;
43 | });
44 |
--------------------------------------------------------------------------------
/smart-contract/scripts/5_presale_close.ts:
--------------------------------------------------------------------------------
1 | import NftContractProvider from '../lib/NftContractProvider';
2 |
3 | async function main() {
4 | // Attach to deployed contract
5 | const contract = await NftContractProvider.getContract();
6 |
7 | // Pause the contract (if needed)
8 | if (!await contract.paused()) {
9 | console.log('Pausing the contract...');
10 |
11 | await (await contract.setPaused(true)).wait();
12 | }
13 |
14 | console.log('Pre-sale is now closed!');
15 | }
16 |
17 | // We recommend this pattern to be able to use async/await everywhere
18 | // and properly handle errors.
19 | main().catch((error) => {
20 | console.error(error);
21 | process.exitCode = 1;
22 | });
23 |
--------------------------------------------------------------------------------
/smart-contract/scripts/6_public_sale_open.ts:
--------------------------------------------------------------------------------
1 | import { utils } from 'ethers';
2 | import CollectionConfig from './../config/CollectionConfig';
3 | import NftContractProvider from '../lib/NftContractProvider';
4 |
5 | async function main() {
6 | // Attach to deployed contract
7 | const contract = await NftContractProvider.getContract();
8 |
9 | if (await contract.whitelistMintEnabled()) {
10 | throw '\x1b[31merror\x1b[0m ' + 'Please close the whitelist sale before opening a public sale.';
11 | }
12 |
13 | // Update sale price (if needed)
14 | const publicSalePrice = utils.parseEther(CollectionConfig.publicSale.price.toString());
15 | if (!await (await contract.cost()).eq(publicSalePrice)) {
16 | console.log(`Updating the token price to ${CollectionConfig.publicSale.price} ${CollectionConfig.mainnet.symbol}...`);
17 |
18 | await (await contract.setCost(publicSalePrice)).wait();
19 | }
20 |
21 | // Update max amount per TX (if needed)
22 | if (!await (await contract.maxMintAmountPerTx()).eq(CollectionConfig.publicSale.maxMintAmountPerTx)) {
23 | console.log(`Updating the max mint amount per TX to ${CollectionConfig.publicSale.maxMintAmountPerTx}...`);
24 |
25 | await (await contract.setMaxMintAmountPerTx(CollectionConfig.publicSale.maxMintAmountPerTx)).wait();
26 | }
27 |
28 | // Unpause the contract (if needed)
29 | if (await contract.paused()) {
30 | console.log('Unpausing the contract...');
31 |
32 | await (await contract.setPaused(false)).wait();
33 | }
34 |
35 | console.log('Public sale is now open!');
36 | }
37 |
38 | // We recommend this pattern to be able to use async/await everywhere
39 | // and properly handle errors.
40 | main().catch((error) => {
41 | console.error(error);
42 | process.exitCode = 1;
43 | });
44 |
--------------------------------------------------------------------------------
/smart-contract/scripts/7_public_sale_close.ts:
--------------------------------------------------------------------------------
1 | import NftContractProvider from '../lib/NftContractProvider';
2 |
3 | async function main() {
4 | // Attach to deployed contract
5 | const contract = await NftContractProvider.getContract();
6 |
7 | // Pause the contract (if needed)
8 | if (!await contract.paused()) {
9 | console.log('Pausing the contract...');
10 |
11 | await (await contract.setPaused(true)).wait();
12 | }
13 |
14 | console.log('Public sale is now closed!');
15 | }
16 |
17 | // We recommend this pattern to be able to use async/await everywhere
18 | // and properly handle errors.
19 | main().catch((error) => {
20 | console.error(error);
21 | process.exitCode = 1;
22 | });
23 |
--------------------------------------------------------------------------------
/smart-contract/scripts/8_reveal.ts:
--------------------------------------------------------------------------------
1 | import NftContractProvider from '../lib/NftContractProvider';
2 |
3 | async function main() {
4 | if (undefined === process.env.COLLECTION_URI_PREFIX || process.env.COLLECTION_URI_PREFIX === 'ipfs://__CID___/') {
5 | throw '\x1b[31merror\x1b[0m ' + 'Please add the URI prefix to the ENV configuration before running this command.';
6 | }
7 |
8 | // Attach to deployed contract
9 | const contract = await NftContractProvider.getContract();
10 |
11 | // Update URI prefix (if changed)
12 | if ((await contract.uriPrefix()) !== process.env.COLLECTION_URI_PREFIX) {
13 | console.log(`Updating the URI prefix to: ${process.env.COLLECTION_URI_PREFIX}`);
14 |
15 | await (await contract.setUriPrefix(process.env.COLLECTION_URI_PREFIX)).wait();
16 | }
17 |
18 | // Revealing the collection (if needed)
19 | if (!await contract.revealed()) {
20 | console.log('Revealing the collection...');
21 |
22 | await (await contract.setRevealed(true)).wait();
23 | }
24 |
25 | console.log('Your collection is now revealed!');
26 | }
27 |
28 | // We recommend this pattern to be able to use async/await everywhere
29 | // and properly handle errors.
30 | main().catch((error) => {
31 | console.error(error);
32 | process.exitCode = 1;
33 | });
34 |
--------------------------------------------------------------------------------
/smart-contract/test/index.ts:
--------------------------------------------------------------------------------
1 | import chai, { expect } from 'chai';
2 | import ChaiAsPromised from 'chai-as-promised';
3 | import { BigNumber, utils } from 'ethers';
4 | import { ethers } from 'hardhat';
5 | import { MerkleTree } from 'merkletreejs';
6 | import keccak256 from 'keccak256';
7 | import CollectionConfig from './../config/CollectionConfig';
8 | import ContractArguments from '../config/ContractArguments';
9 | import { NftContractType } from '../lib/NftContractProvider';
10 | import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers';
11 |
12 | chai.use(ChaiAsPromised);
13 |
14 | enum SaleType {
15 | WHITELIST = CollectionConfig.whitelistSale.price,
16 | PRE_SALE = CollectionConfig.preSale.price,
17 | PUBLIC_SALE = CollectionConfig.publicSale.price,
18 | };
19 |
20 | const whitelistAddresses = [
21 | // Hardhat test addresses...
22 | "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
23 | "0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65",
24 | "0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc",
25 | "0x976EA74026E726554dB657fA54763abd0C3a0aa9",
26 | "0x14dC79964da2C08b23698B3D3cc7Ca32193d9955",
27 | "0x23618e81E3f5cdF7f54C3d65f7FBc0aBf5B21E8f",
28 | "0xa0Ee7A142d267C1f36714E4a8F75612F20a79720",
29 | "0xBcd4042DE499D14e55001CcbB24a551F3b954096",
30 | "0x71bE63f3384f5fb98995898A86B02Fb2426c5788",
31 | "0xFABB0ac9d68B0B445fB7357272Ff202C5651694a",
32 | "0x1CBd3b2770909D4e10f157cABC84C7264073C9Ec",
33 | "0xdF3e18d64BC6A983f673Ab319CCaE4f1a57C7097",
34 | "0xcd3B766CCDd6AE721141F452C550Ca635964ce71",
35 | "0x2546BcD3c84621e976D8185a91A922aE77ECEc30",
36 | "0xbDA5747bFD65F08deb54cb465eB87D40e51B197E",
37 | "0xdD2FD4581271e230360230F9337D5c0430Bf44C0",
38 | "0x8626f6940E2eb28930eFb4CeF49B2d1F2C9C1199"
39 | ];
40 |
41 | function getPrice(saleType: SaleType, mintAmount: number) {
42 | return utils.parseEther(saleType.toString()).mul(mintAmount);
43 | }
44 |
45 | describe(CollectionConfig.contractName, function () {
46 | let owner!: SignerWithAddress;
47 | let whitelistedUser!: SignerWithAddress;
48 | let holder!: SignerWithAddress;
49 | let externalUser!: SignerWithAddress;
50 | let contract!: NftContractType;
51 |
52 | before(async function () {
53 | [owner, whitelistedUser, holder, externalUser] = await ethers.getSigners();
54 | });
55 |
56 | it('Contract deployment', async function () {
57 | const Contract = await ethers.getContractFactory(CollectionConfig.contractName);
58 | contract = await Contract.deploy(...ContractArguments) as NftContractType;
59 |
60 | await contract.deployed();
61 | });
62 |
63 | it('Check initial data', async function () {
64 | expect(await contract.name()).to.equal(CollectionConfig.tokenName);
65 | expect(await contract.symbol()).to.equal(CollectionConfig.tokenSymbol);
66 | expect(await contract.cost()).to.equal(getPrice(SaleType.WHITELIST, 1));
67 | expect(await contract.maxSupply()).to.equal(CollectionConfig.maxSupply);
68 | expect(await contract.maxMintAmountPerTx()).to.equal(CollectionConfig.whitelistSale.maxMintAmountPerTx);
69 | expect(await contract.hiddenMetadataUri()).to.equal(CollectionConfig.hiddenMetadataUri);
70 |
71 | expect(await contract.paused()).to.equal(true);
72 | expect(await contract.whitelistMintEnabled()).to.equal(false);
73 | expect(await contract.revealed()).to.equal(false);
74 |
75 | await expect(contract.tokenURI(1)).to.be.revertedWith('ERC721Metadata: URI query for nonexistent token');
76 | });
77 |
78 | it('Before any sale', async function () {
79 | // Nobody should be able to mint from a paused contract
80 | await expect(contract.connect(whitelistedUser).mint(1, {value: getPrice(SaleType.WHITELIST, 1)})).to.be.revertedWith('The contract is paused!');
81 | await expect(contract.connect(whitelistedUser).whitelistMint(1, [], {value: getPrice(SaleType.WHITELIST, 1)})).to.be.revertedWith('The whitelist sale is not enabled!');
82 | await expect(contract.connect(holder).mint(1, {value: getPrice(SaleType.WHITELIST, 1)})).to.be.revertedWith('The contract is paused!');
83 | await expect(contract.connect(holder).whitelistMint(1, [], {value: getPrice(SaleType.WHITELIST, 1)})).to.be.revertedWith('The whitelist sale is not enabled!');
84 | await expect(contract.connect(owner).mint(1, {value: getPrice(SaleType.WHITELIST, 1)})).to.be.revertedWith('The contract is paused!');
85 | await expect(contract.connect(owner).whitelistMint(1, [], {value: getPrice(SaleType.WHITELIST, 1)})).to.be.revertedWith('The whitelist sale is not enabled!');
86 |
87 | // The owner should always be able to run mintForAddress
88 | await (await contract.mintForAddress(1, await owner.getAddress())).wait();
89 | await (await contract.mintForAddress(1, await whitelistedUser.getAddress())).wait();
90 | // But not over the maxMintAmountPerTx
91 | await expect(contract.mintForAddress(
92 | await (await contract.maxMintAmountPerTx()).add(1),
93 | await holder.getAddress(),
94 | )).to.be.revertedWith('Invalid mint amount!');
95 |
96 | // Check balances
97 | expect(await contract.balanceOf(await owner.getAddress())).to.equal(1);
98 | expect(await contract.balanceOf(await whitelistedUser.getAddress())).to.equal(1);
99 | expect(await contract.balanceOf(await holder.getAddress())).to.equal(0);
100 | expect(await contract.balanceOf(await externalUser.getAddress())).to.equal(0);
101 | });
102 |
103 | it('Whitelist sale', async function () {
104 | // Build MerkleTree
105 | const leafNodes = whitelistAddresses.map(addr => keccak256(addr));
106 | const merkleTree = new MerkleTree(leafNodes, keccak256, { sortPairs: true });
107 | const rootHash = merkleTree.getRoot();
108 | // Update the root hash
109 | await (await contract.setMerkleRoot('0x' + rootHash.toString('hex'))).wait();
110 |
111 | await contract.setWhitelistMintEnabled(true);
112 |
113 | await contract.connect(whitelistedUser).whitelistMint(
114 | 1,
115 | merkleTree.getHexProof(keccak256(await whitelistedUser.getAddress())),
116 | {value: getPrice(SaleType.WHITELIST, 1)},
117 | );
118 | // Trying to mint twice
119 | await expect(contract.connect(whitelistedUser).whitelistMint(
120 | 1,
121 | merkleTree.getHexProof(keccak256(await whitelistedUser.getAddress())),
122 | {value: getPrice(SaleType.WHITELIST, 1)},
123 | )).to.be.revertedWith('Address already claimed!');
124 | // Sending an invalid mint amount
125 | await expect(contract.connect(whitelistedUser).whitelistMint(
126 | await (await contract.maxMintAmountPerTx()).add(1),
127 | merkleTree.getHexProof(keccak256(await whitelistedUser.getAddress())),
128 | {value: getPrice(SaleType.WHITELIST, await (await contract.maxMintAmountPerTx()).add(1).toNumber())},
129 | )).to.be.revertedWith('Invalid mint amount!');
130 | // Sending insufficient funds
131 | await expect(contract.connect(whitelistedUser).whitelistMint(
132 | 1,
133 | merkleTree.getHexProof(keccak256(await whitelistedUser.getAddress())),
134 | {value: getPrice(SaleType.WHITELIST, 1).sub(1)},
135 | )).to.be.rejectedWith(Error, 'insufficient funds for intrinsic transaction cost');
136 | // Pretending to be someone else
137 | await expect(contract.connect(holder).whitelistMint(
138 | 1,
139 | merkleTree.getHexProof(keccak256(await whitelistedUser.getAddress())),
140 | {value: getPrice(SaleType.WHITELIST, 1)},
141 | )).to.be.revertedWith('Invalid proof!');
142 | // Sending an invalid proof
143 | await expect(contract.connect(holder).whitelistMint(
144 | 1,
145 | merkleTree.getHexProof(keccak256(await holder.getAddress())),
146 | {value: getPrice(SaleType.WHITELIST, 1)},
147 | )).to.be.revertedWith('Invalid proof!');
148 | // Sending no proof at all
149 | await expect(contract.connect(holder).whitelistMint(
150 | 1,
151 | [],
152 | {value: getPrice(SaleType.WHITELIST, 1)},
153 | )).to.be.revertedWith('Invalid proof!');
154 |
155 | // Pause whitelist sale
156 | await contract.setWhitelistMintEnabled(false);
157 | await contract.setCost(utils.parseEther(CollectionConfig.preSale.price.toString()));
158 |
159 | // Check balances
160 | expect(await contract.balanceOf(await owner.getAddress())).to.equal(1);
161 | expect(await contract.balanceOf(await whitelistedUser.getAddress())).to.equal(2);
162 | expect(await contract.balanceOf(await holder.getAddress())).to.equal(0);
163 | expect(await contract.balanceOf(await externalUser.getAddress())).to.equal(0);
164 | });
165 |
166 | it('Pre-sale (same as public sale)', async function () {
167 | await contract.setMaxMintAmountPerTx(CollectionConfig.preSale.maxMintAmountPerTx);
168 | await contract.setPaused(false);
169 | await contract.connect(holder).mint(2, {value: getPrice(SaleType.PRE_SALE, 2)});
170 | await contract.connect(whitelistedUser).mint(1, {value: getPrice(SaleType.PRE_SALE, 1)});
171 | // Sending insufficient funds
172 | await expect(contract.connect(holder).mint(1, {value: getPrice(SaleType.PRE_SALE, 1).sub(1)})).to.be.rejectedWith(Error, 'insufficient funds for intrinsic transaction cost');
173 | // Sending an invalid mint amount
174 | await expect(contract.connect(whitelistedUser).mint(
175 | await (await contract.maxMintAmountPerTx()).add(1),
176 | {value: getPrice(SaleType.PRE_SALE, await (await contract.maxMintAmountPerTx()).add(1).toNumber())},
177 | )).to.be.revertedWith('Invalid mint amount!');
178 | // Sending a whitelist mint transaction
179 | await expect(contract.connect(whitelistedUser).whitelistMint(
180 | 1,
181 | [],
182 | {value: getPrice(SaleType.WHITELIST, 1)},
183 | )).to.be.rejectedWith(Error, 'insufficient funds for intrinsic transaction cost');
184 |
185 | // Pause pre-sale
186 | await contract.setPaused(true);
187 | await contract.setCost(utils.parseEther(CollectionConfig.publicSale.price.toString()));
188 | });
189 |
190 | it('Owner only functions', async function () {
191 | await expect(contract.connect(externalUser).mintForAddress(1, await externalUser.getAddress())).to.be.revertedWith('Ownable: caller is not the owner');
192 | await expect(contract.connect(externalUser).setRevealed(false)).to.be.revertedWith('Ownable: caller is not the owner');
193 | await expect(contract.connect(externalUser).setCost(utils.parseEther('0.0000001'))).to.be.revertedWith('Ownable: caller is not the owner');
194 | await expect(contract.connect(externalUser).setMaxMintAmountPerTx(99999)).to.be.revertedWith('Ownable: caller is not the owner');
195 | await expect(contract.connect(externalUser).setHiddenMetadataUri('INVALID_URI')).to.be.revertedWith('Ownable: caller is not the owner');
196 | await expect(contract.connect(externalUser).setUriPrefix('INVALID_PREFIX')).to.be.revertedWith('Ownable: caller is not the owner');
197 | await expect(contract.connect(externalUser).setUriSuffix('INVALID_SUFFIX')).to.be.revertedWith('Ownable: caller is not the owner');
198 | await expect(contract.connect(externalUser).setPaused(false)).to.be.revertedWith('Ownable: caller is not the owner');
199 | await expect(contract.connect(externalUser).setMerkleRoot('0x0000000000000000000000000000000000000000000000000000000000000000')).to.be.revertedWith('Ownable: caller is not the owner');
200 | await expect(contract.connect(externalUser).setWhitelistMintEnabled(false)).to.be.revertedWith('Ownable: caller is not the owner');
201 | await expect(contract.connect(externalUser).withdraw()).to.be.revertedWith('Ownable: caller is not the owner');
202 | });
203 |
204 | it('Wallet of owner', async function () {
205 | expect(await contract.tokensOfOwner(await owner.getAddress())).deep.equal([
206 | BigNumber.from(1),
207 | ]);
208 | expect(await contract.tokensOfOwner(await whitelistedUser.getAddress())).deep.equal([
209 | BigNumber.from(2),
210 | BigNumber.from(3),
211 | BigNumber.from(6),
212 | ]);
213 | expect(await contract.tokensOfOwner(await holder.getAddress())).deep.equal([
214 | BigNumber.from(4),
215 | BigNumber.from(5),
216 | ]);
217 | expect(await contract.tokensOfOwner(await externalUser.getAddress())).deep.equal([]);
218 | });
219 |
220 | it('Supply checks (long)', async function () {
221 | if (process.env.EXTENDED_TESTS === undefined) {
222 | this.skip();
223 | }
224 |
225 | const alreadyMinted = 6;
226 | const maxMintAmountPerTx = 1000;
227 | const iterations = Math.floor((CollectionConfig.maxSupply - alreadyMinted) / maxMintAmountPerTx);
228 | const expectedTotalSupply = iterations * maxMintAmountPerTx + alreadyMinted;
229 | const lastMintAmount = CollectionConfig.maxSupply - expectedTotalSupply;
230 | expect(await contract.totalSupply()).to.equal(alreadyMinted);
231 |
232 | await contract.setPaused(false);
233 | await contract.setMaxMintAmountPerTx(maxMintAmountPerTx);
234 |
235 | await Promise.all([...Array(iterations).keys()].map(async () => await contract.connect(whitelistedUser).mint(maxMintAmountPerTx, {value: getPrice(SaleType.PUBLIC_SALE, maxMintAmountPerTx)})));
236 |
237 | // Try to mint over max supply (before sold-out)
238 | await expect(contract.connect(holder).mint(lastMintAmount + 1, {value: getPrice(SaleType.PUBLIC_SALE, lastMintAmount + 1)})).to.be.revertedWith('Max supply exceeded!');
239 | await expect(contract.connect(holder).mint(lastMintAmount + 2, {value: getPrice(SaleType.PUBLIC_SALE, lastMintAmount + 2)})).to.be.revertedWith('Max supply exceeded!');
240 |
241 | expect(await contract.totalSupply()).to.equal(expectedTotalSupply);
242 |
243 | // Mint last tokens with owner address and test walletOfOwner(...)
244 | await contract.connect(owner).mint(lastMintAmount, {value: getPrice(SaleType.PUBLIC_SALE, lastMintAmount)});
245 | const expectedWalletOfOwner = [
246 | BigNumber.from(1),
247 | ];
248 | for (const i of [...Array(lastMintAmount).keys()].reverse()) {
249 | expectedWalletOfOwner.push(BigNumber.from(CollectionConfig.maxSupply - i));
250 | }
251 | expect(await contract.tokensOfOwner(
252 | await owner.getAddress(),
253 | {
254 | // Set gas limit to the maximum value since this function should be used off-chain only and it would fail otherwise...
255 | gasLimit: BigNumber.from('0xffffffffffffffff'),
256 | },
257 | )).deep.equal(expectedWalletOfOwner);
258 |
259 | // Try to mint over max supply (after sold-out)
260 | await expect(contract.connect(whitelistedUser).mint(1, {value: getPrice(SaleType.PUBLIC_SALE, 1)})).to.be.revertedWith('Max supply exceeded!');
261 |
262 | expect(await contract.totalSupply()).to.equal(CollectionConfig.maxSupply);
263 | });
264 |
265 | it('Token URI generation', async function () {
266 | const uriPrefix = 'ipfs://__COLLECTION_CID__/';
267 | const uriSuffix = '.json';
268 | const totalSupply = await contract.totalSupply();
269 |
270 | expect(await contract.tokenURI(1)).to.equal(CollectionConfig.hiddenMetadataUri);
271 |
272 | // Reveal collection
273 | await contract.setUriPrefix(uriPrefix);
274 | await contract.setRevealed(true);
275 |
276 | // ERC721A uses token IDs starting from 0 internally...
277 | await expect(contract.tokenURI(0)).to.be.revertedWith('ERC721Metadata: URI query for nonexistent token');
278 |
279 | // Testing first and last minted tokens
280 | expect(await contract.tokenURI(1)).to.equal(`${uriPrefix}1${uriSuffix}`);
281 | expect(await contract.tokenURI(totalSupply)).to.equal(`${uriPrefix}${totalSupply}${uriSuffix}`);
282 | });
283 | });
284 |
--------------------------------------------------------------------------------
/smart-contract/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2018",
4 | "module": "commonjs",
5 | "strict": true,
6 | "esModuleInterop": true,
7 | "outDir": "dist",
8 | "declaration": true,
9 | "resolveJsonModule": true
10 | },
11 | "include": ["./scripts", "./test", "./typechain"],
12 | "files": ["./hardhat.config.ts"]
13 | }
14 |
--------------------------------------------------------------------------------
/yarn.lock:
--------------------------------------------------------------------------------
1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
2 | # yarn lockfile v1
3 |
4 |
5 |
--------------------------------------------------------------------------------