├── .github └── workflows │ └── test.yml ├── .gitignore ├── .images └── ink.svg ├── .rustfmt.toml ├── Cargo.toml ├── LICENSE.md ├── README.md ├── README2.md ├── config.json ├── contracts └── shiden34 │ ├── Cargo.toml │ └── lib.rs ├── deploys ├── deployed_info.md ├── metadata.json └── shibuya-test.md ├── logics ├── Cargo.toml ├── impls │ ├── mod.rs │ └── payable_mint │ │ ├── mod.rs │ │ ├── payable_mint.rs │ │ └── types.rs ├── lib.rs └── traits │ ├── mod.rs │ └── payable_mint.rs ├── package.json ├── rust-toolchain.toml ├── tests └── psp34.spec.ts ├── tsconfig.json ├── ui ├── simple-minter │ └── next-env.d.ts └── viewer │ ├── .eslintrc.json │ ├── .gitignore │ ├── LICENSE │ ├── README.md │ ├── components │ ├── Footer.tsx │ ├── Header.tsx │ ├── IndexCanvas.tsx │ └── SampleContractsList.tsx │ ├── metadata │ └── metadata_sample.json │ ├── next.config.js │ ├── package.json │ ├── pages │ ├── _app.tsx │ ├── _document.tsx │ ├── _error.tsx │ └── index.tsx │ ├── postcss.config.js │ ├── public │ ├── favicon.ico │ ├── icon_moon.svg │ ├── icon_sun.svg │ ├── image-placeholder.png │ ├── loading.gif │ ├── loading.svg │ ├── loading_default.svg │ └── vercel.svg │ ├── styles │ ├── Home.module.css │ └── globals.css │ ├── tailwind.config.js │ └── tsconfig.json └── yarn.lock /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: Cache cargo 14 | uses: actions/cache@v2 15 | with: 16 | path: | 17 | ~/.cargo/registry 18 | ~/.cargo/git 19 | target 20 | key: ${{ runner.os }}-cargo 21 | 22 | - name: Set-Up 23 | run: sudo apt install -y git clang curl libssl-dev llvm libudev-dev protobuf-compiler 24 | 25 | - name: Install toolchain 26 | # hacky way to install rust. Rustup is pre-installed on runners. Calling rustup show will detect the rust-toolchain.toml, and subsequently 27 | # download the needed toolchain and components. 28 | run: | 29 | rustup toolchain install stable 30 | rustup show 31 | - name: Check 32 | run: cargo check 33 | 34 | - name: Test 35 | run: cargo test -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/node,macos,visualstudiocode 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=node,macos,visualstudiocode 4 | 5 | ### macOS ### 6 | # General 7 | .DS_Store 8 | .AppleDouble 9 | .LSOverride 10 | 11 | # Icon must end with two \r 12 | Icon 13 | 14 | 15 | # Thumbnails 16 | ._* 17 | 18 | # Files that might appear in the root of a volume 19 | .DocumentRevisions-V100 20 | .fseventsd 21 | .Spotlight-V100 22 | .TemporaryItems 23 | .Trashes 24 | .VolumeIcon.icns 25 | .com.apple.timemachine.donotpresent 26 | 27 | # Directories potentially created on remote AFP share 28 | .AppleDB 29 | .AppleDesktop 30 | Network Trash Folder 31 | Temporary Items 32 | .apdisk 33 | 34 | ### macOS Patch ### 35 | # iCloud generated files 36 | *.icloud 37 | 38 | ### Node ### 39 | # Logs 40 | logs 41 | *.log 42 | npm-debug.log* 43 | yarn-debug.log* 44 | yarn-error.log* 45 | lerna-debug.log* 46 | .pnpm-debug.log* 47 | 48 | # Diagnostic reports (https://nodejs.org/api/report.html) 49 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 50 | 51 | # Runtime data 52 | pids 53 | *.pid 54 | *.seed 55 | *.pid.lock 56 | 57 | # Directory for instrumented libs generated by jscoverage/JSCover 58 | lib-cov 59 | 60 | # Coverage directory used by tools like istanbul 61 | coverage 62 | *.lcov 63 | 64 | # nyc test coverage 65 | .nyc_output 66 | 67 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 68 | .grunt 69 | 70 | # Bower dependency directory (https://bower.io/) 71 | bower_components 72 | 73 | # node-waf configuration 74 | .lock-wscript 75 | 76 | # Compiled binary addons (https://nodejs.org/api/addons.html) 77 | build/Release 78 | 79 | # Dependency directories 80 | node_modules/ 81 | jspm_packages/ 82 | 83 | # Snowpack dependency directory (https://snowpack.dev/) 84 | web_modules/ 85 | 86 | # TypeScript cache 87 | *.tsbuildinfo 88 | 89 | # Optional npm cache directory 90 | .npm 91 | 92 | # Optional eslint cache 93 | .eslintcache 94 | 95 | # Optional stylelint cache 96 | .stylelintcache 97 | 98 | # Microbundle cache 99 | .rpt2_cache/ 100 | .rts2_cache_cjs/ 101 | .rts2_cache_es/ 102 | .rts2_cache_umd/ 103 | 104 | # Optional REPL history 105 | .node_repl_history 106 | 107 | # Output of 'npm pack' 108 | *.tgz 109 | 110 | # Yarn Integrity file 111 | .yarn-integrity 112 | 113 | # dotenv environment variable files 114 | .env 115 | .env.development.local 116 | .env.test.local 117 | .env.production.local 118 | .env.local 119 | 120 | # parcel-bundler cache (https://parceljs.org/) 121 | .cache 122 | .parcel-cache 123 | 124 | # Next.js build output 125 | .next 126 | out 127 | 128 | # Nuxt.js build / generate output 129 | .nuxt 130 | dist 131 | 132 | # Gatsby files 133 | .cache/ 134 | # Comment in the public line in if your project uses Gatsby and not Next.js 135 | # https://nextjs.org/blog/next-9-1#public-directory-support 136 | # public 137 | 138 | # vuepress build output 139 | .vuepress/dist 140 | 141 | # vuepress v2.x temp and cache directory 142 | .temp 143 | 144 | # Docusaurus cache and generated files 145 | .docusaurus 146 | 147 | # Serverless directories 148 | .serverless/ 149 | 150 | # FuseBox cache 151 | .fusebox/ 152 | 153 | # DynamoDB Local files 154 | .dynamodb/ 155 | 156 | # TernJS port file 157 | .tern-port 158 | 159 | # Stores VSCode versions used for testing VSCode extensions 160 | .vscode-test 161 | 162 | # yarn v2 163 | .yarn/cache 164 | .yarn/unplugged 165 | .yarn/build-state.yml 166 | .yarn/install-state.gz 167 | .pnp.* 168 | 169 | ### Node Patch ### 170 | # Serverless Webpack directories 171 | .webpack/ 172 | 173 | # Optional stylelint cache 174 | 175 | # SvelteKit build / generate output 176 | .svelte-kit 177 | 178 | ### VisualStudioCode ### 179 | .vscode/* 180 | !.vscode/settings.json 181 | !.vscode/tasks.json 182 | !.vscode/launch.json 183 | !.vscode/extensions.json 184 | !.vscode/*.code-snippets 185 | 186 | # Local History for Visual Studio Code 187 | .history/ 188 | 189 | # Built Visual Studio Code Extensions 190 | *.vsix 191 | 192 | ### VisualStudioCode Patch ### 193 | # Ignore all local history of files 194 | .history 195 | .ionide 196 | 197 | # Support for Project snippet scope 198 | .vscode/*.code-snippets 199 | 200 | # Ignore code-workspaces 201 | *.code-workspace 202 | 203 | ### Rust ### 204 | # Generated by Cargo 205 | # will have compiled files and executables 206 | debug/ 207 | target/ 208 | 209 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 210 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 211 | Cargo.lock 212 | 213 | # These are backup files generated by rustfmt 214 | **/*.rs.bk 215 | 216 | # MSVC Windows builds of rustc generate these, which store debugging information 217 | *.pdb 218 | 219 | 220 | # End of https://www.toptal.com/developers/gitignore/api/node,macos,visualstudiocode 221 | 222 | *-debug.log 223 | *-error.log 224 | /.nyc_output 225 | /dist 226 | /lib 227 | /package-lock.json 228 | /tmp 229 | node_modules 230 | oclif.manifest.json 231 | artifacts/ 232 | types/ 233 | typed_contracts/ 234 | 235 | -------------------------------------------------------------------------------- /.images/ink.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 100 2 | hard_tabs = false 3 | tab_spaces = 4 4 | newline_style = "Auto" 5 | use_small_heuristics = "Default" 6 | indent_style = "Block" 7 | wrap_comments = false 8 | format_code_in_doc_comments = false 9 | comment_width = 80 10 | normalize_comments = true # changed 11 | normalize_doc_attributes = false 12 | format_strings = false 13 | format_macro_matchers = false 14 | format_macro_bodies = true 15 | empty_item_single_line = true 16 | struct_lit_single_line = true 17 | fn_single_line = false 18 | where_single_line = false 19 | imports_indent = "Block" 20 | imports_layout = "Vertical" # changed 21 | imports_granularity = "Crate" # changed 22 | reorder_imports = true 23 | reorder_modules = true 24 | reorder_impl_items = false 25 | type_punctuation_density = "Wide" 26 | space_before_colon = false 27 | space_after_colon = true 28 | spaces_around_ranges = false 29 | binop_separator = "Front" 30 | remove_nested_parens = true 31 | combine_control_expr = false # changed 32 | overflow_delimited_expr = false 33 | struct_field_align_threshold = 0 34 | enum_discrim_align_threshold = 0 35 | match_arm_blocks = true 36 | force_multiline_blocks = true # changed 37 | fn_params_layout = "Tall" 38 | brace_style = "SameLineWhere" 39 | control_brace_style = "AlwaysSameLine" 40 | trailing_semicolon = false # changed 41 | trailing_comma = "Vertical" 42 | match_block_trailing_comma = false 43 | blank_lines_upper_bound = 1 44 | blank_lines_lower_bound = 0 45 | edition = "2021" # changed 46 | version = "One" 47 | merge_derives = true 48 | use_try_shorthand = true # changed 49 | use_field_init_shorthand = true # changed 50 | force_explicit_abi = true 51 | condense_wildcard_suffixes = false 52 | color = "Auto" 53 | unstable_features = true # changed 54 | disable_all_formatting = false 55 | skip_children = false 56 | hide_parse_errors = false 57 | error_on_line_overflow = false 58 | error_on_unformatted = false 59 | ignore = [] -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "contracts/**", 4 | ] 5 | 6 | exclude = [ 7 | ] 8 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NFT project using PSP34 2 | 3 | [![workflow][a1]][a2] [![stack-exchange][s1]][s2] [![discord][d1]][d2] [![built-with-ink][i1]][i2] [![License][ap1]][ap2] 4 | 5 | [s1]: https://img.shields.io/badge/click-white.svg?logo=StackExchange&label=ink!%20Support%20on%20StackExchange&labelColor=white&color=blue 6 | [s2]: https://substrate.stackexchange.com/questions/tagged/ink?tab=Votes 7 | [a1]: https://github.com/swanky-dapps/nft/actions/workflows/test.yml/badge.svg 8 | [a2]: https://github.com/swanky-dapps/nft/actions/workflows/test.yml 9 | [d1]: https://img.shields.io/discord/722223075629727774?style=flat-square&label=discord 10 | [d2]: https://discord.gg/Z3nC9U4 11 | [i1]: /.images/ink.svg 12 | [i2]: https://github.com/paritytech/ink 13 | [ap1]: https://img.shields.io/badge/License-Apache%202.0-blue.svg 14 | [ap2]: https://opensource.org/licenses/Apache-2.0 15 | 16 | This is an example nft project using ink! smart contract. The project is generated with Openbrush wizard for PSP34 with added PayableMinted trait. 17 | 18 | ### Purpose 19 | This is an unaudited nft project template. 20 | It can be used to speed up wasm nft project on Astar and other networks. 21 | 22 | ### License 23 | Apache 2.0 24 | 25 | ### 🏗️ How to use - Contracts 26 | ##### 💫 Build 27 | - Use this [instructions](https://use.ink/getting-started/setup) to setup your ink!/Rust environment 28 | 29 | ```sh 30 | cargo +nightly contract build 31 | ``` 32 | 33 | ##### 💫 Run unit test 34 | 35 | ```sh 36 | cargo +nightly test 37 | ``` 38 | ##### 💫 Deploy 39 | First start your local node. Recommended [swanky-node](https://github.com/AstarNetwork/swanky-node) v0.13.0 40 | ```sh 41 | ./target/release/swanky-node --dev --tmp -lruntime=trace -lruntime::contracts=debug -lerror 42 | ``` 43 | - or deploy with polkadot.JS. Instructions on [Astar docs](https://docs.astar.network/docs/wasm/sc-dev/polkadotjs-ui) 44 | 45 | ##### 💫 Run integration test 46 | First start your local node. Recommended [swanky-node](https://github.com/AstarNetwork/swanky-node) v0.13.0 47 | And then: 48 | ```sh 49 | yarn 50 | yarn compile 51 | yarn test 52 | ``` 53 | 54 | ##### 💫 Deployed contracts 55 | [Shiden Graffiti](https://github.com/Maar-io/ink-mint-dapp/tree/graffiti) - using ink! v3.4 56 | 57 | #### 📚 Learn 58 | Follow the [From Zero to ink! Hero](https://docs.astar.network/docs/build/wasm/from-zero-to-ink-hero/) tutorial tu learn how to build this smart contract 59 | -------------------------------------------------------------------------------- /README2.md: -------------------------------------------------------------------------------- 1 | # Astar WASM showcase dApps 2 | This repository contains examples of ink! contracts and their respective UIs that can be deployed on Astar network. 3 | If you are looking for unaudited production ready dApps (ink! + UI) this repo is for you. 4 | 5 | #### Contribute to this repository 6 | contributions are welcome: 7 | - If you find an issue or a refactor idea please open an issue 8 | - If you want to add your own example open a Pull Request 9 | 10 | ## dApps 11 | #### Uniswap-V2 - DEX 12 | This folder contains the line by line implementation of [uniswap-v2 core](https://github.com/Uniswap/v2-core) + [uniswap-v2 periphery](https://github.com/Uniswap/v2-periphery) & its tests. It uses latest [ink! 3.4.0](https://github.com/paritytech/ink/tree/v3.4.0) & [Openbrush 2.3.0](https://github.com/Supercolony-net/openbrush-contracts/tree/v2.3.0) 13 | The UI template is in active development and will be public in January 23 14 | 15 | ### Farming 16 | A farming dApp line by line implementation of [ArthSwap master chef](https://github.com/ArthSwap/ArthSwap-MasterChef) adapted from 17 | [sushiswap](https://github.com/sushiswap/sushiswap/blob/archieve/canary/contracts/MasterChefV2.sol) 18 | 19 | ### Fliper + UI 20 | This is an hello world! example with a basic flipper contract and an UI to interact with it. 21 | The UI is a react app and uses polkadotjs to interact with the node 22 | 23 | #### DAO 24 | On Chain governance based on [Governor](https://github.com/OpenZeppelin/openzeppelin-contracts/tree/master/contracts/governance) contracts of OpenZeppelin 25 | 26 | #### Tests 27 | The test folder contains integration tests for the contracts. Tests are made with two different test frameworks, redspot and typechain. 28 | 29 | **Runs the tests** 30 | 1. Run a local node \ 31 | Please use [swanky-node](https://github.com/AstarNetwork/swanky-node/releases) 32 | 2. The integration tests uses typechain. Node version should be >= 16 33 | ```bash 34 | yarn install 35 | yarn compile 36 | yarn test:typechain 37 | ``` 38 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "projectFiles": ["contracts/**/*"], 3 | "typechainGeneratedPath": "types", 4 | "isWorkspace": true, 5 | "workspacePath": "./" 6 | } -------------------------------------------------------------------------------- /contracts/shiden34/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "shiden34" 3 | version = "1.1.0" 4 | authors = ["Astar builder"] 5 | edition = "2021" 6 | 7 | [dependencies] 8 | ink = { version = "~4.2.1", default-features = false} 9 | 10 | scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] } 11 | scale-info = { version = "2.6", default-features = false, features = ["derive"], optional = true } 12 | 13 | openbrush = { tag = "4.0.0-beta", git = "https://github.com/Brushfam/openbrush-contracts", default-features = false, features = ["psp34", "ownable", "reentrancy_guard"] } 14 | payable_mint_pkg = { path = "../../logics", default-features = false } 15 | 16 | [lib] 17 | path = "lib.rs" 18 | 19 | [features] 20 | default = ["std"] 21 | std = [ 22 | "ink/std", 23 | "scale/std", 24 | "scale-info/std", 25 | 26 | "openbrush/std", 27 | "payable_mint_pkg/std", 28 | ] 29 | ink-as-dependency = [] 30 | -------------------------------------------------------------------------------- /contracts/shiden34/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(not(feature = "std"), no_std, no_main)] 2 | 3 | #[openbrush::implementation(PSP34, PSP34Metadata, PSP34Enumerable, Ownable)] 4 | #[openbrush::contract] 5 | pub mod shiden34 { 6 | use ink::codegen::{ 7 | EmitEvent, 8 | Env, 9 | }; 10 | use openbrush::{ 11 | contracts::{ 12 | ownable, 13 | psp34::{ 14 | extensions::{ 15 | enumerable, 16 | metadata, 17 | }, 18 | PSP34Impl, 19 | }, 20 | reentrancy_guard, 21 | }, 22 | traits::Storage, 23 | }; 24 | use payable_mint_pkg::impls::payable_mint::{ 25 | payable_mint::*, 26 | *, 27 | }; 28 | 29 | // Shiden34Contract contract storage 30 | #[ink(storage)] 31 | #[derive(Default, Storage)] 32 | pub struct Shiden34Contract { 33 | #[storage_field] 34 | psp34: psp34::Data, 35 | #[storage_field] 36 | guard: reentrancy_guard::Data, 37 | #[storage_field] 38 | ownable: ownable::Data, 39 | #[storage_field] 40 | metadata: metadata::Data, 41 | #[storage_field] 42 | payable_mint: types::Data, 43 | #[storage_field] 44 | enumerable: enumerable::Data, 45 | } 46 | 47 | /// Event emitted when a token transfer occurs. 48 | #[ink(event)] 49 | pub struct Transfer { 50 | #[ink(topic)] 51 | from: Option, 52 | #[ink(topic)] 53 | to: Option, 54 | #[ink(topic)] 55 | id: Id, 56 | } 57 | 58 | /// Event emitted when a token approve occurs. 59 | #[ink(event)] 60 | pub struct Approval { 61 | #[ink(topic)] 62 | from: AccountId, 63 | #[ink(topic)] 64 | to: AccountId, 65 | #[ink(topic)] 66 | id: Option, 67 | approved: bool, 68 | } 69 | 70 | // Override event emission methods 71 | #[overrider(psp34::Internal)] 72 | fn _emit_transfer_event(&self, from: Option, to: Option, id: Id) { 73 | self.env().emit_event(Transfer { from, to, id }); 74 | } 75 | 76 | #[overrider(psp34::Internal)] 77 | fn _emit_approval_event(&self, from: AccountId, to: AccountId, id: Option, approved: bool) { 78 | self.env().emit_event(Approval { 79 | from, 80 | to, 81 | id, 82 | approved, 83 | }); 84 | } 85 | 86 | impl payable_mint_pkg::impls::payable_mint::payable_mint::Internal for Shiden34Contract {} 87 | impl PayableMintImpl for Shiden34Contract {} 88 | 89 | impl Shiden34Contract { 90 | #[ink(constructor)] 91 | pub fn new( 92 | name: String, 93 | symbol: String, 94 | base_uri: String, 95 | max_supply: u64, 96 | price_per_mint: Balance, 97 | ) -> Self { 98 | let mut instance = Self::default(); 99 | let caller = instance.env().caller(); 100 | ownable::InternalImpl::_init_with_owner(&mut instance, caller); 101 | let collection_id = PSP34Impl::collection_id(&instance); 102 | metadata::InternalImpl::_set_attribute( 103 | &mut instance, 104 | collection_id.clone(), 105 | String::from("name"), 106 | name, 107 | ); 108 | metadata::InternalImpl::_set_attribute( 109 | &mut instance, 110 | collection_id.clone(), 111 | String::from("symbol"), 112 | symbol, 113 | ); 114 | metadata::InternalImpl::_set_attribute( 115 | &mut instance, 116 | collection_id, 117 | String::from("baseUri"), 118 | base_uri, 119 | ); 120 | instance.payable_mint.max_supply = max_supply; 121 | instance.payable_mint.price_per_mint = price_per_mint; 122 | instance.payable_mint.last_token_id = 0; 123 | instance.payable_mint.max_amount = 1; 124 | instance 125 | } 126 | } 127 | 128 | // ------------------- T E S T ----------------------------------------------------- 129 | #[cfg(test)] 130 | mod tests { 131 | use super::*; 132 | use crate::shiden34::PSP34Error::*; 133 | use ink::{ 134 | env::{ 135 | pay_with_call, 136 | test, 137 | }, 138 | prelude::string::String, 139 | }; 140 | use openbrush::contracts::psp34::PSP34Impl; 141 | use payable_mint_pkg::impls::payable_mint::{ 142 | payable_mint::Internal, 143 | types::Shiden34Error, 144 | }; 145 | const PRICE: Balance = 100_000_000_000_000_000; 146 | const BASE_URI: &str = "ipfs://myIpfsUri/"; 147 | const MAX_SUPPLY: u64 = 10; 148 | 149 | #[ink::test] 150 | fn init_works() { 151 | let sh34 = init(); 152 | let collection_id = PSP34Impl::collection_id(&sh34); 153 | assert_eq!( 154 | metadata::PSP34MetadataImpl::get_attribute( 155 | &sh34, 156 | collection_id.clone(), 157 | String::from("name") 158 | ), 159 | Some(String::from("Shiden34")) 160 | ); 161 | assert_eq!( 162 | metadata::PSP34MetadataImpl::get_attribute( 163 | &sh34, 164 | collection_id.clone(), 165 | String::from("symbol") 166 | ), 167 | Some(String::from("SH34")) 168 | ); 169 | assert_eq!( 170 | metadata::PSP34MetadataImpl::get_attribute( 171 | &sh34, 172 | collection_id, 173 | String::from("baseUri") 174 | ), 175 | Some(String::from(BASE_URI)) 176 | ); 177 | assert_eq!(sh34.max_supply(), MAX_SUPPLY); 178 | assert_eq!(sh34.price(), PRICE); 179 | } 180 | 181 | fn init() -> Shiden34Contract { 182 | Shiden34Contract::new( 183 | String::from("Shiden34"), 184 | String::from("SH34"), 185 | String::from(BASE_URI), 186 | MAX_SUPPLY, 187 | PRICE, 188 | ) 189 | } 190 | 191 | #[ink::test] 192 | fn mint_single_works() { 193 | let mut sh34 = init(); 194 | let accounts = default_accounts(); 195 | assert_eq!(Ownable::owner(&sh34).unwrap(), accounts.alice); 196 | set_sender(accounts.bob); 197 | 198 | assert_eq!(PSP34Impl::total_supply(&sh34), 0); 199 | test::set_value_transferred::(PRICE); 200 | assert!(sh34.mint_next().is_ok()); 201 | assert_eq!(PSP34Impl::total_supply(&sh34), 1); 202 | assert_eq!(PSP34Impl::owner_of(&sh34, Id::U64(1)), Some(accounts.bob)); 203 | assert_eq!(PSP34Impl::balance_of(&sh34, accounts.bob), 1); 204 | 205 | assert_eq!( 206 | PSP34EnumerableImpl::owners_token_by_index(&sh34, accounts.bob, 0), 207 | Ok(Id::U64(1)) 208 | ); 209 | assert_eq!(sh34.payable_mint.last_token_id, 1); 210 | assert_eq!(1, ink::env::test::recorded_events().count()); 211 | } 212 | 213 | #[ink::test] 214 | fn mint_multiple_works() { 215 | let mut sh34 = init(); 216 | let accounts = default_accounts(); 217 | set_sender(accounts.alice); 218 | let num_of_mints: u64 = 5; 219 | // Set max limit to 'num_of_mints', fails to mint 'num_of_mints + 1'. Caller is contract owner 220 | assert!(sh34.set_max_mint_amount(num_of_mints).is_ok()); 221 | assert_eq!( 222 | sh34.mint(accounts.bob, num_of_mints + 1), 223 | Err(PSP34Error::Custom( 224 | Shiden34Error::TooManyTokensToMint.as_str() 225 | )) 226 | ); 227 | 228 | assert_eq!(PSP34Impl::total_supply(&sh34), 0); 229 | test::set_value_transferred::( 230 | PRICE * num_of_mints as u128, 231 | ); 232 | assert!(sh34.mint(accounts.bob, num_of_mints).is_ok()); 233 | assert_eq!(PSP34Impl::total_supply(&sh34), num_of_mints as u128); 234 | assert_eq!(PSP34Impl::balance_of(&sh34, accounts.bob), 5); 235 | assert_eq!( 236 | PSP34EnumerableImpl::owners_token_by_index(&sh34, accounts.bob, 0), 237 | Ok(Id::U64(1)) 238 | ); 239 | assert_eq!( 240 | PSP34EnumerableImpl::owners_token_by_index(&sh34, accounts.bob, 1), 241 | Ok(Id::U64(2)) 242 | ); 243 | assert_eq!( 244 | PSP34EnumerableImpl::owners_token_by_index(&sh34, accounts.bob, 2), 245 | Ok(Id::U64(3)) 246 | ); 247 | assert_eq!( 248 | PSP34EnumerableImpl::owners_token_by_index(&sh34, accounts.bob, 3), 249 | Ok(Id::U64(4)) 250 | ); 251 | assert_eq!( 252 | PSP34EnumerableImpl::owners_token_by_index(&sh34, accounts.bob, 4), 253 | Ok(Id::U64(5)) 254 | ); 255 | assert_eq!(5, ink::env::test::recorded_events().count()); 256 | assert_eq!( 257 | PSP34EnumerableImpl::owners_token_by_index(&sh34, accounts.bob, 5), 258 | Err(TokenNotExists) 259 | ); 260 | } 261 | 262 | #[ink::test] 263 | fn mint_above_limit_fails() { 264 | let mut sh34 = init(); 265 | let accounts = default_accounts(); 266 | set_sender(accounts.alice); 267 | let num_of_mints: u64 = MAX_SUPPLY + 1; 268 | 269 | assert_eq!(PSP34Impl::total_supply(&sh34), 0); 270 | test::set_value_transferred::( 271 | PRICE * num_of_mints as u128, 272 | ); 273 | assert!(sh34.set_max_mint_amount(num_of_mints).is_ok()); 274 | assert_eq!( 275 | sh34.mint(accounts.bob, num_of_mints), 276 | Err(PSP34Error::Custom(Shiden34Error::CollectionIsFull.as_str())) 277 | ); 278 | } 279 | 280 | #[ink::test] 281 | fn mint_low_value_fails() { 282 | let mut sh34 = init(); 283 | let accounts = default_accounts(); 284 | set_sender(accounts.bob); 285 | let num_of_mints = 1; 286 | 287 | assert_eq!(PSP34Impl::total_supply(&sh34), 0); 288 | test::set_value_transferred::( 289 | PRICE * num_of_mints as u128 - 1, 290 | ); 291 | assert_eq!( 292 | sh34.mint(accounts.bob, num_of_mints), 293 | Err(PSP34Error::Custom(Shiden34Error::BadMintValue.as_str())) 294 | ); 295 | test::set_value_transferred::( 296 | PRICE * num_of_mints as u128 - 1, 297 | ); 298 | assert_eq!( 299 | sh34.mint_next(), 300 | Err(PSP34Error::Custom(Shiden34Error::BadMintValue.as_str())) 301 | ); 302 | assert_eq!(PSP34Impl::total_supply(&sh34), 0); 303 | } 304 | 305 | #[ink::test] 306 | fn withdrawal_works() { 307 | let mut sh34 = init(); 308 | let accounts = default_accounts(); 309 | set_balance(accounts.bob, PRICE); 310 | set_sender(accounts.bob); 311 | 312 | assert!(pay_with_call!(sh34.mint_next(), PRICE).is_ok()); 313 | let expected_contract_balance = PRICE + sh34.env().minimum_balance(); 314 | assert_eq!(sh34.env().balance(), expected_contract_balance); 315 | 316 | // Bob fails to withdraw 317 | set_sender(accounts.bob); 318 | assert!(sh34.withdraw().is_err()); 319 | assert_eq!(sh34.env().balance(), expected_contract_balance); 320 | 321 | // Alice (contract owner) withdraws. Existential minimum is still set 322 | set_sender(accounts.alice); 323 | assert!(sh34.withdraw().is_ok()); 324 | // assert_eq!(sh34.env().balance(), sh34.env().minimum_balance()); 325 | } 326 | 327 | #[ink::test] 328 | fn token_uri_works() { 329 | let mut sh34 = init(); 330 | let accounts = default_accounts(); 331 | set_sender(accounts.alice); 332 | 333 | test::set_value_transferred::(PRICE); 334 | assert!(sh34.mint_next().is_ok()); 335 | // return error if request is for not yet minted token 336 | assert_eq!(sh34.token_uri(42), Err(TokenNotExists)); 337 | assert_eq!( 338 | sh34.token_uri(1), 339 | Ok(String::from(BASE_URI.to_owned() + "1.json")) 340 | ); 341 | 342 | // return error if request is for not yet minted token 343 | assert_eq!(sh34.token_uri(42), Err(TokenNotExists)); 344 | 345 | // verify token_uri when baseUri is empty 346 | set_sender(accounts.alice); 347 | assert!(sh34.set_base_uri(String::from("")).is_ok()); 348 | assert_eq!( 349 | sh34.token_uri(1), 350 | Ok("".to_owned() + &String::from("1.json")) 351 | ); 352 | } 353 | 354 | #[ink::test] 355 | fn owner_is_set() { 356 | let accounts = default_accounts(); 357 | let sh34 = init(); 358 | assert_eq!(Ownable::owner(&sh34).unwrap(), accounts.alice); 359 | } 360 | 361 | #[ink::test] 362 | fn set_base_uri_works() { 363 | let accounts = default_accounts(); 364 | const NEW_BASE_URI: &str = "new_uri/"; 365 | let mut sh34 = init(); 366 | 367 | set_sender(accounts.alice); 368 | let collection_id = PSP34Impl::collection_id(&sh34); 369 | assert!(sh34.set_base_uri(NEW_BASE_URI.into()).is_ok()); 370 | assert_eq!( 371 | PSP34MetadataImpl::get_attribute(&sh34, collection_id, String::from("baseUri")), 372 | Some(String::from(NEW_BASE_URI)) 373 | ); 374 | set_sender(accounts.bob); 375 | assert_eq!( 376 | sh34.set_base_uri(NEW_BASE_URI.into()), 377 | Err(PSP34Error::Custom(String::from("O::CallerIsNotOwner"))) 378 | ); 379 | } 380 | 381 | #[ink::test] 382 | fn check_supply_overflow_ok() { 383 | let max_supply = u64::MAX - 1; 384 | let mut sh34 = Shiden34Contract::new( 385 | String::from("Shiden34"), 386 | String::from("SH34"), 387 | String::from(BASE_URI), 388 | max_supply, 389 | PRICE, 390 | ); 391 | sh34.payable_mint.last_token_id = max_supply - 1; 392 | 393 | // check case when last_token_id.add(mint_amount) if more than u64::MAX 394 | assert!(sh34.set_max_mint_amount(u64::MAX).is_ok()); 395 | assert_eq!( 396 | sh34.check_amount(3), 397 | Err(PSP34Error::Custom(Shiden34Error::CollectionIsFull.as_str())) 398 | ); 399 | 400 | // check case when mint_amount is 0 401 | assert_eq!( 402 | sh34.check_amount(0), 403 | Err(PSP34Error::Custom( 404 | Shiden34Error::CannotMintZeroTokens.as_str() 405 | )) 406 | ); 407 | } 408 | 409 | #[ink::test] 410 | fn check_value_overflow_ok() { 411 | let max_supply = u64::MAX; 412 | let price = u128::MAX as u128; 413 | let sh34 = Shiden34Contract::new( 414 | String::from("Shiden34"), 415 | String::from("SH34"), 416 | String::from(BASE_URI), 417 | max_supply, 418 | price, 419 | ); 420 | let transferred_value = u128::MAX; 421 | let mint_amount = u64::MAX; 422 | assert_eq!( 423 | sh34.check_value(transferred_value, mint_amount), 424 | Err(PSP34Error::Custom(Shiden34Error::BadMintValue.as_str())) 425 | ); 426 | } 427 | 428 | fn default_accounts() -> test::DefaultAccounts { 429 | test::default_accounts::() 430 | } 431 | 432 | fn set_sender(sender: AccountId) { 433 | ink::env::test::set_caller::(sender); 434 | } 435 | 436 | fn set_balance(account_id: AccountId, balance: Balance) { 437 | ink::env::test::set_account_balance::(account_id, balance) 438 | } 439 | } 440 | } 441 | -------------------------------------------------------------------------------- /deploys/deployed_info.md: -------------------------------------------------------------------------------- 1 | # Test deploy on Shiden 2 | ### Contract address 3 | * b8QeEr72aJQKfaXcEDGwB5iwqGuF1b9oysHtj3nwMsdmgHa 4 | 5 | ### Network 6 | * Shiden 7 | 8 | ### Collection name 9 | * Test34 10 | 11 | ### baseUri 12 | * ipfs://bafybeicfssxspb7dqomv745prumj35ntee3pafx7ybe4uqmos3htnxf7km/ 13 | 14 | ### tokenUri(1) 15 | * ipfs://bafybeicfssxspb7dqomv745prumj35ntee3pafx7ybe4uqmos3htnxf7km/1.json 16 | 17 | ### images 18 | * ipfs://bafybeifpboqpyqyxnz7vu2twnwbp3nkj44icaabnmlhrngzjsptybjzu2y/1.png 19 | 20 | ### Images Gateway url 21 | * [nftstorage pin ](https://nftstorage.link/ipfs/bafybeifpboqpyqyxnz7vu2twnwbp3nkj44icaabnmlhrngzjsptybjzu2y) 22 | -------------------------------------------------------------------------------- /deploys/shibuya-test.md: -------------------------------------------------------------------------------- 1 | ### Test 1 2 | * Images ipfs://bafybeigin3park37zwd7gqi6gmo5okmxi5rq2igblla6wgxgzvbihgofuu 3 | * Metadata ipfs://bafybeic4jk7ohax2ge4ws6vl5jwyxenlaejkf5vktbvaam2sra72bdfx6i 4 | * Contract WyQUZSwGbAkdZZbvJXKijZz45qrk83sALoUk3JELn492cfK 5 | 6 | ### Test 2 7 | * Images ipfs://bafybeihdwa6zff57jmjtsm3b7g6xsk3g4l43vwucbaq4gmbgjl6g3s23da 8 | * Metadata ipfs://bafybeiec35yniqljvb2q7n4cscfohlahbc54tfoa2njl5yuwzidsgsi7qi 9 | * Contract Vytst6Z2YidTL7WGYQzL9wiXqN7Xqc53zLvisctpMDwxyiY 10 | 11 | ### Test 3 12 | * Images ipfs://bafybeicgjnzd7zp2xm5ysvr3rzj5lrczodhgjkbikwf5hr3nrrp564tsue 13 | * Metadata ipfs://bafybeihxjezj43u4is3mdo5umczwszx55xpqw2orqjjftvmlxnfqcuwlmi 14 | * Contract XACLLddBjgcRC8dSRsgcXy1KCKn2xzEfb6PUisPTAY3BH5f 15 | 16 | ### Test 4 17 | * Images ipfs://bafybeiav373f4mwmdhasvnytzwciucypmaz6kvrun62vfnubnbsmqjwfiu 18 | * Metadata ipfs://bafybeibsi2aw3qsgr2pfpzmjnbwxt3xy7layemd3ie6elf5wjj2ehof4ge 19 | * Contract YbRLSapvBHeiCPyYFnp3nPHAPpYqCk5GQSYRkLV15Na74Sw 20 | 21 | ### Test 5 22 | * Images ipfs://bafybeidivmz2htxjdgazk3vuxrirmbteba2u7mnbf66chtc5cc6e7yboee 23 | * Metadata ipfs://bafybeidewz6ck3zbwj326vg6pdygwvxjzca7isudwyoheuv5ro7wwtntpy 24 | * Contract bUrcwSb9aj4qMcX6F8p39tzRp84LeCCmJC7Egp3JpSCSsEC -------------------------------------------------------------------------------- /logics/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "payable_mint_pkg" 3 | version = "0.4.0" 4 | authors = ["Astar builder"] 5 | edition = "2021" 6 | 7 | [dependencies] 8 | ink = { version = "~4.2.1", default-features = false} 9 | 10 | scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] } 11 | scale-info = { version = "2.6", default-features = false, features = ["derive"], optional = true } 12 | 13 | openbrush = { tag = "4.0.0-beta", git = "https://github.com/Brushfam/openbrush-contracts", default-features = false, features = ["psp34", "ownable", "reentrancy_guard"] } 14 | 15 | [lib] 16 | path = "lib.rs" 17 | 18 | [features] 19 | default = ["std"] 20 | std = [ 21 | "ink/std", 22 | "scale/std", 23 | "scale-info", 24 | "openbrush/std", 25 | ] 26 | -------------------------------------------------------------------------------- /logics/impls/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod payable_mint; 2 | -------------------------------------------------------------------------------- /logics/impls/payable_mint/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod payable_mint; 2 | pub mod types; 3 | -------------------------------------------------------------------------------- /logics/impls/payable_mint/payable_mint.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 Astar Network 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining 4 | // a copy of this software and associated documentation files (the"Software"), 5 | // to deal in the Software without restriction, including 6 | // without limitation the rights to use, copy, modify, merge, publish, 7 | // distribute, sublicense, and/or sell copies of the Software, and to 8 | // permit persons to whom the Software is furnished to do so, subject to 9 | // the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be 12 | // included in all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | use ink::prelude::string::{ 23 | String, 24 | ToString, 25 | }; 26 | 27 | use crate::impls::payable_mint::types::{ 28 | Data, 29 | Shiden34Error, 30 | }; 31 | use openbrush::{ 32 | modifiers, 33 | traits::{ 34 | AccountId, 35 | Balance, 36 | Storage, 37 | }, 38 | }; 39 | 40 | use openbrush::contracts::{ 41 | ownable, 42 | ownable::only_owner, 43 | psp34, 44 | psp34::{ 45 | extensions::{ 46 | metadata, 47 | metadata::{ 48 | Id, 49 | PSP34MetadataImpl, 50 | }, 51 | }, 52 | PSP34Error, 53 | PSP34Impl, 54 | }, 55 | reentrancy_guard, 56 | reentrancy_guard::non_reentrant, 57 | }; 58 | 59 | #[openbrush::trait_definition] 60 | pub trait PayableMintImpl: 61 | Storage 62 | + Storage 63 | + Storage 64 | + Storage 65 | + Storage 66 | + PSP34Impl 67 | + PSP34MetadataImpl 68 | + psp34::extensions::metadata::Internal 69 | + Internal 70 | { 71 | /// Mint one or more tokens 72 | #[ink(message, payable)] 73 | #[modifiers(non_reentrant)] 74 | fn mint(&mut self, to: AccountId, mint_amount: u64) -> Result<(), PSP34Error> { 75 | self.check_amount(mint_amount)?; 76 | self.check_value(Self::env().transferred_value(), mint_amount)?; 77 | 78 | let next_to_mint = self.data::().last_token_id + 1; // first mint id is 1 79 | let mint_offset = next_to_mint + mint_amount; 80 | 81 | for mint_id in next_to_mint..mint_offset { 82 | self._mint_to(to, Id::U64(mint_id))?; 83 | self.data::().last_token_id += 1; 84 | } 85 | 86 | Ok(()) 87 | } 88 | 89 | /// Mint next available token for the caller 90 | #[ink(message, payable)] 91 | fn mint_next(&mut self) -> Result<(), PSP34Error> { 92 | self.check_value(Self::env().transferred_value(), 1)?; 93 | let caller = Self::env().caller(); 94 | let token_id = self 95 | .data::() 96 | .last_token_id 97 | .checked_add(1) 98 | .ok_or(PSP34Error::Custom(Shiden34Error::CollectionIsFull.as_str()))?; 99 | self._mint_to(caller, Id::U64(token_id))?; 100 | self.data::().last_token_id += 1; 101 | 102 | Ok(()) 103 | } 104 | 105 | /// Set new value for the baseUri 106 | #[ink(message)] 107 | #[modifiers(only_owner)] 108 | fn set_base_uri(&mut self, uri: String) -> Result<(), PSP34Error> { 109 | let id = PSP34Impl::collection_id(self); 110 | metadata::Internal::_set_attribute(self, id, String::from("baseUri"), uri); 111 | 112 | Ok(()) 113 | } 114 | 115 | /// Withdraws funds to contract owner 116 | #[ink(message)] 117 | #[modifiers(only_owner)] 118 | fn withdraw(&mut self) -> Result<(), PSP34Error> { 119 | let balance = Self::env().balance(); 120 | let current_balance = balance 121 | .checked_sub(Self::env().minimum_balance()) 122 | .unwrap_or_default(); 123 | let owner = self.data::().owner.get().unwrap().unwrap(); 124 | Self::env() 125 | .transfer(owner, current_balance) 126 | .map_err(|_| PSP34Error::Custom(Shiden34Error::WithdrawalFailed.as_str()))?; 127 | Ok(()) 128 | } 129 | 130 | /// Set max number of tokens which could be minted per call 131 | #[ink(message)] 132 | #[modifiers(only_owner)] 133 | fn set_max_mint_amount(&mut self, max_amount: u64) -> Result<(), PSP34Error> { 134 | self.data::().max_amount = max_amount; 135 | 136 | Ok(()) 137 | } 138 | 139 | /// Get URI from token ID 140 | #[ink(message)] 141 | fn token_uri(&self, token_id: u64) -> Result { 142 | self.token_exists(Id::U64(token_id))?; 143 | let base_uri = PSP34MetadataImpl::get_attribute( 144 | self, 145 | PSP34Impl::collection_id(self), 146 | String::from("baseUri"), 147 | ); 148 | let token_uri = base_uri.unwrap() + &token_id.to_string() + &String::from(".json"); 149 | Ok(token_uri) 150 | } 151 | 152 | /// Get max supply of tokens 153 | #[ink(message)] 154 | fn max_supply(&self) -> u64 { 155 | self.data::().max_supply 156 | } 157 | 158 | /// Get token price 159 | #[ink(message)] 160 | fn price(&self) -> Balance { 161 | self.data::().price_per_mint 162 | } 163 | 164 | /// Get max number of tokens which could be minted per call 165 | #[ink(message)] 166 | fn get_max_mint_amount(&mut self) -> u64 { 167 | self.data::().max_amount 168 | } 169 | } 170 | 171 | /// Helper trait for PayableMint 172 | pub trait Internal: Storage + psp34::Internal { 173 | /// Check if the transferred mint values is as expected 174 | fn check_value(&self, transferred_value: u128, mint_amount: u64) -> Result<(), PSP34Error> { 175 | if let Some(value) = (mint_amount as u128).checked_mul(self.data::().price_per_mint) { 176 | if transferred_value == value { 177 | return Ok(()) 178 | } 179 | } 180 | Err(PSP34Error::Custom(Shiden34Error::BadMintValue.as_str())) 181 | } 182 | 183 | /// Check amount of tokens to be minted 184 | fn check_amount(&self, mint_amount: u64) -> Result<(), PSP34Error> { 185 | if mint_amount == 0 { 186 | return Err(PSP34Error::Custom( 187 | Shiden34Error::CannotMintZeroTokens.as_str(), 188 | )) 189 | } 190 | if mint_amount > self.data::().max_amount { 191 | return Err(PSP34Error::Custom( 192 | Shiden34Error::TooManyTokensToMint.as_str(), 193 | )) 194 | } 195 | if let Some(amount) = self.data::().last_token_id.checked_add(mint_amount) { 196 | if amount <= self.data::().max_supply { 197 | return Ok(()) 198 | } 199 | } 200 | 201 | Err(PSP34Error::Custom(Shiden34Error::CollectionIsFull.as_str())) 202 | } 203 | 204 | /// Check if token is minted 205 | fn token_exists(&self, id: Id) -> Result<(), PSP34Error> { 206 | self._owner_of(&id).ok_or(PSP34Error::TokenNotExists)?; 207 | Ok(()) 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /logics/impls/payable_mint/types.rs: -------------------------------------------------------------------------------- 1 | use openbrush::traits::{ 2 | Balance, 3 | String, 4 | }; 5 | 6 | #[derive(Default, Debug)] 7 | #[openbrush::storage_item] 8 | pub struct Data { 9 | pub last_token_id: u64, 10 | pub collection_id: u32, 11 | pub max_supply: u64, 12 | pub price_per_mint: Balance, 13 | pub max_amount: u64, 14 | } 15 | 16 | #[derive(Debug, PartialEq, Eq, scale::Encode, scale::Decode)] 17 | #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] 18 | pub enum Shiden34Error { 19 | BadMintValue, 20 | CannotMintZeroTokens, 21 | CollectionIsFull, 22 | TooManyTokensToMint, 23 | WithdrawalFailed, 24 | } 25 | 26 | impl Shiden34Error { 27 | pub fn as_str(&self) -> String { 28 | match self { 29 | Shiden34Error::BadMintValue => String::from("BadMintValue"), 30 | Shiden34Error::CannotMintZeroTokens => String::from("CannotMintZeroTokens"), 31 | Shiden34Error::CollectionIsFull => String::from("CollectionIsFull"), 32 | Shiden34Error::TooManyTokensToMint => String::from("TooManyTokensToMint"), 33 | Shiden34Error::WithdrawalFailed => String::from("WithdrawalFailed"), 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /logics/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(not(feature = "std"), no_std)] 2 | 3 | pub mod impls; 4 | pub mod traits; 5 | -------------------------------------------------------------------------------- /logics/traits/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod payable_mint; 2 | -------------------------------------------------------------------------------- /logics/traits/payable_mint.rs: -------------------------------------------------------------------------------- 1 | use ink::prelude::string::String; 2 | 3 | use openbrush::{ 4 | contracts::psp34::PSP34Error, 5 | traits::{ 6 | AccountId, 7 | Balance, 8 | }, 9 | }; 10 | 11 | #[openbrush::wrapper] 12 | pub type PayableMintRef = dyn PayableMint; 13 | 14 | #[openbrush::trait_definition] 15 | pub trait PayableMint { 16 | /// Mint one or more tokens 17 | #[ink(message, payable)] 18 | fn mint(&mut self, to: AccountId, mint_amount: u64) -> Result<(), PSP34Error>; 19 | 20 | /// Mint next available token for the caller 21 | #[ink(message, payable)] 22 | fn mint_next(&mut self) -> Result<(), PSP34Error>; 23 | 24 | /// Set new value for the baseUri 25 | #[ink(message)] 26 | fn set_base_uri(&mut self, uri: String) -> Result<(), PSP34Error>; 27 | 28 | /// Withdraws funds to contract owner 29 | 30 | fn withdraw(&mut self) -> Result<(), PSP34Error>; 31 | 32 | /// Set max number of tokens which could be minted per call 33 | #[ink(message)] 34 | fn set_max_mint_amount(&mut self, max_amount: u64) -> Result<(), PSP34Error>; 35 | 36 | /// Get URI from token ID 37 | #[ink(message)] 38 | fn token_uri(&self, token_id: u64) -> Result; 39 | 40 | /// Get max supply of tokens 41 | #[ink(message)] 42 | fn max_supply(&self) -> u64; 43 | 44 | /// Get token price 45 | #[ink(message)] 46 | fn price(&self) -> Balance; 47 | 48 | /// Get max number of tokens which could be minted per call 49 | #[ink(message)] 50 | fn get_max_mint_amount(&mut self) -> u64; 51 | } 52 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shiden34", 3 | "version": "0.2.0", 4 | "private": true, 5 | "dependencies": { 6 | "@727-ventures/typechain-types": "^1.1.0", 7 | "@types/chai": "^4.3.0", 8 | "@types/chai-as-promised": "^7.1.5", 9 | "@types/mocha": "^8.0.3", 10 | "@typescript-eslint/eslint-plugin": "^4.8.2", 11 | "@typescript-eslint/parser": "^4.8.2", 12 | "chai": "^4.3.6", 13 | "chai-as-promised": "^7.1.1", 14 | "eslint": "^7.26.0", 15 | "eslint-plugin-import": "^2.22.1", 16 | "eslint-plugin-node": "^11.1.0", 17 | "eslint-plugin-promise": "^5.1.0", 18 | "mocha": "10.1.0", 19 | "patch-package": "^6.4.7", 20 | "ts-node": "^10.8.0" 21 | }, 22 | "scripts": { 23 | "compile": "cargo contract build --manifest-path contracts/shiden34/Cargo.toml && typechain-polkadot --in ./target/ink/shiden34 --out ./typed_contracts", 24 | "compile:release": "cargo contract build --manifest-path contracts/shiden34/Cargo.toml --release && typechain-polkadot --in ./target/ink/shiden34 --out ./typed_contracts", 25 | "test": "mocha --require ts-node/register --recursive ./tests --extension \".spec.ts\" --exit --timeout 20000", 26 | "test:single": "mocha --require ts-node/register --extension \".ts\" --exit --timeout 20000", 27 | "postinstall": "patch-package" 28 | }, 29 | "resolutions": { 30 | "@polkadot/api": "^9.6.1", 31 | "@polkadot/api-contract": "^9.6.1" 32 | }, 33 | "devDependencies": { 34 | "@727-ventures/typechain-compiler": "^1.1.1", 35 | "@727-ventures/typechain-polkadot": "^1.1.0" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.69" 3 | components = [ "rustfmt", "clippy" ] 4 | targets = [ "wasm32-unknown-unknown"] 5 | profile = "minimal" -------------------------------------------------------------------------------- /tests/psp34.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, use } from 'chai'; 2 | import chaiAsPromised from 'chai-as-promised'; 3 | import { encodeAddress } from '@polkadot/keyring'; 4 | import BN from 'bn.js'; 5 | import Shiden_factory from '../typed_contracts/constructors/shiden34'; 6 | import Shiden from '../typed_contracts/contracts/shiden34'; 7 | 8 | import { ApiPromise, WsProvider, Keyring } from '@polkadot/api'; 9 | import type { WeightV2, Weight } from '@polkadot/types/interfaces'; 10 | import { KeyringPair } from '@polkadot/keyring/types'; 11 | import { ReturnNumber } from '@727-ventures/typechain-types'; 12 | 13 | use(chaiAsPromised); 14 | 15 | const MAX_SUPPLY = 888; 16 | const BASE_URI = "ipfs://tokenUriPrefix/"; 17 | const COLLECTION_METADATA = "ipfs://collectionMetadata/data.json"; 18 | const TOKEN_URI_1 = "ipfs://tokenUriPrefix/1.json"; 19 | const TOKEN_URI_5 = "ipfs://tokenUriPrefix/5.json"; 20 | const ONE = new BN(10).pow(new BN(18)); 21 | const PRICE_PER_MINT = ONE; 22 | 23 | // Create a new instance of contract 24 | const wsProvider = new WsProvider('ws://127.0.0.1:9944'); 25 | // Create a keyring instance 26 | const keyring = new Keyring({ type: 'sr25519' }); 27 | 28 | describe('Minting psp34 tokens', () => { 29 | let shidenFactory: Shiden_factory; 30 | let api: ApiPromise; 31 | let deployer: KeyringPair; 32 | let bob: KeyringPair; 33 | let contract: Shiden; 34 | 35 | const gasLimit = 18750000000; 36 | const ZERO_ADDRESS = encodeAddress( 37 | '0x0000000000000000000000000000000000000000000000000000000000000000', 38 | ); 39 | let gasRequired: bigint; 40 | 41 | async function setup(): Promise { 42 | api = await ApiPromise.create({ provider: wsProvider }); 43 | deployer = keyring.addFromUri('//Alice'); 44 | bob = keyring.addFromUri('//Bob'); 45 | shidenFactory = new Shiden_factory(api, deployer); 46 | contract = new Shiden((await shidenFactory.new( 47 | 'Shiden34', 48 | 'SH34', 49 | BASE_URI, 50 | MAX_SUPPLY, 51 | PRICE_PER_MINT, 52 | )).address, deployer, api); 53 | } 54 | 55 | it('Create collection works', async () => { 56 | await setup(); 57 | const queryList = await contract.query; 58 | expect((await contract.query.totalSupply()).value.unwrap().toNumber()).to.equal(0); 59 | expect((await contract.query.owner()).value.unwrap()).to.equal(deployer.address); 60 | expect((await contract.query.maxSupply()).value.unwrap()).to.equal(MAX_SUPPLY); 61 | expect((await contract.query.price()).value.unwrap().toString()).to.equal(PRICE_PER_MINT.toString()); 62 | const collectionId = (await contract.query.collectionId()); 63 | 64 | // expect((await contract.query.getAttribute({u128: collectionId}, ["baseUri"])).value).to.equal(BASE_URI); 65 | // expect((await contract.query.getAttribute(collectionId, ["baseUri"])).value).to.equal(BASE_URI); 66 | }) 67 | 68 | it('Use mintNext works', async () => { 69 | await setup(); 70 | const tokenId = 1; 71 | 72 | expect((await contract.query.totalSupply()).value.unwrap().toNumber()).to.equal(0); 73 | 74 | // mint 75 | const { gasRequired } = await contract.withSigner(bob).query.mintNext(); 76 | let mintResult = await contract.withSigner(bob).tx.mintNext({ value: PRICE_PER_MINT, gasLimit: getEstimatedGas(gasRequired) }); 77 | 78 | // verify minting results. The totalSupply value is BN 79 | expect((await contract.query.totalSupply()).value.unwrap().toNumber()).to.equal(1); 80 | expect((await contract.query.balanceOf(bob.address)).value.unwrap()).to.equal(1); 81 | expect((await contract.query.ownerOf({ u64: tokenId })).value.unwrap()).to.equal(bob.address); 82 | emit(mintResult, 'Transfer', { from: null, to: bob.address, id: { u64: tokenId }, }); 83 | 84 | // TODO verify tokenUri call 85 | // console.log("tokenUri", (await contract.query.tokenUri(1)).value); 86 | // expect((await contract.query.tokenUri(1))).to.equal(TOKEN_URI_1); 87 | }) 88 | 89 | it('Mint 5 tokens works', async () => { 90 | await setup(); 91 | 92 | expect((await contract.query.totalSupply()).value.unwrap().toNumber()).to.equal(0); 93 | 94 | const gasRequiredMaxAmount = (await contract.withSigner(bob).query.setMaxMintAmount(5)).gasRequired; 95 | await contract.withSigner(deployer).tx.setMaxMintAmount(5, { gasLimit: getEstimatedGas(gasRequiredMaxAmount) }); 96 | 97 | const { gasRequired } = await contract.withSigner(bob).query.mint(bob.address, 5); 98 | await contract.withSigner(bob).tx.mint(bob.address, 5, { value: PRICE_PER_MINT.muln(5), gasLimit: getEstimatedGas(gasRequired)}); 99 | 100 | expect((await contract.query.totalSupply()).value.unwrap().toNumber()).to.equal(5); 101 | expect((await contract.query.ownerOf({ u64: 5 })).value.unwrap()).to.equal(bob.address); 102 | }) 103 | 104 | it('Token transfer works', async () => { 105 | await setup(); 106 | 107 | // Bob mints 108 | let { gasRequired } = await contract.withSigner(bob).query.mintNext(); 109 | let mintResult = await contract.withSigner(bob).tx.mintNext({ value: PRICE_PER_MINT, gasLimit: getEstimatedGas(gasRequired) }); 110 | emit(mintResult, 'Transfer', { from: null, to: bob.address, id: { u64: 1 }, }); 111 | 112 | // Bob transfers token to Deployer 113 | const transferGas = (await contract.withSigner(bob).query.transfer(deployer.address, { u64: 1 }, [])).gasRequired; 114 | let transferResult = await contract.withSigner(bob).tx.transfer(deployer.address, { u64: 1 }, [], { gasLimit: getEstimatedGas(transferGas) }); 115 | 116 | // Verify transfer 117 | expect((await contract.query.ownerOf({ u64: 1 })).value.unwrap()).to.equal(deployer.address); 118 | expect((await contract.query.balanceOf(bob.address)).value.ok).to.equal(0); 119 | emit(transferResult, 'Transfer', { from: bob.address, to: deployer.address, id: { u64: 1 }, }); 120 | }) 121 | 122 | it('Token approval works', async () => { 123 | await setup(); 124 | 125 | // Bob mints 126 | let { gasRequired } = await contract.withSigner(bob).query.mintNext(); 127 | await contract.withSigner(bob).tx.mintNext({ value: PRICE_PER_MINT, gasLimit: getEstimatedGas(gasRequired) }); 128 | 129 | // Bob approves deployer to be operator of the token 130 | const approveGas = (await contract.withSigner(bob).query.approve(deployer.address, { u64: 1 }, true)).gasRequired; 131 | let approveResult = await contract.withSigner(bob).tx.approve(deployer.address, { u64: 1 }, true, { gasLimit: getEstimatedGas(approveGas) }); 132 | 133 | // Verify that Bob is still the owner and allowance is set 134 | expect((await contract.query.ownerOf({ u64: 1 })).value.unwrap()).to.equal(bob.address); 135 | expect((await contract.query.allowance(bob.address, deployer.address, { u64: 1 })).value.unwrap()).to.equal(true); 136 | emit(approveResult, 'Approval', { from: bob.address, to: deployer.address, id: { u64: 1 }, approved: true, }); 137 | }) 138 | 139 | it('Minting token without funds should fail', async () => { 140 | await setup(); 141 | 142 | // Bob tries to mint without funding 143 | let mintResult = await contract.withSigner(bob).query.mintNext(); 144 | expect(mintResult.value?.unwrap().err.custom).to.be.equal('BadMintValue'); 145 | }) 146 | 147 | function getEstimatedGas(gasRequired: Weight): WeightV2 { 148 | // For some reason Typechain returns wrong type Weigh, although under the hood 149 | // WeightV2 structure is stored 150 | const gasRequiredV2 = gasRequired as unknown as WeightV2; 151 | return api.registry.createType( 152 | 'WeightV2', 153 | { 154 | refTime: gasRequiredV2.refTime.toBn().muln(4), 155 | proofSize: gasRequiredV2.proofSize.toBn().muln(4), 156 | } 157 | ) as WeightV2; 158 | } 159 | }) 160 | 161 | // Helper function to parse Events 162 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 163 | function emit(result: { events?: any }, name: string, args: any): void { 164 | const event = result.events.find( 165 | (event: { name: string }) => event.name === name, 166 | ); 167 | for (const key of Object.keys(event.args)) { 168 | if (event.args[key] instanceof ReturnNumber) { 169 | event.args[key] = event.args[key].toNumber(); 170 | } 171 | } 172 | expect(event).eql({ name, args, }); 173 | } 174 | 175 | // Helper function to convert error code to string 176 | function hex2a(psp34CustomError: any): string { 177 | var hex = psp34CustomError.toString(); //force conversion 178 | var str = ''; 179 | for (var i = 0; i < hex.length; i += 2) 180 | str += String.fromCharCode(parseInt(hex.substr(i, 2), 16)); 181 | return str.substring(1); 182 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "ES2020", 5 | "outDir": "build", 6 | "baseUrl": "./types", 7 | "moduleResolution": "node", 8 | "importHelpers": true, 9 | "skipLibCheck": true, 10 | "removeComments": true, 11 | "preserveConstEnums": true, 12 | "sourceMap": true, 13 | "esModuleInterop": true, 14 | "resolveJsonModule": true, 15 | "forceConsistentCasingInFileNames": true 16 | }, 17 | "include": ["tests/**/*.ts"], 18 | "exclude": ["node_modules/"] 19 | } 20 | -------------------------------------------------------------------------------- /ui/simple-minter/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 | -------------------------------------------------------------------------------- /ui/viewer/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /ui/viewer/.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 | 14 | # production 15 | /build 16 | 17 | # misc 18 | .DS_Store 19 | *.pem 20 | 21 | # debug 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | .pnpm-debug.log* 26 | 27 | # local env files 28 | .env*.local 29 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | -------------------------------------------------------------------------------- /ui/viewer/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Cielo Chiara 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /ui/viewer/README.md: -------------------------------------------------------------------------------- 1 | ## PSP34 Viewer Sample 2 | 3 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 4 | 5 | ## Getting Started 6 | 7 | First, run the development server: 8 | 9 | ```bash 10 | npm install 11 | npm run dev 12 | # or 13 | yarn install 14 | yarn dev 15 | yarn build 16 | ``` 17 | 18 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 19 | 20 | You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file. 21 | 22 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`. 23 | 24 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. 25 | 26 | ## Learn More 27 | 28 | To learn more about Next.js, take a look at the following resources: 29 | 30 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 31 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 32 | 33 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 34 | 35 | ## Deploy on Vercel 36 | 37 | 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. 38 | 39 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 40 | -------------------------------------------------------------------------------- /ui/viewer/components/Footer.tsx: -------------------------------------------------------------------------------- 1 | 2 | const Footer = (): JSX.Element => { 3 | 4 | return ( 5 |
6 | PSP34 Viewer Sample 7 |
8 | ); 9 | }; 10 | 11 | export default Footer; -------------------------------------------------------------------------------- /ui/viewer/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import { useEffect, useState } from 'react'; 3 | import { useTheme } from 'next-themes'; 4 | 5 | const Header = (): JSX.Element => { 6 | 7 | const { theme, setTheme } = useTheme(); 8 | 9 | const [mounted, setMounted] = useState(false); 10 | useEffect(() => setMounted(true), []); 11 | 12 | return ( 13 |
14 |
15 |
16 |

17 | 18 | PSP34 Viewer Sample 19 | 20 |

21 |
22 | 23 |
24 | 39 |
40 | 41 |
42 |
43 | ); 44 | }; 45 | 46 | export default Header; -------------------------------------------------------------------------------- /ui/viewer/components/IndexCanvas.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { ApiPromise, WsProvider } from '@polkadot/api'; 3 | import { ContractPromise } from '@polkadot/api-contract'; 4 | import axios from 'axios'; 5 | import Header from './Header'; 6 | import Footer from './Footer'; 7 | import SampleContractsList from './SampleContractsList'; 8 | 9 | // Specify the metadata of the contract. 10 | import abi from '../metadata/metadata_sample.json'; 11 | 12 | const IndexCanvas = () => { 13 | 14 | const blockchains = [ 15 | { 16 | name: 'Shiden', 17 | url: 'wss://shiden.api.onfinality.io/public-ws', 18 | subscan_url: 'https://shiden.subscan.io/account/', 19 | }, 20 | { 21 | name: 'Shibuya', 22 | url: 'wss://rpc.shibuya.astar.network', 23 | subscan_url: 'https://shibuya.subscan.io/account/', 24 | }, 25 | { 26 | name: 'Local', 27 | url: 'ws://127.0.0.1:9944', 28 | }, 29 | { 30 | name: 'Custom', 31 | url: '', 32 | //url: 'wss://astar-collator.cielo.works:11443', 33 | }, 34 | ]; 35 | 36 | const [block, setBlock] = useState(0); 37 | const [blockchainUrl, setBlockchainUrl] = useState(''); 38 | const [blockchainName, setBlockchainName] = useState(''); 39 | const [actingChainName, setActingChainName] = useState(''); 40 | const [actingChainUrl, setActingChainUrl] = useState(''); 41 | const [customUrl, setCustomUrl] = useState(''); 42 | 43 | const [api, setApi] = useState(); 44 | 45 | const [contractAddress, setContractAddress] = useState(''); 46 | const [tokenId, setTokenId] = useState(''); 47 | const [tokenURI, setTokenURI] = useState(''); 48 | const [ownerAddress, setOwnerAddress] = useState(''); 49 | 50 | const [result, setResult] = useState(''); 51 | const [outcome, setOutcome] = useState(''); 52 | 53 | const [tokenImageURI, setTokenImageURI] = useState(''); 54 | const [ipfsImageURI, setIpfsImageURI] = useState(''); 55 | const [tokenName, setTokenName] = useState(''); 56 | const [tokenDescription, setTokenDescription] = useState(''); 57 | const [subScanUri, setSubScanUri] = useState(''); 58 | const [subScanTitle, setSubScanTitle] = useState(''); 59 | 60 | const [ipfsGateway, setIpfsGateway] = useState(''); 61 | const [actingIpfsGateway, setActingIpfsGateway] = useState('Pinata'); 62 | 63 | useEffect(() => { 64 | setIpfsGateway(actingIpfsGateway); 65 | const url:any = localStorage.getItem('customUrl'); 66 | setCustomUrl(url); 67 | },[]); 68 | 69 | async function getTokenURI() { 70 | if (!blockchainUrl || !block) { 71 | alert('Please select Blockchain and click "Set Blockchain" button.'); 72 | return; 73 | } 74 | 75 | setTokenURI(''); 76 | setTokenImageURI(''); 77 | setIpfsImageURI(''); 78 | setTokenName(''); 79 | setTokenDescription(''); 80 | setOwnerAddress(''); 81 | 82 | const contract = new ContractPromise(api, abi, contractAddress); 83 | const {result, output} = 84 | await contract.query['shiden34Trait::tokenUri']( 85 | contractAddress, 86 | {value: 0, gasLimit: -1}, 87 | {u64: tokenId}); 88 | 89 | setResult(JSON.stringify(result.toHuman())); 90 | 91 | // The actual result from RPC as `ContractExecResult` 92 | console.log(result.toHuman()); 93 | 94 | // check if the call was successful 95 | if (result.isOk) { 96 | // output the return value 97 | console.log('Success', output?.toHuman()); 98 | const outputData: any = output; 99 | setOutcome(outputData.toString()); 100 | 101 | if (outputData.isOk) { 102 | const url = outputData.inner.toString(); 103 | if (url !== undefined) { 104 | setTokenURI(url); 105 | const matadataUrl = getIpfsGatewayUri(url); 106 | axios.get(matadataUrl).then(res => { 107 | // TokenURI metadata 108 | console.log(res.data); 109 | const imageUrl = res.data.image.toString(); 110 | let metadataImageUrl = getIpfsGatewayUri(imageUrl); 111 | setTokenImageURI(imageUrl); 112 | setIpfsImageURI(metadataImageUrl); 113 | setTokenName(res.data.name.toString()); 114 | setTokenDescription(res.data.description.toString()); 115 | }); 116 | } 117 | 118 | if (actingChainName === 'Shiden' || actingChainName === 'Shibuya') { 119 | const newDataset = blockchains.filter(data => data.name === actingChainName); 120 | const subScanBaseUri = newDataset[0]?.subscan_url; 121 | setSubScanUri(subScanBaseUri + contractAddress); 122 | setSubScanTitle('Show on Subscan (' + actingChainName + ')'); 123 | } else { 124 | setSubScanTitle(''); 125 | } 126 | 127 | getOwnerOf(); 128 | 129 | } else { 130 | setOutcome(outputData.toString()); 131 | setTokenURI(''); 132 | setTokenImageURI(''); 133 | setIpfsImageURI(''); 134 | setTokenName(''); 135 | setTokenDescription(''); 136 | setOwnerAddress(''); 137 | } 138 | 139 | } else { 140 | setOutcome(''); 141 | setTokenURI(''); 142 | setTokenImageURI(''); 143 | setIpfsImageURI(''); 144 | setTokenName(''); 145 | setTokenDescription(''); 146 | setOwnerAddress(''); 147 | } 148 | }; 149 | 150 | async function getOwnerOf() { 151 | const contract = new ContractPromise(api, abi, contractAddress); 152 | const {result, output} = 153 | await contract.query['psp34::ownerOf']( 154 | contractAddress, 155 | {value: 0, gasLimit: -1}, 156 | {u64: tokenId}); 157 | 158 | // The actual result from RPC as `ContractExecResult` 159 | console.log(result.toHuman()); 160 | 161 | // check if the call was successful 162 | if (result.isOk) { 163 | // output the return value 164 | console.log('Success', output?.toHuman()); 165 | const outcome: any = output; 166 | const resultStr: string = outcome.toHuman()?.toString()!; 167 | if (resultStr) { 168 | setOwnerAddress(resultStr); 169 | } else { 170 | setOwnerAddress('none'); 171 | } 172 | } 173 | }; 174 | 175 | const setup = async () => { 176 | 177 | const newDataset = blockchains.filter(data => data.name === blockchainName); 178 | 179 | let chainUrl = ''; 180 | if (blockchainName === 'Custom') { 181 | chainUrl = customUrl; 182 | } else { 183 | chainUrl = newDataset[0]?.url; 184 | } 185 | setBlockchainUrl(chainUrl); 186 | 187 | if (!chainUrl) { 188 | return; 189 | } 190 | 191 | setActingChainName(''); 192 | setBlock(0); 193 | setActingChainUrl(''); 194 | 195 | const wsProvider = new WsProvider(chainUrl); 196 | const api = await ApiPromise.create({provider: wsProvider}); 197 | const unsubscribe = await api.rpc.chain.subscribeNewHeads((lastHeader) => { 198 | setApi(api); 199 | setActingChainName(blockchainName); 200 | setBlock(lastHeader.number.toNumber()); 201 | setActingChainUrl(chainUrl); 202 | unsubscribe(); 203 | }); 204 | }; 205 | 206 | const saveIpfsGateway = () => { 207 | localStorage.setItem("ipfsGateway", ipfsGateway); 208 | setActingIpfsGateway(ipfsGateway); 209 | }; 210 | 211 | const saveCustomURL = (url: string) => { 212 | setCustomUrl(url); 213 | localStorage.setItem('customUrl', url); 214 | }; 215 | 216 | const getIpfsGatewayUri = (uri: string) => { 217 | if (!uri) { 218 | return ''; 219 | } 220 | 221 | const scheme = uri.slice(0, 7); 222 | let cid = ''; 223 | let fileName = ''; 224 | if (scheme === 'ipfs://') { 225 | let tmp = uri.substr(7); 226 | cid = uri.substr(7, tmp.indexOf('/')); 227 | fileName = uri.substr(tmp.indexOf('/') + 8); 228 | if (actingIpfsGateway === 'ipfs.io') { 229 | uri = 'https://ipfs.io/ipfs/' + cid + '/' + fileName; 230 | } else if (actingIpfsGateway === 'Crust Network') { 231 | uri = 'https://gw.crustapps.net/ipfs/' + cid + '/' + fileName; 232 | } else if (actingIpfsGateway === 'Cloudflare') { 233 | uri = 'https://cloudflare-ipfs.com/ipfs/' + cid + '/' + fileName; 234 | } else if (actingIpfsGateway === 'dweb.link') { 235 | uri = 'https://dweb.link/ipfs/' + cid + '/' + fileName; 236 | //cid = cid.toV1().toString('base32'); 237 | //uri = 'https://' + cid + '.ipfs.dweb.link' + '/' + fileName; 238 | } else if (actingIpfsGateway === 'Pinata') { 239 | uri = 'https://cielo.mypinata.cloud/ipfs/' + cid + '/' + fileName; 240 | } 241 | } 242 | console.log('ipfs_uri: ', uri); 243 | return uri; 244 | }; 245 | 246 | return ( 247 |
248 |
249 |
250 |
Select blockchain
251 | 255 | 267 | {blockchainName === 'Custom' ? 268 | saveCustomURL(event.target.value)} 271 | placeholder="Custom URL" 272 | value={customUrl} 273 | /> 274 | : <> } 275 |
Current BlockchainName: {actingChainName? actingChainName : "---"}
276 |
URL: {actingChainUrl? actingChainUrl : "---"}
277 |
278 | 279 |
280 |
281 |
NFT View
282 | 286 | setContractAddress(event.target.value.trim())} 289 | placeholder="ContractAddress" 290 | /> 291 | setTokenId(event.target.value.trim())} 294 | placeholder="TokenID" 295 | /> 296 |
297 | 298 |
299 |
300 |
301 | {tokenURI ? ipfsImageURI ? 302 | : 303 | : 304 | 305 | } 306 |
307 |

{tokenName}

308 |

{tokenDescription}

309 |

{subScanTitle}

310 |
311 |
312 | 313 |
314 |

Result: {result}

315 |

OutputData: {outcome}

316 |

TokenId: {tokenId}

317 |

TokenURI: {tokenURI}

318 |

ImageURI: {tokenImageURI}

319 |

OwnerAddress: {ownerAddress}

320 |
321 |
322 | 323 |
324 |
Select ipfs Gateway
325 | 331 | 343 |
Current ipfs Gateway: {actingIpfsGateway? actingIpfsGateway : "---"}
344 |
345 | 346 | 347 |
348 |
349 | ); 350 | }; 351 | 352 | export default IndexCanvas; -------------------------------------------------------------------------------- /ui/viewer/components/SampleContractsList.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | 3 | const SampleContractsList = (): JSX.Element => { 4 | 5 | const navigation = { 6 | shiden: [ 7 | { name: 'PSP34Sample', address: 'soon' }, 8 | ], 9 | shibuya: [ 10 | { name: 'PSP34Sample', address: 'YSXjTTTiqYuUQT51WgMCQspKsw7qiY4Ng2Crp3Mc2hNmATc' }, 11 | ], 12 | }; 13 | 14 | const [shidenContractLabel, setShidenContractLabel] = useState(''); 15 | const [shidenContractAddress, setShidenContractAddress] = useState(''); 16 | const [shibuyaContractLabel, setShibuyaContractLabel] = useState(''); 17 | const [shibuyaContractAddress, setShibuyaContractAddress] = useState(''); 18 | const [localContractLabel, setLocalContractLabel] = useState(''); 19 | const [localContractAddress, setLocalContractAddress] = useState(''); 20 | const [customContractLabel, setCustomContractLabel] = useState(''); 21 | const [customContractAddress, setCustomContractAddress] = useState(''); 22 | 23 | useEffect(() => { 24 | const shidenContractLabel: any = localStorage.getItem('shidenContractLabel'); 25 | setShidenContractLabel(shidenContractLabel); 26 | const shidenContractAddress: any = localStorage.getItem('shidenContractAddress'); 27 | setShidenContractAddress(shidenContractAddress); 28 | const shibuyaContractLabel: any = localStorage.getItem('shibuyaContractLabel'); 29 | setShibuyaContractLabel(shibuyaContractLabel); 30 | const shibuyaContractAddress: any = localStorage.getItem('shibuyaContractAddress'); 31 | setShibuyaContractAddress(shibuyaContractAddress); 32 | const localContractLabel: any = localStorage.getItem('localContractLabel'); 33 | setLocalContractLabel(localContractLabel); 34 | const localContractAddress: any = localStorage.getItem('localContractAddress'); 35 | setLocalContractAddress(localContractAddress); 36 | const customContractLabel: any = localStorage.getItem('customContractLabel'); 37 | setCustomContractLabel(customContractLabel); 38 | const customContractAddress: any = localStorage.getItem('customContractAddress'); 39 | setCustomContractAddress(customContractAddress); 40 | },[]); 41 | 42 | const saveContractInfo = (chain: string, type: string, str: string) => { 43 | str = str.trim(); 44 | if (chain === 'shiden') { 45 | if (type === 'label') { 46 | setShidenContractLabel(str); 47 | localStorage.setItem('shidenContractLabel', str); 48 | } else if (type === 'address') { 49 | setShidenContractAddress(str); 50 | localStorage.setItem('shidenContractAddress', str); 51 | } 52 | } else if (chain === 'shibuya') { 53 | if (type === 'label') { 54 | setShibuyaContractLabel(str); 55 | localStorage.setItem('shibuyaContractLabel', str); 56 | } else if (type === 'address') { 57 | setShibuyaContractAddress(str); 58 | localStorage.setItem('shibuyaContractAddress', str); 59 | } 60 | } else if (chain === 'local') { 61 | if (type === 'label') { 62 | setLocalContractLabel(str); 63 | localStorage.setItem('localContractLabel', str); 64 | } else if (type === 'address') { 65 | setLocalContractAddress(str); 66 | localStorage.setItem('localContractAddress', str); 67 | } 68 | } else if (chain === 'custom') { 69 | if (type === 'label') { 70 | setCustomContractLabel(str); 71 | localStorage.setItem('customContractLabel', str); 72 | } else if (type === 'address') { 73 | setCustomContractAddress(str); 74 | localStorage.setItem('customContractAddress', str); 75 | } 76 | } 77 | }; 78 | 79 | return ( 80 |
81 |

Sample Contracts

82 |
83 |
Shiden
84 | {navigation.shiden.map((item) => ( 85 |
{item.name}: {item.address}
86 | ))} 87 |
88 |
89 | saveContractInfo('shiden', 'label', event.target.value)} 92 | placeholder="Label" 93 | value={shidenContractLabel} 94 | /> 95 | saveContractInfo('shiden', 'address', event.target.value)} 98 | placeholder="Shiden contract address here" 99 | value={shidenContractAddress} 100 | /> 101 |
102 |
103 |
104 |
105 |
Shibuya
106 | {navigation.shibuya.map((item) => ( 107 |
{item.name} : {item.address}
108 | ))} 109 |
110 |
111 | saveContractInfo('shibuya', 'label', event.target.value)} 114 | placeholder="Label" 115 | value={shibuyaContractLabel} 116 | /> 117 | saveContractInfo('shibuya', 'address', event.target.value)} 120 | placeholder="Shibuya contract address here" 121 | value={shibuyaContractAddress} 122 | /> 123 |
124 |
125 |
126 |
127 |
Local
128 |
129 |
130 | saveContractInfo('local', 'label', event.target.value)} 133 | placeholder="Label" 134 | value={localContractLabel} 135 | /> 136 | saveContractInfo('local', 'address', event.target.value)} 139 | placeholder="Local contract address here" 140 | value={localContractAddress} 141 | /> 142 |
143 |
144 |
145 |
146 |
Custom
147 |
148 |
149 | saveContractInfo('custom', 'label', event.target.value)} 152 | placeholder="Label" 153 | value={customContractLabel} 154 | /> 155 | saveContractInfo('custom', 'address', event.target.value)} 158 | placeholder="Custom contract address here" 159 | value={customContractAddress} 160 | /> 161 |
162 |
163 |
164 |
165 | ); 166 | }; 167 | 168 | export default SampleContractsList; -------------------------------------------------------------------------------- /ui/viewer/metadata/metadata_sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "source": { 3 | "hash": "0x8f215d99a8e26ce07b0d0999621a526888a09d5e4fda037f548c4a96d31bc89c", 4 | "language": "ink! 3.3.1", 5 | "compiler": "rustc 1.64.0-nightly" 6 | }, 7 | "contract": { 8 | "name": "shiden34", 9 | "version": "0.1.0", 10 | "authors": [ 11 | "Mar.io" 12 | ] 13 | }, 14 | "V3": { 15 | "spec": { 16 | "constructors": [ 17 | { 18 | "args": [ 19 | { 20 | "label": "name", 21 | "type": { 22 | "displayName": [ 23 | "String" 24 | ], 25 | "type": 26 26 | } 27 | }, 28 | { 29 | "label": "symbol", 30 | "type": { 31 | "displayName": [ 32 | "String" 33 | ], 34 | "type": 26 35 | } 36 | }, 37 | { 38 | "label": "base_uri", 39 | "type": { 40 | "displayName": [ 41 | "String" 42 | ], 43 | "type": 26 44 | } 45 | }, 46 | { 47 | "label": "max_supply", 48 | "type": { 49 | "displayName": [ 50 | "u64" 51 | ], 52 | "type": 5 53 | } 54 | }, 55 | { 56 | "label": "price_per_mint", 57 | "type": { 58 | "displayName": [ 59 | "Balance" 60 | ], 61 | "type": 6 62 | } 63 | } 64 | ], 65 | "docs": [], 66 | "label": "new", 67 | "payable": false, 68 | "selector": "0x9bae9d5e" 69 | } 70 | ], 71 | "docs": [], 72 | "events": [], 73 | "messages": [ 74 | { 75 | "args": [ 76 | { 77 | "label": "to", 78 | "type": { 79 | "displayName": [ 80 | "psp34_external", 81 | "TransferInput1" 82 | ], 83 | "type": 8 84 | } 85 | }, 86 | { 87 | "label": "id", 88 | "type": { 89 | "displayName": [ 90 | "psp34_external", 91 | "TransferInput2" 92 | ], 93 | "type": 1 94 | } 95 | }, 96 | { 97 | "label": "data", 98 | "type": { 99 | "displayName": [ 100 | "psp34_external", 101 | "TransferInput3" 102 | ], 103 | "type": 7 104 | } 105 | } 106 | ], 107 | "docs": [ 108 | " Transfer approved or owned token from caller.", 109 | "", 110 | " On success a `Transfer` event is emitted.", 111 | "", 112 | " # Errors", 113 | "", 114 | " Returns `TokenNotExists` error if `id` does not exist.", 115 | "", 116 | " Returns `NotApproved` error if `from` doesn't have allowance for transferring.", 117 | "", 118 | " Returns `SafeTransferCheckFailed` error if `to` doesn't accept transfer." 119 | ], 120 | "label": "PSP34::transfer", 121 | "mutates": true, 122 | "payable": false, 123 | "returnType": { 124 | "displayName": [ 125 | "psp34_external", 126 | "TransferOutput" 127 | ], 128 | "type": 27 129 | }, 130 | "selector": "0x3128d61b" 131 | }, 132 | { 133 | "args": [], 134 | "docs": [ 135 | " Returns the collection `Id` of the NFT token.", 136 | "", 137 | " This can represents the relationship between tokens/contracts/pallets." 138 | ], 139 | "label": "PSP34::collection_id", 140 | "mutates": false, 141 | "payable": false, 142 | "returnType": { 143 | "displayName": [ 144 | "psp34_external", 145 | "CollectionIdOutput" 146 | ], 147 | "type": 1 148 | }, 149 | "selector": "0xffa27a5f" 150 | }, 151 | { 152 | "args": [ 153 | { 154 | "label": "owner", 155 | "type": { 156 | "displayName": [ 157 | "psp34_external", 158 | "BalanceOfInput1" 159 | ], 160 | "type": 8 161 | } 162 | } 163 | ], 164 | "docs": [ 165 | " Returns the balance of the owner.", 166 | "", 167 | " This represents the amount of unique tokens the owner has." 168 | ], 169 | "label": "PSP34::balance_of", 170 | "mutates": false, 171 | "payable": false, 172 | "returnType": { 173 | "displayName": [ 174 | "psp34_external", 175 | "BalanceOfOutput" 176 | ], 177 | "type": 4 178 | }, 179 | "selector": "0xcde7e55f" 180 | }, 181 | { 182 | "args": [ 183 | { 184 | "label": "owner", 185 | "type": { 186 | "displayName": [ 187 | "psp34_external", 188 | "AllowanceInput1" 189 | ], 190 | "type": 8 191 | } 192 | }, 193 | { 194 | "label": "operator", 195 | "type": { 196 | "displayName": [ 197 | "psp34_external", 198 | "AllowanceInput2" 199 | ], 200 | "type": 8 201 | } 202 | }, 203 | { 204 | "label": "id", 205 | "type": { 206 | "displayName": [ 207 | "psp34_external", 208 | "AllowanceInput3" 209 | ], 210 | "type": 14 211 | } 212 | } 213 | ], 214 | "docs": [ 215 | " Returns `true` if the operator is approved by the owner to withdraw `id` token.", 216 | " If `id` is `None`, returns `true` if the operator is approved to withdraw all owner's tokens." 217 | ], 218 | "label": "PSP34::allowance", 219 | "mutates": false, 220 | "payable": false, 221 | "returnType": { 222 | "displayName": [ 223 | "psp34_external", 224 | "AllowanceOutput" 225 | ], 226 | "type": 29 227 | }, 228 | "selector": "0x4790f55a" 229 | }, 230 | { 231 | "args": [ 232 | { 233 | "label": "operator", 234 | "type": { 235 | "displayName": [ 236 | "psp34_external", 237 | "ApproveInput1" 238 | ], 239 | "type": 8 240 | } 241 | }, 242 | { 243 | "label": "id", 244 | "type": { 245 | "displayName": [ 246 | "psp34_external", 247 | "ApproveInput2" 248 | ], 249 | "type": 14 250 | } 251 | }, 252 | { 253 | "label": "approved", 254 | "type": { 255 | "displayName": [ 256 | "psp34_external", 257 | "ApproveInput3" 258 | ], 259 | "type": 29 260 | } 261 | } 262 | ], 263 | "docs": [ 264 | " Approves `operator` to withdraw the `id` token from the caller's account.", 265 | " If `id` is `None` approves or disapproves the operator for all tokens of the caller.", 266 | "", 267 | " On success a `Approval` event is emitted.", 268 | "", 269 | " # Errors", 270 | "", 271 | " Returns `SelfApprove` error if it is self approve.", 272 | "", 273 | " Returns `NotApproved` error if caller is not owner of `id`." 274 | ], 275 | "label": "PSP34::approve", 276 | "mutates": true, 277 | "payable": false, 278 | "returnType": { 279 | "displayName": [ 280 | "psp34_external", 281 | "ApproveOutput" 282 | ], 283 | "type": 27 284 | }, 285 | "selector": "0x1932a8b0" 286 | }, 287 | { 288 | "args": [], 289 | "docs": [ 290 | " Returns current NFT total supply." 291 | ], 292 | "label": "PSP34::total_supply", 293 | "mutates": false, 294 | "payable": false, 295 | "returnType": { 296 | "displayName": [ 297 | "psp34_external", 298 | "TotalSupplyOutput" 299 | ], 300 | "type": 6 301 | }, 302 | "selector": "0x628413fe" 303 | }, 304 | { 305 | "args": [ 306 | { 307 | "label": "id", 308 | "type": { 309 | "displayName": [ 310 | "psp34_external", 311 | "OwnerOfInput1" 312 | ], 313 | "type": 1 314 | } 315 | } 316 | ], 317 | "docs": [ 318 | " Returns the owner of the token if any." 319 | ], 320 | "label": "PSP34::owner_of", 321 | "mutates": false, 322 | "payable": false, 323 | "returnType": { 324 | "displayName": [ 325 | "psp34_external", 326 | "OwnerOfOutput" 327 | ], 328 | "type": 19 329 | }, 330 | "selector": "0x1168624d" 331 | }, 332 | { 333 | "args": [ 334 | { 335 | "label": "owner", 336 | "type": { 337 | "displayName": [ 338 | "psp34enumerable_external", 339 | "OwnersTokenByIndexInput1" 340 | ], 341 | "type": 8 342 | } 343 | }, 344 | { 345 | "label": "index", 346 | "type": { 347 | "displayName": [ 348 | "psp34enumerable_external", 349 | "OwnersTokenByIndexInput2" 350 | ], 351 | "type": 6 352 | } 353 | } 354 | ], 355 | "docs": [ 356 | " Returns a token `Id` owned by `owner` at a given `index` of its token list.", 357 | " Use along with `balance_of` to enumerate all of ``owner``'s tokens." 358 | ], 359 | "label": "PSP34Enumerable::owners_token_by_index", 360 | "mutates": false, 361 | "payable": false, 362 | "returnType": { 363 | "displayName": [ 364 | "psp34enumerable_external", 365 | "OwnersTokenByIndexOutput" 366 | ], 367 | "type": 30 368 | }, 369 | "selector": "0x3bcfb511" 370 | }, 371 | { 372 | "args": [ 373 | { 374 | "label": "index", 375 | "type": { 376 | "displayName": [ 377 | "psp34enumerable_external", 378 | "TokenByIndexInput1" 379 | ], 380 | "type": 6 381 | } 382 | } 383 | ], 384 | "docs": [ 385 | " Returns a token `Id` at a given `index` of all the tokens stored by the contract.", 386 | " Use along with `total_supply` to enumerate all tokens." 387 | ], 388 | "label": "PSP34Enumerable::token_by_index", 389 | "mutates": false, 390 | "payable": false, 391 | "returnType": { 392 | "displayName": [ 393 | "psp34enumerable_external", 394 | "TokenByIndexOutput" 395 | ], 396 | "type": 30 397 | }, 398 | "selector": "0xcd0340d0" 399 | }, 400 | { 401 | "args": [ 402 | { 403 | "label": "id", 404 | "type": { 405 | "displayName": [ 406 | "psp34metadata_external", 407 | "GetAttributeInput1" 408 | ], 409 | "type": 1 410 | } 411 | }, 412 | { 413 | "label": "key", 414 | "type": { 415 | "displayName": [ 416 | "psp34metadata_external", 417 | "GetAttributeInput2" 418 | ], 419 | "type": 7 420 | } 421 | } 422 | ], 423 | "docs": [ 424 | " Returns the attribute of `id` for the given `key`.", 425 | "", 426 | " If `id` is a collection id of the token, it returns attributes for collection." 427 | ], 428 | "label": "PSP34Metadata::get_attribute", 429 | "mutates": false, 430 | "payable": false, 431 | "returnType": { 432 | "displayName": [ 433 | "psp34metadata_external", 434 | "GetAttributeOutput" 435 | ], 436 | "type": 31 437 | }, 438 | "selector": "0xf19d48d1" 439 | }, 440 | { 441 | "args": [ 442 | { 443 | "label": "new_owner", 444 | "type": { 445 | "displayName": [ 446 | "ownable_external", 447 | "TransferOwnershipInput1" 448 | ], 449 | "type": 8 450 | } 451 | } 452 | ], 453 | "docs": [ 454 | " Transfers ownership of the contract to a `new_owner`.", 455 | " Can only be called by the current owner.", 456 | "", 457 | " On success a `OwnershipTransferred` event is emitted.", 458 | "", 459 | " # Errors", 460 | "", 461 | " Panics with `CallerIsNotOwner` error if caller is not owner.", 462 | "", 463 | " Panics with `NewOwnerIsZero` error if new owner's address is zero." 464 | ], 465 | "label": "Ownable::transfer_ownership", 466 | "mutates": true, 467 | "payable": false, 468 | "returnType": { 469 | "displayName": [ 470 | "ownable_external", 471 | "TransferOwnershipOutput" 472 | ], 473 | "type": 32 474 | }, 475 | "selector": "0x11f43efd" 476 | }, 477 | { 478 | "args": [], 479 | "docs": [ 480 | " Returns the address of the current owner." 481 | ], 482 | "label": "Ownable::owner", 483 | "mutates": false, 484 | "payable": false, 485 | "returnType": { 486 | "displayName": [ 487 | "ownable_external", 488 | "OwnerOutput" 489 | ], 490 | "type": 8 491 | }, 492 | "selector": "0x4fa43c8c" 493 | }, 494 | { 495 | "args": [], 496 | "docs": [ 497 | " Leaves the contract without owner. It will not be possible to call", 498 | " owner's functions anymore. Can only be called by the current owner.", 499 | "", 500 | " NOTE: Renouncing ownership will leave the contract without an owner,", 501 | " thereby removing any functionality that is only available to the owner.", 502 | "", 503 | " On success a `OwnershipTransferred` event is emitted.", 504 | "", 505 | " # Errors", 506 | "", 507 | " Panics with `CallerIsNotOwner` error if caller is not owner" 508 | ], 509 | "label": "Ownable::renounce_ownership", 510 | "mutates": true, 511 | "payable": false, 512 | "returnType": { 513 | "displayName": [ 514 | "ownable_external", 515 | "RenounceOwnershipOutput" 516 | ], 517 | "type": 32 518 | }, 519 | "selector": "0x5e228753" 520 | }, 521 | { 522 | "args": [ 523 | { 524 | "label": "account", 525 | "type": { 526 | "displayName": [ 527 | "psp34mintable_external", 528 | "MintInput1" 529 | ], 530 | "type": 8 531 | } 532 | }, 533 | { 534 | "label": "id", 535 | "type": { 536 | "displayName": [ 537 | "psp34mintable_external", 538 | "MintInput2" 539 | ], 540 | "type": 1 541 | } 542 | } 543 | ], 544 | "docs": [ 545 | " This mint function is not used since it can't be made payable" 546 | ], 547 | "label": "PSP34Mintable::mint", 548 | "mutates": true, 549 | "payable": false, 550 | "returnType": { 551 | "displayName": [ 552 | "psp34mintable_external", 553 | "MintOutput" 554 | ], 555 | "type": 27 556 | }, 557 | "selector": "0x6c41f2ec" 558 | }, 559 | { 560 | "args": [ 561 | { 562 | "label": "to", 563 | "type": { 564 | "displayName": [ 565 | "shiden34trait_external", 566 | "MintForInput1" 567 | ], 568 | "type": 8 569 | } 570 | }, 571 | { 572 | "label": "mint_amount", 573 | "type": { 574 | "displayName": [ 575 | "shiden34trait_external", 576 | "MintForInput2" 577 | ], 578 | "type": 5 579 | } 580 | } 581 | ], 582 | "docs": [ 583 | " Mint several tokens" 584 | ], 585 | "label": "Shiden34Trait::mint_for", 586 | "mutates": true, 587 | "payable": true, 588 | "returnType": { 589 | "displayName": [ 590 | "shiden34trait_external", 591 | "MintForOutput" 592 | ], 593 | "type": 27 594 | }, 595 | "selector": "0x156e41a4" 596 | }, 597 | { 598 | "args": [], 599 | "docs": [ 600 | " Get max supply of tokens" 601 | ], 602 | "label": "Shiden34Trait::withdraw", 603 | "mutates": true, 604 | "payable": false, 605 | "returnType": { 606 | "displayName": [ 607 | "shiden34trait_external", 608 | "WithdrawOutput" 609 | ], 610 | "type": 27 611 | }, 612 | "selector": "0xb5405681" 613 | }, 614 | { 615 | "args": [], 616 | "docs": [ 617 | " Mint next available token for the caller" 618 | ], 619 | "label": "Shiden34Trait::mint_next", 620 | "mutates": true, 621 | "payable": true, 622 | "returnType": { 623 | "displayName": [ 624 | "shiden34trait_external", 625 | "MintNextOutput" 626 | ], 627 | "type": 27 628 | }, 629 | "selector": "0xae0978d0" 630 | }, 631 | { 632 | "args": [ 633 | { 634 | "label": "token_id", 635 | "type": { 636 | "displayName": [ 637 | "shiden34trait_external", 638 | "TokenUriInput1" 639 | ], 640 | "type": 5 641 | } 642 | } 643 | ], 644 | "docs": [ 645 | " Get URI from token ID" 646 | ], 647 | "label": "Shiden34Trait::token_uri", 648 | "mutates": false, 649 | "payable": false, 650 | "returnType": { 651 | "displayName": [ 652 | "shiden34trait_external", 653 | "TokenUriOutput" 654 | ], 655 | "type": 34 656 | }, 657 | "selector": "0x06118780" 658 | }, 659 | { 660 | "args": [ 661 | { 662 | "label": "uri", 663 | "type": { 664 | "displayName": [ 665 | "shiden34trait_external", 666 | "SetBaseUriInput1" 667 | ], 668 | "type": 26 669 | } 670 | } 671 | ], 672 | "docs": [ 673 | " Set new value for the baseUri" 674 | ], 675 | "label": "Shiden34Trait::set_base_uri", 676 | "mutates": true, 677 | "payable": false, 678 | "returnType": { 679 | "displayName": [ 680 | "shiden34trait_external", 681 | "SetBaseUriOutput" 682 | ], 683 | "type": 27 684 | }, 685 | "selector": "0xdcf96e02" 686 | }, 687 | { 688 | "args": [], 689 | "docs": [ 690 | " Get max supply of tokens" 691 | ], 692 | "label": "Shiden34Trait::max_supply", 693 | "mutates": false, 694 | "payable": false, 695 | "returnType": { 696 | "displayName": [ 697 | "shiden34trait_external", 698 | "MaxSupplyOutput" 699 | ], 700 | "type": 5 701 | }, 702 | "selector": "0x622ed8e9" 703 | } 704 | ] 705 | }, 706 | "storage": { 707 | "struct": { 708 | "fields": [ 709 | { 710 | "layout": { 711 | "struct": { 712 | "fields": [ 713 | { 714 | "layout": { 715 | "cell": { 716 | "key": "0x0dbe693b00000000000000000000000000000000000000000000000000000000", 717 | "ty": 0 718 | } 719 | }, 720 | "name": "token_owner" 721 | }, 722 | { 723 | "layout": { 724 | "cell": { 725 | "key": "0x0ebe693b00000000000000000000000000000000000000000000000000000000", 726 | "ty": 12 727 | } 728 | }, 729 | "name": "operator_approvals" 730 | }, 731 | { 732 | "layout": { 733 | "struct": { 734 | "fields": [ 735 | { 736 | "layout": { 737 | "cell": { 738 | "key": "0x4cefab1200000000000000000000000000000000000000000000000000000000", 739 | "ty": 18 740 | } 741 | }, 742 | "name": "enumerable" 743 | }, 744 | { 745 | "layout": { 746 | "enum": { 747 | "dispatchKey": "0x4defab1200000000000000000000000000000000000000000000000000000000", 748 | "variants": { 749 | "0": { 750 | "fields": [ 751 | { 752 | "layout": { 753 | "cell": { 754 | "key": "0x4eefab1200000000000000000000000000000000000000000000000000000000", 755 | "ty": 15 756 | } 757 | }, 758 | "name": null 759 | } 760 | ] 761 | }, 762 | "1": { 763 | "fields": [] 764 | } 765 | } 766 | } 767 | }, 768 | "name": "_reserved" 769 | } 770 | ] 771 | } 772 | }, 773 | "name": "balances" 774 | }, 775 | { 776 | "layout": { 777 | "enum": { 778 | "dispatchKey": "0x0fbe693b00000000000000000000000000000000000000000000000000000000", 779 | "variants": { 780 | "0": { 781 | "fields": [ 782 | { 783 | "layout": { 784 | "cell": { 785 | "key": "0x10be693b00000000000000000000000000000000000000000000000000000000", 786 | "ty": 15 787 | } 788 | }, 789 | "name": null 790 | } 791 | ] 792 | }, 793 | "1": { 794 | "fields": [] 795 | } 796 | } 797 | } 798 | }, 799 | "name": "_reserved" 800 | } 801 | ] 802 | } 803 | }, 804 | "name": "psp34" 805 | }, 806 | { 807 | "layout": { 808 | "struct": { 809 | "fields": [ 810 | { 811 | "layout": { 812 | "cell": { 813 | "key": "0xc4c906f100000000000000000000000000000000000000000000000000000000", 814 | "ty": 22 815 | } 816 | }, 817 | "name": "attributes" 818 | }, 819 | { 820 | "layout": { 821 | "enum": { 822 | "dispatchKey": "0xc5c906f100000000000000000000000000000000000000000000000000000000", 823 | "variants": { 824 | "0": { 825 | "fields": [ 826 | { 827 | "layout": { 828 | "cell": { 829 | "key": "0xc6c906f100000000000000000000000000000000000000000000000000000000", 830 | "ty": 15 831 | } 832 | }, 833 | "name": null 834 | } 835 | ] 836 | }, 837 | "1": { 838 | "fields": [] 839 | } 840 | } 841 | } 842 | }, 843 | "name": "_reserved" 844 | } 845 | ] 846 | } 847 | }, 848 | "name": "metadata" 849 | }, 850 | { 851 | "layout": { 852 | "struct": { 853 | "fields": [ 854 | { 855 | "layout": { 856 | "cell": { 857 | "key": "0xf9c17de900000000000000000000000000000000000000000000000000000000", 858 | "ty": 2 859 | } 860 | }, 861 | "name": "status" 862 | }, 863 | { 864 | "layout": { 865 | "enum": { 866 | "dispatchKey": "0xfac17de900000000000000000000000000000000000000000000000000000000", 867 | "variants": { 868 | "0": { 869 | "fields": [ 870 | { 871 | "layout": { 872 | "cell": { 873 | "key": "0xfbc17de900000000000000000000000000000000000000000000000000000000", 874 | "ty": 15 875 | } 876 | }, 877 | "name": null 878 | } 879 | ] 880 | }, 881 | "1": { 882 | "fields": [] 883 | } 884 | } 885 | } 886 | }, 887 | "name": "_reserved" 888 | } 889 | ] 890 | } 891 | }, 892 | "name": "guard" 893 | }, 894 | { 895 | "layout": { 896 | "struct": { 897 | "fields": [ 898 | { 899 | "layout": { 900 | "cell": { 901 | "key": "0xb36ee29c00000000000000000000000000000000000000000000000000000000", 902 | "ty": 8 903 | } 904 | }, 905 | "name": "owner" 906 | }, 907 | { 908 | "layout": { 909 | "enum": { 910 | "dispatchKey": "0xb46ee29c00000000000000000000000000000000000000000000000000000000", 911 | "variants": { 912 | "0": { 913 | "fields": [ 914 | { 915 | "layout": { 916 | "cell": { 917 | "key": "0xb56ee29c00000000000000000000000000000000000000000000000000000000", 918 | "ty": 15 919 | } 920 | }, 921 | "name": null 922 | } 923 | ] 924 | }, 925 | "1": { 926 | "fields": [] 927 | } 928 | } 929 | } 930 | }, 931 | "name": "_reserved" 932 | } 933 | ] 934 | } 935 | }, 936 | "name": "ownable" 937 | }, 938 | { 939 | "layout": { 940 | "cell": { 941 | "key": "0x0000000000000000000000000000000000000000000000000000000000000000", 942 | "ty": 5 943 | } 944 | }, 945 | "name": "last_token_id" 946 | }, 947 | { 948 | "layout": { 949 | "cell": { 950 | "key": "0x0100000000000000000000000000000000000000000000000000000000000000", 951 | "ty": 4 952 | } 953 | }, 954 | "name": "collection_id" 955 | }, 956 | { 957 | "layout": { 958 | "cell": { 959 | "key": "0x0200000000000000000000000000000000000000000000000000000000000000", 960 | "ty": 5 961 | } 962 | }, 963 | "name": "max_supply" 964 | }, 965 | { 966 | "layout": { 967 | "cell": { 968 | "key": "0x0300000000000000000000000000000000000000000000000000000000000000", 969 | "ty": 6 970 | } 971 | }, 972 | "name": "price_per_mint" 973 | } 974 | ] 975 | } 976 | }, 977 | "types": [ 978 | { 979 | "id": 0, 980 | "type": { 981 | "def": { 982 | "composite": { 983 | "fields": [ 984 | { 985 | "type": 10 986 | } 987 | ] 988 | } 989 | }, 990 | "params": [ 991 | { 992 | "name": "K", 993 | "type": 1 994 | }, 995 | { 996 | "name": "V", 997 | "type": 8 998 | } 999 | ], 1000 | "path": [ 1001 | "openbrush_lang", 1002 | "storage", 1003 | "mapping", 1004 | "Mapping" 1005 | ] 1006 | } 1007 | }, 1008 | { 1009 | "id": 1, 1010 | "type": { 1011 | "def": { 1012 | "variant": { 1013 | "variants": [ 1014 | { 1015 | "fields": [ 1016 | { 1017 | "type": 2, 1018 | "typeName": "u8" 1019 | } 1020 | ], 1021 | "index": 0, 1022 | "name": "U8" 1023 | }, 1024 | { 1025 | "fields": [ 1026 | { 1027 | "type": 3, 1028 | "typeName": "u16" 1029 | } 1030 | ], 1031 | "index": 1, 1032 | "name": "U16" 1033 | }, 1034 | { 1035 | "fields": [ 1036 | { 1037 | "type": 4, 1038 | "typeName": "u32" 1039 | } 1040 | ], 1041 | "index": 2, 1042 | "name": "U32" 1043 | }, 1044 | { 1045 | "fields": [ 1046 | { 1047 | "type": 5, 1048 | "typeName": "u64" 1049 | } 1050 | ], 1051 | "index": 3, 1052 | "name": "U64" 1053 | }, 1054 | { 1055 | "fields": [ 1056 | { 1057 | "type": 6, 1058 | "typeName": "u128" 1059 | } 1060 | ], 1061 | "index": 4, 1062 | "name": "U128" 1063 | }, 1064 | { 1065 | "fields": [ 1066 | { 1067 | "type": 7, 1068 | "typeName": "Vec" 1069 | } 1070 | ], 1071 | "index": 5, 1072 | "name": "Bytes" 1073 | } 1074 | ] 1075 | } 1076 | }, 1077 | "path": [ 1078 | "openbrush_contracts", 1079 | "traits", 1080 | "types", 1081 | "Id" 1082 | ] 1083 | } 1084 | }, 1085 | { 1086 | "id": 2, 1087 | "type": { 1088 | "def": { 1089 | "primitive": "u8" 1090 | } 1091 | } 1092 | }, 1093 | { 1094 | "id": 3, 1095 | "type": { 1096 | "def": { 1097 | "primitive": "u16" 1098 | } 1099 | } 1100 | }, 1101 | { 1102 | "id": 4, 1103 | "type": { 1104 | "def": { 1105 | "primitive": "u32" 1106 | } 1107 | } 1108 | }, 1109 | { 1110 | "id": 5, 1111 | "type": { 1112 | "def": { 1113 | "primitive": "u64" 1114 | } 1115 | } 1116 | }, 1117 | { 1118 | "id": 6, 1119 | "type": { 1120 | "def": { 1121 | "primitive": "u128" 1122 | } 1123 | } 1124 | }, 1125 | { 1126 | "id": 7, 1127 | "type": { 1128 | "def": { 1129 | "sequence": { 1130 | "type": 2 1131 | } 1132 | } 1133 | } 1134 | }, 1135 | { 1136 | "id": 8, 1137 | "type": { 1138 | "def": { 1139 | "composite": { 1140 | "fields": [ 1141 | { 1142 | "type": 9, 1143 | "typeName": "[u8; 32]" 1144 | } 1145 | ] 1146 | } 1147 | }, 1148 | "path": [ 1149 | "ink_env", 1150 | "types", 1151 | "AccountId" 1152 | ] 1153 | } 1154 | }, 1155 | { 1156 | "id": 9, 1157 | "type": { 1158 | "def": { 1159 | "array": { 1160 | "len": 32, 1161 | "type": 2 1162 | } 1163 | } 1164 | } 1165 | }, 1166 | { 1167 | "id": 10, 1168 | "type": { 1169 | "def": { 1170 | "sequence": { 1171 | "type": 11 1172 | } 1173 | } 1174 | } 1175 | }, 1176 | { 1177 | "id": 11, 1178 | "type": { 1179 | "def": { 1180 | "tuple": [ 1181 | 1, 1182 | 8 1183 | ] 1184 | } 1185 | } 1186 | }, 1187 | { 1188 | "id": 12, 1189 | "type": { 1190 | "def": { 1191 | "composite": { 1192 | "fields": [ 1193 | { 1194 | "type": 16 1195 | } 1196 | ] 1197 | } 1198 | }, 1199 | "params": [ 1200 | { 1201 | "name": "K", 1202 | "type": 13 1203 | }, 1204 | { 1205 | "name": "V", 1206 | "type": 15 1207 | } 1208 | ], 1209 | "path": [ 1210 | "openbrush_lang", 1211 | "storage", 1212 | "mapping", 1213 | "Mapping" 1214 | ] 1215 | } 1216 | }, 1217 | { 1218 | "id": 13, 1219 | "type": { 1220 | "def": { 1221 | "tuple": [ 1222 | 8, 1223 | 8, 1224 | 14 1225 | ] 1226 | } 1227 | } 1228 | }, 1229 | { 1230 | "id": 14, 1231 | "type": { 1232 | "def": { 1233 | "variant": { 1234 | "variants": [ 1235 | { 1236 | "index": 0, 1237 | "name": "None" 1238 | }, 1239 | { 1240 | "fields": [ 1241 | { 1242 | "type": 1 1243 | } 1244 | ], 1245 | "index": 1, 1246 | "name": "Some" 1247 | } 1248 | ] 1249 | } 1250 | }, 1251 | "params": [ 1252 | { 1253 | "name": "T", 1254 | "type": 1 1255 | } 1256 | ], 1257 | "path": [ 1258 | "Option" 1259 | ] 1260 | } 1261 | }, 1262 | { 1263 | "id": 15, 1264 | "type": { 1265 | "def": { 1266 | "tuple": [] 1267 | } 1268 | } 1269 | }, 1270 | { 1271 | "id": 16, 1272 | "type": { 1273 | "def": { 1274 | "sequence": { 1275 | "type": 17 1276 | } 1277 | } 1278 | } 1279 | }, 1280 | { 1281 | "id": 17, 1282 | "type": { 1283 | "def": { 1284 | "tuple": [ 1285 | 13, 1286 | 15 1287 | ] 1288 | } 1289 | } 1290 | }, 1291 | { 1292 | "id": 18, 1293 | "type": { 1294 | "def": { 1295 | "composite": { 1296 | "fields": [ 1297 | { 1298 | "type": 20 1299 | } 1300 | ] 1301 | } 1302 | }, 1303 | "params": [ 1304 | { 1305 | "name": "K", 1306 | "type": 19 1307 | }, 1308 | { 1309 | "name": "V", 1310 | "type": 1 1311 | } 1312 | ], 1313 | "path": [ 1314 | "openbrush_lang", 1315 | "storage", 1316 | "multi_mapping", 1317 | "MultiMapping" 1318 | ] 1319 | } 1320 | }, 1321 | { 1322 | "id": 19, 1323 | "type": { 1324 | "def": { 1325 | "variant": { 1326 | "variants": [ 1327 | { 1328 | "index": 0, 1329 | "name": "None" 1330 | }, 1331 | { 1332 | "fields": [ 1333 | { 1334 | "type": 8 1335 | } 1336 | ], 1337 | "index": 1, 1338 | "name": "Some" 1339 | } 1340 | ] 1341 | } 1342 | }, 1343 | "params": [ 1344 | { 1345 | "name": "T", 1346 | "type": 8 1347 | } 1348 | ], 1349 | "path": [ 1350 | "Option" 1351 | ] 1352 | } 1353 | }, 1354 | { 1355 | "id": 20, 1356 | "type": { 1357 | "def": { 1358 | "sequence": { 1359 | "type": 21 1360 | } 1361 | } 1362 | } 1363 | }, 1364 | { 1365 | "id": 21, 1366 | "type": { 1367 | "def": { 1368 | "tuple": [ 1369 | 19, 1370 | 1 1371 | ] 1372 | } 1373 | } 1374 | }, 1375 | { 1376 | "id": 22, 1377 | "type": { 1378 | "def": { 1379 | "composite": { 1380 | "fields": [ 1381 | { 1382 | "type": 24 1383 | } 1384 | ] 1385 | } 1386 | }, 1387 | "params": [ 1388 | { 1389 | "name": "K", 1390 | "type": 23 1391 | }, 1392 | { 1393 | "name": "V", 1394 | "type": 7 1395 | } 1396 | ], 1397 | "path": [ 1398 | "openbrush_lang", 1399 | "storage", 1400 | "mapping", 1401 | "Mapping" 1402 | ] 1403 | } 1404 | }, 1405 | { 1406 | "id": 23, 1407 | "type": { 1408 | "def": { 1409 | "tuple": [ 1410 | 1, 1411 | 7 1412 | ] 1413 | } 1414 | } 1415 | }, 1416 | { 1417 | "id": 24, 1418 | "type": { 1419 | "def": { 1420 | "sequence": { 1421 | "type": 25 1422 | } 1423 | } 1424 | } 1425 | }, 1426 | { 1427 | "id": 25, 1428 | "type": { 1429 | "def": { 1430 | "tuple": [ 1431 | 23, 1432 | 7 1433 | ] 1434 | } 1435 | } 1436 | }, 1437 | { 1438 | "id": 26, 1439 | "type": { 1440 | "def": { 1441 | "primitive": "str" 1442 | } 1443 | } 1444 | }, 1445 | { 1446 | "id": 27, 1447 | "type": { 1448 | "def": { 1449 | "variant": { 1450 | "variants": [ 1451 | { 1452 | "fields": [ 1453 | { 1454 | "type": 15 1455 | } 1456 | ], 1457 | "index": 0, 1458 | "name": "Ok" 1459 | }, 1460 | { 1461 | "fields": [ 1462 | { 1463 | "type": 28 1464 | } 1465 | ], 1466 | "index": 1, 1467 | "name": "Err" 1468 | } 1469 | ] 1470 | } 1471 | }, 1472 | "params": [ 1473 | { 1474 | "name": "T", 1475 | "type": 15 1476 | }, 1477 | { 1478 | "name": "E", 1479 | "type": 28 1480 | } 1481 | ], 1482 | "path": [ 1483 | "Result" 1484 | ] 1485 | } 1486 | }, 1487 | { 1488 | "id": 28, 1489 | "type": { 1490 | "def": { 1491 | "variant": { 1492 | "variants": [ 1493 | { 1494 | "fields": [ 1495 | { 1496 | "type": 26, 1497 | "typeName": "String" 1498 | } 1499 | ], 1500 | "index": 0, 1501 | "name": "Custom" 1502 | }, 1503 | { 1504 | "index": 1, 1505 | "name": "SelfApprove" 1506 | }, 1507 | { 1508 | "index": 2, 1509 | "name": "NotApproved" 1510 | }, 1511 | { 1512 | "index": 3, 1513 | "name": "TokenExists" 1514 | }, 1515 | { 1516 | "index": 4, 1517 | "name": "TokenNotExists" 1518 | }, 1519 | { 1520 | "fields": [ 1521 | { 1522 | "type": 26, 1523 | "typeName": "String" 1524 | } 1525 | ], 1526 | "index": 5, 1527 | "name": "SafeTransferCheckFailed" 1528 | } 1529 | ] 1530 | } 1531 | }, 1532 | "path": [ 1533 | "openbrush_contracts", 1534 | "traits", 1535 | "errors", 1536 | "psp34", 1537 | "PSP34Error" 1538 | ] 1539 | } 1540 | }, 1541 | { 1542 | "id": 29, 1543 | "type": { 1544 | "def": { 1545 | "primitive": "bool" 1546 | } 1547 | } 1548 | }, 1549 | { 1550 | "id": 30, 1551 | "type": { 1552 | "def": { 1553 | "variant": { 1554 | "variants": [ 1555 | { 1556 | "fields": [ 1557 | { 1558 | "type": 1 1559 | } 1560 | ], 1561 | "index": 0, 1562 | "name": "Ok" 1563 | }, 1564 | { 1565 | "fields": [ 1566 | { 1567 | "type": 28 1568 | } 1569 | ], 1570 | "index": 1, 1571 | "name": "Err" 1572 | } 1573 | ] 1574 | } 1575 | }, 1576 | "params": [ 1577 | { 1578 | "name": "T", 1579 | "type": 1 1580 | }, 1581 | { 1582 | "name": "E", 1583 | "type": 28 1584 | } 1585 | ], 1586 | "path": [ 1587 | "Result" 1588 | ] 1589 | } 1590 | }, 1591 | { 1592 | "id": 31, 1593 | "type": { 1594 | "def": { 1595 | "variant": { 1596 | "variants": [ 1597 | { 1598 | "index": 0, 1599 | "name": "None" 1600 | }, 1601 | { 1602 | "fields": [ 1603 | { 1604 | "type": 7 1605 | } 1606 | ], 1607 | "index": 1, 1608 | "name": "Some" 1609 | } 1610 | ] 1611 | } 1612 | }, 1613 | "params": [ 1614 | { 1615 | "name": "T", 1616 | "type": 7 1617 | } 1618 | ], 1619 | "path": [ 1620 | "Option" 1621 | ] 1622 | } 1623 | }, 1624 | { 1625 | "id": 32, 1626 | "type": { 1627 | "def": { 1628 | "variant": { 1629 | "variants": [ 1630 | { 1631 | "fields": [ 1632 | { 1633 | "type": 15 1634 | } 1635 | ], 1636 | "index": 0, 1637 | "name": "Ok" 1638 | }, 1639 | { 1640 | "fields": [ 1641 | { 1642 | "type": 33 1643 | } 1644 | ], 1645 | "index": 1, 1646 | "name": "Err" 1647 | } 1648 | ] 1649 | } 1650 | }, 1651 | "params": [ 1652 | { 1653 | "name": "T", 1654 | "type": 15 1655 | }, 1656 | { 1657 | "name": "E", 1658 | "type": 33 1659 | } 1660 | ], 1661 | "path": [ 1662 | "Result" 1663 | ] 1664 | } 1665 | }, 1666 | { 1667 | "id": 33, 1668 | "type": { 1669 | "def": { 1670 | "variant": { 1671 | "variants": [ 1672 | { 1673 | "index": 0, 1674 | "name": "CallerIsNotOwner" 1675 | }, 1676 | { 1677 | "index": 1, 1678 | "name": "NewOwnerIsZero" 1679 | } 1680 | ] 1681 | } 1682 | }, 1683 | "path": [ 1684 | "openbrush_contracts", 1685 | "traits", 1686 | "errors", 1687 | "ownable", 1688 | "OwnableError" 1689 | ] 1690 | } 1691 | }, 1692 | { 1693 | "id": 34, 1694 | "type": { 1695 | "def": { 1696 | "variant": { 1697 | "variants": [ 1698 | { 1699 | "fields": [ 1700 | { 1701 | "type": 26 1702 | } 1703 | ], 1704 | "index": 0, 1705 | "name": "Ok" 1706 | }, 1707 | { 1708 | "fields": [ 1709 | { 1710 | "type": 28 1711 | } 1712 | ], 1713 | "index": 1, 1714 | "name": "Err" 1715 | } 1716 | ] 1717 | } 1718 | }, 1719 | "params": [ 1720 | { 1721 | "name": "T", 1722 | "type": 26 1723 | }, 1724 | { 1725 | "name": "E", 1726 | "type": 28 1727 | } 1728 | ], 1729 | "path": [ 1730 | "Result" 1731 | ] 1732 | } 1733 | } 1734 | ] 1735 | } 1736 | } -------------------------------------------------------------------------------- /ui/viewer/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | swcMinify: true, 5 | assetPrefix: "/viewer", 6 | trailingSlash: true, 7 | async rewrites() { 8 | return [ 9 | { 10 | source: "/viewer/api/:path*", 11 | destination: "/api/:path*", 12 | }, 13 | { 14 | source: "/viewer/images/:query*", 15 | destination: '/_next/image/:query*' 16 | }, 17 | { 18 | source: "/viewer/_next/:path*", 19 | destination: "/_next/:path*", 20 | }, 21 | ] 22 | } 23 | } 24 | 25 | module.exports = nextConfig 26 | -------------------------------------------------------------------------------- /ui/viewer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nftviewer_test", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build && next export", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@polkadot/api": "^9.4.3", 13 | "@polkadot/api-contract": "^9.4.3", 14 | "@polkadot/extension-dapp": "^0.44.6", 15 | "@polkadot/extension-inject": "^0.44.6", 16 | "axios": "^1.1.0", 17 | "next": "^12.3.1", 18 | "next-themes": "^0.2.1", 19 | "react": "18.2.0", 20 | "react-dom": "18.2.0" 21 | }, 22 | "devDependencies": { 23 | "@types/node": "18.11.7", 24 | "@types/react": "18.0.24", 25 | "@types/react-dom": "18.0.6", 26 | "autoprefixer": "^10.4.12", 27 | "eslint": "8.24.0", 28 | "eslint-config-next": "12.3.1", 29 | "postcss": "^8.4.17", 30 | "tailwindcss": "^3.1.8", 31 | "typescript": "4.8.4" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /ui/viewer/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import '../styles/globals.css' 2 | import type { AppProps } from 'next/app' 3 | import { ThemeProvider } from 'next-themes'; 4 | 5 | function MyApp({ Component, pageProps }: AppProps) { 6 | return ( 7 | 8 | 9 | 10 | ) 11 | } 12 | 13 | export default MyApp 14 | -------------------------------------------------------------------------------- /ui/viewer/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import Document, { DocumentContext, DocumentInitialProps } from 'next/document' 2 | 3 | class MyDocument extends Document { 4 | static async getInitialProps( 5 | ctx: DocumentContext 6 | ): Promise { 7 | const initialProps = await Document.getInitialProps(ctx) 8 | return initialProps 9 | } 10 | } 11 | 12 | export default MyDocument -------------------------------------------------------------------------------- /ui/viewer/pages/_error.tsx: -------------------------------------------------------------------------------- 1 | import { NextPage, NextPageContext } from 'next' 2 | import Error from 'next/error' 3 | interface Props { 4 | statusCode?: number 5 | } 6 | 7 | const ErrorPage: NextPage = ({ statusCode }) => { 8 | return statusCode ? ( 9 | 10 | ) : ( 11 |

An error occurred on client

12 | ) 13 | } 14 | 15 | ErrorPage.getInitialProps = ({ res, err }: NextPageContext) => { 16 | const statusCode = res ? res.statusCode : err ? err.statusCode : 404 17 | return { statusCode } 18 | } 19 | 20 | export default ErrorPage -------------------------------------------------------------------------------- /ui/viewer/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { NextPage } from 'next' 2 | import dynamic from 'next/dynamic' 3 | 4 | const Canvas = dynamic(() => import('../components/IndexCanvas'), { 5 | ssr: false, 6 | loading: () => 7 |

8 | Now loading... 9 |

, 10 | }) 11 | 12 | const IndexPage: NextPage = () => { 13 | return ( 14 |
15 | 16 |
17 | ) 18 | }; 19 | 20 | export default IndexPage; -------------------------------------------------------------------------------- /ui/viewer/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /ui/viewer/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inkdevhub/nft/8da48e4c3f84891878d85bf82d987ccdda3f5e73/ui/viewer/public/favicon.ico -------------------------------------------------------------------------------- /ui/viewer/public/icon_moon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 11 | 13 | 15 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /ui/viewer/public/icon_sun.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /ui/viewer/public/image-placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inkdevhub/nft/8da48e4c3f84891878d85bf82d987ccdda3f5e73/ui/viewer/public/image-placeholder.png -------------------------------------------------------------------------------- /ui/viewer/public/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inkdevhub/nft/8da48e4c3f84891878d85bf82d987ccdda3f5e73/ui/viewer/public/loading.gif -------------------------------------------------------------------------------- /ui/viewer/public/loading.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 20 | 21 | 22 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /ui/viewer/public/loading_default.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 9 | 10 | 11 | 12 | 14 | 15 | 16 | 17 | 19 | 20 | 21 | 22 | 24 | 25 | 26 | 27 | 29 | 30 | 31 | 32 | 35 | 36 | 37 | 38 | 40 | 41 | 42 | 43 | 45 | 46 | 47 | 48 | 50 | 51 | 52 | 53 | 55 | 56 | 57 | 58 | 60 | 61 | 62 | 63 | 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /ui/viewer/public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /ui/viewer/styles/Home.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | padding: 0 2rem; 3 | } 4 | 5 | .main { 6 | min-height: 100vh; 7 | padding: 4rem 0; 8 | flex: 1; 9 | display: flex; 10 | flex-direction: column; 11 | justify-content: center; 12 | align-items: center; 13 | } 14 | 15 | .footer { 16 | display: flex; 17 | flex: 1; 18 | padding: 2rem 0; 19 | border-top: 1px solid #eaeaea; 20 | justify-content: center; 21 | align-items: center; 22 | } 23 | 24 | .footer a { 25 | display: flex; 26 | justify-content: center; 27 | align-items: center; 28 | flex-grow: 1; 29 | } 30 | 31 | .title a { 32 | color: #0070f3; 33 | text-decoration: none; 34 | } 35 | 36 | .title a:hover, 37 | .title a:focus, 38 | .title a:active { 39 | text-decoration: underline; 40 | } 41 | 42 | .title { 43 | margin: 0; 44 | line-height: 1.15; 45 | font-size: 4rem; 46 | } 47 | 48 | .title, 49 | .description { 50 | text-align: center; 51 | } 52 | 53 | .description { 54 | margin: 4rem 0; 55 | line-height: 1.5; 56 | font-size: 1.5rem; 57 | } 58 | 59 | .code { 60 | background: #fafafa; 61 | border-radius: 5px; 62 | padding: 0.75rem; 63 | font-size: 1.1rem; 64 | font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, 65 | Bitstream Vera Sans Mono, Courier New, monospace; 66 | } 67 | 68 | .grid { 69 | display: flex; 70 | align-items: center; 71 | justify-content: center; 72 | flex-wrap: wrap; 73 | max-width: 800px; 74 | } 75 | 76 | .card { 77 | margin: 1rem; 78 | padding: 1.5rem; 79 | text-align: left; 80 | color: inherit; 81 | text-decoration: none; 82 | border: 1px solid #eaeaea; 83 | border-radius: 10px; 84 | transition: color 0.15s ease, border-color 0.15s ease; 85 | max-width: 300px; 86 | } 87 | 88 | .card:hover, 89 | .card:focus, 90 | .card:active { 91 | color: #0070f3; 92 | border-color: #0070f3; 93 | } 94 | 95 | .card h2 { 96 | margin: 0 0 1rem 0; 97 | font-size: 1.5rem; 98 | } 99 | 100 | .card p { 101 | margin: 0; 102 | font-size: 1.25rem; 103 | line-height: 1.5; 104 | } 105 | 106 | .logo { 107 | height: 1em; 108 | margin-left: 0.5rem; 109 | } 110 | 111 | @media (max-width: 600px) { 112 | .grid { 113 | width: 100%; 114 | flex-direction: column; 115 | } 116 | } 117 | 118 | @media (prefers-color-scheme: dark) { 119 | .card, 120 | .footer { 121 | border-color: #222; 122 | } 123 | .code { 124 | background: #111; 125 | } 126 | .logo img { 127 | filter: invert(1); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /ui/viewer/styles/globals.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 7 | } 8 | 9 | a { 10 | color: inherit; 11 | text-decoration: none; 12 | } 13 | 14 | * { 15 | box-sizing: border-box; 16 | } 17 | 18 | .nav-link-default-color { 19 | color: #888888; 20 | } 21 | 22 | /* 23 | @media (prefers-color-scheme: dark) { 24 | html { 25 | color-scheme: dark; 26 | } 27 | body { 28 | color: #f0eee0; 29 | background: #0d1117; 30 | } 31 | } 32 | @media (prefers-color-scheme: light) { 33 | html { 34 | color-scheme: light; 35 | } 36 | body { 37 | color: #1d2127; 38 | background: #ffffff; 39 | } 40 | } 41 | */ 42 | 43 | @tailwind base; 44 | @tailwind components; 45 | @tailwind utilities; 46 | -------------------------------------------------------------------------------- /ui/viewer/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | mode: 'jit', 4 | darkMode: 'class', 5 | content: [ 6 | "./pages/**/*.{js,ts,jsx,tsx}", 7 | "./components/**/*.{js,ts,jsx,tsx}", 8 | ], 9 | theme: { 10 | extend: { 11 | colors: { 12 | }, 13 | }, 14 | }, 15 | plugins: [], 16 | } -------------------------------------------------------------------------------- /ui/viewer/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | 8 | "strict": true, 9 | "strictPropertyInitialization": true, 10 | "strictNullChecks": true, 11 | 12 | "forceConsistentCasingInFileNames": true, 13 | "noEmit": true, 14 | "esModuleInterop": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "jsx": "preserve", 20 | "incremental": true 21 | }, 22 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 23 | "exclude": ["node_modules"] 24 | } 25 | --------------------------------------------------------------------------------