├── .github └── dependabot.yml ├── LICENSE ├── README.md ├── contracts ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── build.sh └── src │ ├── approval.rs │ ├── enumeration.rs │ ├── events.rs │ ├── internal.rs │ ├── lib.rs │ ├── metadata.rs │ ├── mint.rs │ ├── nft_core.rs │ └── royalty.rs └── dspyt ├── .env.example ├── .gitignore ├── README.md ├── config-overrides.js ├── package.json ├── public ├── assets │ └── images │ │ ├── Pin.png │ │ ├── PinSave.png │ │ ├── PinSaveB.png │ │ └── PinSaveL.png ├── index.html ├── manifest.json └── robots.txt ├── src ├── App.jsx ├── App.test.js ├── components │ ├── Home.jsx │ ├── Navigation.jsx │ ├── Post.jsx │ ├── PostCard.jsx │ ├── SavedPosts.jsx │ ├── Upload.jsx │ └── index.js ├── config.js ├── index.css ├── index.js ├── reportWebVitals.js ├── setupTests.js └── store.js └── tailwind.config.js /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | # Configuration for npm 9 | - package-ecosystem: "npm" 10 | directory: "/dspyt/" 11 | schedule: 12 | interval: "daily" 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Creative Commons Legal Code 2 | 3 | Attribution-ShareAlike 3.0 Unported 4 | 5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE 6 | LEGAL SERVICES. DISTRIBUTION OF THIS LICENSE DOES NOT CREATE AN 7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS 8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES 9 | REGARDING THE INFORMATION PROVIDED, AND DISCLAIMS LIABILITY FOR 10 | DAMAGES RESULTING FROM ITS USE. 11 | 12 | License 13 | 14 | THE WORK (AS DEFINED BELOW) IS PROVIDED UNDER THE TERMS OF THIS CREATIVE 15 | COMMONS PUBLIC LICENSE ("CCPL" OR "LICENSE"). THE WORK IS PROTECTED BY 16 | COPYRIGHT AND/OR OTHER APPLICABLE LAW. ANY USE OF THE WORK OTHER THAN AS 17 | AUTHORIZED UNDER THIS LICENSE OR COPYRIGHT LAW IS PROHIBITED. 18 | 19 | BY EXERCISING ANY RIGHTS TO THE WORK PROVIDED HERE, YOU ACCEPT AND AGREE 20 | TO BE BOUND BY THE TERMS OF THIS LICENSE. TO THE EXTENT THIS LICENSE MAY 21 | BE CONSIDERED TO BE A CONTRACT, THE LICENSOR GRANTS YOU THE RIGHTS 22 | CONTAINED HERE IN CONSIDERATION OF YOUR ACCEPTANCE OF SUCH TERMS AND 23 | CONDITIONS. 24 | 25 | 1. Definitions 26 | 27 | a. "Adaptation" means a work based upon the Work, or upon the Work and 28 | other pre-existing works, such as a translation, adaptation, 29 | derivative work, arrangement of music or other alterations of a 30 | literary or artistic work, or phonogram or performance and includes 31 | cinematographic adaptations or any other form in which the Work may be 32 | recast, transformed, or adapted including in any form recognizably 33 | derived from the original, except that a work that constitutes a 34 | Collection will not be considered an Adaptation for the purpose of 35 | this License. For the avoidance of doubt, where the Work is a musical 36 | work, performance or phonogram, the synchronization of the Work in 37 | timed-relation with a moving image ("synching") will be considered an 38 | Adaptation for the purpose of this License. 39 | b. "Collection" means a collection of literary or artistic works, such as 40 | encyclopedias and anthologies, or performances, phonograms or 41 | broadcasts, or other works or subject matter other than works listed 42 | in Section 1(f) below, which, by reason of the selection and 43 | arrangement of their contents, constitute intellectual creations, in 44 | which the Work is included in its entirety in unmodified form along 45 | with one or more other contributions, each constituting separate and 46 | independent works in themselves, which together are assembled into a 47 | collective whole. A work that constitutes a Collection will not be 48 | considered an Adaptation (as defined below) for the purposes of this 49 | License. 50 | c. "Creative Commons Compatible License" means a license that is listed 51 | at https://creativecommons.org/compatiblelicenses that has been 52 | approved by Creative Commons as being essentially equivalent to this 53 | License, including, at a minimum, because that license: (i) contains 54 | terms that have the same purpose, meaning and effect as the License 55 | Elements of this License; and, (ii) explicitly permits the relicensing 56 | of adaptations of works made available under that license under this 57 | License or a Creative Commons jurisdiction license with the same 58 | License Elements as this License. 59 | d. "Distribute" means to make available to the public the original and 60 | copies of the Work or Adaptation, as appropriate, through sale or 61 | other transfer of ownership. 62 | e. "License Elements" means the following high-level license attributes 63 | as selected by Licensor and indicated in the title of this License: 64 | Attribution, ShareAlike. 65 | f. "Licensor" means the individual, individuals, entity or entities that 66 | offer(s) the Work under the terms of this License. 67 | g. "Original Author" means, in the case of a literary or artistic work, 68 | the individual, individuals, entity or entities who created the Work 69 | or if no individual or entity can be identified, the publisher; and in 70 | addition (i) in the case of a performance the actors, singers, 71 | musicians, dancers, and other persons who act, sing, deliver, declaim, 72 | play in, interpret or otherwise perform literary or artistic works or 73 | expressions of folklore; (ii) in the case of a phonogram the producer 74 | being the person or legal entity who first fixes the sounds of a 75 | performance or other sounds; and, (iii) in the case of broadcasts, the 76 | organization that transmits the broadcast. 77 | h. "Work" means the literary and/or artistic work offered under the terms 78 | of this License including without limitation any production in the 79 | literary, scientific and artistic domain, whatever may be the mode or 80 | form of its expression including digital form, such as a book, 81 | pamphlet and other writing; a lecture, address, sermon or other work 82 | of the same nature; a dramatic or dramatico-musical work; a 83 | choreographic work or entertainment in dumb show; a musical 84 | composition with or without words; a cinematographic work to which are 85 | assimilated works expressed by a process analogous to cinematography; 86 | a work of drawing, painting, architecture, sculpture, engraving or 87 | lithography; a photographic work to which are assimilated works 88 | expressed by a process analogous to photography; a work of applied 89 | art; an illustration, map, plan, sketch or three-dimensional work 90 | relative to geography, topography, architecture or science; a 91 | performance; a broadcast; a phonogram; a compilation of data to the 92 | extent it is protected as a copyrightable work; or a work performed by 93 | a variety or circus performer to the extent it is not otherwise 94 | considered a literary or artistic work. 95 | i. "You" means an individual or entity exercising rights under this 96 | License who has not previously violated the terms of this License with 97 | respect to the Work, or who has received express permission from the 98 | Licensor to exercise rights under this License despite a previous 99 | violation. 100 | j. "Publicly Perform" means to perform public recitations of the Work and 101 | to communicate to the public those public recitations, by any means or 102 | process, including by wire or wireless means or public digital 103 | performances; to make available to the public Works in such a way that 104 | members of the public may access these Works from a place and at a 105 | place individually chosen by them; to perform the Work to the public 106 | by any means or process and the communication to the public of the 107 | performances of the Work, including by public digital performance; to 108 | broadcast and rebroadcast the Work by any means including signs, 109 | sounds or images. 110 | k. "Reproduce" means to make copies of the Work by any means including 111 | without limitation by sound or visual recordings and the right of 112 | fixation and reproducing fixations of the Work, including storage of a 113 | protected performance or phonogram in digital form or other electronic 114 | medium. 115 | 116 | 2. Fair Dealing Rights. Nothing in this License is intended to reduce, 117 | limit, or restrict any uses free from copyright or rights arising from 118 | limitations or exceptions that are provided for in connection with the 119 | copyright protection under copyright law or other applicable laws. 120 | 121 | 3. License Grant. Subject to the terms and conditions of this License, 122 | Licensor hereby grants You a worldwide, royalty-free, non-exclusive, 123 | perpetual (for the duration of the applicable copyright) license to 124 | exercise the rights in the Work as stated below: 125 | 126 | a. to Reproduce the Work, to incorporate the Work into one or more 127 | Collections, and to Reproduce the Work as incorporated in the 128 | Collections; 129 | b. to create and Reproduce Adaptations provided that any such Adaptation, 130 | including any translation in any medium, takes reasonable steps to 131 | clearly label, demarcate or otherwise identify that changes were made 132 | to the original Work. For example, a translation could be marked "The 133 | original work was translated from English to Spanish," or a 134 | modification could indicate "The original work has been modified."; 135 | c. to Distribute and Publicly Perform the Work including as incorporated 136 | in Collections; and, 137 | d. to Distribute and Publicly Perform Adaptations. 138 | e. For the avoidance of doubt: 139 | 140 | i. Non-waivable Compulsory License Schemes. In those jurisdictions in 141 | which the right to collect royalties through any statutory or 142 | compulsory licensing scheme cannot be waived, the Licensor 143 | reserves the exclusive right to collect such royalties for any 144 | exercise by You of the rights granted under this License; 145 | ii. Waivable Compulsory License Schemes. In those jurisdictions in 146 | which the right to collect royalties through any statutory or 147 | compulsory licensing scheme can be waived, the Licensor waives the 148 | exclusive right to collect such royalties for any exercise by You 149 | of the rights granted under this License; and, 150 | iii. Voluntary License Schemes. The Licensor waives the right to 151 | collect royalties, whether individually or, in the event that the 152 | Licensor is a member of a collecting society that administers 153 | voluntary licensing schemes, via that society, from any exercise 154 | by You of the rights granted under this License. 155 | 156 | The above rights may be exercised in all media and formats whether now 157 | known or hereafter devised. The above rights include the right to make 158 | such modifications as are technically necessary to exercise the rights in 159 | other media and formats. Subject to Section 8(f), all rights not expressly 160 | granted by Licensor are hereby reserved. 161 | 162 | 4. Restrictions. The license granted in Section 3 above is expressly made 163 | subject to and limited by the following restrictions: 164 | 165 | a. You may Distribute or Publicly Perform the Work only under the terms 166 | of this License. You must include a copy of, or the Uniform Resource 167 | Identifier (URI) for, this License with every copy of the Work You 168 | Distribute or Publicly Perform. You may not offer or impose any terms 169 | on the Work that restrict the terms of this License or the ability of 170 | the recipient of the Work to exercise the rights granted to that 171 | recipient under the terms of the License. You may not sublicense the 172 | Work. You must keep intact all notices that refer to this License and 173 | to the disclaimer of warranties with every copy of the Work You 174 | Distribute or Publicly Perform. When You Distribute or Publicly 175 | Perform the Work, You may not impose any effective technological 176 | measures on the Work that restrict the ability of a recipient of the 177 | Work from You to exercise the rights granted to that recipient under 178 | the terms of the License. This Section 4(a) applies to the Work as 179 | incorporated in a Collection, but this does not require the Collection 180 | apart from the Work itself to be made subject to the terms of this 181 | License. If You create a Collection, upon notice from any Licensor You 182 | must, to the extent practicable, remove from the Collection any credit 183 | as required by Section 4(c), as requested. If You create an 184 | Adaptation, upon notice from any Licensor You must, to the extent 185 | practicable, remove from the Adaptation any credit as required by 186 | Section 4(c), as requested. 187 | b. You may Distribute or Publicly Perform an Adaptation only under the 188 | terms of: (i) this License; (ii) a later version of this License with 189 | the same License Elements as this License; (iii) a Creative Commons 190 | jurisdiction license (either this or a later license version) that 191 | contains the same License Elements as this License (e.g., 192 | Attribution-ShareAlike 3.0 US)); (iv) a Creative Commons Compatible 193 | License. If you license the Adaptation under one of the licenses 194 | mentioned in (iv), you must comply with the terms of that license. If 195 | you license the Adaptation under the terms of any of the licenses 196 | mentioned in (i), (ii) or (iii) (the "Applicable License"), you must 197 | comply with the terms of the Applicable License generally and the 198 | following provisions: (I) You must include a copy of, or the URI for, 199 | the Applicable License with every copy of each Adaptation You 200 | Distribute or Publicly Perform; (II) You may not offer or impose any 201 | terms on the Adaptation that restrict the terms of the Applicable 202 | License or the ability of the recipient of the Adaptation to exercise 203 | the rights granted to that recipient under the terms of the Applicable 204 | License; (III) You must keep intact all notices that refer to the 205 | Applicable License and to the disclaimer of warranties with every copy 206 | of the Work as included in the Adaptation You Distribute or Publicly 207 | Perform; (IV) when You Distribute or Publicly Perform the Adaptation, 208 | You may not impose any effective technological measures on the 209 | Adaptation that restrict the ability of a recipient of the Adaptation 210 | from You to exercise the rights granted to that recipient under the 211 | terms of the Applicable License. This Section 4(b) applies to the 212 | Adaptation as incorporated in a Collection, but this does not require 213 | the Collection apart from the Adaptation itself to be made subject to 214 | the terms of the Applicable License. 215 | c. If You Distribute, or Publicly Perform the Work or any Adaptations or 216 | Collections, You must, unless a request has been made pursuant to 217 | Section 4(a), keep intact all copyright notices for the Work and 218 | provide, reasonable to the medium or means You are utilizing: (i) the 219 | name of the Original Author (or pseudonym, if applicable) if supplied, 220 | and/or if the Original Author and/or Licensor designate another party 221 | or parties (e.g., a sponsor institute, publishing entity, journal) for 222 | attribution ("Attribution Parties") in Licensor's copyright notice, 223 | terms of service or by other reasonable means, the name of such party 224 | or parties; (ii) the title of the Work if supplied; (iii) to the 225 | extent reasonably practicable, the URI, if any, that Licensor 226 | specifies to be associated with the Work, unless such URI does not 227 | refer to the copyright notice or licensing information for the Work; 228 | and (iv) , consistent with Ssection 3(b), in the case of an 229 | Adaptation, a credit identifying the use of the Work in the Adaptation 230 | (e.g., "French translation of the Work by Original Author," or 231 | "Screenplay based on original Work by Original Author"). The credit 232 | required by this Section 4(c) may be implemented in any reasonable 233 | manner; provided, however, that in the case of a Adaptation or 234 | Collection, at a minimum such credit will appear, if a credit for all 235 | contributing authors of the Adaptation or Collection appears, then as 236 | part of these credits and in a manner at least as prominent as the 237 | credits for the other contributing authors. For the avoidance of 238 | doubt, You may only use the credit required by this Section for the 239 | purpose of attribution in the manner set out above and, by exercising 240 | Your rights under this License, You may not implicitly or explicitly 241 | assert or imply any connection with, sponsorship or endorsement by the 242 | Original Author, Licensor and/or Attribution Parties, as appropriate, 243 | of You or Your use of the Work, without the separate, express prior 244 | written permission of the Original Author, Licensor and/or Attribution 245 | Parties. 246 | d. Except as otherwise agreed in writing by the Licensor or as may be 247 | otherwise permitted by applicable law, if You Reproduce, Distribute or 248 | Publicly Perform the Work either by itself or as part of any 249 | Adaptations or Collections, You must not distort, mutilate, modify or 250 | take other derogatory action in relation to the Work which would be 251 | prejudicial to the Original Author's honor or reputation. Licensor 252 | agrees that in those jurisdictions (e.g. Japan), in which any exercise 253 | of the right granted in Section 3(b) of this License (the right to 254 | make Adaptations) would be deemed to be a distortion, mutilation, 255 | modification or other derogatory action prejudicial to the Original 256 | Author's honor and reputation, the Licensor will waive or not assert, 257 | as appropriate, this Section, to the fullest extent permitted by the 258 | applicable national law, to enable You to reasonably exercise Your 259 | right under Section 3(b) of this License (right to make Adaptations) 260 | but not otherwise. 261 | 262 | 5. Representations, Warranties and Disclaimer 263 | 264 | UNLESS OTHERWISE MUTUALLY AGREED TO BY THE PARTIES IN WRITING, LICENSOR 265 | OFFERS THE WORK AS-IS AND MAKES NO REPRESENTATIONS OR WARRANTIES OF ANY 266 | KIND CONCERNING THE WORK, EXPRESS, IMPLIED, STATUTORY OR OTHERWISE, 267 | INCLUDING, WITHOUT LIMITATION, WARRANTIES OF TITLE, MERCHANTIBILITY, 268 | FITNESS FOR A PARTICULAR PURPOSE, NONINFRINGEMENT, OR THE ABSENCE OF 269 | LATENT OR OTHER DEFECTS, ACCURACY, OR THE PRESENCE OF ABSENCE OF ERRORS, 270 | WHETHER OR NOT DISCOVERABLE. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION 271 | OF IMPLIED WARRANTIES, SO SUCH EXCLUSION MAY NOT APPLY TO YOU. 272 | 273 | 6. Limitation on Liability. EXCEPT TO THE EXTENT REQUIRED BY APPLICABLE 274 | LAW, IN NO EVENT WILL LICENSOR BE LIABLE TO YOU ON ANY LEGAL THEORY FOR 275 | ANY SPECIAL, INCIDENTAL, CONSEQUENTIAL, PUNITIVE OR EXEMPLARY DAMAGES 276 | ARISING OUT OF THIS LICENSE OR THE USE OF THE WORK, EVEN IF LICENSOR HAS 277 | BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 278 | 279 | 7. Termination 280 | 281 | a. This License and the rights granted hereunder will terminate 282 | automatically upon any breach by You of the terms of this License. 283 | Individuals or entities who have received Adaptations or Collections 284 | from You under this License, however, will not have their licenses 285 | terminated provided such individuals or entities remain in full 286 | compliance with those licenses. Sections 1, 2, 5, 6, 7, and 8 will 287 | survive any termination of this License. 288 | b. Subject to the above terms and conditions, the license granted here is 289 | perpetual (for the duration of the applicable copyright in the Work). 290 | Notwithstanding the above, Licensor reserves the right to release the 291 | Work under different license terms or to stop distributing the Work at 292 | any time; provided, however that any such election will not serve to 293 | withdraw this License (or any other license that has been, or is 294 | required to be, granted under the terms of this License), and this 295 | License will continue in full force and effect unless terminated as 296 | stated above. 297 | 298 | 8. Miscellaneous 299 | 300 | a. Each time You Distribute or Publicly Perform the Work or a Collection, 301 | the Licensor offers to the recipient a license to the Work on the same 302 | terms and conditions as the license granted to You under this License. 303 | b. Each time You Distribute or Publicly Perform an Adaptation, Licensor 304 | offers to the recipient a license to the original Work on the same 305 | terms and conditions as the license granted to You under this License. 306 | c. If any provision of this License is invalid or unenforceable under 307 | applicable law, it shall not affect the validity or enforceability of 308 | the remainder of the terms of this License, and without further action 309 | by the parties to this agreement, such provision shall be reformed to 310 | the minimum extent necessary to make such provision valid and 311 | enforceable. 312 | d. No term or provision of this License shall be deemed waived and no 313 | breach consented to unless such waiver or consent shall be in writing 314 | and signed by the party to be charged with such waiver or consent. 315 | e. This License constitutes the entire agreement between the parties with 316 | respect to the Work licensed here. There are no understandings, 317 | agreements or representations with respect to the Work not specified 318 | here. Licensor shall not be bound by any additional provisions that 319 | may appear in any communication from You. This License may not be 320 | modified without the mutual written agreement of the Licensor and You. 321 | f. The rights granted under, and the subject matter referenced, in this 322 | License were drafted utilizing the terminology of the Berne Convention 323 | for the Protection of Literary and Artistic Works (as amended on 324 | September 28, 1979), the Rome Convention of 1961, the WIPO Copyright 325 | Treaty of 1996, the WIPO Performances and Phonograms Treaty of 1996 326 | and the Universal Copyright Convention (as revised on July 24, 1971). 327 | These rights and subject matter take effect in the relevant 328 | jurisdiction in which the License terms are sought to be enforced 329 | according to the corresponding provisions of the implementation of 330 | those treaty provisions in the applicable national law. If the 331 | standard suite of rights granted under applicable copyright law 332 | includes additional rights not granted under this License, such 333 | additional rights are deemed to be included in the License; this 334 | License is not intended to restrict the license of any rights under 335 | applicable law. 336 | 337 | 338 | Creative Commons Notice 339 | 340 | Creative Commons is not a party to this License, and makes no warranty 341 | whatsoever in connection with the Work. Creative Commons will not be 342 | liable to You or any party on any legal theory for any damages 343 | whatsoever, including without limitation any general, special, 344 | incidental or consequential damages arising in connection to this 345 | license. Notwithstanding the foregoing two (2) sentences, if Creative 346 | Commons has expressly identified itself as the Licensor hereunder, it 347 | shall have all rights and obligations of Licensor. 348 | 349 | Except for the limited purpose of indicating to the public that the 350 | Work is licensed under the CCPL, Creative Commons does not authorize 351 | the use by either party of the trademark "Creative Commons" or any 352 | related trademark or logo of Creative Commons without the prior 353 | written consent of Creative Commons. Any permitted use will be in 354 | compliance with Creative Commons' then-current trademark usage 355 | guidelines, as may be published on its website or otherwise made 356 | available upon request from time to time. For the avoidance of doubt, 357 | this trademark restriction does not form part of the License. 358 | 359 | Creative Commons may be contacted at https://creativecommons.org/. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PinSave 2 | 3 | ## Project Description 4 | 5 | Pin Save is a decentralized image sharing and content aggregation platform where users can not only control the content, but also the platform itself. 6 | 7 | Developed as part of [Encode Filecoin Accelerator](https://medium.com/encode-club/announcing-the-encode-filecoin-accelerator-c55f09264e8c) 8 | 9 | ## Previews 10 | 11 | [Live Demo](https://pinsave.app/) 12 | 13 | ## Demo 14 | 15 | [YouTube](https://www.youtube.com/watch?v=B78cZ-UvuIU) 16 | 17 | ## Team Info 18 | 19 | ### Team members 20 | 21 | [Pavel Fedotov](https://github.com/Pfed-prog) 22 | 23 | [Grigore Gabriel Trifan](https://github.com/GregTrifan) 24 | 25 | ## How the community can engage 26 | 27 | [GitHub Filecoin Discussion](https://github.com/filecoin-project/community/discussions/466) 28 | 29 | [GitHub IPFS Discussion](https://github.com/ipfs/community/discussions/738) 30 | 31 | [Twitter](https://twitter.com/PinSav3) 32 | 33 | [Discord](https://discord.gg/peRHyNZrss) 34 | 35 | ## How to Contribute 36 | 37 | Contact us on Social Media and/or open an Issue on GitHub. 38 | 39 | We really appreciate your feedback. 40 | -------------------------------------------------------------------------------- /contracts/.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | out/ -------------------------------------------------------------------------------- /contracts/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "Inflector" 7 | version = "0.11.4" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" 10 | 11 | [[package]] 12 | name = "ahash" 13 | version = "0.4.7" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "739f4a8db6605981345c5654f3a85b056ce52f37a39d34da03f25bf2151ea16e" 16 | 17 | [[package]] 18 | name = "aho-corasick" 19 | version = "0.7.18" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" 22 | dependencies = [ 23 | "memchr", 24 | ] 25 | 26 | [[package]] 27 | name = "autocfg" 28 | version = "1.1.0" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 31 | 32 | [[package]] 33 | name = "base64" 34 | version = "0.13.0" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" 37 | 38 | [[package]] 39 | name = "block-buffer" 40 | version = "0.9.0" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" 43 | dependencies = [ 44 | "block-padding", 45 | "generic-array", 46 | ] 47 | 48 | [[package]] 49 | name = "block-padding" 50 | version = "0.2.1" 51 | source = "registry+https://github.com/rust-lang/crates.io-index" 52 | checksum = "8d696c370c750c948ada61c69a0ee2cbbb9c50b1019ddb86d9317157a99c2cae" 53 | 54 | [[package]] 55 | name = "borsh" 56 | version = "0.8.2" 57 | source = "registry+https://github.com/rust-lang/crates.io-index" 58 | checksum = "09a7111f797cc721407885a323fb071636aee57f750b1a4ddc27397eba168a74" 59 | dependencies = [ 60 | "borsh-derive", 61 | "hashbrown 0.9.1", 62 | ] 63 | 64 | [[package]] 65 | name = "borsh-derive" 66 | version = "0.8.2" 67 | source = "registry+https://github.com/rust-lang/crates.io-index" 68 | checksum = "307f3740906bac2c118a8122fe22681232b244f1369273e45f1156b45c43d2dd" 69 | dependencies = [ 70 | "borsh-derive-internal", 71 | "borsh-schema-derive-internal", 72 | "proc-macro-crate", 73 | "proc-macro2", 74 | "syn", 75 | ] 76 | 77 | [[package]] 78 | name = "borsh-derive-internal" 79 | version = "0.8.2" 80 | source = "registry+https://github.com/rust-lang/crates.io-index" 81 | checksum = "d2104c73179359431cc98e016998f2f23bc7a05bc53e79741bcba705f30047bc" 82 | dependencies = [ 83 | "proc-macro2", 84 | "quote", 85 | "syn", 86 | ] 87 | 88 | [[package]] 89 | name = "borsh-schema-derive-internal" 90 | version = "0.8.2" 91 | source = "registry+https://github.com/rust-lang/crates.io-index" 92 | checksum = "ae29eb8418fcd46f723f8691a2ac06857d31179d33d2f2d91eb13967de97c728" 93 | dependencies = [ 94 | "proc-macro2", 95 | "quote", 96 | "syn", 97 | ] 98 | 99 | [[package]] 100 | name = "bs58" 101 | version = "0.4.0" 102 | source = "registry+https://github.com/rust-lang/crates.io-index" 103 | checksum = "771fe0050b883fcc3ea2359b1a96bcfbc090b7116eae7c3c512c7a083fdf23d3" 104 | 105 | [[package]] 106 | name = "byteorder" 107 | version = "1.4.3" 108 | source = "registry+https://github.com/rust-lang/crates.io-index" 109 | checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" 110 | 111 | [[package]] 112 | name = "cfg-if" 113 | version = "0.1.10" 114 | source = "registry+https://github.com/rust-lang/crates.io-index" 115 | checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" 116 | 117 | [[package]] 118 | name = "cfg-if" 119 | version = "1.0.0" 120 | source = "registry+https://github.com/rust-lang/crates.io-index" 121 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 122 | 123 | [[package]] 124 | name = "contracts" 125 | version = "0.1.0" 126 | dependencies = [ 127 | "near-sdk", 128 | "serde_json", 129 | ] 130 | 131 | [[package]] 132 | name = "convert_case" 133 | version = "0.4.0" 134 | source = "registry+https://github.com/rust-lang/crates.io-index" 135 | checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" 136 | 137 | [[package]] 138 | name = "cpufeatures" 139 | version = "0.2.1" 140 | source = "registry+https://github.com/rust-lang/crates.io-index" 141 | checksum = "95059428f66df56b63431fdb4e1947ed2190586af5c5a8a8b71122bdf5a7f469" 142 | dependencies = [ 143 | "libc", 144 | ] 145 | 146 | [[package]] 147 | name = "derive_more" 148 | version = "0.99.17" 149 | source = "registry+https://github.com/rust-lang/crates.io-index" 150 | checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" 151 | dependencies = [ 152 | "convert_case", 153 | "proc-macro2", 154 | "quote", 155 | "rustc_version", 156 | "syn", 157 | ] 158 | 159 | [[package]] 160 | name = "digest" 161 | version = "0.9.0" 162 | source = "registry+https://github.com/rust-lang/crates.io-index" 163 | checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" 164 | dependencies = [ 165 | "generic-array", 166 | ] 167 | 168 | [[package]] 169 | name = "generic-array" 170 | version = "0.14.5" 171 | source = "registry+https://github.com/rust-lang/crates.io-index" 172 | checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803" 173 | dependencies = [ 174 | "typenum", 175 | "version_check", 176 | ] 177 | 178 | [[package]] 179 | name = "hashbrown" 180 | version = "0.9.1" 181 | source = "registry+https://github.com/rust-lang/crates.io-index" 182 | checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" 183 | dependencies = [ 184 | "ahash", 185 | ] 186 | 187 | [[package]] 188 | name = "hashbrown" 189 | version = "0.11.2" 190 | source = "registry+https://github.com/rust-lang/crates.io-index" 191 | checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" 192 | 193 | [[package]] 194 | name = "hex" 195 | version = "0.4.3" 196 | source = "registry+https://github.com/rust-lang/crates.io-index" 197 | checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" 198 | 199 | [[package]] 200 | name = "indexmap" 201 | version = "1.8.0" 202 | source = "registry+https://github.com/rust-lang/crates.io-index" 203 | checksum = "282a6247722caba404c065016bbfa522806e51714c34f5dfc3e4a3a46fcb4223" 204 | dependencies = [ 205 | "autocfg", 206 | "hashbrown 0.11.2", 207 | ] 208 | 209 | [[package]] 210 | name = "itoa" 211 | version = "1.0.1" 212 | source = "registry+https://github.com/rust-lang/crates.io-index" 213 | checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" 214 | 215 | [[package]] 216 | name = "keccak" 217 | version = "0.1.0" 218 | source = "registry+https://github.com/rust-lang/crates.io-index" 219 | checksum = "67c21572b4949434e4fc1e1978b99c5f77064153c59d998bf13ecd96fb5ecba7" 220 | 221 | [[package]] 222 | name = "lazy_static" 223 | version = "1.4.0" 224 | source = "registry+https://github.com/rust-lang/crates.io-index" 225 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 226 | 227 | [[package]] 228 | name = "libc" 229 | version = "0.2.119" 230 | source = "registry+https://github.com/rust-lang/crates.io-index" 231 | checksum = "1bf2e165bb3457c8e098ea76f3e3bc9db55f87aa90d52d0e6be741470916aaa4" 232 | 233 | [[package]] 234 | name = "memchr" 235 | version = "2.4.1" 236 | source = "registry+https://github.com/rust-lang/crates.io-index" 237 | checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" 238 | 239 | [[package]] 240 | name = "memory_units" 241 | version = "0.4.0" 242 | source = "registry+https://github.com/rust-lang/crates.io-index" 243 | checksum = "8452105ba047068f40ff7093dd1d9da90898e63dd61736462e9cdda6a90ad3c3" 244 | 245 | [[package]] 246 | name = "near-primitives-core" 247 | version = "0.4.0" 248 | source = "registry+https://github.com/rust-lang/crates.io-index" 249 | checksum = "c2b3fb5acf3a494aed4e848446ef2d6ebb47dbe91c681105d4d1786c2ee63e52" 250 | dependencies = [ 251 | "base64", 252 | "borsh", 253 | "bs58", 254 | "derive_more", 255 | "hex", 256 | "lazy_static", 257 | "num-rational", 258 | "serde", 259 | "serde_json", 260 | "sha2", 261 | ] 262 | 263 | [[package]] 264 | name = "near-rpc-error-core" 265 | version = "0.1.0" 266 | source = "registry+https://github.com/rust-lang/crates.io-index" 267 | checksum = "ffa8dbf8437a28ac40fcb85859ab0d0b8385013935b000c7a51ae79631dd74d9" 268 | dependencies = [ 269 | "proc-macro2", 270 | "quote", 271 | "serde", 272 | "serde_json", 273 | "syn", 274 | ] 275 | 276 | [[package]] 277 | name = "near-rpc-error-macro" 278 | version = "0.1.0" 279 | source = "registry+https://github.com/rust-lang/crates.io-index" 280 | checksum = "0c6111d713e90c7c551dee937f4a06cb9ea2672243455a4454cc7566387ba2d9" 281 | dependencies = [ 282 | "near-rpc-error-core", 283 | "proc-macro2", 284 | "quote", 285 | "serde", 286 | "serde_json", 287 | "syn", 288 | ] 289 | 290 | [[package]] 291 | name = "near-runtime-utils" 292 | version = "4.0.0-pre.1" 293 | source = "registry+https://github.com/rust-lang/crates.io-index" 294 | checksum = "a48d80c4ca1d4cf99bc16490e1e3d49826c150dfc4410ac498918e45c7d98e07" 295 | dependencies = [ 296 | "lazy_static", 297 | "regex", 298 | ] 299 | 300 | [[package]] 301 | name = "near-sdk" 302 | version = "4.0.0-pre.4" 303 | source = "registry+https://github.com/rust-lang/crates.io-index" 304 | checksum = "10f570e260eae9169dd5ffe0764728b872c2df79b8786dfb5e57183bd383908e" 305 | dependencies = [ 306 | "base64", 307 | "borsh", 308 | "bs58", 309 | "near-primitives-core", 310 | "near-sdk-macros", 311 | "near-sys", 312 | "near-vm-logic", 313 | "serde", 314 | "serde_json", 315 | "wee_alloc", 316 | ] 317 | 318 | [[package]] 319 | name = "near-sdk-macros" 320 | version = "4.0.0-pre.4" 321 | source = "registry+https://github.com/rust-lang/crates.io-index" 322 | checksum = "65ddca18086f1ab14ce8541e0c23f503a322d45914f2fe08f571844045d32bde" 323 | dependencies = [ 324 | "Inflector", 325 | "proc-macro2", 326 | "quote", 327 | "syn", 328 | ] 329 | 330 | [[package]] 331 | name = "near-sys" 332 | version = "0.1.0" 333 | source = "registry+https://github.com/rust-lang/crates.io-index" 334 | checksum = "f6a7aa3f46fac44416d8a93d14f30a562c4d730a1c6bf14bffafab5f475c244a" 335 | 336 | [[package]] 337 | name = "near-vm-errors" 338 | version = "4.0.0-pre.1" 339 | source = "registry+https://github.com/rust-lang/crates.io-index" 340 | checksum = "e281d8730ed8cb0e3e69fb689acee6b93cdb43824cd69a8ffd7e1bfcbd1177d7" 341 | dependencies = [ 342 | "borsh", 343 | "hex", 344 | "near-rpc-error-macro", 345 | "serde", 346 | ] 347 | 348 | [[package]] 349 | name = "near-vm-logic" 350 | version = "4.0.0-pre.1" 351 | source = "registry+https://github.com/rust-lang/crates.io-index" 352 | checksum = "e11cb28a2d07f37680efdaf860f4c9802828c44fc50c08009e7884de75d982c5" 353 | dependencies = [ 354 | "base64", 355 | "borsh", 356 | "bs58", 357 | "byteorder", 358 | "near-primitives-core", 359 | "near-runtime-utils", 360 | "near-vm-errors", 361 | "serde", 362 | "sha2", 363 | "sha3", 364 | ] 365 | 366 | [[package]] 367 | name = "num-bigint" 368 | version = "0.3.3" 369 | source = "registry+https://github.com/rust-lang/crates.io-index" 370 | checksum = "5f6f7833f2cbf2360a6cfd58cd41a53aa7a90bd4c202f5b1c7dd2ed73c57b2c3" 371 | dependencies = [ 372 | "autocfg", 373 | "num-integer", 374 | "num-traits", 375 | ] 376 | 377 | [[package]] 378 | name = "num-integer" 379 | version = "0.1.44" 380 | source = "registry+https://github.com/rust-lang/crates.io-index" 381 | checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" 382 | dependencies = [ 383 | "autocfg", 384 | "num-traits", 385 | ] 386 | 387 | [[package]] 388 | name = "num-rational" 389 | version = "0.3.2" 390 | source = "registry+https://github.com/rust-lang/crates.io-index" 391 | checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07" 392 | dependencies = [ 393 | "autocfg", 394 | "num-bigint", 395 | "num-integer", 396 | "num-traits", 397 | "serde", 398 | ] 399 | 400 | [[package]] 401 | name = "num-traits" 402 | version = "0.2.14" 403 | source = "registry+https://github.com/rust-lang/crates.io-index" 404 | checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" 405 | dependencies = [ 406 | "autocfg", 407 | ] 408 | 409 | [[package]] 410 | name = "opaque-debug" 411 | version = "0.3.0" 412 | source = "registry+https://github.com/rust-lang/crates.io-index" 413 | checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" 414 | 415 | [[package]] 416 | name = "proc-macro-crate" 417 | version = "0.1.5" 418 | source = "registry+https://github.com/rust-lang/crates.io-index" 419 | checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" 420 | dependencies = [ 421 | "toml", 422 | ] 423 | 424 | [[package]] 425 | name = "proc-macro2" 426 | version = "1.0.36" 427 | source = "registry+https://github.com/rust-lang/crates.io-index" 428 | checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029" 429 | dependencies = [ 430 | "unicode-xid", 431 | ] 432 | 433 | [[package]] 434 | name = "quote" 435 | version = "1.0.15" 436 | source = "registry+https://github.com/rust-lang/crates.io-index" 437 | checksum = "864d3e96a899863136fc6e99f3d7cae289dafe43bf2c5ac19b70df7210c0a145" 438 | dependencies = [ 439 | "proc-macro2", 440 | ] 441 | 442 | [[package]] 443 | name = "regex" 444 | version = "1.5.6" 445 | source = "registry+https://github.com/rust-lang/crates.io-index" 446 | checksum = "d83f127d94bdbcda4c8cc2e50f6f84f4b611f69c902699ca385a39c3a75f9ff1" 447 | dependencies = [ 448 | "aho-corasick", 449 | "memchr", 450 | "regex-syntax", 451 | ] 452 | 453 | [[package]] 454 | name = "regex-syntax" 455 | version = "0.6.26" 456 | source = "registry+https://github.com/rust-lang/crates.io-index" 457 | checksum = "49b3de9ec5dc0a3417da371aab17d729997c15010e7fd24ff707773a33bddb64" 458 | 459 | [[package]] 460 | name = "rustc_version" 461 | version = "0.4.0" 462 | source = "registry+https://github.com/rust-lang/crates.io-index" 463 | checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" 464 | dependencies = [ 465 | "semver", 466 | ] 467 | 468 | [[package]] 469 | name = "ryu" 470 | version = "1.0.9" 471 | source = "registry+https://github.com/rust-lang/crates.io-index" 472 | checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f" 473 | 474 | [[package]] 475 | name = "semver" 476 | version = "1.0.6" 477 | source = "registry+https://github.com/rust-lang/crates.io-index" 478 | checksum = "a4a3381e03edd24287172047536f20cabde766e2cd3e65e6b00fb3af51c4f38d" 479 | 480 | [[package]] 481 | name = "serde" 482 | version = "1.0.118" 483 | source = "registry+https://github.com/rust-lang/crates.io-index" 484 | checksum = "06c64263859d87aa2eb554587e2d23183398d617427327cf2b3d0ed8c69e4800" 485 | dependencies = [ 486 | "serde_derive", 487 | ] 488 | 489 | [[package]] 490 | name = "serde_derive" 491 | version = "1.0.118" 492 | source = "registry+https://github.com/rust-lang/crates.io-index" 493 | checksum = "c84d3526699cd55261af4b941e4e725444df67aa4f9e6a3564f18030d12672df" 494 | dependencies = [ 495 | "proc-macro2", 496 | "quote", 497 | "syn", 498 | ] 499 | 500 | [[package]] 501 | name = "serde_json" 502 | version = "1.0.79" 503 | source = "registry+https://github.com/rust-lang/crates.io-index" 504 | checksum = "8e8d9fa5c3b304765ce1fd9c4c8a3de2c8db365a5b91be52f186efc675681d95" 505 | dependencies = [ 506 | "indexmap", 507 | "itoa", 508 | "ryu", 509 | "serde", 510 | ] 511 | 512 | [[package]] 513 | name = "sha2" 514 | version = "0.9.9" 515 | source = "registry+https://github.com/rust-lang/crates.io-index" 516 | checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" 517 | dependencies = [ 518 | "block-buffer", 519 | "cfg-if 1.0.0", 520 | "cpufeatures", 521 | "digest", 522 | "opaque-debug", 523 | ] 524 | 525 | [[package]] 526 | name = "sha3" 527 | version = "0.9.1" 528 | source = "registry+https://github.com/rust-lang/crates.io-index" 529 | checksum = "f81199417d4e5de3f04b1e871023acea7389672c4135918f05aa9cbf2f2fa809" 530 | dependencies = [ 531 | "block-buffer", 532 | "digest", 533 | "keccak", 534 | "opaque-debug", 535 | ] 536 | 537 | [[package]] 538 | name = "syn" 539 | version = "1.0.57" 540 | source = "registry+https://github.com/rust-lang/crates.io-index" 541 | checksum = "4211ce9909eb971f111059df92c45640aad50a619cf55cd76476be803c4c68e6" 542 | dependencies = [ 543 | "proc-macro2", 544 | "quote", 545 | "unicode-xid", 546 | ] 547 | 548 | [[package]] 549 | name = "toml" 550 | version = "0.5.8" 551 | source = "registry+https://github.com/rust-lang/crates.io-index" 552 | checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" 553 | dependencies = [ 554 | "serde", 555 | ] 556 | 557 | [[package]] 558 | name = "typenum" 559 | version = "1.15.0" 560 | source = "registry+https://github.com/rust-lang/crates.io-index" 561 | checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" 562 | 563 | [[package]] 564 | name = "unicode-xid" 565 | version = "0.2.2" 566 | source = "registry+https://github.com/rust-lang/crates.io-index" 567 | checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" 568 | 569 | [[package]] 570 | name = "version_check" 571 | version = "0.9.4" 572 | source = "registry+https://github.com/rust-lang/crates.io-index" 573 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 574 | 575 | [[package]] 576 | name = "wee_alloc" 577 | version = "0.4.5" 578 | source = "registry+https://github.com/rust-lang/crates.io-index" 579 | checksum = "dbb3b5a6b2bb17cb6ad44a2e68a43e8d2722c997da10e928665c72ec6c0a0b8e" 580 | dependencies = [ 581 | "cfg-if 0.1.10", 582 | "libc", 583 | "memory_units", 584 | "winapi", 585 | ] 586 | 587 | [[package]] 588 | name = "winapi" 589 | version = "0.3.9" 590 | source = "registry+https://github.com/rust-lang/crates.io-index" 591 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 592 | dependencies = [ 593 | "winapi-i686-pc-windows-gnu", 594 | "winapi-x86_64-pc-windows-gnu", 595 | ] 596 | 597 | [[package]] 598 | name = "winapi-i686-pc-windows-gnu" 599 | version = "0.4.0" 600 | source = "registry+https://github.com/rust-lang/crates.io-index" 601 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 602 | 603 | [[package]] 604 | name = "winapi-x86_64-pc-windows-gnu" 605 | version = "0.4.0" 606 | source = "registry+https://github.com/rust-lang/crates.io-index" 607 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 608 | -------------------------------------------------------------------------------- /contracts/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "contracts" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [lib] 7 | crate-type = ["cdylib", "rlib"] 8 | 9 | [dependencies] 10 | near-sdk = "=4.0.0-pre.4" 11 | serde_json = "1.0" 12 | 13 | [profile.release] 14 | codegen-units=1 15 | opt-level = "z" 16 | lto = true 17 | debug = false 18 | panic = "abort" 19 | overflow-checks = true -------------------------------------------------------------------------------- /contracts/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e && RUSTFLAGS='-C link-arg=-s' cargo build --target wasm32-unknown-unknown --release && mkdir -p ./out && cp target/wasm32-unknown-unknown/release/*.wasm ./out/main.wasm -------------------------------------------------------------------------------- /contracts/src/approval.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | use near_sdk::{ext_contract, Gas}; 3 | 4 | const GAS_FOR_NFT_APPROVE: Gas = Gas(10_000_000_000_000); 5 | const NO_DEPOSIT: Balance = 0; 6 | 7 | pub trait NonFungibleTokenCore { 8 | //approve an account ID to transfer a token on your behalf 9 | fn nft_approve(&mut self, token_id: TokenId, account_id: AccountId, msg: Option); 10 | 11 | //check if the passed in account has access to approve the token ID 12 | fn nft_is_approved( 13 | &self, 14 | token_id: TokenId, 15 | approved_account_id: AccountId, 16 | approval_id: Option, 17 | ) -> bool; 18 | 19 | //revoke a specific account from transferring the token on your behalf 20 | fn nft_revoke(&mut self, token_id: TokenId, account_id: AccountId); 21 | 22 | //revoke all accounts from transferring the token on your behalf 23 | fn nft_revoke_all(&mut self, token_id: TokenId); 24 | } 25 | 26 | #[ext_contract(ext_non_fungible_approval_receiver)] 27 | trait NonFungibleTokenApprovalsReceiver { 28 | //cross contract call to an external contract that is initiated during nft_approve 29 | fn nft_on_approve( 30 | &mut self, 31 | token_id: TokenId, 32 | owner_id: AccountId, 33 | approval_id: u64, 34 | msg: String, 35 | ); 36 | } 37 | 38 | #[near_bindgen] 39 | impl NonFungibleTokenCore for Contract { 40 | //allow a specific account ID to approve a token on your behalf 41 | #[payable] 42 | fn nft_approve(&mut self, token_id: TokenId, account_id: AccountId, msg: Option) { 43 | assert_at_least_one_yocto(); 44 | let mut token = self.tokens_by_id.get(&token_id).expect("No token"); 45 | assert_eq!( 46 | &env::predecessor_account_id(), 47 | &token.owner_id, 48 | "Predecessor must be the token owner." 49 | ); 50 | let approval_id: u64 = token.next_approval_id; 51 | let is_new_approval = token 52 | .approved_account_ids 53 | .insert(account_id.clone(), approval_id) 54 | .is_none(); 55 | let storage_used = if is_new_approval { 56 | bytes_for_approved_account_id(&account_id) 57 | //if it was not a new approval, we used no storage. 58 | } else { 59 | 0 60 | }; 61 | 62 | token.next_approval_id += 1; 63 | self.tokens_by_id.insert(&token_id, &token); 64 | refund_deposit(storage_used); 65 | if let Some(msg) = msg { 66 | ext_non_fungible_approval_receiver::nft_on_approve( 67 | token_id, 68 | token.owner_id, 69 | approval_id, 70 | msg, 71 | account_id, //contract account we're calling 72 | NO_DEPOSIT, //NEAR deposit we attach to the call 73 | env::prepaid_gas() - GAS_FOR_NFT_APPROVE, //GAS we're attaching 74 | ) 75 | .as_return(); // Returning this promise 76 | } 77 | } 78 | 79 | //check if the passed in account has access to approve the token ID 80 | fn nft_is_approved( 81 | &self, 82 | token_id: TokenId, 83 | approved_account_id: AccountId, 84 | approval_id: Option, 85 | ) -> bool { 86 | //get the token object from the token_id 87 | let token = self.tokens_by_id.get(&token_id).expect("No token"); 88 | 89 | //get the approval number for the passed in account ID 90 | let approval = token.approved_account_ids.get(&approved_account_id); 91 | 92 | //if there was some approval ID found for the account ID 93 | if let Some(approval) = approval { 94 | //if a specific approval_id was passed into the function 95 | if let Some(approval_id) = approval_id { 96 | //return if the approval ID passed in matches the actual approval ID for the account 97 | approval_id == *approval 98 | //if there was no approval_id passed into the function, we simply return true 99 | } else { 100 | true 101 | } 102 | //if there was no approval ID found for the account ID, we simply return false 103 | } else { 104 | false 105 | } 106 | } 107 | 108 | //revoke a specific account from transferring the token on your behalf 109 | #[payable] 110 | fn nft_revoke(&mut self, token_id: TokenId, account_id: AccountId) { 111 | assert_one_yocto(); 112 | //get the token object using the passed in token_id 113 | let mut token = self.tokens_by_id.get(&token_id).expect("No token"); 114 | 115 | //get the caller of the function and assert that they are the owner of the token 116 | let predecessor_account_id = env::predecessor_account_id(); 117 | assert_eq!(&predecessor_account_id, &token.owner_id); 118 | 119 | //if the account ID was in the token's approval, we remove it and the if statement logic executes 120 | if token.approved_account_ids.remove(&account_id).is_some() { 121 | //refund the funds released by removing the approved_account_id to the caller of the function 122 | refund_approved_account_ids_iter(predecessor_account_id, [account_id].iter()); 123 | 124 | //insert the token back into the tokens_by_id collection with the account_id removed from the approval list 125 | self.tokens_by_id.insert(&token_id, &token); 126 | } 127 | } 128 | 129 | //revoke all accounts from transferring the token on your behalf 130 | #[payable] 131 | fn nft_revoke_all(&mut self, token_id: TokenId) { 132 | //assert that the caller attached exactly 1 yoctoNEAR for security 133 | assert_one_yocto(); 134 | 135 | //get the token object from the passed in token ID 136 | let mut token = self.tokens_by_id.get(&token_id).expect("No token"); 137 | //get the caller and make sure they are the owner of the tokens 138 | let predecessor_account_id = env::predecessor_account_id(); 139 | assert_eq!(&predecessor_account_id, &token.owner_id); 140 | 141 | //only revoke if the approved account IDs for the token is not empty 142 | if !token.approved_account_ids.is_empty() { 143 | //refund the approved account IDs to the caller of the function 144 | refund_approved_account_ids(predecessor_account_id, &token.approved_account_ids); 145 | //clear the approved account IDs 146 | token.approved_account_ids.clear(); 147 | //insert the token back into the tokens_by_id collection with the approved account IDs cleared 148 | self.tokens_by_id.insert(&token_id, &token); 149 | } 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /contracts/src/enumeration.rs: -------------------------------------------------------------------------------- 1 | use crate::nft_core::NonFungibleTokenCore; 2 | use crate::*; 3 | #[near_bindgen] 4 | impl Contract { 5 | //Query for the total supply of NFTs on the contract 6 | pub fn nft_total_supply(&self) -> U128 { 7 | //return the length of the token metadata by ID 8 | U128(self.token_metadata_by_id.len() as u128) 9 | } 10 | 11 | //Query for nft tokens on the contract regardless of the owner using pagination 12 | pub fn nft_tokens(&self, from_index: Option, limit: Option) -> Vec { 13 | //where to start pagination - if we have a from_index, we'll use that - otherwise start from 0 index 14 | let start = u128::from(from_index.unwrap_or(U128(0))); 15 | 16 | //iterate through each token using an iterator 17 | self.token_metadata_by_id 18 | .keys() 19 | //skip to the index we specified in the start variable 20 | .skip(start as usize) 21 | //take the first "limit" elements in the vector. If we didn't specify a limit, use 50 22 | .take(limit.unwrap_or(50) as usize) 23 | //we'll map the token IDs which are strings into Json Tokens 24 | .map(|token_id| self.nft_token(token_id.clone()).unwrap()) 25 | //since we turned the keys into an iterator, we need to turn it back into a vector to return 26 | .collect() 27 | } 28 | 29 | //get the total supply of NFTs for a given owner 30 | pub fn nft_supply_for_owner(&self, account_id: AccountId) -> U128 { 31 | //get the set of tokens for the passed in owner 32 | let tokens_for_owner_set = self.tokens_per_owner.get(&account_id); 33 | 34 | //if there is some set of tokens, we'll return the length as a U128 35 | if let Some(tokens_for_owner_set) = tokens_for_owner_set { 36 | U128(tokens_for_owner_set.len() as u128) 37 | } else { 38 | //if there isn't a set of tokens for the passed in account ID, we'll return 0 39 | U128(0) 40 | } 41 | } 42 | 43 | //Query for all the tokens for an owner 44 | pub fn nft_tokens_for_owner( 45 | &self, 46 | account_id: AccountId, 47 | from_index: Option, 48 | limit: Option, 49 | ) -> Vec { 50 | //get the set of tokens for the passed in owner 51 | let tokens_for_owner_set = self.tokens_per_owner.get(&account_id); 52 | //if there is some set of tokens, we'll set the tokens variable equal to that set 53 | let tokens = if let Some(tokens_for_owner_set) = tokens_for_owner_set { 54 | tokens_for_owner_set 55 | } else { 56 | //if there is no set of tokens, we'll simply return an empty vector. 57 | return vec![]; 58 | }; 59 | 60 | //where to start pagination - if we have a from_index, we'll use that - otherwise start from 0 index 61 | let start = u128::from(from_index.unwrap_or(U128(0))); 62 | 63 | //iterate through the keys vector 64 | tokens 65 | .iter() 66 | //skip to the index we specified in the start variable 67 | .skip(start as usize) 68 | //take the first "limit" elements in the vector. If we didn't specify a limit, use 50 69 | .take(limit.unwrap_or(50) as usize) 70 | //we'll map the token IDs which are strings into Json Tokens 71 | .map(|token_id| self.nft_token(token_id.clone()).unwrap()) 72 | //since we turned the keys into an iterator, we need to turn it back into a vector to return 73 | .collect() 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /contracts/src/events.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use near_sdk::serde::{Deserialize, Serialize}; 4 | 5 | /// Enum that represents the data type of the EventLog. 6 | /// The enum can either be an NftMint or an NftTransfer. 7 | #[derive(Serialize, Deserialize, Debug)] 8 | #[serde(tag = "event", content = "data")] 9 | #[serde(rename_all = "snake_case")] 10 | #[serde(crate = "near_sdk::serde")] 11 | #[non_exhaustive] 12 | pub enum EventLogVariant { 13 | NftMint(Vec), 14 | NftTransfer(Vec), 15 | } 16 | 17 | /// Interface to capture data about an event 18 | /// 19 | /// Arguments: 20 | /// * `standard`: name of standard e.g. nep171 21 | /// * `version`: e.g. 1.0.0 22 | /// * `event`: associate event data 23 | #[derive(Serialize, Deserialize, Debug)] 24 | #[serde(crate = "near_sdk::serde")] 25 | pub struct EventLog { 26 | pub standard: String, 27 | pub version: String, 28 | 29 | // `flatten` to not have "event": {} in the JSON, just have the contents of {}. 30 | #[serde(flatten)] 31 | pub event: EventLogVariant, 32 | } 33 | 34 | impl fmt::Display for EventLog { 35 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 36 | f.write_fmt(format_args!( 37 | "EVENT_JSON:{}", 38 | &serde_json::to_string(self).map_err(|_| fmt::Error)? 39 | )) 40 | } 41 | } 42 | 43 | /// An event log to capture token minting 44 | /// 45 | /// Arguments 46 | /// * `owner_id`: "account.near" 47 | /// * `token_ids`: ["1", "abc"] 48 | /// * `memo`: optional message 49 | #[derive(Serialize, Deserialize, Debug)] 50 | #[serde(crate = "near_sdk::serde")] 51 | pub struct NftMintLog { 52 | pub owner_id: String, 53 | pub token_ids: Vec, 54 | 55 | #[serde(skip_serializing_if = "Option::is_none")] 56 | pub memo: Option, 57 | } 58 | 59 | /// An event log to capture token transfer 60 | /// 61 | /// Arguments 62 | /// * `authorized_id`: approved account to transfer 63 | /// * `old_owner_id`: "owner.near" 64 | /// * `new_owner_id`: "receiver.near" 65 | /// * `token_ids`: ["1", "12345abc"] 66 | /// * `memo`: optional message 67 | #[derive(Serialize, Deserialize, Debug)] 68 | #[serde(crate = "near_sdk::serde")] 69 | pub struct NftTransferLog { 70 | #[serde(skip_serializing_if = "Option::is_none")] 71 | pub authorized_id: Option, 72 | 73 | pub old_owner_id: String, 74 | pub new_owner_id: String, 75 | pub token_ids: Vec, 76 | 77 | #[serde(skip_serializing_if = "Option::is_none")] 78 | pub memo: Option, 79 | } 80 | -------------------------------------------------------------------------------- /contracts/src/internal.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | use near_sdk::CryptoHash; 3 | use std::mem::size_of; 4 | 5 | //calculate how many bytes the account ID is taking up 6 | pub(crate) fn bytes_for_approved_account_id(account_id: &AccountId) -> u64 { 7 | // The extra 4 bytes are coming from Borsh serialization to store the length of the string. 8 | account_id.as_str().len() as u64 + 4 + size_of::() as u64 9 | } 10 | 11 | //refund the storage taken up by passed in approved account IDs and send the funds to the passed in account ID. 12 | pub(crate) fn refund_approved_account_ids_iter<'a, I>( 13 | account_id: AccountId, 14 | approved_account_ids: I, //the approved account IDs must be passed in as an iterator 15 | ) -> Promise 16 | where 17 | I: Iterator, 18 | { 19 | //get the storage total by going through and summing all the bytes for each approved account IDs 20 | let storage_released: u64 = approved_account_ids 21 | .map(bytes_for_approved_account_id) 22 | .sum(); 23 | //transfer the account the storage that is released 24 | Promise::new(account_id).transfer(Balance::from(storage_released) * env::storage_byte_cost()) 25 | } 26 | 27 | //refund a map of approved account IDs and send the funds to the passed in account ID 28 | pub(crate) fn refund_approved_account_ids( 29 | account_id: AccountId, 30 | approved_account_ids: &HashMap, 31 | ) -> Promise { 32 | //call the refund_approved_account_ids_iter with the approved account IDs as keys 33 | refund_approved_account_ids_iter(account_id, approved_account_ids.keys()) 34 | } 35 | 36 | //used to generate a unique prefix in our storage collections (this is to avoid data collisions) 37 | pub(crate) fn hash_account_id(account_id: &AccountId) -> CryptoHash { 38 | //get the default hash 39 | let mut hash = CryptoHash::default(); 40 | //we hash the account ID and return it 41 | hash.copy_from_slice(&env::sha256(account_id.as_bytes())); 42 | hash 43 | } 44 | //used to make sure the user attached exactly 1 yoctoNEAR 45 | pub(crate) fn assert_one_yocto() { 46 | assert_eq!( 47 | env::attached_deposit(), 48 | 1, 49 | "Requires attached deposit of exactly 1 yoctoNEAR", 50 | ) 51 | } 52 | 53 | pub(crate) fn assert_at_least_one_yocto() { 54 | assert!( 55 | env::attached_deposit() >= 1, 56 | "Requires attached deposit of at least 1 yoctoNEAR", 57 | ) 58 | } 59 | //refund the initial deposit based on the amount of storage that was used up 60 | pub(crate) fn refund_deposit(storage_used: u64) { 61 | //get how much it would cost to store the information 62 | let required_cost = env::storage_byte_cost() * Balance::from(storage_used); 63 | //get the attached deposit 64 | let attached_deposit = env::attached_deposit(); 65 | 66 | //make sure that the attached deposit is greater than or equal to the required cost 67 | assert!( 68 | required_cost <= attached_deposit, 69 | "Must attach {} yoctoNEAR to cover storage", 70 | required_cost, 71 | ); 72 | 73 | //get the refund amount from the attached deposit - required cost 74 | let refund = attached_deposit - required_cost; 75 | 76 | //if the refund is greater than 1 yocto NEAR, we refund the predecessor that amount 77 | if refund > 1 { 78 | Promise::new(env::predecessor_account_id()).transfer(refund); 79 | } 80 | } 81 | 82 | impl Contract { 83 | //add a token to the set of tokens an owner has 84 | pub(crate) fn internal_add_token_to_owner( 85 | &mut self, 86 | account_id: &AccountId, 87 | token_id: &TokenId, 88 | ) { 89 | //get the set of tokens for the given account 90 | let mut tokens_set = self.tokens_per_owner.get(account_id).unwrap_or_else(|| { 91 | //if the account doesn't have any tokens, we create a new unordered set 92 | UnorderedSet::new( 93 | StorageKey::TokenPerOwnerInner { 94 | //we get a new unique prefix for the collection 95 | account_id_hash: hash_account_id(&account_id), 96 | } 97 | .try_to_vec() 98 | .unwrap(), 99 | ) 100 | }); 101 | 102 | //we insert the token ID into the set 103 | tokens_set.insert(token_id); 104 | 105 | //we insert that set for the given account ID. 106 | self.tokens_per_owner.insert(account_id, &tokens_set); 107 | } 108 | 109 | pub(crate) fn internal_remove_token_from_owner( 110 | &mut self, 111 | account_id: &AccountId, 112 | token_id: &TokenId, 113 | ) { 114 | //we get the set of tokens that the owner has 115 | let mut tokens_set = self 116 | .tokens_per_owner 117 | .get(account_id) 118 | //if there is no set of tokens for the owner, we panic with the following message: 119 | .expect("Token should be owned by the sender"); 120 | 121 | //we remove the the token_id from the set of tokens 122 | tokens_set.remove(token_id); 123 | 124 | //if the token set is now empty, we remove the owner from the tokens_per_owner collection 125 | if tokens_set.is_empty() { 126 | self.tokens_per_owner.remove(account_id); 127 | } else { 128 | //if the token set is not empty, we simply insert it back for the account ID. 129 | self.tokens_per_owner.insert(account_id, &tokens_set); 130 | } 131 | } 132 | 133 | pub(crate) fn internal_transfer( 134 | &mut self, 135 | sender_id: &AccountId, 136 | receiver_id: &AccountId, 137 | token_id: &TokenId, 138 | approval_id: Option, 139 | memo: Option, 140 | ) -> Token { 141 | //get the token object by passing in the token_id 142 | let token = self.tokens_by_id.get(token_id).expect("No token"); 143 | 144 | //if the sender doesn't equal the owner, we check if the sender is in the approval list 145 | if sender_id != &token.owner_id { 146 | //if the token's approved account IDs doesn't contain the sender, we panic 147 | if !token.approved_account_ids.contains_key(sender_id) { 148 | env::panic_str("Unauthorized"); 149 | } 150 | 151 | // If they included an approval_id, check if the sender's actual approval_id is the same as the one included 152 | if let Some(enforced_approval_id) = approval_id { 153 | //get the actual approval ID 154 | let actual_approval_id = token 155 | .approved_account_ids 156 | .get(sender_id) 157 | //if the sender isn't in the map, we panic 158 | .expect("Sender is not approved account"); 159 | 160 | //make sure that the actual approval ID is the same as the one provided 161 | assert_eq!( 162 | actual_approval_id, &enforced_approval_id, 163 | "The actual approval_id {} is different from the given approval_id {}", 164 | actual_approval_id, enforced_approval_id, 165 | ); 166 | } 167 | } 168 | 169 | //we make sure that the sender isn't sending the token to themselves 170 | assert_ne!( 171 | &token.owner_id, receiver_id, 172 | "The token owner and the receiver should be different" 173 | ); 174 | 175 | //we remove the token from it's current owner's set 176 | self.internal_remove_token_from_owner(&token.owner_id, token_id); 177 | //we then add the token to the receiver_id's set 178 | self.internal_add_token_to_owner(receiver_id, token_id); 179 | 180 | //we create a new token struct 181 | let new_token = Token { 182 | owner_id: receiver_id.clone(), 183 | //reset the approval account IDs 184 | approved_account_ids: Default::default(), 185 | next_approval_id: token.next_approval_id, 186 | }; 187 | //insert that new token into the tokens_by_id, replacing the old entry 188 | self.tokens_by_id.insert(token_id, &new_token); 189 | 190 | //if there was some memo attached, we log it. 191 | if let Some(memo) = memo.as_ref() { 192 | env::log_str(&format!("Memo: {}", memo).to_string()); 193 | } 194 | 195 | // Default the authorized ID to be None for the logs. 196 | let mut authorized_id = None; 197 | //if the approval ID was provided, set the authorized ID equal to the sender 198 | if approval_id.is_some() { 199 | authorized_id = Some(sender_id.to_string()); 200 | } 201 | 202 | // Construct the transfer log as per the events standard. 203 | let nft_transfer_log: EventLog = EventLog { 204 | // Standard name ("nep171"). 205 | standard: NFT_STANDARD_NAME.to_string(), 206 | // Version of the standard ("nft-1.0.0"). 207 | version: NFT_METADATA_SPEC.to_string(), 208 | // The data related with the event stored in a vector. 209 | event: EventLogVariant::NftTransfer(vec![NftTransferLog { 210 | // The optional authorized account ID to transfer the token on behalf of the old owner. 211 | authorized_id, 212 | // The old owner's account ID. 213 | old_owner_id: token.owner_id.to_string(), 214 | // The account ID of the new owner of the token. 215 | new_owner_id: receiver_id.to_string(), 216 | // A vector containing the token IDs as strings. 217 | token_ids: vec![token_id.to_string()], 218 | // An optional memo to include. 219 | memo, 220 | }]), 221 | }; 222 | 223 | // Log the serialized json. 224 | env::log_str(&nft_transfer_log.to_string()); 225 | 226 | //return the preivous token object that was transferred. 227 | token 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /contracts/src/lib.rs: -------------------------------------------------------------------------------- 1 | use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; 2 | use near_sdk::collections::{LazyOption, LookupMap, UnorderedMap, UnorderedSet}; 3 | use near_sdk::json_types::{Base64VecU8, U128}; 4 | use near_sdk::serde::{Deserialize, Serialize}; 5 | use near_sdk::{ 6 | env, near_bindgen, AccountId, Balance, CryptoHash, PanicOnDefault, Promise, PromiseOrValue, 7 | }; 8 | use std::collections::HashMap; 9 | 10 | pub use crate::approval::*; 11 | pub use crate::events::*; 12 | use crate::internal::*; 13 | pub use crate::metadata::*; 14 | pub use crate::mint::*; 15 | pub use crate::nft_core::*; 16 | pub use crate::royalty::*; 17 | 18 | mod approval; 19 | mod enumeration; 20 | mod events; 21 | mod internal; 22 | mod metadata; 23 | mod mint; 24 | mod nft_core; 25 | mod royalty; 26 | 27 | /// This spec can be treated like a version of the standard. 28 | pub const NFT_METADATA_SPEC: &str = "1.0.0"; 29 | /// This is the name of the NFT standard we're using 30 | pub const NFT_STANDARD_NAME: &str = "nep171"; 31 | 32 | #[near_bindgen] 33 | #[derive(BorshDeserialize, BorshSerialize, PanicOnDefault)] 34 | pub struct Contract { 35 | pub owner_id: AccountId, 36 | pub tokens_per_owner: LookupMap>, 37 | pub tokens_by_id: LookupMap, 38 | pub token_metadata_by_id: UnorderedMap, 39 | pub metadata: LazyOption, 40 | } 41 | 42 | /// Helper structure for keys of the persistent collections. 43 | #[derive(BorshSerialize)] 44 | pub enum StorageKey { 45 | TokensPerOwner, 46 | TokenPerOwnerInner { account_id_hash: CryptoHash }, 47 | TokensById, 48 | TokenMetadataById, 49 | NFTContractMetadata, 50 | TokensPerType, 51 | TokensPerTypeInner { token_type_hash: CryptoHash }, 52 | TokenTypesLocked, 53 | } 54 | 55 | #[near_bindgen] 56 | impl Contract { 57 | /* 58 | initialization function (can only be called once). 59 | this initializes the contract with default metadata so the 60 | user doesn't have to manually type metadata. 61 | */ 62 | #[init] 63 | pub fn new_default_meta(owner_id: AccountId) -> Self { 64 | Self::new( 65 | owner_id, 66 | NFTContractMetadata { 67 | spec: "nft-1.0.0".to_string(), 68 | name: "Dspyt NFTs".to_string(), 69 | symbol: "DSPYT".to_string(), 70 | icon: None, 71 | base_uri: None, 72 | reference: None, 73 | reference_hash: None, 74 | }, 75 | ) 76 | } 77 | 78 | /* 79 | initialization function (can only be called once). 80 | this initializes the contract with metadata that was passed in and 81 | the owner_id. 82 | */ 83 | #[init] 84 | pub fn new(owner_id: AccountId, metadata: NFTContractMetadata) -> Self { 85 | let this = Self { 86 | tokens_per_owner: LookupMap::new(StorageKey::TokensPerOwner.try_to_vec().unwrap()), 87 | tokens_by_id: LookupMap::new(StorageKey::TokensById.try_to_vec().unwrap()), 88 | token_metadata_by_id: UnorderedMap::new( 89 | StorageKey::TokenMetadataById.try_to_vec().unwrap(), 90 | ), 91 | owner_id, 92 | metadata: LazyOption::new( 93 | StorageKey::NFTContractMetadata.try_to_vec().unwrap(), 94 | Some(&metadata), 95 | ), 96 | }; 97 | this 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /contracts/src/metadata.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | pub type TokenId = String; 3 | //defines the payout type we'll be returning as a part of the royalty standards. 4 | #[derive(Serialize, Deserialize)] 5 | #[serde(crate = "near_sdk::serde")] 6 | pub struct Payout { 7 | pub payout: HashMap, 8 | } 9 | 10 | #[derive(BorshDeserialize, BorshSerialize, Serialize, Deserialize, Clone)] 11 | #[serde(crate = "near_sdk::serde")] 12 | pub struct NFTContractMetadata { 13 | pub spec: String, 14 | pub name: String, 15 | pub symbol: String, 16 | pub icon: Option, // Data URL 17 | pub base_uri: Option, // Centralized gateway known to have reliable access to decentralized storage assets referenced by `reference` or `media` URLs 18 | pub reference: Option, // URL to a JSON file with more info 19 | pub reference_hash: Option, // Base64-encoded sha256 hash of JSON from reference field. Required if `reference` is included. 20 | } 21 | 22 | #[derive(BorshDeserialize, BorshSerialize, Serialize, Deserialize)] 23 | #[serde(crate = "near_sdk::serde")] 24 | pub struct TokenMetadata { 25 | pub title: Option, 26 | pub description: Option, // free-form description 27 | pub media: Option, // URL to associated media, preferably to decentralized, content-addressed storage 28 | pub media_hash: Option, // Base64-encoded sha256 hash of content referenced by the `media` field. Required if `media` is included. 29 | pub copies: Option, // number of copies of this set of metadata in existence when token was minted. 30 | pub issued_at: Option, // When token was issued or minted, Unix epoch in milliseconds 31 | pub expires_at: Option, // When token expires, Unix epoch in milliseconds 32 | pub starts_at: Option, // When token starts being valid, Unix epoch in milliseconds 33 | pub updated_at: Option, // When token was last updated, Unix epoch in milliseconds 34 | pub extra: Option, // anything extra the NFT wants to store on-chain. Can be stringified JSON. 35 | pub reference: Option, // URL to an off-chain JSON file with more info. 36 | pub reference_hash: Option, // Base64-encoded sha256 hash of JSON from reference field. Required if `reference` is included. 37 | } 38 | 39 | #[derive(BorshDeserialize, BorshSerialize)] 40 | pub struct Token { 41 | pub owner_id: AccountId, 42 | //list of approved account IDs that have access to transfer the token 43 | pub approved_account_ids: HashMap, 44 | //the next approval ID to give out. 45 | pub next_approval_id: u64, 46 | } 47 | 48 | //The Json token is what will be returned from view calls. 49 | #[derive(Serialize, Deserialize)] 50 | #[serde(crate = "near_sdk::serde")] 51 | pub struct JsonToken { 52 | //token ID 53 | pub token_id: TokenId, 54 | //owner of the token 55 | pub owner_id: AccountId, 56 | //token metadata 57 | pub metadata: TokenMetadata, 58 | // list of approved account IDs that have access to transfer the token 59 | pub approved_account_ids: HashMap, 60 | } 61 | 62 | pub trait NonFungibleTokenMetadata { 63 | //view call for returning the contract metadata 64 | fn nft_metadata(&self) -> NFTContractMetadata; 65 | } 66 | 67 | #[near_bindgen] 68 | impl NonFungibleTokenMetadata for Contract { 69 | fn nft_metadata(&self) -> NFTContractMetadata { 70 | self.metadata.get().unwrap() 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /contracts/src/mint.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | #[near_bindgen] 4 | impl Contract { 5 | #[payable] 6 | pub fn nft_mint(&mut self, token_id: TokenId, metadata: TokenMetadata, receiver_id: AccountId) { 7 | let initial_storage_usage = env::storage_usage(); 8 | 9 | let token = Token { 10 | owner_id: receiver_id, 11 | //we set the approved account IDs to the default value (an empty map) 12 | approved_account_ids: Default::default(), 13 | //the next approval ID is set to 0 14 | next_approval_id: 0, 15 | }; 16 | 17 | assert!( 18 | self.tokens_by_id.insert(&token_id, &token).is_none(), 19 | "Token already exists" 20 | ); 21 | 22 | self.token_metadata_by_id.insert(&token_id, &metadata); 23 | 24 | //call the internal method for adding the token to the owner 25 | self.internal_add_token_to_owner(&token.owner_id, &token_id); 26 | 27 | // Construct the mint log as per the events standard. 28 | let nft_mint_log: EventLog = EventLog { 29 | // Standard name ("nep171"). 30 | standard: NFT_STANDARD_NAME.to_string(), 31 | // Version of the standard ("nft-1.0.0"). 32 | version: NFT_METADATA_SPEC.to_string(), 33 | // The data related with the event stored in a vector. 34 | event: EventLogVariant::NftMint(vec![NftMintLog { 35 | // Owner of the token. 36 | owner_id: token.owner_id.to_string(), 37 | // Vector of token IDs that were minted. 38 | token_ids: vec![token_id.to_string()], 39 | // An optional memo to include. 40 | memo: None, 41 | }]), 42 | }; 43 | 44 | // Log the serialized json. 45 | env::log_str(&nft_mint_log.to_string()); 46 | 47 | //calculate the required storage which was the used - initial 48 | let required_storage_in_bytes = env::storage_usage() - initial_storage_usage; 49 | 50 | //refund any excess storage if the user attached too much. Panic if they didn't attach enough to cover the required. 51 | refund_deposit(required_storage_in_bytes); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /contracts/src/nft_core.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | use near_sdk::{ext_contract, log, Gas, PromiseResult}; 3 | 4 | const GAS_FOR_RESOLVE_TRANSFER: Gas = Gas(10_000_000_000_000); 5 | const GAS_FOR_NFT_TRANSFER_CALL: Gas = Gas(25_000_000_000_000 + GAS_FOR_RESOLVE_TRANSFER.0); 6 | const MIN_GAS_FOR_NFT_TRANSFER_CALL: Gas = Gas(100_000_000_000_000); 7 | const NO_DEPOSIT: Balance = 0; 8 | 9 | pub trait NonFungibleTokenCore { 10 | //transfers an NFT to a receiver ID 11 | fn nft_transfer( 12 | &mut self, 13 | receiver_id: AccountId, 14 | token_id: TokenId, 15 | approval_id: u64, 16 | memo: Option, 17 | ); 18 | 19 | //transfers an NFT to a receiver and calls a function on the receiver ID's contract 20 | /// Returns `true` if the token was transferred from the sender's account. 21 | fn nft_transfer_call( 22 | &mut self, 23 | receiver_id: AccountId, 24 | token_id: TokenId, 25 | approval_id: u64, 26 | memo: Option, 27 | msg: String, 28 | ) -> PromiseOrValue; 29 | 30 | //get information about the NFT token passed in 31 | fn nft_token(&self, token_id: TokenId) -> Option; 32 | } 33 | 34 | #[ext_contract(ext_non_fungible_token_receiver)] 35 | trait NonFungibleTokenReceiver { 36 | //Method stored on the receiver contract that is called via cross contract call when nft_transfer_call is called 37 | /// Returns `true` if the token should be returned back to the sender. 38 | fn nft_on_transfer( 39 | &mut self, 40 | sender_id: AccountId, 41 | previous_owner_id: AccountId, 42 | token_id: TokenId, 43 | msg: String, 44 | ) -> Promise; 45 | } 46 | 47 | #[ext_contract(ext_self)] 48 | trait NonFungibleTokenResolver { 49 | /* 50 | resolves the promise of the cross contract call to the receiver contract 51 | this is stored on THIS contract and is meant to analyze what happened in the cross contract call when nft_on_transfer was called 52 | as part of the nft_transfer_call method 53 | */ 54 | fn nft_resolve_transfer( 55 | &mut self, 56 | //we introduce an authorized ID for logging the transfer event 57 | authorized_id: Option, 58 | owner_id: AccountId, 59 | receiver_id: AccountId, 60 | token_id: TokenId, 61 | //we introduce the approval map so we can keep track of what the approvals were before the transfer 62 | approved_account_ids: HashMap, 63 | //we introduce a memo for logging the transfer event 64 | memo: Option, 65 | ) -> bool; 66 | } 67 | 68 | /* 69 | resolves the promise of the cross contract call to the receiver contract 70 | this is stored on THIS contract and is meant to analyze what happened in the cross contract call when nft_on_transfer was called 71 | as part of the nft_transfer_call method 72 | */ 73 | trait NonFungibleTokenResolver { 74 | fn nft_resolve_transfer( 75 | &mut self, 76 | //we introduce an authorized ID for logging the transfer event 77 | authorized_id: Option, 78 | owner_id: AccountId, 79 | receiver_id: AccountId, 80 | token_id: TokenId, 81 | //we introduce the approval map so we can keep track of what the approvals were before the transfer 82 | approved_account_ids: HashMap, 83 | //we introduce a memo for logging the transfer event 84 | memo: Option, 85 | ) -> bool; 86 | } 87 | 88 | #[near_bindgen] 89 | impl NonFungibleTokenCore for Contract { 90 | //implementation of the nft_transfer method. This transfers the NFT from the current owner to the receiver. 91 | #[payable] 92 | fn nft_transfer( 93 | &mut self, 94 | receiver_id: AccountId, 95 | token_id: TokenId, 96 | //we introduce an approval ID so that people with that approval ID can transfer the token 97 | approval_id: u64, 98 | memo: Option, 99 | ) { 100 | //assert that the user attached exactly 1 yoctoNEAR. This is for security and so that the user will be redirected to the NEAR wallet. 101 | assert_one_yocto(); 102 | //get the sender to transfer the token from the sender to the receiver 103 | let sender_id = env::predecessor_account_id(); 104 | 105 | //call the internal transfer method and get back the previous token so we can refund the approved account IDs 106 | let previous_token = 107 | self.internal_transfer(&sender_id, &receiver_id, &token_id, Some(approval_id), memo); 108 | 109 | //we refund the owner for releasing the storage used up by the approved account IDs 110 | refund_approved_account_ids( 111 | previous_token.owner_id.clone(), 112 | &previous_token.approved_account_ids, 113 | ); 114 | } 115 | 116 | //implementation of the transfer call method. This will transfer the NFT and call a method on the reciver_id contract 117 | #[payable] 118 | fn nft_transfer_call( 119 | &mut self, 120 | receiver_id: AccountId, 121 | token_id: TokenId, 122 | //we introduce an approval ID so that people with that approval ID can transfer the token 123 | approval_id: u64, 124 | memo: Option, 125 | msg: String, 126 | ) -> PromiseOrValue { 127 | //assert that the user attached exactly 1 yocto for security reasons. 128 | assert_one_yocto(); 129 | 130 | //get the GAS attached to the call 131 | let attached_gas = env::prepaid_gas(); 132 | 133 | /* 134 | make sure that the attached gas is greater than the minimum GAS for NFT transfer call. 135 | This is to ensure that the cross contract call to nft_on_transfer won't cause a prepaid GAS error. 136 | If this happens, the event will be logged in internal_transfer but the actual transfer logic will be 137 | reverted due to the panic. This will result in the databases thinking the NFT belongs to the wrong person. 138 | */ 139 | assert!( 140 | attached_gas >= MIN_GAS_FOR_NFT_TRANSFER_CALL, 141 | "You cannot attach less than {:?} Gas to nft_transfer_call", 142 | MIN_GAS_FOR_NFT_TRANSFER_CALL 143 | ); 144 | 145 | //get the sender ID 146 | let sender_id = env::predecessor_account_id(); 147 | 148 | //transfer the token and get the previous token object 149 | let previous_token = self.internal_transfer( 150 | &sender_id, 151 | &receiver_id, 152 | &token_id, 153 | Some(approval_id), 154 | memo.clone(), 155 | ); 156 | 157 | //default the authorized_id to none 158 | let mut authorized_id = None; 159 | //if the sender isn't the owner of the token, we set the authorized ID equal to the sender. 160 | if sender_id != previous_token.owner_id { 161 | authorized_id = Some(sender_id.to_string()); 162 | } 163 | 164 | // Initiating receiver's call and the callback 165 | ext_non_fungible_token_receiver::nft_on_transfer( 166 | sender_id, 167 | previous_token.owner_id.clone(), 168 | token_id.clone(), 169 | msg, 170 | receiver_id.clone(), //contract account to make the call to 171 | NO_DEPOSIT, //attached deposit 172 | env::prepaid_gas() - GAS_FOR_NFT_TRANSFER_CALL, //attached GAS 173 | ) 174 | //we then resolve the promise and call nft_resolve_transfer on our own contract 175 | .then(ext_self::nft_resolve_transfer( 176 | authorized_id, // we introduce an authorized ID so that we can log the transfer 177 | previous_token.owner_id, 178 | receiver_id, 179 | token_id, 180 | previous_token.approved_account_ids, 181 | memo, // we introduce a memo for logging in the events standard 182 | env::current_account_id(), //contract account to make the call to 183 | NO_DEPOSIT, //attached deposit 184 | GAS_FOR_RESOLVE_TRANSFER, //GAS attached to the call 185 | )) 186 | .into() 187 | } 188 | 189 | //get the information for a specific token ID 190 | fn nft_token(&self, token_id: TokenId) -> Option { 191 | //if there is some token ID in the tokens_by_id collection 192 | if let Some(token) = self.tokens_by_id.get(&token_id) { 193 | //we'll get the metadata for that token 194 | let metadata = self.token_metadata_by_id.get(&token_id).unwrap(); 195 | //we return the JsonToken (wrapped by Some since we return an option) 196 | Some(JsonToken { 197 | token_id, 198 | owner_id: token.owner_id, 199 | metadata, 200 | approved_account_ids: token.approved_account_ids, 201 | }) 202 | } else { 203 | //if there wasn't a token ID in the tokens_by_id collection, we return None 204 | None 205 | } 206 | } 207 | } 208 | 209 | #[near_bindgen] 210 | impl NonFungibleTokenResolver for Contract { 211 | //resolves the cross contract call when calling nft_on_transfer in the nft_transfer_call method 212 | //returns true if the token was successfully transferred to the receiver_id 213 | #[private] 214 | fn nft_resolve_transfer( 215 | &mut self, 216 | //we introduce an authorized ID for logging the transfer event 217 | authorized_id: Option, 218 | owner_id: AccountId, 219 | receiver_id: AccountId, 220 | token_id: TokenId, 221 | //we introduce the approval map so we can keep track of what the approvals were before the transfer 222 | approved_account_ids: HashMap, 223 | //we introduce a memo for logging the transfer event 224 | memo: Option, 225 | ) -> bool { 226 | // Whether receiver wants to return token back to the sender, based on `nft_on_transfer` 227 | // call result. 228 | if let PromiseResult::Successful(value) = env::promise_result(0) { 229 | //As per the standard, the nft_on_transfer should return whether we should return the token to it's owner or not 230 | if let Ok(return_token) = near_sdk::serde_json::from_slice::(&value) { 231 | //if we need don't need to return the token, we simply return true meaning everything went fine 232 | if !return_token { 233 | /* 234 | since we've already transferred the token and nft_on_transfer returned false, we don't have to 235 | revert the original transfer and thus we can just return true since nothing went wrong. 236 | */ 237 | //we refund the owner for releasing the storage used up by the approved account IDs 238 | refund_approved_account_ids(owner_id, &approved_account_ids); 239 | return true; 240 | } 241 | } 242 | } 243 | 244 | //get the token object if there is some token object 245 | let mut token = if let Some(token) = self.tokens_by_id.get(&token_id) { 246 | if token.owner_id != receiver_id { 247 | //we refund the owner for releasing the storage used up by the approved account IDs 248 | refund_approved_account_ids(owner_id, &approved_account_ids); 249 | // The token is not owner by the receiver anymore. Can't return it. 250 | return true; 251 | } 252 | token 253 | //if there isn't a token object, it was burned and so we return true 254 | } else { 255 | //we refund the owner for releasing the storage used up by the approved account IDs 256 | refund_approved_account_ids(owner_id, &approved_account_ids); 257 | return true; 258 | }; 259 | 260 | //we remove the token from the receiver 261 | self.internal_remove_token_from_owner(&receiver_id.clone(), &token_id); 262 | //we add the token to the original owner 263 | self.internal_add_token_to_owner(&owner_id, &token_id); 264 | 265 | //we change the token struct's owner to be the original owner 266 | token.owner_id = owner_id.clone(); 267 | 268 | //we refund the receiver any approved account IDs that they may have set on the token 269 | refund_approved_account_ids(receiver_id.clone(), &token.approved_account_ids); 270 | //reset the approved account IDs to what they were before the transfer 271 | token.approved_account_ids = approved_account_ids; 272 | 273 | //we inset the token back into the tokens_by_id collection 274 | self.tokens_by_id.insert(&token_id, &token); 275 | 276 | /* 277 | We need to log that the NFT was reverted back to the original owner. 278 | The old_owner_id will be the receiver and the new_owner_id will be the 279 | original owner of the token since we're reverting the transfer. 280 | */ 281 | let nft_transfer_log: EventLog = EventLog { 282 | // Standard name ("nep171"). 283 | standard: NFT_STANDARD_NAME.to_string(), 284 | // Version of the standard ("nft-1.0.0"). 285 | version: NFT_METADATA_SPEC.to_string(), 286 | // The data related with the event stored in a vector. 287 | event: EventLogVariant::NftTransfer(vec![NftTransferLog { 288 | // The optional authorized account ID to transfer the token on behalf of the old owner. 289 | authorized_id, 290 | // The old owner's account ID. 291 | old_owner_id: receiver_id.to_string(), 292 | // The account ID of the new owner of the token. 293 | new_owner_id: owner_id.to_string(), 294 | // A vector containing the token IDs as strings. 295 | token_ids: vec![token_id.to_string()], 296 | // An optional memo to include. 297 | memo, 298 | }]), 299 | }; 300 | 301 | //we perform the actual logging 302 | env::log_str(&nft_transfer_log.to_string()); 303 | 304 | //return false 305 | false 306 | } 307 | } 308 | -------------------------------------------------------------------------------- /contracts/src/royalty.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | pub trait NonFungibleTokenCore { 4 | //calculates the payout for a token given the passed in balance. This is a view method 5 | fn nft_payout(&self, token_id: TokenId, balance: U128, max_len_payout: u32); 6 | 7 | //transfers the token to the receiver ID and returns the payout object that should be payed given the passed in balance. 8 | fn nft_transfer_payout( 9 | &mut self, 10 | receiver_id: AccountId, 11 | token_id: TokenId, 12 | approval_id: u64, 13 | memo: String, 14 | balance: U128, 15 | max_len_payout: u32, 16 | ); 17 | } 18 | 19 | #[near_bindgen] 20 | impl NonFungibleTokenCore for Contract { 21 | //calculates the payout for a token given the passed in balance. This is a view method 22 | fn nft_payout(&self, token_id: TokenId, balance: U128, max_len_payout: u32) { 23 | /* 24 | FILL THIS IN 25 | */ 26 | } 27 | 28 | //transfers the token to the receiver ID and returns the payout object that should be payed given the passed in balance. 29 | #[payable] 30 | fn nft_transfer_payout( 31 | &mut self, 32 | receiver_id: AccountId, 33 | token_id: TokenId, 34 | approval_id: u64, 35 | memo: String, 36 | balance: U128, 37 | max_len_payout: u32, 38 | ) { 39 | /* 40 | FILL THIS IN 41 | */ 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /dspyt/.env.example: -------------------------------------------------------------------------------- 1 | REACT_APP_API=Bearer API_KEY 2 | REACT_APP_UPLOAD=https://api.nft.storage/upload 3 | REACT_APP_URL=https://api.nft.storage/?limit=100 4 | REACT_CONTRACT_NAME=yourcontract.near -------------------------------------------------------------------------------- /dspyt/.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 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | -------------------------------------------------------------------------------- /dspyt/README.md: -------------------------------------------------------------------------------- 1 | ## Installation 2 | 3 | ```bash 4 | npm install 5 | ``` 6 | 7 | Copy `.env.example` to `.env` and replace the API_KEY with your api key from [nft.storage](https://nft.storage/manage/) 8 | 9 | ## Start the Project 10 | 11 | ```bash 12 | npm start 13 | ``` 14 | 15 | ### Links 16 | 17 | /saved 18 | 19 | / 20 | 21 | /upload -------------------------------------------------------------------------------- /dspyt/config-overrides.js: -------------------------------------------------------------------------------- 1 | const NodePolyfillPlugin = require("node-polyfill-webpack-plugin"); 2 | 3 | module.exports = function override(config, env) { 4 | config.plugins.push( 5 | new NodePolyfillPlugin({ 6 | excludeAliases: ["buffer"], 7 | }) 8 | ); 9 | return config; 10 | }; 11 | -------------------------------------------------------------------------------- /dspyt/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dspyt", 3 | "version": "0.1.0", 4 | "homepage": ".", 5 | "private": true, 6 | "browser": { 7 | "crypto": false, 8 | "buffer": false, 9 | "fs": false, 10 | "path": false, 11 | "os": false, 12 | "net": false, 13 | "stream": false, 14 | "tls": false 15 | }, 16 | "dependencies": { 17 | "@headlessui/react": "^2.1.4", 18 | "@heroicons/react": "^2.0.7", 19 | "@testing-library/jest-dom": "^6.4.2", 20 | "@testing-library/react": "^16.0.1", 21 | "@testing-library/user-event": "^14.4.3", 22 | "axios": "^1.0.0", 23 | "big.js": "^6.2.1", 24 | "fs": "^0.0.1-security", 25 | "near-api-js": "^5.0.1", 26 | "nth-check": "^2.1.1", 27 | "phosphor-react": "^1.4.0", 28 | "react": "^18.3.1", 29 | "react-dom": "^19.0.0", 30 | "react-hot-toast": "^2.3.0", 31 | "react-router-dom": "^7.0.1", 32 | "react-scripts": "^5.0.0", 33 | "web-vitals": "^4.0.1", 34 | "zustand": "^5.0.0" 35 | }, 36 | "scripts": { 37 | "build": "CI=false react-app-rewired build", 38 | "start": "set WDS_SOCKET_PORT=0 && react-app-rewired start", 39 | "test": "react-scripts test", 40 | "eject": "react-scripts eject" 41 | }, 42 | "eslintConfig": { 43 | "extends": [ 44 | "react-app", 45 | "react-app/jest" 46 | ] 47 | }, 48 | "browserslist": { 49 | "production": [ 50 | ">0.2%", 51 | "not dead", 52 | "not op_mini all" 53 | ], 54 | "development": [ 55 | "last 1 chrome version", 56 | "last 1 firefox version", 57 | "last 1 safari version" 58 | ] 59 | }, 60 | "devDependencies": { 61 | "@types/big.js": "^6.1.5", 62 | "@types/react": "^19.0.1", 63 | "@types/react-dom": "^19.0.1", 64 | "autoprefixer": "^10.4.8", 65 | "node-polyfill-webpack-plugin": "^4.0.0", 66 | "postcss": "^8.4.16", 67 | "react-app-rewired": "^2.1.11", 68 | "tailwindcss": "^4.0.8" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /dspyt/public/assets/images/Pin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PinSaveDAO/PinSaveNear/abb8cf4f481be4d20eecd5a542bd4c3cc0037500/dspyt/public/assets/images/Pin.png -------------------------------------------------------------------------------- /dspyt/public/assets/images/PinSave.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PinSaveDAO/PinSaveNear/abb8cf4f481be4d20eecd5a542bd4c3cc0037500/dspyt/public/assets/images/PinSave.png -------------------------------------------------------------------------------- /dspyt/public/assets/images/PinSaveB.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PinSaveDAO/PinSaveNear/abb8cf4f481be4d20eecd5a542bd4c3cc0037500/dspyt/public/assets/images/PinSaveB.png -------------------------------------------------------------------------------- /dspyt/public/assets/images/PinSaveL.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PinSaveDAO/PinSaveNear/abb8cf4f481be4d20eecd5a542bd4c3cc0037500/dspyt/public/assets/images/PinSaveL.png -------------------------------------------------------------------------------- /dspyt/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | Pin Save 13 | 14 | 15 | 16 |
17 | 18 | 19 | -------------------------------------------------------------------------------- /dspyt/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /dspyt/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /dspyt/src/App.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import { Toaster } from "react-hot-toast"; 3 | import { BrowserRouter as Router, Route, Routes } from "react-router-dom"; 4 | 5 | import { Navigation, Home, Upload, Post } from "./components"; 6 | import SavedPosts from "./components/SavedPosts"; 7 | import { useStore } from "./store"; 8 | 9 | const App = ({ contract, currentUser, nearConfig, wallet, didcontract }) => { 10 | const initNear = useStore((state) => state.setUpStore); 11 | 12 | useEffect(() => { 13 | initNear(contract, currentUser, nearConfig, wallet, didcontract); 14 | }, [initNear, contract, currentUser, nearConfig, wallet, didcontract]); 15 | 16 | return ( 17 | 18 | 19 | 20 | 21 | } /> 22 | } /> 23 | } /> 24 | } /> 25 | 26 | 27 | ); 28 | }; 29 | 30 | export default App; 31 | -------------------------------------------------------------------------------- /dspyt/src/App.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from "@testing-library/react"; 2 | import { BrowserRouter } from "react-router-dom"; 3 | import { Navigation } from "./components"; 4 | 5 | test("renders title", () => { 6 | render( 7 | 8 | 9 | 10 | ); 11 | const linkElement = screen.getByText(/Dspyt-NFTs/i); 12 | expect(linkElement).toBeInTheDocument(); 13 | }); 14 | -------------------------------------------------------------------------------- /dspyt/src/components/Home.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | import { Link } from "react-router-dom"; 3 | 4 | import { useStore } from "../store"; 5 | 6 | 7 | function Home() { 8 | const [posts, setPosts] = useState([]); 9 | const [isLoading, setIsLoading] = useState(true); 10 | const contract = useStore((state) => state.contract); 11 | 12 | useEffect(() => { 13 | async function fetchposts(){ 14 | const items = await contract.nft_tokens({ 15 | limit: 10 16 | }) 17 | 18 | setPosts(items); 19 | setIsLoading(false); 20 | } 21 | fetchposts(); 22 | }, [contract]); 23 | 24 | if (isLoading) { 25 | return
Loading...
; 26 | } 27 | 28 | return ( 29 |
30 | 31 |
32 | {posts.map((item) => ( 33 | 34 |
35 |
36 | 37 | 42 | 43 |
44 | 52 |
53 | 54 | ))} 55 |
56 |
57 | 58 | ); 59 | } 60 | 61 | export default Home; 62 | -------------------------------------------------------------------------------- /dspyt/src/components/Navigation.jsx: -------------------------------------------------------------------------------- 1 | import { NavLink } from "react-router-dom"; 2 | import { Fragment } from "react"; 3 | import { Disclosure, Menu, Transition } from "@headlessui/react"; 4 | import { SignOut } from "phosphor-react"; 5 | import { MenuIcon, XIcon } from '@heroicons/react/outline' 6 | 7 | import { useStore } from "../store"; 8 | 9 | const navigation = [ 10 | { name: 'Home', href: '/', current: false }, 11 | { name: 'Upload', href: '/upload', current: false }, 12 | { name: 'Saved', href: '/saved', current: false }, 13 | ] 14 | 15 | //const userNavigation = [ 16 | //{name : 'Upload', href: '/upload'} 17 | // { name: 'Your Profile', href: '/' }, 18 | //{ name: 'Settings', href: '/saved' }, 19 | //{ name: 'Sign out', href: '/' }, 20 | //] 21 | 22 | function classNames(...classes) { 23 | return classes.filter(Boolean).join(' ') 24 | } 25 | 26 | export default function Navigation() { 27 | 28 | const wallet = useStore((state) => state.wallet); 29 | const contract = useStore((state) => state.contract); 30 | const nearConfig = useStore((state) => state.nearConfig); 31 | const currentUser = useStore((state) => state.currentUser); 32 | const ConnectWallet = () => { 33 | wallet.requestSignIn( 34 | { 35 | contractId: nearConfig.contractName, 36 | methodNames: [contract.nft_mint.name], 37 | }, //contract requesting access 38 | "PinSave", 39 | null, 40 | null 41 | ); 42 | }; 43 | 44 | 45 | return ( 46 | 47 | {({ open }) => ( 48 | <> 49 |
50 |
51 |
52 |
53 | {/* Mobile menu */} 54 | 55 | Open main menu 56 | {open ? ( 57 | 62 |
63 | 64 |
65 | 66 | Pin Save 71 | Pin Save 76 | 77 |
78 | 79 | 80 |
81 | {navigation.map((item) => ( 82 | 91 | {item.name} 92 | 93 | ))} 94 |
95 | 96 |
97 |
98 |
99 | { !currentUser ? ( 100 | 108 | ) : ( 109 | 110 | 111 | {currentUser.accountId} 112 | 113 | 114 | 123 | 124 |
125 | 126 |
127 | Ⓝ {( 128 | Number(currentUser.balance) / Math.pow(10, 24) 129 | ).toFixed(3)} 130 |
131 |
132 | 133 | {({ active }) => ( 134 | 152 | )} 153 | 154 |
155 |
156 |
157 |
158 | 159 | ) } 160 |
161 | 162 | 163 |
164 |
165 |
166 | 167 | 168 |
169 | {navigation.map((item) => ( 170 | 180 | {item.name} 181 | 182 | ))} 183 |
184 | {currentUser ? ( 185 |
186 |
187 | 188 | {/*userNavigation.map((item) => ( 189 | 194 | {item.name} 195 | 196 | ))*/} 197 | 198 | 199 | Ⓝ {( 200 | Number(currentUser.balance) / Math.pow(10, 24) 201 | ).toFixed(3)} 202 | 203 | { ( 204 | { 206 | wallet.signOut(); 207 | window.location.replace( 208 | window.location.origin + window.location.pathname 209 | ); 210 | }} 211 | to="/" 212 | className="text-gray-200 group flex rounded-b-md transition items-center w-full px-2 py-2 text-sm hover:text-white hover:bg-red-600" 213 | > 214 | 220 | )} 221 | 222 |
223 |
224 | ) : ""} 225 |
226 | 227 | )} 228 | 229 |
230 | ); 231 | } -------------------------------------------------------------------------------- /dspyt/src/components/Post.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { useParams } from "react-router-dom"; 3 | import { ArrowSquareOut, Tray } from "phosphor-react"; 4 | 5 | import { useStore } from "../store"; 6 | 7 | const Post = () => { 8 | const params = useParams(); 9 | const [userpost, setUserPost] = useState([]); 10 | const [saved, setSaved] = useState(false); 11 | const [isLoading, setIsLoading] = useState(true); 12 | const contract = useStore((state) => state.contract); 13 | 14 | useEffect(() => { 15 | const fetchPost = async () => { 16 | const item = await contract.nft_token({ 17 | token_id: params.token_id, 18 | }); 19 | let posts = JSON.parse(localStorage.getItem("saved_posts")) || []; 20 | if (posts.includes(item.token_id)) setSaved(true); 21 | setUserPost(item); 22 | setIsLoading(false); 23 | }; 24 | fetchPost(); 25 | }, [contract, params]); 26 | 27 | const savePost = async () => { 28 | if (!saved) { 29 | let posts = JSON.parse(localStorage.getItem("saved_posts")) || []; 30 | posts.push(userpost.token_id); 31 | localStorage.setItem("saved_posts", JSON.stringify(posts)); 32 | setSaved(true); 33 | } else { 34 | let posts = JSON.parse(localStorage.getItem("saved_posts")) || []; 35 | let filteredPosts = posts.filter((val, i) => val !== userpost.token_id); 36 | 37 | localStorage.setItem("saved_posts", JSON.stringify(filteredPosts)); 38 | setSaved(false); 39 | } 40 | }; 41 | if (isLoading) 42 | return ( 43 |
44 |

45 | Welcome 46 |

47 |

Loading...

48 |
49 | ); 50 | 51 | if (userpost) 52 | return ( 53 |
54 |
55 | 61 |
62 |
63 |
64 |

{userpost.metadata.title}

65 |

66 | Owned by{" "} 67 | {userpost.owner_id} 68 |

69 |
{ 71 | window.open( 72 | `https://explorer.testnet.near.org/accounts/${userpost.owner_id}` 73 | ); 74 | }} 75 | className="mb-4 flex text-black/50" 76 | > 77 | View on Explorer 78 | 79 |
80 |

81 | Description: 82 |
83 | {userpost.metadata.description} 84 |

85 |
86 | 93 |
94 |
95 | ); 96 | 97 | return ( 98 |
99 |

100 | 404 101 |

102 |

Post not found

103 |
104 | ); 105 | }; 106 | 107 | export default Post; 108 | -------------------------------------------------------------------------------- /dspyt/src/components/PostCard.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { Link } from "react-router-dom"; 3 | 4 | import { useStore } from "../store"; 5 | 6 | const PostCard = ({ tokenId }) => { 7 | const [item, setItem] = useState(); 8 | const contract = useStore((state) => state.contract); 9 | 10 | const initPost = async () => { 11 | const post = await contract.nft_token({ 12 | token_id: tokenId, 13 | }); 14 | setItem(post); 15 | }; 16 | 17 | useEffect(() => { 18 | initPost(); 19 | }, []); 20 | 21 | if (item) 22 | return ( 23 | 24 |
28 |
29 | 34 |
35 | 43 |
44 | 45 | ); 46 | 47 | return ( 48 |
49 | Loading... 50 |
51 | ); 52 | }; 53 | 54 | export default PostCard; 55 | -------------------------------------------------------------------------------- /dspyt/src/components/SavedPosts.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | 3 | import { useStore } from "../store"; 4 | import PostCard from "./PostCard"; 5 | 6 | const SavedPosts = () => { 7 | const [posts, setPosts] = useState([]); 8 | const [isLoading, setIsLoading] = useState(true); 9 | 10 | const contract = useStore((state) => state.contract); 11 | 12 | useEffect(() => { 13 | if (contract) 14 | fetchposts(); 15 | }, [contract]); 16 | 17 | const fetchposts = () => { 18 | let items = JSON.parse(localStorage.getItem("saved_posts")) || [] 19 | setPosts(items); 20 | setIsLoading(false); 21 | }; 22 | 23 | if (isLoading) { 24 | return
Loading...
; 25 | } 26 | 27 | return ( 28 |
29 |
30 | {posts.map((post) => )} 31 |
32 |
33 | ); 34 | } 35 | 36 | export default SavedPosts -------------------------------------------------------------------------------- /dspyt/src/components/Upload.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useRef } from "react"; 2 | import toast from "react-hot-toast"; 3 | import axios from "axios"; 4 | import { SpinnerGap } from "phosphor-react"; 5 | import Big from "big.js"; 6 | 7 | import { useStore } from "../store"; 8 | 9 | 10 | const BOATLOAD_OF_GAS = Big(3) 11 | .times(10 ** 13) 12 | .toFixed(); 13 | 14 | function Upload() { 15 | ///TODO: Switch to nft.storage lib 16 | 17 | const currentUser = useStore((state) => state.currentUser); 18 | const contract = useStore((state) => state.contract); 19 | const [selectedImage, setSelectedImage] = useState(null); 20 | const [state, setState] = useState(false); 21 | const [postId, setPostId] = useState(""); 22 | const [postDesc, setPostDesc] = useState(""); 23 | const [postTitle, setPostTitle] = useState(""); 24 | const [uploading, setUploading] = useState(); 25 | const hiddenFileInput = useRef(); 26 | 27 | if (!currentUser) { 28 | return ( 29 |
30 |

31 | 420 32 |

33 |

Please Log In

34 |
35 | ) 36 | } 37 | 38 | function filledFields() { 39 | return postId !== "" && postDesc !== "" && postTitle !== ""; 40 | } 41 | 42 | async function str() { 43 | const filled = filledFields(); 44 | 45 | if (!uploading && currentUser && contract && filled) { 46 | setUploading(true); 47 | await axios({ 48 | method: "post", 49 | url: process.env.REACT_APP_UPLOAD, 50 | headers: { 51 | Authorization: process.env.REACT_APP_API, 52 | "Content-Type": "image/*", 53 | }, 54 | data: selectedImage, 55 | }).then(async (r) => { 56 | await contract 57 | .nft_mint( 58 | { 59 | token_id: postId, 60 | metadata: { 61 | title: postTitle, 62 | description: postDesc, 63 | media: `https://${r.data.value.cid}.ipfs.dweb.link/`, 64 | }, 65 | receiver_id: `${currentUser.accountId}`, 66 | }, 67 | BOATLOAD_OF_GAS, 68 | Big("1") 69 | .times(10 ** 22) 70 | .toFixed() 71 | ) 72 | .then(() => { 73 | toast.success("Image Uploaded Successfully", { 74 | style: { 75 | borderRadius: "10px", 76 | background: "#222", 77 | color: "#fff", 78 | }, 79 | }); 80 | setUploading(false); 81 | }); 82 | }); 83 | } 84 | } 85 | 86 | return ( 87 |
88 |

Upload a new Post

89 |
90 |
91 |
92 | 95 | setPostId(e.target.value)} 98 | className="block w-11/12 md:w-full px-4 py-2 text-gray-700 bg-white border rounded-md focus:border-blue-400 focus:ring-blue-300 dark:focus:border-blue-300 focus:outline-none focus:ring focus:ring-opacity-40" 99 | type="text" 100 | /> 101 |
102 | 103 |
104 | 107 | setPostTitle(e.target.value)} 110 | className="block w-11/12 md:w-full px-4 py-2 text-gray-700 bg-white border rounded-md focus:border-blue-400 focus:ring-blue-300 dark:focus:border-blue-300 focus:outline-none focus:ring focus:ring-opacity-40" 111 | type="text" 112 | /> 113 |
114 |
115 | 116 |
117 | 120 | 121 | 126 |
127 |
128 |
129 |
130 | { 136 | if (event.target.files[0].type.includes("image")) { 137 | setSelectedImage(event.target.files[0]); 138 | setState(true); 139 | } 140 | }} 141 | /> 142 | 143 | {selectedImage && ( 144 | 149 | )} 150 | 158 |
159 |
160 | {state && ( 161 |
162 |
163 | 174 |
175 | )} 176 |
177 | ); 178 | } 179 | 180 | export default Upload; 181 | -------------------------------------------------------------------------------- /dspyt/src/components/index.js: -------------------------------------------------------------------------------- 1 | export { default as Navigation } from "./Navigation"; 2 | export { default as Home } from "./Home"; 3 | export { default as Upload } from "./Upload"; 4 | export { default as Post } from "./Post"; 5 | -------------------------------------------------------------------------------- /dspyt/src/config.js: -------------------------------------------------------------------------------- 1 | const CONTRACT_NAME = process.env.REACT_CONTRACT_NAME || "pinsave.testnet"; 2 | 3 | function getConfig(env) { 4 | switch (env) { 5 | case "mainnet": 6 | return { 7 | networkId: "mainnet", 8 | nodeUrl: "https://rpc.mainnet.near.org", 9 | contractName: CONTRACT_NAME, 10 | walletUrl: "https://wallet.near.org", 11 | helperUrl: "https://helper.mainnet.near.org", 12 | }; 13 | case "testnet": 14 | return { 15 | networkId: "testnet", 16 | nodeUrl: "https://rpc.testnet.near.org", 17 | contractName: CONTRACT_NAME, 18 | walletUrl: "https://wallet.testnet.near.org", 19 | helperUrl: "https://helper.testnet.near.org", 20 | }; 21 | case "betanet": 22 | return { 23 | networkId: "betanet", 24 | nodeUrl: "https://rpc.betanet.near.org", 25 | contractName: CONTRACT_NAME, 26 | walletUrl: "https://wallet.betanet.near.org", 27 | helperUrl: "https://helper.betanet.near.org", 28 | }; 29 | case "local": 30 | return { 31 | networkId: "local", 32 | nodeUrl: "http://localhost:3030", 33 | keyPath: `${process.env.HOME}/.near/validator_key.json`, 34 | walletUrl: "http://localhost:4000/wallet", 35 | contractName: CONTRACT_NAME, 36 | }; 37 | case "ci": 38 | return { 39 | networkId: "shared-test", 40 | nodeUrl: "https://rpc.ci-testnet.near.org", 41 | contractName: CONTRACT_NAME, 42 | masterAccount: "test.near", 43 | }; 44 | case "ci-betanet": 45 | return { 46 | networkId: "shared-test-staging", 47 | nodeUrl: "https://rpc.ci-betanet.near.org", 48 | contractName: CONTRACT_NAME, 49 | masterAccount: "test.near", 50 | }; 51 | default: 52 | throw Error( 53 | `Unconfigured environment '${env}'. Can be configured in src/config.js.` 54 | ); 55 | } 56 | } 57 | 58 | module.exports = getConfig; 59 | -------------------------------------------------------------------------------- /dspyt/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; -------------------------------------------------------------------------------- /dspyt/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import { Contract, connect, keyStores, WalletConnection } from "near-api-js"; 4 | 5 | import "./index.css"; 6 | import App from "./App"; 7 | import getConfig from "./config.js"; 8 | 9 | 10 | // Initializing contract 11 | async function initContract() { 12 | // get network configuration values from config.js 13 | // based on the network ID we pass to getConfig() 14 | const nearConfig = getConfig(process.env.NEAR_ENV || "testnet"); 15 | 16 | // create a keyStore for signing transactions using the user's key 17 | // which is located in the browser local storage after user logs in 18 | const keyStore = new keyStores.BrowserLocalStorageKeyStore(); 19 | 20 | // Initializing connection to the NEAR testnet 21 | const near = await connect({ keyStore, ...nearConfig }); 22 | 23 | // Initialize wallet connection 24 | const walletConnection = new WalletConnection(near); 25 | 26 | // Load in user's account data 27 | let currentUser; 28 | if (walletConnection.getAccountId()) { 29 | currentUser = { 30 | // Gets the accountId as a string 31 | accountId: walletConnection.getAccountId(), 32 | // Gets the user's token balance 33 | balance: (await walletConnection.account().state()).amount, 34 | }; 35 | } 36 | 37 | // Initializing our contract APIs by contract name and configuration 38 | const contract = new Contract( 39 | // User's accountId as a string 40 | walletConnection.account(), 41 | // accountId of the contract we will be loading 42 | // NOTE: All contracts on NEAR are deployed to an account and 43 | // accounts can only have one contract deployed to them. 44 | nearConfig.contractName, 45 | { 46 | // View methods are read-only – they don't modify the state, but usually return some value 47 | viewMethods: ["nft_total_supply", "nft_supply_for_owner", "nft_tokens","nft_token"], 48 | // Change methods can modify the state, but you don't receive the returned value when called 49 | changeMethods: ["nft_mint"], 50 | // Sender is the account ID to initialize transactions. 51 | // getAccountId() will return empty string if user is still unauthorized 52 | sender: walletConnection.getAccountId(), 53 | } 54 | ); 55 | 56 | //return { contract, currentUser, nearConfig, walletConnection }; 57 | 58 | const didContract = new Contract(walletConnection.account(), 'dids.vitalpointai.testnet', { 59 | viewMethods: ['getDID', 'hasDID', 'findAlias', 'getAliases', 'getDefinitions'], 60 | changeMethods: [] 61 | }) 62 | 63 | return { contract, currentUser, nearConfig, walletConnection, didContract }; 64 | 65 | } 66 | 67 | initContract().then( 68 | ({ contract, currentUser, nearConfig, walletConnection, didContract }) => { 69 | ReactDOM.render( 70 | 71 | 78 | , 79 | 80 | document.getElementById("root") 81 | ); 82 | } 83 | ); 84 | -------------------------------------------------------------------------------- /dspyt/src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = (onPerfEntry) => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import("web-vitals").then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /dspyt/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import "@testing-library/jest-dom"; 6 | -------------------------------------------------------------------------------- /dspyt/src/store.js: -------------------------------------------------------------------------------- 1 | import create from "zustand"; 2 | 3 | export const useStore = create((set) => ({ 4 | contract: null, 5 | currentUser: null, 6 | nearConfig: null, 7 | wallet: null, 8 | didcontract: null, 9 | setUpStore: (contract, currentUser, nearConfig, wallet, didcontract) => 10 | set(() => ({ 11 | contract: contract, 12 | currentUser: currentUser, 13 | nearConfig: nearConfig, 14 | wallet: wallet, 15 | didcontract: didcontract, 16 | })), 17 | })); 18 | -------------------------------------------------------------------------------- /dspyt/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: ["./src/**/*.{js,jsx,ts,tsx}"], 3 | theme: { 4 | extend: { 5 | backgroundSize: { 6 | "size-200": "200% 200%", 7 | }, 8 | backgroundPosition: { 9 | "pos-0": "0% 0%", 10 | "pos-100": "100% 100%", 11 | }, 12 | }, 13 | }, 14 | plugins: [], 15 | }; 16 | --------------------------------------------------------------------------------