├── .eslintrc.js ├── .gitattributes ├── .github └── workflows │ ├── cd.yml │ └── run-tests.yml ├── .gitignore ├── LICENSE ├── README.md ├── babel.config.js ├── browser ├── index.html ├── ipfs.js ├── list.html ├── polkadot-utils.js ├── polkadot-utils.js.LICENSE.txt ├── polkadot-utils.ts ├── rmrk-tools.js └── rmrk-tools.js.LICENSE.txt ├── cli ├── consolidate.ts ├── fetch.ts ├── getevents.ts ├── metadata.ts ├── run-listener.ts ├── seed.ts └── validate.ts ├── config ├── rollup-cjs.js ├── rollup.js ├── webpack.config.polkadot-umd.js └── webpack.config.umd.js ├── dumps ├── remarks-4892957-6588851-0x726d726b,0x524d524b.json └── remarks-4892957-6619194-0x726d726b,0x524d524b.json ├── jest.config.1.0.0.js ├── jest.config.2.0.0.js ├── jest.config.js ├── metadata-seed.example.json ├── package.json ├── prettierrc.js ├── somefile.json ├── src ├── rmrk0.1 │ ├── classes │ │ ├── collection.ts │ │ ├── nft.ts │ │ ├── rmrk.ts │ │ └── state │ │ │ └── static.ts │ ├── types.ts │ └── types │ │ └── state.ts ├── rmrk1.0.0 │ ├── changelog.ts │ ├── classes │ │ ├── buy.ts │ │ ├── changeissuer.ts │ │ ├── collection.ts │ │ ├── consume.ts │ │ ├── emote.ts │ │ ├── list.ts │ │ ├── nft.ts │ │ └── send.ts │ ├── constants.ts │ ├── index.ts │ ├── listener.ts │ ├── tools │ │ ├── consolidator │ │ │ ├── adapters │ │ │ │ ├── in-memory-adapter.ts │ │ │ │ ├── interface.ts │ │ │ │ ├── json.ts │ │ │ │ └── types.ts │ │ │ ├── consolidator.ts │ │ │ ├── interactions │ │ │ │ ├── buy.ts │ │ │ │ ├── changeIssuer.ts │ │ │ │ ├── consume.ts │ │ │ │ ├── emote.ts │ │ │ │ ├── list.ts │ │ │ │ ├── mint.ts │ │ │ │ ├── mintNFT.ts │ │ │ │ └── send.ts │ │ │ ├── interface.ts │ │ │ ├── remark.ts │ │ │ └── utils.ts │ │ ├── constants.ts │ │ ├── deriveMultisigAddress.ts │ │ ├── fetchRemarks.ts │ │ ├── metadata-to-ipfs.ts │ │ ├── types.ts │ │ ├── utils.ts │ │ ├── validate-metadata.ts │ │ └── validate-remark.ts │ └── types.ts └── rmrk2.0.0 │ ├── changelog.ts │ ├── classes │ ├── accept.ts │ ├── base.ts │ ├── burn.ts │ ├── buy.ts │ ├── changeissuer.ts │ ├── collection.ts │ ├── destroy.ts │ ├── emote.ts │ ├── equip.ts │ ├── equippable.ts │ ├── list.ts │ ├── lock.ts │ ├── nft.ts │ ├── resadd.ts │ ├── send.ts │ ├── setpriority.ts │ ├── setproperty.ts │ └── themeadd.ts │ ├── constants.ts │ ├── index.ts │ ├── listener.ts │ └── tools │ ├── consolidator │ ├── adapters │ │ ├── in-memory-adapter.ts │ │ └── types.ts │ ├── consolidator.ts │ ├── interactions │ │ ├── accept.ts │ │ ├── base.ts │ │ ├── burn.ts │ │ ├── buy.ts │ │ ├── changeIssuer.ts │ │ ├── create.ts │ │ ├── destroy.ts │ │ ├── emote.ts │ │ ├── equip.ts │ │ ├── equippable.ts │ │ ├── list.ts │ │ ├── lock.ts │ │ ├── mint.ts │ │ ├── resadd.ts │ │ ├── send.ts │ │ ├── setpriority.ts │ │ ├── setproperty.ts │ │ └── themeadd.ts │ ├── remark.ts │ └── utils.ts │ ├── constants.ts │ ├── deriveMultisigAddress.ts │ ├── fetchRemarks.ts │ ├── get-polkadot-api-with-reconnect.ts │ ├── metadata-to-ipfs.ts │ ├── polyfill-string-from-codepoint.ts │ ├── types.ts │ ├── utils.ts │ ├── validate-emoji.ts │ ├── validate-metadata.ts │ └── validate-remark.ts ├── test ├── 1.0.0 │ ├── __snapshots__ │ │ └── consolidator.test.ts.snap │ ├── consolidator.test.ts │ ├── mocks │ │ ├── blocks-dump-recent.ts │ │ ├── blocks-dump.ts │ │ ├── blocks.ts │ │ ├── metadata-valid.ts │ │ └── remark-mocks.ts │ └── utils │ │ ├── __snapshots__ │ │ └── utils.test.ts.snap │ │ ├── utils.test.ts │ │ ├── validate-base.test.ts │ │ ├── validate-buy.test.ts │ │ ├── validate-collection.test.ts │ │ ├── validate-metadata.test.ts │ │ ├── validate-mint-ids.test.ts │ │ ├── validate-nft.test.ts │ │ ├── validate-send.test.ts │ │ └── validate-string-is-a-valid-url.ts ├── 2.0.0 │ ├── __snapshots__ │ │ └── consolidator.test.ts.snap │ ├── classes │ │ ├── __snapshots__ │ │ │ ├── base.test.ts.snap │ │ │ ├── nft-class.test.ts.snap │ │ │ └── nft.test.ts.snap │ │ ├── base.test.ts │ │ ├── nft-class.test.ts │ │ └── nft.test.ts │ ├── consolidator.test.ts │ ├── consolidator │ │ ├── __snapshots__ │ │ │ ├── accept.test.ts.snap │ │ │ ├── base.test.ts.snap │ │ │ ├── burn.test.ts.snap │ │ │ ├── buy.test.ts.snap │ │ │ ├── changeissuer.test.ts.snap │ │ │ ├── destroy.test.ts.snap │ │ │ ├── equip.test.ts.snap │ │ │ ├── equippable.test.ts.snap │ │ │ ├── list.test.ts.snap │ │ │ ├── lock.test.ts.snap │ │ │ ├── mint.test.ts.snap │ │ │ ├── send.test.ts.snap │ │ │ ├── setproperty.test.ts.snap │ │ │ └── themeadd.test.ts.snap │ │ ├── accept.test.ts │ │ ├── base.test.ts │ │ ├── burn.test.ts │ │ ├── buy.test.ts │ │ ├── changeissuer.test.ts │ │ ├── destroy.test.ts │ │ ├── equip.test.ts │ │ ├── equippable.test.ts │ │ ├── list.test.ts │ │ ├── lock.test.ts │ │ ├── mint.test.ts │ │ ├── resadd.test.ts │ │ ├── send.test.ts │ │ ├── setpriority.test.ts │ │ ├── setproperty.test.ts │ │ └── themeadd.test.ts │ ├── interactions │ │ └── equippable.test.ts │ ├── mocks.ts │ ├── mocks │ │ ├── metadata-valid.ts │ │ └── remark-mocks.ts │ └── utils │ │ ├── append-json-stream.ts │ │ ├── validate-emoji.test.ts │ │ ├── validate-metadata.test.ts │ │ └── validate-string-is-a-valid-url.ts └── seed │ ├── default │ ├── multiple.ts │ └── single.ts │ ├── devaccs.ts │ ├── seeder.ts │ └── types.ts ├── tsconfig.cli-dist.json ├── tsconfig.cli.json ├── tsconfig.json └── yarn.lock /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: "@typescript-eslint/parser", // Specifies the ESLint parser 3 | extends: [ 4 | "plugin:@typescript-eslint/recommended", // Uses the recommended rules from the @typescript-eslint/eslint-plugin 5 | "plugin:security/recommended", 6 | "prettier/@typescript-eslint", // Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier 7 | "plugin:prettier/recommended", // Enables eslint-plugin-prettier and eslint-config-prettier. This will display prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array. 8 | ], 9 | parserOptions: { 10 | ecmaVersion: 2018, // Allows for the parsing of modern ECMAScript features 11 | sourceType: "module", // Allows for the use of imports 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.snap linguist-generated=true 2 | test/mocks/* linguist-generated=true 3 | -------------------------------------------------------------------------------- /.github/workflows/cd.yml: -------------------------------------------------------------------------------- 1 | name: NPM publish CD workflow 2 | 3 | on: 4 | release: 5 | # This specifies that the build will be triggered when we publish a release 6 | types: [published] 7 | 8 | jobs: 9 | build: 10 | 11 | # Run on latest version of ubuntu 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | with: 16 | # "ref" specifies the branch to check out. 17 | # "github.event.release.target_commitish" is a global variable and specifies the branch the release targeted 18 | ref: ${{ github.event.release.target_commitish }} 19 | # install Node.js 20 | - name: Use Node.js 18 21 | uses: actions/setup-node@v1 22 | with: 23 | node-version: 18 24 | # Specifies the registry, this field is required! 25 | registry-url: 'https://registry.npmjs.org' 26 | # clean install of your projects' deps. We use "npm ci" to avoid package lock changes 27 | - run: yarn install 28 | # set up git since we will later push to the repo 29 | - run: git config --global user.email "$GITHUB_ACTOR@users.noreply.github.com" && git config --global user.name "$GITHUB_ACTOR" 30 | # upgrade npm version in package.json to the tag used in the release. 31 | - run: yarn version --new-version ${{ github.event.release.tag_name }} 32 | # run tests just in case 33 | - run: yarn test:2.0.0 34 | # publish to NPM -> there is one caveat, continue reading for the fix 35 | - run: npm publish 36 | env: 37 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 38 | # push the version changes to GitHub 39 | - run: git push 40 | env: 41 | # The secret is passed automatically. Nothing to configure. 42 | github-token: ${{ secrets.GITHUB_TOKEN }} 43 | -------------------------------------------------------------------------------- /.github/workflows/run-tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests CI 2 | on: [push, pull_request] 3 | jobs: 4 | test: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v2 8 | - name: Run unit tests 9 | uses: actions/setup-node@v1 10 | with: 11 | node-version: '18' 12 | - run: yarn install 13 | - run: yarn test:2.0.0 14 | 15 | 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | *.log 4 | build 5 | archive 6 | .vscode 7 | .idea 8 | .npmrc 9 | dist 10 | dist-cli 11 | .DS_Store 12 | 13 | /remarks*.json 14 | /dump-*.json 15 | /consolidated-*.json 16 | throwaway.js 17 | coverage 18 | metadata-images 19 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = (api) => { 2 | if (api) api.cache(true); 3 | return { 4 | presets: [ 5 | "@babel/typescript", 6 | [ 7 | "@babel/preset-env", 8 | { 9 | targets: { browsers: "defaults, not ie 11", node: true }, 10 | modules: false, 11 | useBuiltIns: false, 12 | loose: true, 13 | }, 14 | ], 15 | ], 16 | plugins: [ 17 | "@babel/plugin-proposal-optional-chaining", 18 | "@babel/plugin-transform-modules-commonjs", 19 | ], 20 | }; 21 | }; 22 | -------------------------------------------------------------------------------- /browser/polkadot-utils.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /*! 2 | * The buffer module from node.js, for the browser. 3 | * 4 | * @author Feross Aboukhadijeh 5 | * @license MIT 6 | */ 7 | 8 | /*! ***************************************************************************** 9 | Copyright (c) Microsoft Corporation. 10 | 11 | Permission to use, copy, modify, and/or distribute this software for any 12 | purpose with or without fee is hereby granted. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 15 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 16 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 17 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 18 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 19 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 20 | PERFORMANCE OF THIS SOFTWARE. 21 | ***************************************************************************** */ 22 | 23 | /*! ieee754. BSD-3-Clause License. Feross Aboukhadijeh */ 24 | 25 | /*! safe-buffer. MIT License. Feross Aboukhadijeh */ 26 | 27 | /** 28 | * [js-sha3]{@link https://github.com/emn178/js-sha3} 29 | * 30 | * @version 0.8.0 31 | * @author Chen, Yi-Cyuan [emn178@gmail.com] 32 | * @copyright Chen, Yi-Cyuan 2015-2018 33 | * @license MIT 34 | */ 35 | -------------------------------------------------------------------------------- /browser/polkadot-utils.ts: -------------------------------------------------------------------------------- 1 | export * as extensionDapps from "@polkadot/extension-dapp"; 2 | export * as utilCrypto from "@polkadot/util-crypto"; 3 | export * as util from "@polkadot/util"; 4 | export * as api from "@polkadot/api"; 5 | -------------------------------------------------------------------------------- /browser/rmrk-tools.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /*! 2 | * The buffer module from node.js, for the browser. 3 | * 4 | * @author Feross Aboukhadijeh 5 | * @license MIT 6 | */ 7 | 8 | /*! ieee754. BSD-3-Clause License. Feross Aboukhadijeh */ 9 | 10 | /*! safe-buffer. MIT License. Feross Aboukhadijeh */ 11 | -------------------------------------------------------------------------------- /cli/consolidate.ts: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | import "@polkadot/api-augment"; 3 | import fs, { WriteStream } from "fs"; 4 | import { Consolidator } from "../src/rmrk2.0.0/tools/consolidator/consolidator"; 5 | import arg from "arg"; 6 | import { 7 | filterBlocksByCollection, 8 | getApi, 9 | getRemarksFromBlocks, 10 | prefixToArray, 11 | } from "../src/rmrk2.0.0/tools/utils"; 12 | import { appendPromise } from "../test/2.0.0/utils/append-json-stream"; 13 | import { BlockCalls } from "../src/rmrk2.0.0/tools/types"; 14 | import { Remark } from "../src/rmrk2.0.0/tools/consolidator/remark"; 15 | import { JsonStreamStringify } from "json-stream-stringify"; 16 | 17 | const getRemarks = ( 18 | inputData: any, 19 | prefixes: string[], 20 | collectionFilter?: string, 21 | ss58Format?: number 22 | ): Remark[] => { 23 | let blocks = inputData; 24 | if (collectionFilter) { 25 | blocks = filterBlocksByCollection( 26 | blocks, 27 | prefixes, 28 | collectionFilter, 29 | ss58Format 30 | ); 31 | } 32 | return getRemarksFromBlocks(blocks, prefixes); 33 | }; 34 | 35 | const consolidate = async () => { 36 | const args = arg({ 37 | "--json": String, // The JSON file from which to consolidate 38 | "--collection": String, // Filter by specific collection 39 | "--ws": String, // Optional websocket url 40 | "--prefixes": String, // Limit remarks to prefix. No default. Can be hex (0x726d726b,0x524d524b) or string (rmrk,RMRK), or combination (rmrk,0x524d524b), separate with comma for multiple 41 | "--to": Number, // Optional take block to inclusive 42 | }); 43 | 44 | const ws = args["--ws"] || "ws://127.0.0.1:9944"; 45 | const api = await getApi(ws); 46 | 47 | const prefixes = prefixToArray(args["--prefixes"] || "0x726d726b,0x524d524b"); 48 | 49 | const systemProperties = await api.rpc.system.properties(); 50 | const { ss58Format: chainSs58Format } = systemProperties.toHuman(); 51 | 52 | const ss58Format = (chainSs58Format as number) || 2; 53 | 54 | const file = args["--json"]; 55 | const collectionFilter = args["--collection"]; 56 | if (!file) { 57 | console.error("File path must be provided through --json arg"); 58 | process.exit(1); 59 | } 60 | 61 | const toBlock = args["--to"]; 62 | 63 | // Check the JSON file exists and is reachable 64 | try { 65 | fs.accessSync(file, fs.constants.R_OK); 66 | } catch (e) { 67 | console.error("File is not readable. Are you providing the right path?"); 68 | process.exit(1); 69 | } 70 | let rawdata = await appendPromise(file); 71 | if (toBlock) { 72 | rawdata = rawdata.filter((obj: BlockCalls) => obj.block <= Number(toBlock)); 73 | console.log(`Take blocks to: ${toBlock}`); 74 | } 75 | 76 | console.log(`Loaded ${rawdata.length} blocks with remark calls`); 77 | 78 | const remarks = getRemarks(rawdata, prefixes, collectionFilter, ss58Format); 79 | const con = new Consolidator(ss58Format); 80 | 81 | const ret = await con.consolidate(remarks); 82 | // Optimisation: return last 500 invalid items 83 | ret.invalid = ret.invalid.slice(ret.invalid.length - 500); 84 | 85 | // @ts-ignore 86 | BigInt.prototype.toJSON = function () { 87 | return this.toString(); 88 | }; 89 | 90 | const lastBlock = rawdata[rawdata.length - 1]?.block || 0; 91 | 92 | const writeStream = fs.createWriteStream(`consolidated-from-${file}`, { 93 | encoding: "utf8", 94 | }); 95 | 96 | const waitForStreamClose = (stream: WriteStream): Promise => { 97 | stream.end(); 98 | return new Promise((resolve) => { 99 | stream.once("finish", () => { 100 | resolve(); 101 | }); 102 | }); 103 | }; 104 | 105 | new JsonStreamStringify({ ...ret, lastBlock }) 106 | .on("data", (chunk) => { 107 | writeStream.write(chunk); 108 | }) 109 | .once("end", () => { 110 | console.log("SUCCESS writing dump"); 111 | waitForStreamClose(writeStream).then(() => { 112 | process.exit(0); 113 | }); 114 | }) 115 | .once("error", (err) => { 116 | console.log("ERROR writing dump", err); 117 | process.exit(0); 118 | }); 119 | }; 120 | 121 | consolidate(); 122 | -------------------------------------------------------------------------------- /cli/getevents.ts: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | import "@polkadot/api-augment"; 3 | import { deeplog, getApi } from "../src/rmrk2.0.0/tools/utils"; 4 | import arg from "arg"; 5 | 6 | const getEvents = async () => { 7 | const args = arg({ 8 | // Types 9 | "--ws": String, // The websocket URL 10 | "--blocks": String, // Blocks to extract events from, comma separated 11 | }); 12 | 13 | const api = await getApi(args["--ws"] || "ws://127.0.0.1:9944"); 14 | console.log("Connecting to " + args["--ws"]); 15 | const blocks = (args["--blocks"] || "").split(",").map(parseInt); 16 | console.log(`Processing blocks: ` + blocks.toString()); 17 | for (const blockNum of blocks) { 18 | console.log(`========== Block ${blockNum} =============`); 19 | const blockHash = await api.rpc.chain.getBlockHash(blockNum); 20 | const events = await api.query.system.events.at(blockHash); 21 | const block = await api.rpc.chain.getBlock(blockHash); 22 | if (block.block === undefined) { 23 | console.error("Block is undefined for block " + blockHash); 24 | continue; 25 | } 26 | console.log(`Found ${events.length} events`); 27 | console.log(`Found ${block.block.extrinsics.length} extrincics`); 28 | for (const e of events) { 29 | console.log(`~~~~ Event ${e.event.method.toString()} ~~~~`); 30 | deeplog(e.toHuman()); 31 | deeplog(e.event.meta.toHuman()); 32 | console.log(e.event.section.toString()); 33 | console.log(e.event.method.toString()); 34 | console.log(`~~~~ Event ${e.event.method.toString()} END ~~~~`); 35 | } 36 | let index = 0; 37 | for (const ex of block.block.extrinsics) { 38 | console.log(`=== Extrinsic ${blockNum}-${index} =============`); 39 | deeplog(ex.toHuman()); 40 | console.log(`=== Extrinsic ${blockNum}-${index} END =============`); 41 | index++; 42 | } 43 | console.log(`========== Block ${blockNum} END =============`); 44 | } 45 | process.exit(0); 46 | }; 47 | 48 | getEvents(); 49 | -------------------------------------------------------------------------------- /cli/metadata.ts: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | import "@polkadot/api-augment"; 3 | import { uploadRMRKMetadata } from "../src/rmrk2.0.0/tools/metadata-to-ipfs"; 4 | import arg from "arg"; 5 | import fs from "fs"; 6 | import { Metadata } from "../src/rmrk2.0.0/tools/types"; 7 | 8 | const fsPromises = fs.promises; 9 | 10 | interface MetadataSeedItem { 11 | metadataFields: Metadata; 12 | imagePath?: string; 13 | } 14 | 15 | const validateMetadataSeedFields = (metadataSeed: MetadataSeedItem[]) => { 16 | metadataSeed.forEach((metadata) => { 17 | const { name, external_url, description } = metadata.metadataFields; 18 | if (!name || !external_url || !description) { 19 | throw new Error( 20 | "provided metadata has 1 or more fields missing (!name || !external_url || !description)" 21 | ); 22 | } 23 | }); 24 | }; 25 | 26 | const validateMetadataSeedImages = async (metadataSeed: MetadataSeedItem[]) => { 27 | const promises = metadataSeed.map(async (metadataSeedItem) => { 28 | const error = new Error( 29 | `Cannot read Image from path: ${metadataSeedItem.imagePath}` 30 | ); 31 | try { 32 | if (!metadataSeedItem.imagePath) { 33 | throw error; 34 | } 35 | const imageFile = await fsPromises.readFile( 36 | `${process.cwd()}${metadataSeedItem.imagePath}` 37 | ); 38 | if (!imageFile) { 39 | throw error; 40 | } 41 | } catch (error) { 42 | throw error; 43 | } 44 | }); 45 | 46 | return await Promise.all(promises); 47 | }; 48 | 49 | const uploadMetadata = async () => { 50 | const args = arg({ 51 | "--input": String, // metadata input file 52 | "--output": String, // metadata output file 53 | }); 54 | 55 | const inputFile = await fsPromises.readFile( 56 | args["--input"] || "./metadata-seed.example.json" 57 | ); 58 | const metadataInput = JSON.parse(inputFile.toString()); 59 | 60 | if (!metadataInput?.metadata || !Array.isArray(metadataInput?.metadata)) { 61 | throw new Error("Metadata json file missing 'metadata' array"); 62 | } 63 | 64 | // Validate seed JSON before trying to upload and pin it 65 | validateMetadataSeedFields(metadataInput.metadata); 66 | await validateMetadataSeedImages(metadataInput.metadata); 67 | 68 | const promises = (metadataInput.metadata as MetadataSeedItem[]).map( 69 | async (metadata, index) => { 70 | if (!metadata.imagePath && metadata.metadataFields?.image) { 71 | // This item already has valid image 72 | return metadata.metadataFields; 73 | } 74 | const metadataItem = await uploadRMRKMetadata( 75 | `${process.cwd()}${metadata.imagePath}`, 76 | metadata.metadataFields 77 | ); 78 | return { 79 | [metadata.metadataFields.name || 80 | metadata.imagePath || 81 | index]: metadataItem, 82 | }; 83 | } 84 | ); 85 | 86 | const metadataOutput = await Promise.all(promises); 87 | 88 | fs.writeFileSync( 89 | args["--output"] || "./metadata-output.json", 90 | JSON.stringify(metadataOutput) 91 | ); 92 | process.exit(0); 93 | }; 94 | 95 | uploadMetadata(); 96 | -------------------------------------------------------------------------------- /cli/run-listener.ts: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | import "@polkadot/api-augment"; 3 | import { IStorageProvider, RemarkListener } from "../src/rmrk2.0.0/listener"; 4 | import { getApi } from "../src/rmrk2.0.0/tools/utils"; 5 | import { Remark } from "../src/rmrk2.0.0/tools/consolidator/remark"; 6 | import { Consolidator } from "../src/rmrk2.0.0"; 7 | import { getLatestFinalizedBlock } from "../src/rmrk1.0.0"; 8 | 9 | /** 10 | * RMRK listener storage provider to save latest block 11 | */ 12 | class StorageProvider implements IStorageProvider { 13 | readonly storageKey: string = "latestBlock"; 14 | public latestBlock = 0; 15 | 16 | constructor(storageKey = "latestBlock", blockNum = 0) { 17 | this.storageKey = storageKey; 18 | this.latestBlock = blockNum; 19 | } 20 | 21 | public set = async (latestBlock: number) => { 22 | this.latestBlock = latestBlock; 23 | }; 24 | 25 | public get = async () => { 26 | return this.latestBlock; 27 | }; 28 | } 29 | 30 | const runListener = async () => { 31 | const api = await getApi("wss://kusama-rpc.polkadot.io"); 32 | const consolidateFunction = async (remarks: Remark[]) => { 33 | const consolidator = new Consolidator(); 34 | return await consolidator.consolidate(remarks); 35 | }; 36 | const latestBlock = await getLatestFinalizedBlock(api); 37 | const storageProvider = new StorageProvider("latestBlock", latestBlock); 38 | 39 | const listener = new RemarkListener({ 40 | polkadotApi: api, 41 | prefixes: ["0x726d726b", "0x524d524b"], 42 | consolidateFunction, 43 | storageProvider, 44 | loggerEnabled: true, 45 | }); 46 | const subscriber = listener.initialiseObservable(); 47 | subscriber.subscribe((val) => console.log(val)); 48 | const unfinilisedSubscriber = listener.initialiseObservableUnfinalised(); 49 | unfinilisedSubscriber.subscribe((val) => 50 | console.log("Unfinalised remarks:", val) 51 | ); 52 | }; 53 | 54 | runListener(); 55 | -------------------------------------------------------------------------------- /cli/seed.ts: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | import "@polkadot/api-augment"; 3 | import { getApi } from "../src/rmrk2.0.0/tools/utils"; 4 | // import { Seeder } from "../test/seed/2.0.0/seeder"; 5 | import readline from "readline"; 6 | import arg from "arg"; 7 | import { Keyring } from "@polkadot/api"; 8 | import { KeyringPair } from "@polkadot/keyring/types"; 9 | 10 | const seed = async () => { 11 | const args = arg({ 12 | // Types 13 | "--folder": String, // The folder from which to read seeds 14 | "--command": String, // Which seed to run. If "file" looks in the `folder` location provided 15 | "--ws": String, // Which remote to connect to, defaults to local (ws://127.0.0.1:9944) 16 | "--phrase": String, // Mnemonic phrase of the wallet from which to seed. Defaults to `//Alice` for dev chain. 17 | }); 18 | 19 | let folder = args["--folder"] || "default"; 20 | let command = args["--command"] || "file"; 21 | let ws = args["--ws"] || "ws://127.0.0.1:9944"; 22 | let phrase = args["--phrase"] || "//Alice"; 23 | if (!folder.startsWith("test/seed")) folder = "test/seed/" + folder; 24 | console.log("Connecting..."); 25 | const api = await getApi(ws); 26 | console.log("Connected."); 27 | 28 | const kp = getKeyringFromUri(phrase); 29 | const kp2 = getKeyringFromUri("//Bob"); 30 | console.log(`Will seed from ${kp.address}`); 31 | 32 | if ((await api.rpc.system.chain()).toHuman() != "Development") { 33 | console.warn("Warning: you are seeding a non-development chain!"); 34 | askQuestion( 35 | "⚠⚠⚠ Are you sure you want to proceed? This might be expensive! Enter YES to override: " 36 | ).then(async (answer) => { 37 | if (answer === "YES") { 38 | if (phrase == "//Alice") { 39 | console.error( 40 | "You cannot seed a non-development chain from the //Alice account. This account is only available in dev." 41 | ); 42 | process.exit(1); 43 | } 44 | // await goSeed(command, kp); 45 | } else { 46 | console.log("Execution stopped"); 47 | process.exit(1); 48 | } 49 | }); 50 | } else { 51 | // await goSeed(command, kp); 52 | } 53 | 54 | // async function goSeed(command: string, kp: KeyringPair) { 55 | // const s = new Seeder(api, kp, kp2); 56 | // 57 | // switch (command) { 58 | // case "file": 59 | // console.log("Looking for seed files inside " + folder); 60 | // await s.seedBase(); 61 | // break; 62 | // default: 63 | // console.error(`Unknown command ${command}`); 64 | // break; 65 | // } 66 | // process.exit(0); 67 | // } 68 | }; 69 | 70 | function askQuestion(query: string) { 71 | const rl = readline.createInterface({ 72 | input: process.stdin, 73 | output: process.stdout, 74 | }); 75 | 76 | return new Promise((resolve) => 77 | rl.question(query, (ans) => { 78 | rl.close(); 79 | resolve(ans); 80 | }) 81 | ); 82 | } 83 | 84 | function getKeyringFromUri(phrase: string): KeyringPair { 85 | const keyring = new Keyring({ type: "sr25519" }); 86 | return keyring.addFromUri(phrase); 87 | } 88 | 89 | // seed(); 90 | -------------------------------------------------------------------------------- /cli/validate.ts: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | import "@polkadot/api-augment"; 3 | import { NFT as N100 } from "../src/rmrk2.0.0/classes/nft"; 4 | import { deeplog } from "../src/rmrk2.0.0/tools/utils"; 5 | import { OP_TYPES } from "../src/rmrk2.0.0/tools/constants"; 6 | import arg from "arg"; 7 | 8 | const validate = async () => { 9 | const args = arg({ 10 | // Types 11 | "--remark": String, // The remark to validate 12 | }); 13 | 14 | const remark = args["--remark"] || ""; 15 | const exploded = remark.split("::"); 16 | if (exploded.length < 2) { 17 | throw new Error("Invalid RMRK remark, cannot explode by double-colon (::)"); 18 | } 19 | if (exploded[0].toUpperCase() !== "RMRK") { 20 | throw new Error( 21 | "This is not a RMRK remark - does not begin with RMRK/rmrk" 22 | ); 23 | } 24 | switch (exploded[1]) { 25 | case OP_TYPES.MINT: 26 | console.log(`Identified as ${OP_TYPES.MINT}`); 27 | const n = N100.fromRemark(remark); 28 | deeplog(n); 29 | break; 30 | default: 31 | throw new Error(`${exploded[1]} interaction not supported`); 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /config/rollup-cjs.js: -------------------------------------------------------------------------------- 1 | import config from "./rollup"; 2 | 3 | config.output = { 4 | file: "./dist/index.cjs", 5 | format: "cjs", 6 | name: "rmrkTools", 7 | sourcemap: true, 8 | }; 9 | 10 | export default config; 11 | -------------------------------------------------------------------------------- /config/rollup.js: -------------------------------------------------------------------------------- 1 | import babel from "@rollup/plugin-babel"; 2 | import cjs from "@rollup/plugin-commonjs"; 3 | import node from "@rollup/plugin-node-resolve"; 4 | import json from "@rollup/plugin-json"; 5 | import strip from "@rollup/plugin-strip"; 6 | 7 | export default { 8 | input: "./dist/index.js", 9 | output: { 10 | file: "./dist/index.es.js", 11 | format: "es", 12 | sourcemap: true, 13 | }, 14 | plugins: [ 15 | babel({ 16 | exclude: "node_modules/**", 17 | sourceMap: true, 18 | babelrc: false, 19 | extensions: [".ts"], 20 | presets: [ 21 | "@babel/typescript", 22 | [ 23 | "@babel/preset-env", 24 | { 25 | targets: { browsers: "defaults, not ie 11", node: true }, 26 | modules: false, 27 | useBuiltIns: false, 28 | loose: true, 29 | }, 30 | ], 31 | ], 32 | }), 33 | 34 | cjs({ 35 | sourceMap: true, 36 | }), 37 | 38 | node({ 39 | extensions: [".ts"], 40 | }), 41 | 42 | strip({ 43 | include: ["**/*.(mjs|js|ts)"], 44 | }), 45 | 46 | json(), 47 | ], 48 | }; 49 | -------------------------------------------------------------------------------- /config/webpack.config.polkadot-umd.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const webpack = require("webpack"); 3 | const config = require("./webpack.config.umd"); 4 | 5 | module.exports = { 6 | ...config, 7 | entry: "./browser/polkadot-utils.ts", 8 | output: { 9 | path: path.resolve(__dirname, "../browser"), 10 | filename: "polkadot-utils.js", 11 | library: "polkadotUtils", 12 | libraryTarget: "umd", 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /config/webpack.config.umd.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const webpack = require("webpack"); 3 | 4 | module.exports = { 5 | entry: "./src/index.ts", 6 | module: { 7 | rules: [ 8 | { 9 | test: /\.tsx?$/, 10 | use: "ts-loader", 11 | exclude: /node_modules/, 12 | }, 13 | ], 14 | }, 15 | plugins: [ 16 | new webpack.ProvidePlugin({ 17 | Buffer: ["buffer", "Buffer"], 18 | process: path.resolve("node_modules/process/browser.js"), 19 | }), 20 | ], 21 | externals: { 22 | crypto: "crypto", 23 | url: "url", 24 | }, 25 | resolve: { 26 | extensions: [".ts", ".js"], 27 | }, 28 | output: { 29 | path: path.resolve(__dirname, "../browser"), 30 | filename: "rmrk-tools.js", 31 | library: "rmrkTools", 32 | libraryTarget: "umd", 33 | }, 34 | }; 35 | -------------------------------------------------------------------------------- /jest.config.1.0.0.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-var-requires 2 | const jestConfig = require("./jest.config"); 3 | 4 | module.exports = { 5 | ...jestConfig, 6 | collectCoverageFrom: [ 7 | "src/rmrk1.0.0/tools/**/*.ts", 8 | "src/rmrk1.0.0/classes/**/*.ts", 9 | ], 10 | }; 11 | -------------------------------------------------------------------------------- /jest.config.2.0.0.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-var-requires 2 | const jestConfig = require("./jest.config"); 3 | 4 | module.exports = { 5 | ...jestConfig, 6 | collectCoverageFrom: [ 7 | "src/rmrk2.0.0/classes/**/*", 8 | "src/rmrk2.0.0/tools/**/*", 9 | ], 10 | }; 11 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | transform: { 3 | "^.+\\.(t|j)sx?$": "babel-jest", 4 | }, 5 | transformIgnorePatterns: [ 6 | "/node_modules/(?!(@polkadot|@babel/runtime/helpers/esm/))", 7 | ], 8 | collectCoverage: true, 9 | collectCoverageFrom: [ 10 | "src/tools/utils.ts", 11 | "src/tools/validate-remark.ts", 12 | "src/tools/consolidator/consolidator.ts", 13 | "src/tools/consolidator/interactions/*", 14 | "src/rmrk1.0.0/classes/*", 15 | ], 16 | }; 17 | -------------------------------------------------------------------------------- /metadata-seed.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "metadata": [ 3 | { 4 | "metadataFields": { 5 | "external_url":"https://rmrk.app", 6 | "description":"Limited Edition NFT", 7 | "name":"Limited Edition", 8 | "background_color":"#FFFFFF" 9 | }, 10 | "imagePath": "/metadata-images/limited-edition.svg" 11 | }, 12 | { 13 | "metadataFields": { 14 | "external_url":"https://rmrk.app", 15 | "description":"Rare NFT", 16 | "name":"Rare", 17 | "background_color":"#FFFFFF" 18 | }, 19 | "imagePath": "/metadata-images/rare.svg" 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: true, 3 | trailingComma: "all", 4 | singleQuote: true, 5 | printWidth: 120, 6 | tabWidth: 2, 7 | proseWrap: "always", 8 | }; 9 | -------------------------------------------------------------------------------- /src/rmrk0.1/classes/collection.ts: -------------------------------------------------------------------------------- 1 | import { Attribute } from "../types"; 2 | 3 | export interface Collection { 4 | readonly version: string; 5 | readonly name: string; 6 | readonly max: number; 7 | readonly issuer: string; 8 | readonly symbol: string; 9 | readonly id: string; 10 | readonly metadata: CollectionMetadata; 11 | mint(): Collection; 12 | change_issuer(): Collection; 13 | } 14 | 15 | export interface CollectionMetadata { 16 | description?: string; 17 | attributes: Attribute[]; 18 | external_url?: string; 19 | image?: string; 20 | image_data?: string; 21 | } 22 | -------------------------------------------------------------------------------- /src/rmrk0.1/classes/nft.ts: -------------------------------------------------------------------------------- 1 | import { Collection } from "./collection"; 2 | import { Attribute } from "../types"; 3 | 4 | export class NFT { 5 | // @todo discard because of 1.0.0 6 | // readonly collection: Collection; 7 | // readonly name: string; 8 | // readonly instance: string; 9 | // readonly transferable: number; 10 | // readonly sn: string; 11 | // readonly metadata: NFTMetadata; 12 | // static mintnft(): NFT { 13 | // return new NFT(); 14 | // } 15 | // id(): string { 16 | // return `${this.collection.id}-${this.instance}-${this.sn}`; 17 | // } 18 | // send(): NFT { 19 | // return this; 20 | // } 21 | // consume(): NFT { 22 | // return this; 23 | // } 24 | // list(): NFT { 25 | // return this; 26 | // } 27 | // buy(): NFT { 28 | // return this; 29 | // } 30 | } 31 | 32 | export interface NFTMetadata { 33 | external_url?: string; 34 | image?: string; 35 | image_data?: string; 36 | description?: string; 37 | name?: string; 38 | attributes: Attribute[]; 39 | background_color?: string; 40 | animation_url?: string; 41 | youtube_url?: string; 42 | } 43 | -------------------------------------------------------------------------------- /src/rmrk0.1/classes/rmrk.ts: -------------------------------------------------------------------------------- 1 | import { State } from "../types/state"; 2 | import { ApiPromise } from "@polkadot/api"; 3 | import { NFT } from "../classes/nft"; 4 | import { Collection } from "../classes/collection"; 5 | 6 | export class RMRK { 7 | static version = 0.1; 8 | public state: State; 9 | private api: ApiPromise; 10 | public constructor(state: State, api: ApiPromise) { 11 | this.state = state; 12 | this.api = api; 13 | } 14 | public persist = async function (set: []): Promise { 15 | return true; 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /src/rmrk0.1/classes/state/static.ts: -------------------------------------------------------------------------------- 1 | import { State } from "../../types/state"; 2 | import { Collection } from "../collection"; 3 | import { NFT } from "../nft"; 4 | 5 | export class StaticState implements State { 6 | private filepath: string; 7 | constructor(filepath: string) { 8 | this.filepath = filepath; 9 | } 10 | getAllCollections(): Promise { 11 | return new Promise(() => []); 12 | } 13 | getCollection(id: string): Promise { 14 | return new Promise(() => []); 15 | } 16 | getLastSyncedBlock(): Promise { 17 | return new Promise(() => []); 18 | } 19 | getNFTsForCollection(id: string): Promise { 20 | return new Promise(() => []); 21 | } 22 | getNFT(id: string): Promise { 23 | return new Promise(() => []); 24 | } 25 | getNFTsForAccount(account: string): Promise { 26 | return new Promise(() => []); 27 | } 28 | refresh(): Promise { 29 | return new Promise(() => []); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/rmrk0.1/types.ts: -------------------------------------------------------------------------------- 1 | export type DisplayType = 2 | | "boost_number" 3 | | "boost_percentage" 4 | | "number" 5 | | "date"; 6 | 7 | export interface Attribute { 8 | display_type?: DisplayType; 9 | trait_type?: string; 10 | value: number | string; 11 | max_value?: number; 12 | } 13 | -------------------------------------------------------------------------------- /src/rmrk0.1/types/state.ts: -------------------------------------------------------------------------------- 1 | import { Collection } from "../classes/collection"; 2 | import { NFT } from "../classes/nft"; 3 | 4 | export interface State { 5 | getAllCollections(): Promise; 6 | getNFTsForCollection(id: string): Promise; 7 | getNFT(id: string): Promise; 8 | getCollection(id: string): Promise; 9 | getNFTsForAccount(account: string): Promise; 10 | getLastSyncedBlock(): Promise; 11 | refresh(): Promise; 12 | } 13 | -------------------------------------------------------------------------------- /src/rmrk1.0.0/changelog.ts: -------------------------------------------------------------------------------- 1 | import { OP_TYPES } from "./tools/constants"; 2 | 3 | export type Change = { 4 | field: string; 5 | old: any; 6 | new: any; 7 | caller: string; 8 | block: number; 9 | opType: OP_TYPES; 10 | }; 11 | -------------------------------------------------------------------------------- /src/rmrk1.0.0/classes/buy.ts: -------------------------------------------------------------------------------- 1 | import { validateBuy } from "../tools/validate-remark"; 2 | 3 | export class Buy { 4 | id: string; 5 | 6 | constructor(id: string) { 7 | this.id = id; 8 | } 9 | 10 | static fromRemark(remark: string): Buy | string { 11 | try { 12 | validateBuy(remark); 13 | const [_prefix, _op_type, _version, id] = remark.split("::"); 14 | return new Buy(id); 15 | } catch (e: any) { 16 | console.error(e.message); 17 | console.log(`BUY error: full input was ${remark}`); 18 | return e.message; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/rmrk1.0.0/classes/changeissuer.ts: -------------------------------------------------------------------------------- 1 | import { validateChangeIssuer } from "../tools/validate-remark"; 2 | 3 | export class ChangeIssuer { 4 | issuer: string; 5 | id: string; 6 | 7 | constructor(issuer: string, id: string) { 8 | this.issuer = issuer; 9 | this.id = id; 10 | } 11 | 12 | static fromRemark(remark: string): ChangeIssuer | string { 13 | const exploded = remark.split("::"); 14 | try { 15 | validateChangeIssuer(remark); 16 | const [prefix, op_type, version, id, issuer] = remark.split("::"); 17 | return new ChangeIssuer(issuer, id); 18 | } catch (e: any) { 19 | console.error(e.message); 20 | console.log(`CHANGEISSUER error: full input was ${remark}`); 21 | return e.message; 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/rmrk1.0.0/classes/collection.ts: -------------------------------------------------------------------------------- 1 | // @todo: add data! 2 | import { Change } from "../changelog"; 3 | import { validateCollection } from "../tools/validate-remark"; 4 | import { getRemarkData } from "../tools/utils"; 5 | import { OP_TYPES, VERSION } from "../tools/constants"; 6 | import { Attribute } from "../types"; 7 | 8 | export class Collection { 9 | readonly block: number; 10 | readonly name: string; 11 | readonly max: number; 12 | issuer: string; 13 | readonly symbol: string; 14 | readonly id: string; 15 | readonly metadata: string; 16 | changes: Change[] = []; 17 | loadedMetadata?: CollectionMetadata; 18 | 19 | constructor( 20 | block: number, 21 | name: string, 22 | max: number, 23 | issuer: string, 24 | symbol: string, 25 | id: string, 26 | metadata: string 27 | ) { 28 | this.block = block; 29 | this.name = name; 30 | this.max = max; 31 | this.issuer = issuer; 32 | this.symbol = symbol; 33 | this.id = id; 34 | this.metadata = metadata; 35 | } 36 | 37 | public mint(): string { 38 | if (this.block) { 39 | throw new Error("An already existing collection cannot be minted!"); 40 | } 41 | return `RMRK::${OP_TYPES.MINT}::${VERSION}::${encodeURIComponent( 42 | JSON.stringify({ 43 | name: this.name, 44 | max: this.max, 45 | issuer: this.issuer, 46 | symbol: this.symbol.toUpperCase(), 47 | id: this.id, 48 | metadata: this.metadata, 49 | }) 50 | )}`; 51 | } 52 | 53 | public change_issuer(address: string): string { 54 | if (this.block === 0) { 55 | throw new Error( 56 | "This collection is new, so there's no issuer to change." + 57 | " If it has been deployed on chain, load the existing " + 58 | "collection as a new instance first, then change issuer." 59 | ); 60 | } 61 | return `RMRK::CHANGEISSUER::${VERSION}::${this.id}::${address}`; 62 | } 63 | 64 | public addChange(c: Change): Collection { 65 | this.changes.push(c); 66 | return this; 67 | } 68 | 69 | public getChanges(): Change[] { 70 | return this.changes; 71 | } 72 | 73 | static generateId(pubkey: string, symbol: string): string { 74 | if (!pubkey.startsWith("0x")) { 75 | throw new Error("This is not a valid pubkey, it does not start with 0x"); 76 | } 77 | //console.log(pubkey); 78 | return ( 79 | pubkey.substr(2, 10) + 80 | pubkey.substring(pubkey.length - 8) + 81 | "-" + 82 | symbol.toUpperCase() 83 | ); 84 | } 85 | 86 | static fromRemark(remark: string, block = 0): Collection | string { 87 | try { 88 | validateCollection(remark); 89 | const [prefix, op_type, version, dataString] = remark.split("::"); 90 | const obj = getRemarkData(dataString); 91 | return new this( 92 | block, 93 | obj.name, 94 | obj.max, 95 | obj.issuer, 96 | obj.symbol, 97 | obj.id, 98 | obj.metadata 99 | ); 100 | } catch (e: any) { 101 | console.error(e.message); 102 | console.log(`${OP_TYPES.MINT} error: full input was ${remark}`); 103 | return e.message; 104 | } 105 | } 106 | 107 | /** 108 | * TBD - hard dependency on Axios / IPFS to fetch remote 109 | */ 110 | private async load_metadata(): Promise { 111 | if (this.loadedMetadata) return this.loadedMetadata; 112 | return {} as CollectionMetadata; 113 | } 114 | } 115 | 116 | export interface CollectionMetadata { 117 | description?: string; 118 | attributes: Attribute[]; 119 | external_url?: string; 120 | image?: string; 121 | image_data?: string; 122 | } 123 | -------------------------------------------------------------------------------- /src/rmrk1.0.0/classes/consume.ts: -------------------------------------------------------------------------------- 1 | import { validateConsume } from "../tools/validate-remark"; 2 | 3 | export class Consume { 4 | id: string; 5 | 6 | constructor(id: string) { 7 | this.id = id; 8 | } 9 | 10 | static fromRemark(remark: string): Consume | string { 11 | try { 12 | validateConsume(remark); 13 | const [_prefix, _op_type, _version, id] = remark.split("::"); 14 | return new Consume(id); 15 | } catch (e: any) { 16 | console.error(e.message); 17 | console.log(`CONSUME error: full input was ${remark}`); 18 | return e.message; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/rmrk1.0.0/classes/emote.ts: -------------------------------------------------------------------------------- 1 | import { validateEmote } from "../tools/validate-remark"; 2 | 3 | export class Emote { 4 | unicode: string; 5 | id: string; 6 | static V = "1.0.0"; 7 | 8 | constructor(id: string, unicode: string) { 9 | this.unicode = unicode; 10 | this.id = id; 11 | } 12 | 13 | static fromRemark(remark: string): Emote | string { 14 | try { 15 | validateEmote(remark); 16 | const [_prefix, _op_type, _version, id, unicode] = remark.split("::"); 17 | return new Emote(id, unicode); 18 | } catch (e: any) { 19 | console.error(e.message); 20 | console.log(`EMOTE error: full input was ${remark}`); 21 | return e.message; 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/rmrk1.0.0/classes/list.ts: -------------------------------------------------------------------------------- 1 | import { validateList } from "../tools/validate-remark"; 2 | 3 | export class List { 4 | price: bigint; 5 | id: string; 6 | 7 | constructor(id: string, price: bigint) { 8 | this.price = price; 9 | this.id = id; 10 | } 11 | 12 | static fromRemark(remark: string): List | string { 13 | try { 14 | validateList(remark); 15 | const [_prefix, _op_type, _version, id, price] = remark.split("::"); 16 | return new List(id, BigInt(price)); 17 | } catch (e: any) { 18 | console.error(e.message); 19 | console.log(`LIST error: full input was ${remark}`); 20 | return e.message; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/rmrk1.0.0/classes/send.ts: -------------------------------------------------------------------------------- 1 | import { validateSend } from "../tools/validate-remark"; 2 | 3 | export class Send { 4 | recipient: string; 5 | id: string; 6 | 7 | constructor(id: string, recipient: string) { 8 | this.recipient = recipient; 9 | this.id = id; 10 | } 11 | 12 | static fromRemark(remark: string): Send | string { 13 | try { 14 | validateSend(remark); 15 | const [_prefix, _op_type, _version, id, recipient] = remark.split("::"); 16 | return new Send(id, recipient); 17 | } catch (e: any) { 18 | console.error(e.message); 19 | console.log(`SEND error: full input was ${remark}`); 20 | return e.message; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/rmrk1.0.0/constants.ts: -------------------------------------------------------------------------------- 1 | export const LATEST_DUMP = "QmVUCSXjuBXC6WJga2yckM3seXJj1MNKgFk6VLB34ExhhW"; // up to 6619194 2 | -------------------------------------------------------------------------------- /src/rmrk1.0.0/index.ts: -------------------------------------------------------------------------------- 1 | export { Consolidator } from "./tools/consolidator/consolidator"; 2 | export { Collection } from "./classes/collection"; 3 | export { NFT } from "./classes/nft"; 4 | export { List } from "./classes/list"; 5 | export { Consume } from "./classes/consume"; 6 | export { ChangeIssuer } from "./classes/changeissuer"; 7 | export { Buy } from "./classes/buy"; 8 | export { Send } from "./classes/send"; 9 | export { Emote } from "./classes/emote"; 10 | export { default as fetchRemarks } from "./tools/fetchRemarks"; 11 | export { getLatestFinalizedBlock, getRemarksFromBlocks } from "./tools/utils"; 12 | export * from "./tools/consolidator/utils"; 13 | export { RemarkListener } from "./listener"; 14 | export { validateMintNFT } from "./tools/consolidator/interactions/mintNFT"; 15 | export { validateMintIds } from "./tools/consolidator/interactions/mint"; 16 | export { listForSaleInteraction } from "./tools/consolidator/interactions/list"; 17 | export { emoteInteraction } from "./tools/consolidator/interactions/emote"; 18 | export { consumeInteraction } from "./tools/consolidator/interactions/consume"; 19 | export { changeIssuerInteraction } from "./tools/consolidator/interactions/changeIssuer"; 20 | export { buyInteraction } from "./tools/consolidator/interactions/buy"; 21 | export { sendInteraction } from "./tools/consolidator/interactions/send"; 22 | export * from "./tools/constants"; 23 | export * from "./tools/validate-remark"; 24 | export * from "./tools/validate-metadata"; 25 | -------------------------------------------------------------------------------- /src/rmrk1.0.0/tools/consolidator/adapters/interface.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmrk-team/rmrk-tools/05eba7ef4f8e740c916bfebab3b264c3422dd3fc/src/rmrk1.0.0/tools/consolidator/adapters/interface.ts -------------------------------------------------------------------------------- /src/rmrk1.0.0/tools/consolidator/adapters/json.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import { Remark } from "../remark"; 3 | import { filterBlocksByCollection, getRemarksFromBlocks } from "../../utils"; 4 | import { BlockCall } from "../../types"; 5 | 6 | /** 7 | * The JSON adapter expects to find a JSON array with elements 8 | * adhering to the following format in the provided filepath: 9 | * 10 | { 11 | block: 5437981, 12 | calls: [ 13 | { 14 | call: 'system.remark', 15 | value: '0x726d726b3a3a53454e443a...633350444e4336706533', 16 | caller: 'DmUVjSi8id22vcH26btyVsVq39p8EVPiepdBEYhzoLL8Qby' 17 | } 18 | ] 19 | } 20 | */ 21 | export default class JsonAdapter { 22 | private inputData: JsonRow[]; 23 | private collectionFilter?: string; 24 | private prefixes: string[]; 25 | 26 | constructor(filePath: string, prefixes: string[], collectionFilter?: string) { 27 | // eslint-disable-next-line security/detect-non-literal-fs-filename 28 | const rawdata = fs.readFileSync(filePath); 29 | this.inputData = JSON.parse(rawdata.toString()); 30 | this.collectionFilter = collectionFilter; 31 | this.prefixes = prefixes; 32 | //console.log(this.inputData); 33 | console.log(`Loaded ${this.inputData.length} blocks with remark calls`); 34 | } 35 | 36 | public getInputDataRaw(): JsonRow[] { 37 | return this.inputData; 38 | } 39 | 40 | public getRemarks(): Remark[] { 41 | let blocks = this.inputData; 42 | if (this.collectionFilter) { 43 | blocks = filterBlocksByCollection( 44 | blocks, 45 | this.collectionFilter, 46 | this.prefixes 47 | ); 48 | } 49 | return getRemarksFromBlocks(blocks, this.prefixes); 50 | } 51 | 52 | public getLastBlock(): number { 53 | const blocks = this.inputData; 54 | const lastBlock = blocks[blocks.length - 1]; 55 | return lastBlock.block || 0; 56 | } 57 | } 58 | 59 | type JsonRow = { 60 | block: number; 61 | calls: BlockCall[]; 62 | }; 63 | -------------------------------------------------------------------------------- /src/rmrk1.0.0/tools/consolidator/adapters/types.ts: -------------------------------------------------------------------------------- 1 | import { NFT } from "../../../classes/nft"; 2 | import { Collection } from "../../../classes/collection"; 3 | import { CollectionConsolidated, NFTConsolidated } from "../consolidator"; 4 | 5 | export interface IConsolidatorAdapter { 6 | updateNFTEmote( 7 | nft: NFT, 8 | consolidatedNFT: NFTConsolidated, 9 | updatedAtBlock: number 10 | ): Promise; 11 | updateNFTList( 12 | nft: NFT, 13 | consolidatedNFT: NFTConsolidated, 14 | updatedAtBlock: number 15 | ): Promise; 16 | updateNFTBuy( 17 | nft: NFT, 18 | consolidatedNFT: NFTConsolidated, 19 | updatedAtBlock: number 20 | ): Promise; 21 | updateNFTSend( 22 | nft: NFT, 23 | consolidatedNFT: NFTConsolidated, 24 | updatedAtBlock: number 25 | ): Promise; 26 | updateNFTConsume( 27 | nft: NFT, 28 | consolidatedNFT: NFTConsolidated, 29 | updatedAtBlock: number 30 | ): Promise; 31 | updateNFTMint(nft: NFT, updatedAtBlock: number): Promise; 32 | updateCollectionMint(collection: CollectionConsolidated): Promise; 33 | updateCollectionIssuer( 34 | collection: Collection, 35 | consolidatedCollection: CollectionConsolidated, 36 | updatedAtBlock: number 37 | ): Promise; 38 | getNFTById(id: string): Promise; 39 | getCollectionById(id: string): Promise; 40 | getNFTByIdUnique(id: string): Promise; 41 | getAllNFTs?: () => Promise; 42 | getAllCollections?: () => Promise; 43 | } 44 | -------------------------------------------------------------------------------- /src/rmrk1.0.0/tools/consolidator/interactions/buy.ts: -------------------------------------------------------------------------------- 1 | import { Buy } from "../../../classes/buy"; 2 | import { OP_TYPES } from "../../constants"; 3 | import { BlockCall } from "../../types"; 4 | import { Change } from "../../../changelog"; 5 | import { Remark } from "../remark"; 6 | import { NFT as N100 } from "../../../index"; 7 | import { encodeAddress } from "@polkadot/keyring"; 8 | 9 | export const buyInteraction = ( 10 | remark: Remark, // Current remark 11 | buyEntity: Buy, 12 | nft?: N100, // NFT in current state 13 | ss58Format?: number 14 | ): void => { 15 | // An NFT was bought after having been LISTed for sale 16 | if (!nft) { 17 | throw new Error( 18 | `[${OP_TYPES.BUY}] Attempting to buy non-existant NFT ${buyEntity.id}` 19 | ); 20 | } 21 | 22 | validate(remark, buyEntity, nft, ss58Format); 23 | nft.updatedAtBlock = remark.block; 24 | 25 | nft.addChange({ 26 | field: "owner", 27 | old: nft.owner, 28 | new: remark.caller, 29 | caller: remark.caller, 30 | block: remark.block, 31 | opType: OP_TYPES.BUY, 32 | } as Change); 33 | nft.owner = remark.caller; 34 | 35 | nft.addChange({ 36 | field: "forsale", 37 | old: nft.forsale, 38 | new: BigInt(0), 39 | caller: remark.caller, 40 | block: remark.block, 41 | opType: OP_TYPES.BUY, 42 | } as Change); 43 | nft.forsale = BigInt(0); 44 | }; 45 | 46 | const isTransferValid = (remark: Remark, nft: N100, ss58Format?: number) => { 47 | let transferValid = false; 48 | let transferValue = ""; 49 | remark.extra_ex?.forEach((el: BlockCall) => { 50 | if (el.call === "balances.transfer") { 51 | const [owner, forsale] = el.value.split(","); 52 | const ownerEncoded = ss58Format 53 | ? encodeAddress(owner, ss58Format) 54 | : owner; 55 | transferValue = [ownerEncoded, forsale].join(","); 56 | if (transferValue === `${nft.owner},${nft.forsale}`) { 57 | transferValid = true; 58 | } 59 | } 60 | }); 61 | return { transferValid, transferValue }; 62 | }; 63 | 64 | const validate = ( 65 | remark: Remark, 66 | buyEntity: Buy, 67 | nft: N100, 68 | ss58Format?: number 69 | ) => { 70 | const { transferValid, transferValue } = isTransferValid( 71 | remark, 72 | nft, 73 | ss58Format 74 | ); 75 | 76 | switch (true) { 77 | case Boolean(nft.burned): 78 | throw new Error( 79 | `[${OP_TYPES.BUY}] Attempting to buy burned NFT ${buyEntity.id}` 80 | ); 81 | case nft.forsale <= BigInt(0): 82 | throw new Error( 83 | `[${OP_TYPES.BUY}] Attempting to buy not-for-sale NFT ${buyEntity.id}` 84 | ); 85 | case nft.transferable === 0 || nft.transferable >= remark.block: 86 | throw new Error( 87 | `[${OP_TYPES.BUY}] Attempting to buy non-transferable NFT ${buyEntity.id}.` 88 | ); 89 | case remark.extra_ex?.length === 0: 90 | throw new Error( 91 | `[${OP_TYPES.BUY}] No accompanying transfer found for purchase of NFT with ID ${buyEntity.id}.` 92 | ); 93 | case !transferValid: 94 | throw new Error( 95 | `[${OP_TYPES.BUY}] Transfer for the purchase of NFT ID ${buyEntity.id} not valid. Recipient, amount should be ${nft.owner},${nft.forsale}, is ${transferValue}.` 96 | ); 97 | } 98 | }; 99 | -------------------------------------------------------------------------------- /src/rmrk1.0.0/tools/consolidator/interactions/changeIssuer.ts: -------------------------------------------------------------------------------- 1 | import { Collection } from "../../../index"; 2 | import { OP_TYPES } from "../../constants"; 3 | import { Change } from "../../../changelog"; 4 | import { Remark } from "../remark"; 5 | import { ChangeIssuer } from "../../../classes/changeissuer"; 6 | 7 | export const changeIssuerInteraction = ( 8 | remark: Remark, 9 | changeIssuerEntity: ChangeIssuer, 10 | collection?: Collection // Collection in current state 11 | ) => { 12 | if (!collection) { 13 | throw new Error( 14 | `This ${OP_TYPES.CHANGEISSUER} remark is invalid - no such collection with ID ${changeIssuerEntity.id} found before block ${remark.block}!` 15 | ); 16 | } 17 | 18 | if (remark.caller != collection.issuer) { 19 | throw new Error( 20 | `Attempting to change issuer of collection ${changeIssuerEntity.id} when not issuer!` 21 | ); 22 | } 23 | 24 | collection.addChange({ 25 | field: "issuer", 26 | old: collection.issuer, 27 | new: changeIssuerEntity.issuer, 28 | caller: remark.caller, 29 | block: remark.block, 30 | opType: OP_TYPES.CHANGEISSUER, 31 | } as Change); 32 | 33 | collection.issuer = changeIssuerEntity.issuer; 34 | }; 35 | -------------------------------------------------------------------------------- /src/rmrk1.0.0/tools/consolidator/interactions/consume.ts: -------------------------------------------------------------------------------- 1 | import { OP_TYPES, PREFIX } from "../../constants"; 2 | import { BlockCall } from "../../types"; 3 | import { Change } from "../../../changelog"; 4 | import { Remark } from "../remark"; 5 | import { Consume } from "../../../classes/consume"; 6 | import { NFT } from "../../../index"; 7 | import { hexToString } from "@polkadot/util"; 8 | 9 | export const consumeInteraction = ( 10 | remark: Remark, 11 | consumeEntity: Consume, 12 | nft?: NFT 13 | ): void => { 14 | if (!nft) { 15 | throw new Error( 16 | `[${OP_TYPES.CONSUME}] Attempting to CONSUME non-existant NFT ${consumeEntity.id}` 17 | ); 18 | } 19 | 20 | if (Boolean(nft.burned)) { 21 | throw new Error( 22 | `[${OP_TYPES.CONSUME}] Attempting to burn already burned NFT ${consumeEntity.id}` 23 | ); 24 | } 25 | 26 | // Check if burner is owner of NFT 27 | if (nft.owner != remark.caller) { 28 | throw new Error( 29 | `[${OP_TYPES.CONSUME}] Attempting to CONSUME non-owned NFT ${consumeEntity.id}` 30 | ); 31 | } 32 | 33 | // Burn and note reason 34 | 35 | const burnReasons: string[] = []; 36 | // Check if we have extra calls in the batch 37 | if (remark.extra_ex?.length) { 38 | // Check if the transfer is valid, i.e. matches target recipient and value. 39 | remark.extra_ex?.forEach((el: BlockCall) => { 40 | const str = hexToString(el.value); 41 | // Grab a reason in same batchAll array that is not another remark 42 | if (!str.toUpperCase().startsWith(PREFIX)) { 43 | burnReasons.push(el.value); 44 | } 45 | }); 46 | } 47 | 48 | // const [prefix, op_type, version] = remark.split("::"); 49 | 50 | nft.updatedAtBlock = remark.block; 51 | const burnReason = burnReasons.length < 1 ? "true" : burnReasons.join("~~~"); 52 | nft.addChange({ 53 | field: "burned", 54 | old: "", 55 | new: burnReason, 56 | caller: remark.caller, 57 | block: remark.block, 58 | opType: OP_TYPES.CONSUME, 59 | } as Change); 60 | nft.burned = burnReason; 61 | 62 | // De list if listed for sale 63 | nft.addChange({ 64 | field: "forsale", 65 | old: nft.forsale, 66 | new: BigInt(0), 67 | caller: remark.caller, 68 | block: remark.block, 69 | opType: OP_TYPES.CONSUME, 70 | } as Change); 71 | nft.forsale = BigInt(0); 72 | }; 73 | -------------------------------------------------------------------------------- /src/rmrk1.0.0/tools/consolidator/interactions/emote.ts: -------------------------------------------------------------------------------- 1 | import { OP_TYPES } from "../../constants"; 2 | import { Remark } from "../remark"; 3 | import { Emote } from "../../../classes/emote"; 4 | import { NFT } from "../../../index"; 5 | import { Change } from "../../../changelog"; 6 | 7 | const addEmoteChange = ( 8 | remark: Remark, 9 | emoteEntity: Emote, 10 | nft: NFT, 11 | removing = false 12 | ) => { 13 | nft.addChange({ 14 | field: "reactions", 15 | old: "", 16 | new: `${removing ? "-" : "+"}${emoteEntity.unicode}`, 17 | caller: remark.caller, 18 | block: remark.block, 19 | opType: OP_TYPES.EMOTE, 20 | } as Change); 21 | }; 22 | 23 | export const emoteInteraction = ( 24 | remark: Remark, 25 | emoteEntity: Emote, 26 | nft?: NFT, 27 | emitEmoteChanges?: boolean 28 | ): void => { 29 | if (!nft) { 30 | throw new Error( 31 | `[${OP_TYPES.EMOTE}] Attempting to emote on non-existant NFT ${emoteEntity.id}` 32 | ); 33 | } 34 | 35 | if (Boolean(nft.burned)) { 36 | throw new Error( 37 | `[${OP_TYPES.EMOTE}] Cannot emote to a burned NFT ${emoteEntity.id}` 38 | ); 39 | } 40 | 41 | nft.updatedAtBlock = remark.block; 42 | if (!nft.reactions[emoteEntity.unicode]) { 43 | nft.reactions[emoteEntity.unicode] = []; 44 | } 45 | const index = nft.reactions[emoteEntity.unicode].indexOf(remark.caller, 0); 46 | 47 | const removing = index > -1; 48 | if (removing) { 49 | nft.reactions[emoteEntity.unicode].splice(index, 1); 50 | } else { 51 | nft.reactions[emoteEntity.unicode].push(remark.caller); 52 | } 53 | 54 | if (emitEmoteChanges) { 55 | addEmoteChange(remark, emoteEntity, nft, removing); 56 | } 57 | }; 58 | -------------------------------------------------------------------------------- /src/rmrk1.0.0/tools/consolidator/interactions/list.ts: -------------------------------------------------------------------------------- 1 | import { Remark } from "../remark"; 2 | import { List } from "../../../classes/list"; 3 | import { NFT } from "../../../index"; 4 | import { OP_TYPES } from "../../constants"; 5 | import { Change } from "../../../changelog"; 6 | 7 | export const listForSaleInteraction = ( 8 | remark: Remark, 9 | listEntity: List, 10 | nft?: NFT 11 | ): void => { 12 | if (!nft) { 13 | throw new Error( 14 | `[${OP_TYPES.LIST}] Attempting to list non-existant NFT ${listEntity.id}` 15 | ); 16 | } 17 | 18 | if (Boolean(nft.burned)) { 19 | throw new Error( 20 | `[${OP_TYPES.LIST}] Attempting to list burned NFT ${listEntity.id}` 21 | ); 22 | } 23 | 24 | // Check if allowed to issue send - if owner == caller 25 | if (nft.owner != remark.caller) { 26 | throw new Error( 27 | `[${OP_TYPES.LIST}] Attempting to list non-owned NFT ${listEntity.id}, real owner: ${nft.owner}` 28 | ); 29 | } 30 | 31 | if (nft.transferable === 0 || nft.transferable >= remark.block) { 32 | throw new Error( 33 | `[${OP_TYPES.LIST}] Attempting to list non-transferable NFT ${listEntity.id}.` 34 | ); 35 | } 36 | 37 | if (listEntity.price !== nft.forsale) { 38 | nft.updatedAtBlock = remark.block; 39 | nft.addChange({ 40 | field: "forsale", 41 | old: nft.forsale, 42 | new: listEntity.price, 43 | caller: remark.caller, 44 | block: remark.block, 45 | opType: OP_TYPES.LIST, 46 | } as Change); 47 | 48 | nft.forsale = listEntity.price; 49 | } 50 | }; 51 | -------------------------------------------------------------------------------- /src/rmrk1.0.0/tools/consolidator/interactions/mint.ts: -------------------------------------------------------------------------------- 1 | import { decodeAddress } from "@polkadot/keyring"; 2 | import { Collection as C100 } from "../../../index"; 3 | import { u8aToHex } from "@polkadot/util"; 4 | import { Remark } from "../remark"; 5 | 6 | export const validateMintIds = (collection: C100, remark: Remark) => { 7 | const pubkey = decodeAddress(remark.caller); 8 | const pubkeyString = u8aToHex(pubkey); 9 | const pubkeyStart = pubkeyString.substr(2, 8); 10 | const pubkeyEnd = pubkeyString.substring(pubkeyString.length - 8); 11 | const id = C100.generateId(u8aToHex(pubkey), collection.symbol); 12 | const idStart = id.substr(0, 8); 13 | const idEnd = id.substring(pubkeyString.length - 8); 14 | if (idStart === pubkeyStart && idEnd === pubkeyEnd) { 15 | throw new Error( 16 | `Caller's pubkey ${u8aToHex(pubkey)} (${id}) does not match generated ID` 17 | ); 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /src/rmrk1.0.0/tools/consolidator/interactions/mintNFT.ts: -------------------------------------------------------------------------------- 1 | import { Remark } from "../remark"; 2 | import { NFT } from "../../../index"; 3 | import { OP_TYPES } from "../../constants"; 4 | import { Collection } from "../../../index"; 5 | 6 | export const validateMintNFT = ( 7 | remark: Remark, 8 | nft: NFT, 9 | nftParentCollection?: Collection 10 | ) => { 11 | if (!nftParentCollection) { 12 | throw new Error( 13 | `NFT referencing non-existant parent collection ${nft.collection}` 14 | ); 15 | } 16 | 17 | nft.owner = nftParentCollection.issuer; 18 | if (remark.caller != nft.owner) { 19 | throw new Error( 20 | `Attempted issue of NFT in non-owned collection. Issuer: ${nftParentCollection.issuer}, caller: ${remark.caller}` 21 | ); 22 | } 23 | 24 | if (nft.owner === "") { 25 | throw new Error( 26 | `[${OP_TYPES.MINTNFT}] Somehow this NFT still doesn't have an owner.` 27 | ); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /src/rmrk1.0.0/tools/consolidator/interactions/send.ts: -------------------------------------------------------------------------------- 1 | import { OP_TYPES } from "../../constants"; 2 | import { Change } from "../../../changelog"; 3 | import { Remark } from "../remark"; 4 | import { Send } from "../../../classes/send"; 5 | import { NFT } from "../../../index"; 6 | 7 | export const sendInteraction = ( 8 | remark: Remark, 9 | sendEntity: Send, 10 | nft?: NFT 11 | ): void => { 12 | if (!nft) { 13 | throw new Error( 14 | `[${OP_TYPES.SEND}] Attempting to send non-existant NFT ${sendEntity.id}` 15 | ); 16 | } 17 | 18 | if (Boolean(nft.burned)) { 19 | throw new Error( 20 | `[${OP_TYPES.SEND}] Attempting to send burned NFT ${sendEntity.id}` 21 | ); 22 | } 23 | 24 | // Check if allowed to issue send - if owner == caller 25 | if (nft.owner != remark.caller) { 26 | throw new Error( 27 | `[${OP_TYPES.SEND}] Attempting to send non-owned NFT ${sendEntity.id}, real owner: ${nft.owner}` 28 | ); 29 | } 30 | 31 | if (nft.transferable === 0 || nft.transferable >= remark.block) { 32 | throw new Error( 33 | `[${OP_TYPES.SEND}] Attempting to send non-transferable NFT ${sendEntity.id}.` 34 | ); 35 | } 36 | 37 | nft.updatedAtBlock = remark.block; 38 | nft.addChange({ 39 | field: "owner", 40 | old: nft.owner, 41 | new: sendEntity.recipient, 42 | caller: remark.caller, 43 | block: remark.block, 44 | opType: OP_TYPES.SEND, 45 | } as Change); 46 | 47 | nft.owner = sendEntity.recipient; 48 | 49 | // Cancel LIST, if any 50 | if (nft.forsale > BigInt(0)) { 51 | nft.addChange({ 52 | field: "forsale", 53 | old: nft.forsale, 54 | new: BigInt(0), 55 | caller: remark.caller, 56 | block: remark.block, 57 | opType: OP_TYPES.SEND, 58 | } as Change); 59 | nft.forsale = BigInt(0); 60 | } 61 | }; 62 | -------------------------------------------------------------------------------- /src/rmrk1.0.0/tools/consolidator/interface.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmrk-team/rmrk-tools/05eba7ef4f8e740c916bfebab3b264c3422dd3fc/src/rmrk1.0.0/tools/consolidator/interface.ts -------------------------------------------------------------------------------- /src/rmrk1.0.0/tools/consolidator/remark.ts: -------------------------------------------------------------------------------- 1 | import { BlockCall } from "../types"; 2 | 3 | export type Remark = { 4 | block: number; 5 | interaction_type: string; 6 | caller: string; 7 | version: string; 8 | remark: string; 9 | extra_ex?: BlockCall[]; 10 | }; 11 | 12 | export type Extrinsic = { 13 | module: string; 14 | method: string; 15 | arg: string; 16 | }; 17 | -------------------------------------------------------------------------------- /src/rmrk1.0.0/tools/consolidator/utils.ts: -------------------------------------------------------------------------------- 1 | import { NFT } from "../../classes/nft"; 2 | import { CollectionConsolidated, NFTConsolidated } from "./consolidator"; 3 | import { Collection as C100, Collection } from "../../classes/collection"; 4 | import { Remark } from "./remark"; 5 | import { ChangeIssuer } from "../../classes/changeissuer"; 6 | import { OP_TYPES } from "../constants"; 7 | 8 | export const consolidatedNFTtoInstance = ( 9 | nft?: NFTConsolidated 10 | ): NFT | undefined => { 11 | if (!nft) { 12 | return undefined; 13 | } 14 | const { 15 | block, 16 | collection, 17 | name, 18 | instance, 19 | transferable, 20 | sn, 21 | metadata, 22 | id, 23 | data, 24 | updatedAtBlock, 25 | ...rest 26 | } = nft || {}; 27 | const nftClass = new NFT( 28 | block, 29 | collection, 30 | name, 31 | instance, 32 | transferable, 33 | sn, 34 | metadata, 35 | data, 36 | updatedAtBlock 37 | ); 38 | const { owner, forsale, reactions, changes, loadedMetadata, burned } = rest; 39 | nftClass.owner = owner; 40 | nftClass.forsale = forsale; 41 | nftClass.reactions = reactions; 42 | nftClass.changes = changes; 43 | nftClass.loadedMetadata = loadedMetadata; 44 | nftClass.burned = burned; 45 | 46 | return nftClass; 47 | }; 48 | 49 | export const consolidatedCollectionToInstance = ( 50 | collection?: CollectionConsolidated 51 | ): Collection | undefined => { 52 | if (!collection) { 53 | return undefined; 54 | } 55 | const { block, name, metadata, id, issuer, max, symbol, ...rest } = 56 | collection || {}; 57 | const colleactionClass = new Collection( 58 | block, 59 | name, 60 | max, 61 | issuer, 62 | symbol, 63 | id, 64 | metadata 65 | ); 66 | const { changes, loadedMetadata } = rest; 67 | 68 | colleactionClass.changes = changes; 69 | colleactionClass.loadedMetadata = loadedMetadata; 70 | 71 | return colleactionClass; 72 | }; 73 | export const getChangeIssuerEntity = (remark: Remark): ChangeIssuer => { 74 | const changeIssuerEntity = ChangeIssuer.fromRemark(remark.remark); 75 | 76 | if (typeof changeIssuerEntity === "string") { 77 | throw new Error( 78 | `[${OP_TYPES.CHANGEISSUER}] Dead before instantiation: ${changeIssuerEntity}` 79 | ); 80 | } 81 | return changeIssuerEntity; 82 | }; 83 | export const getCollectionFromRemark = (remark: Remark) => { 84 | const collection = C100.fromRemark(remark.remark, remark.block); 85 | if (typeof collection === "string") { 86 | throw new Error( 87 | `[${OP_TYPES.MINT}] Dead before instantiation: ${collection}` 88 | ); 89 | } 90 | return collection; 91 | }; 92 | -------------------------------------------------------------------------------- /src/rmrk1.0.0/tools/constants.ts: -------------------------------------------------------------------------------- 1 | export const VERSION = "1.0.0"; 2 | export const PREFIX = "RMRK"; 3 | 4 | export enum OP_TYPES { 5 | BUY = "BUY", 6 | LIST = "LIST", 7 | MINT = "MINT", 8 | MINTNFT = "MINTNFT", 9 | SEND = "SEND", 10 | EMOTE = "EMOTE", 11 | CHANGEISSUER = "CHANGEISSUER", 12 | CONSUME = "CONSUME", 13 | } 14 | -------------------------------------------------------------------------------- /src/rmrk1.0.0/tools/deriveMultisigAddress.ts: -------------------------------------------------------------------------------- 1 | import { encodeAddress, decodeAddress } from "@polkadot/keyring"; 2 | import { u8aSorted } from "@polkadot/util"; 3 | import { blake2AsU8a } from "@polkadot/util-crypto"; 4 | 5 | type Options = { 6 | addresses: string[]; // array of the addresses. 7 | ss58Prefix: number; // Prefix for the network encoding to use. 8 | threshold: number; // Number of addresses that are needed to approve an action. 9 | }; 10 | 11 | const derivePubkey = (addresses: string[], threshold = 1): Uint8Array => { 12 | const prefix = "modlpy/utilisuba"; 13 | const payload = new Uint8Array(prefix.length + 1 + 32 * addresses.length + 2); 14 | payload.set( 15 | Array.from(prefix).map((c) => c.charCodeAt(0)), 16 | 0 17 | ); 18 | payload[prefix.length] = addresses.length << 2; 19 | const pubkeys = addresses.map((addr) => decodeAddress(addr)); 20 | u8aSorted(pubkeys).forEach((pubkey, idx) => { 21 | payload.set(pubkey, prefix.length + 1 + idx * 32); 22 | }); 23 | payload[prefix.length + 1 + 32 * addresses.length] = threshold; 24 | 25 | return blake2AsU8a(payload); 26 | }; 27 | 28 | export const deriveMultisigAddress = (opts: Options): string => { 29 | const { addresses, ss58Prefix, threshold } = opts; 30 | 31 | if (!addresses) throw new Error("Please provide the addresses option."); 32 | 33 | const addrs = addresses.filter((x) => !!x); 34 | 35 | const pubkey = derivePubkey(addrs, threshold); 36 | const msig = encodeAddress(pubkey, ss58Prefix); 37 | 38 | return msig; 39 | }; 40 | -------------------------------------------------------------------------------- /src/rmrk1.0.0/tools/fetchRemarks.ts: -------------------------------------------------------------------------------- 1 | import { BlockCalls } from "./types"; 2 | import { deeplog, getBlockCallsFromSignedBlock } from "./utils"; 3 | import { ApiPromise } from "@polkadot/api"; 4 | 5 | export default async ( 6 | api: ApiPromise, 7 | from: number, 8 | to: number, 9 | prefixes: string[], 10 | ss58Format = 2 11 | ): Promise => { 12 | const bcs: BlockCalls[] = []; 13 | for (let i = from; i <= to; i++) { 14 | if (i % 1000 === 0) { 15 | const event = new Date(); 16 | console.log(`Block ${i} at time ${event.toTimeString()}`); 17 | if (i % 5000 === 0) { 18 | console.log(`Currently at ${bcs.length} remarks.`); 19 | } 20 | } 21 | 22 | const blockHash = await api.rpc.chain.getBlockHash(i); 23 | const block = await api.rpc.chain.getBlock(blockHash); 24 | 25 | if (block.block === undefined) { 26 | console.error("block.block is undefined for block " + i); 27 | deeplog(block); 28 | continue; 29 | } 30 | 31 | const blockCalls = await getBlockCallsFromSignedBlock( 32 | block, 33 | prefixes, 34 | api, 35 | ss58Format 36 | ); 37 | 38 | if (blockCalls.length) { 39 | bcs.push({ 40 | block: i, 41 | calls: blockCalls, 42 | } as BlockCalls); 43 | } 44 | } 45 | return bcs; 46 | }; 47 | -------------------------------------------------------------------------------- /src/rmrk1.0.0/tools/metadata-to-ipfs.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | // @ts-ignore 3 | import pinataSDK from "@pinata/sdk"; 4 | import { NFTMetadata } from "../classes/nft"; 5 | 6 | const pinata = pinataSDK(process.env.PINATA_KEY, process.env.PINATA_SECRET); 7 | 8 | const defaultOptions = { 9 | pinataOptions: { 10 | cidVersion: 1, 11 | }, 12 | }; 13 | 14 | export const pinToIpfs = async (filePath: string, name?: string) => { 15 | const options = { ...defaultOptions, pinataMetadata: { name } }; 16 | try { 17 | const readableStreamForFile = fs.createReadStream(filePath); 18 | const result = await pinata.pinFileToIPFS(readableStreamForFile, options); 19 | return result.IpfsHash; 20 | } catch (error: any) { 21 | console.error(error); 22 | } 23 | }; 24 | 25 | export const uploadRMRKMetadata = async ( 26 | imagePath: string, 27 | metadataFields: NFTMetadata 28 | ): Promise => { 29 | const options = { 30 | ...defaultOptions, 31 | pinataMetadata: { name: metadataFields.name }, 32 | }; 33 | try { 34 | const imageHash = await pinToIpfs(imagePath, metadataFields.name); 35 | const metadata = { ...metadataFields, image: `ipfs://ipfs/${imageHash}` }; 36 | const metadataHashResult = await pinata.pinJSONToIPFS(metadata, options); 37 | return `ipfs://ipfs/${metadataHashResult.IpfsHash}`; 38 | } catch (error: any) { 39 | return ""; 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /src/rmrk1.0.0/tools/types.ts: -------------------------------------------------------------------------------- 1 | import { Send } from "../classes/send"; 2 | import { Buy } from "../classes/buy"; 3 | import { List } from "../classes/list"; 4 | import { NFT } from "../classes/nft"; 5 | import { Emote } from "../classes/emote"; 6 | import { ChangeIssuer } from "../classes/changeissuer"; 7 | 8 | export type DisplayType = 9 | | "boost_number" 10 | | "boost_percentage" 11 | | "number" 12 | | "date"; 13 | 14 | export interface Attribute { 15 | display_type?: DisplayType; 16 | trait_type?: string; 17 | value: number | string; 18 | max_value?: number; 19 | } 20 | 21 | export type Options = { 22 | ws: string; 23 | from: string; 24 | to: string; 25 | prefixes: string; 26 | blocks: string; 27 | json: string; 28 | folder: string; 29 | append: string; 30 | remark: string; 31 | }; 32 | 33 | export type BlockCalls = { 34 | block: number; 35 | calls: BlockCall[]; 36 | }; 37 | 38 | export type BlockCall = { 39 | call: string; 40 | value: string; 41 | caller: string; 42 | extras?: BlockCall[]; 43 | }; 44 | 45 | export enum OP_TYPES { 46 | BUY = "BUY", 47 | LIST = "LIST", 48 | MINT = "MINT", 49 | MINTNFT = "MINTNFT", 50 | SEND = "SEND", 51 | EMOTE = "EMOTE", 52 | CONSUME = "CONSUME", 53 | CHANGEISSUER = "CHANGEISSUER", 54 | } 55 | 56 | export type Interaction = Send | Buy | List | Emote | ChangeIssuer; 57 | -------------------------------------------------------------------------------- /src/rmrk1.0.0/tools/validate-metadata.ts: -------------------------------------------------------------------------------- 1 | import { NFTMetadata } from "../classes/nft"; 2 | import { CollectionMetadata } from "../classes/collection"; 3 | import { 4 | number, 5 | optional, 6 | pattern, 7 | string, 8 | type, 9 | any, 10 | assert, 11 | object, 12 | union, 13 | enums, 14 | array, 15 | } from "superstruct"; 16 | import { Attribute } from "./types"; 17 | 18 | const MetadataStruct = type({ 19 | name: optional(string()), 20 | description: optional(string()), 21 | image: optional(pattern(string(), new RegExp("^(https?|ipfs)://.*$"))), 22 | animation_url: optional( 23 | pattern(string(), new RegExp("^(https?|ipfs)://.*$")) 24 | ), 25 | image_data: optional(string()), 26 | background_color: optional(string()), 27 | youtube_url: optional(pattern(string(), new RegExp("^https://.*$"))), 28 | attributes: any(), 29 | external_url: optional(pattern(string(), new RegExp("^(https?|ipfs)://.*$"))), 30 | }); 31 | 32 | const AttributeStruct = object({ 33 | value: union([string(), number()]), 34 | trait_type: optional(string()), 35 | display_type: optional( 36 | enums(["boost_number", "boost_percentage", "number", "date"]) 37 | ), 38 | max_value: optional(number()), 39 | }); 40 | 41 | export const validateAttributes = (attributes?: Attribute[]) => { 42 | if (!attributes) { 43 | return true; 44 | } 45 | assert(attributes, array(AttributeStruct)); 46 | 47 | attributes.forEach((attribute) => { 48 | const { value, display_type, max_value } = attribute; 49 | if ( 50 | display_type === "boost_number" || 51 | display_type === "boost_percentage" || 52 | display_type === "number" || 53 | display_type === "date" 54 | ) { 55 | if (typeof value !== "number") { 56 | throw new Error( 57 | "for 'boost_number' | 'boost_percentage' | 'number' | 'date' attributes 'value' has to be a number" 58 | ); 59 | } 60 | } 61 | 62 | if (max_value && max_value > 0) { 63 | if (value > max_value) { 64 | throw new Error("'value' cannot be greater than 'max_value'"); 65 | } 66 | } 67 | 68 | if ( 69 | typeof value === "number" && 70 | (!display_type || 71 | !["boost_number", "boost_percentage", "number", "date"].includes( 72 | display_type 73 | )) 74 | ) { 75 | throw new Error( 76 | "'value' of type number can only be paired with appropriate 'display_type'" 77 | ); 78 | } 79 | 80 | if (display_type === "date") { 81 | const date = new Date(value); 82 | if (!(date instanceof Date) || date.getFullYear() < 1971) { 83 | throw new Error( 84 | "when 'display_type' is 'date', then 'value' has to be of unix timestamp type" 85 | ); 86 | } 87 | } 88 | }); 89 | 90 | return true; 91 | }; 92 | 93 | /** 94 | * Validate Metadata according to OpenSea docs 95 | * https://docs.opensea.io/docs/metadata-standards 96 | * @param metadata 97 | */ 98 | export const validateMetadata = ( 99 | metadata: NFTMetadata | CollectionMetadata 100 | ) => { 101 | assert(metadata, MetadataStruct); 102 | 103 | if (!metadata.image && !(metadata as NFTMetadata).animation_url) { 104 | throw new Error("image or animation_url is missing"); 105 | } 106 | 107 | validateAttributes(metadata.attributes); 108 | return true; 109 | }; 110 | -------------------------------------------------------------------------------- /src/rmrk1.0.0/types.ts: -------------------------------------------------------------------------------- 1 | export type DisplayType = 2 | | "boost_number" 3 | | "boost_percentage" 4 | | "number" 5 | | "date"; 6 | 7 | export interface Attribute { 8 | display_type?: DisplayType; 9 | trait_type?: string; 10 | value: number | string; 11 | max_value?: number; 12 | } 13 | -------------------------------------------------------------------------------- /src/rmrk2.0.0/changelog.ts: -------------------------------------------------------------------------------- 1 | import { OP_TYPES } from "./tools/constants"; 2 | 3 | export type ChangeExtraBalanceTransfer = { 4 | receiver: string; 5 | amount: string; 6 | }; 7 | 8 | export type Change = { 9 | field: string; 10 | old: any; 11 | new: any; 12 | caller: string; 13 | block: number; 14 | opType: OP_TYPES; 15 | extraTransfers?: ChangeExtraBalanceTransfer[]; 16 | }; 17 | -------------------------------------------------------------------------------- /src/rmrk2.0.0/classes/accept.ts: -------------------------------------------------------------------------------- 1 | import { validateAccept } from "../tools/validate-remark"; 2 | 3 | export type AcceptEntityType = "NFT" | "RES"; 4 | 5 | export class Accept { 6 | readonly id: string; 7 | readonly nftId: string; 8 | readonly entity: AcceptEntityType; 9 | 10 | constructor(nftId: string, entity: AcceptEntityType, id: string) { 11 | this.id = id; 12 | this.nftId = nftId; 13 | this.entity = entity; 14 | } 15 | 16 | static fromRemark(remark: string): Accept | string { 17 | try { 18 | validateAccept(remark); 19 | const [_prefix, _op_type, _version, nftId, entity, id] = remark.split( 20 | "::" 21 | ); 22 | return new this(nftId, entity as AcceptEntityType, id); 23 | } catch (e: any) { 24 | console.error(e.message); 25 | console.log(`ACCEPT error: fu ll input was ${remark}`); 26 | return e.message; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/rmrk2.0.0/classes/burn.ts: -------------------------------------------------------------------------------- 1 | import { validateBurn } from "../tools/validate-remark"; 2 | 3 | export class Burn { 4 | id: string; 5 | 6 | constructor(id: string) { 7 | this.id = id; 8 | } 9 | 10 | static fromRemark(remark: string): Burn | string { 11 | try { 12 | validateBurn(remark); 13 | const [_prefix, _op_type, _version, id] = remark.split("::"); 14 | return new Burn(id); 15 | } catch (e: any) { 16 | console.error(e.message); 17 | console.log(`BURN error: full input was ${remark}`); 18 | return e.message; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/rmrk2.0.0/classes/buy.ts: -------------------------------------------------------------------------------- 1 | import { validateBuy } from "../tools/validate-remark"; 2 | 3 | export class Buy { 4 | id: string; 5 | readonly recipient?: string; 6 | 7 | constructor(id: string, recipient?: string) { 8 | this.id = id; 9 | this.recipient = recipient; 10 | } 11 | 12 | static fromRemark(remark: string): Buy | string { 13 | try { 14 | validateBuy(remark); 15 | const [_prefix, _op_type, _version, id, recipient] = remark.split("::"); 16 | return new Buy(id, recipient); 17 | } catch (e: any) { 18 | console.error(e.message); 19 | console.log(`BUY error: full input was ${remark}`); 20 | return e.message; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/rmrk2.0.0/classes/changeissuer.ts: -------------------------------------------------------------------------------- 1 | import { validateChangeIssuer } from "../tools/validate-remark"; 2 | import { isValidAddressPolkadotAddress } from "../tools/consolidator/utils"; 3 | import { encodeAddress } from "@polkadot/keyring"; 4 | 5 | export class ChangeIssuer { 6 | issuer: string; 7 | id: string; 8 | 9 | constructor(issuer: string, id: string) { 10 | this.issuer = issuer; 11 | this.id = id; 12 | } 13 | 14 | static fromRemark( 15 | remark: string, 16 | ss58Format?: number 17 | ): ChangeIssuer | string { 18 | try { 19 | validateChangeIssuer(remark); 20 | const [prefix, op_type, version, id, issuer] = remark.split("::"); 21 | let encodedIssuer = issuer; 22 | if (isValidAddressPolkadotAddress(issuer)) { 23 | encodedIssuer = encodeAddress(issuer, ss58Format); 24 | } 25 | return new ChangeIssuer(encodedIssuer, id); 26 | } catch (e: any) { 27 | console.error(e.message); 28 | console.log(`CHANGEISSUER error: full input was ${remark}`); 29 | return e.message; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/rmrk2.0.0/classes/collection.ts: -------------------------------------------------------------------------------- 1 | import { Change } from "../changelog"; 2 | import { validateCollection } from "../tools/validate-remark"; 3 | import { getRemarkData } from "../tools/utils"; 4 | import { OP_TYPES, PREFIX, VERSION } from "../tools/constants"; 5 | import { IProperties } from "../tools/types"; 6 | import { encodeAddress } from "@polkadot/keyring"; 7 | 8 | export class Collection { 9 | readonly block: number; 10 | readonly max: number; 11 | issuer: string; 12 | readonly symbol: string; 13 | readonly id: string; 14 | readonly metadata: string; 15 | changes: Change[] = []; 16 | count = 0; 17 | 18 | constructor( 19 | block: number, 20 | max: number, 21 | issuer: string, 22 | symbol: string, 23 | id: string, 24 | metadata: string 25 | ) { 26 | this.block = block; 27 | this.max = max; 28 | this.issuer = issuer; 29 | this.symbol = symbol; 30 | this.id = id; 31 | this.metadata = metadata; 32 | } 33 | 34 | public create(): string { 35 | if (this.block) { 36 | throw new Error("An already existing collection cannot be created!"); 37 | } 38 | return `${PREFIX}::${OP_TYPES.CREATE}::${VERSION}::${encodeURIComponent( 39 | JSON.stringify({ 40 | max: this.max, 41 | issuer: this.issuer, 42 | symbol: this.symbol.toUpperCase(), 43 | id: this.id, 44 | metadata: this.metadata, 45 | }) 46 | )}`; 47 | } 48 | 49 | public destroy(): string { 50 | if (this.block === 0) { 51 | throw new Error( 52 | "This collection is new" + 53 | " If it has been deployed on chain, load the existing " + 54 | "collection as a new instance first, then destroy it." 55 | ); 56 | } 57 | return `${PREFIX}::${OP_TYPES.DESTROY}::${VERSION}::${this.id}`; 58 | } 59 | 60 | public change_issuer(address: string): string { 61 | if (this.block === 0) { 62 | throw new Error( 63 | "This collection is new, so there's no issuer to change." + 64 | " If it has been deployed on chain, load the existing " + 65 | "collection as a new instance first, then change issuer." 66 | ); 67 | } 68 | return `${PREFIX}::${OP_TYPES.CHANGEISSUER}::${VERSION}::${this.id}::${address}`; 69 | } 70 | 71 | public lock(): string { 72 | if (this.block === 0) { 73 | throw new Error( 74 | "This collection is new" + 75 | " If it has been deployed on chain, load the existing " + 76 | "collection as a new instance first, then lock it." 77 | ); 78 | } 79 | return `${PREFIX}::${OP_TYPES.LOCK}::${VERSION}::${this.id}`; 80 | } 81 | 82 | public addChange(c: Change): Collection { 83 | this.changes.push(c); 84 | return this; 85 | } 86 | 87 | public getChanges(): Change[] { 88 | return this.changes; 89 | } 90 | 91 | static generateId(pubkey: string, symbol: string): string { 92 | if (!pubkey.startsWith("0x")) { 93 | throw new Error("This is not a valid pubkey, it does not start with 0x"); 94 | } 95 | return ( 96 | pubkey.substr(2, 10) + 97 | pubkey.substring(pubkey.length - 8) + 98 | "-" + 99 | symbol.toUpperCase() 100 | ); 101 | } 102 | 103 | static fromRemark( 104 | remark: string, 105 | block = 0, 106 | ss58Format?: number 107 | ): Collection | string { 108 | try { 109 | validateCollection(remark); 110 | const [prefix, op_type, version, dataString] = remark.split("::"); 111 | const obj = getRemarkData(dataString); 112 | return new this( 113 | block, 114 | obj.max, 115 | encodeAddress(obj.issuer, ss58Format), 116 | obj.symbol, 117 | obj.id, 118 | obj.metadata 119 | ); 120 | } catch (e: any) { 121 | console.error(e.message); 122 | console.log(`${OP_TYPES.CREATE} error: full input was ${remark}`); 123 | return e.message; 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/rmrk2.0.0/classes/destroy.ts: -------------------------------------------------------------------------------- 1 | import { validateDestroy } from "../tools/validate-remark"; 2 | 3 | export class Destroy { 4 | id: string; 5 | 6 | constructor(id: string) { 7 | this.id = id; 8 | } 9 | 10 | static fromRemark(remark: string): Destroy | string { 11 | try { 12 | validateDestroy(remark); 13 | const [_prefix, _op_type, _version, id] = remark.split("::"); 14 | return new this(id); 15 | } catch (e: any) { 16 | console.error(e.message); 17 | console.log(`DESTROY error: full input was ${remark}`); 18 | return e.message; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/rmrk2.0.0/classes/emote.ts: -------------------------------------------------------------------------------- 1 | import { validateEmote } from "../tools/validate-remark"; 2 | import { VERSION } from "../tools/constants"; 3 | import { isValidEmoji } from "../tools/validate-emoji"; 4 | 5 | export enum EMOTE_NAMESPACES { 6 | RMRK1 = "RMRK1", 7 | RMRK2 = "RMRK2", 8 | PUBKEY = "PUBKEY", 9 | } 10 | 11 | const validateNamespace = (namespace: EMOTE_NAMESPACES) => { 12 | return ["RMRK1", "RMRK2", "PUBKEY"].includes(namespace); 13 | }; 14 | 15 | export class Emote { 16 | unicode: string; 17 | id: string; 18 | namespace: EMOTE_NAMESPACES; 19 | static V = VERSION; 20 | 21 | constructor(namespace: EMOTE_NAMESPACES, id: string, unicode: string) { 22 | this.unicode = unicode; 23 | this.id = id; 24 | this.namespace = namespace; 25 | } 26 | 27 | static fromRemark(remark: string): Emote | string { 28 | try { 29 | validateEmote(remark); 30 | const [ 31 | _prefix, 32 | _op_type, 33 | _version, 34 | namespace, 35 | id, 36 | unicode, 37 | ] = remark.split("::"); 38 | if (!validateNamespace(namespace as EMOTE_NAMESPACES)) { 39 | throw new Error("Not a valid emote namespace"); 40 | } 41 | if (!isValidEmoji(unicode)) { 42 | throw new Error(`Invalid emoji unicode ${unicode}`); 43 | } 44 | return new Emote(namespace as EMOTE_NAMESPACES, id, unicode); 45 | } catch (e: any) { 46 | console.error(e.message); 47 | console.log(`EMOTE error: full input was ${remark}`); 48 | return e.message; 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/rmrk2.0.0/classes/equip.ts: -------------------------------------------------------------------------------- 1 | import { validateEquip } from "../tools/validate-remark"; 2 | 3 | export class Equip { 4 | readonly id: string; 5 | readonly baseslot: string; 6 | 7 | constructor(id: string, baseslot: string) { 8 | this.id = id; 9 | this.baseslot = baseslot; 10 | } 11 | 12 | static fromRemark(remark: string): Equip | string { 13 | try { 14 | validateEquip(remark); 15 | const [_prefix, _op_type, _version, id, baseslot = ""] = remark.split( 16 | "::" 17 | ); 18 | return new this(id, baseslot); 19 | } catch (e: any) { 20 | console.error(e.message); 21 | console.log(`EQUIP error: full input was ${remark}`); 22 | return e.message; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/rmrk2.0.0/classes/equippable.ts: -------------------------------------------------------------------------------- 1 | import { validateEquippable } from "../tools/validate-remark"; 2 | import { VERSION } from "../tools/constants"; 3 | 4 | export const collectionRegexPattern = "^([-+])?(\\S+)$"; 5 | const collectionRegex = new RegExp(collectionRegexPattern); 6 | 7 | export class Equippable { 8 | slot: string; 9 | id: string; 10 | equippableChange: string; 11 | static V = VERSION; 12 | 13 | constructor(id: string, slot: string, equippableChange: string) { 14 | this.slot = slot; 15 | this.id = id; 16 | if (!collectionRegex.test(equippableChange)) { 17 | throw new Error(`Not a valid equippable change ${equippableChange}`); 18 | } 19 | this.equippableChange = equippableChange; 20 | } 21 | 22 | static fromRemark(remark: string): Equippable | string { 23 | try { 24 | validateEquippable(remark); 25 | const [ 26 | _prefix, 27 | _op_type, 28 | _version, 29 | id, 30 | slot, 31 | equippableChange, 32 | ] = remark.split("::"); 33 | 34 | if (!collectionRegex.test(equippableChange)) { 35 | throw new Error(`Not a valid equippable change ${equippableChange}`); 36 | } 37 | 38 | return new Equippable(id, slot, equippableChange); 39 | } catch (e: any) { 40 | console.error(e.message); 41 | console.log(`EQUIPPABLE error: full input was ${remark}`); 42 | return e.message; 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/rmrk2.0.0/classes/list.ts: -------------------------------------------------------------------------------- 1 | import { validateList } from "../tools/validate-remark"; 2 | 3 | export class List { 4 | price: bigint; 5 | id: string; 6 | 7 | constructor(id: string, price: bigint) { 8 | this.price = price; 9 | this.id = id; 10 | } 11 | 12 | static fromRemark(remark: string): List | string { 13 | try { 14 | validateList(remark); 15 | const [_prefix, _op_type, _version, id, price] = remark.split("::"); 16 | return new List(id, BigInt(price)); 17 | } catch (e: any) { 18 | console.error(e.message); 19 | console.log(`LIST error: full input was ${remark}`); 20 | return e.message; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/rmrk2.0.0/classes/lock.ts: -------------------------------------------------------------------------------- 1 | import { validateLock } from "../tools/validate-remark"; 2 | 3 | export class Lock { 4 | id: string; 5 | 6 | constructor(id: string) { 7 | this.id = id; 8 | } 9 | 10 | static fromRemark(remark: string): Lock | string { 11 | try { 12 | validateLock(remark); 13 | const [_prefix, _op_type, _version, id] = remark.split("::"); 14 | return new this(id); 15 | } catch (e: any) { 16 | console.error(e.message); 17 | console.log(`LOCK error: full input was ${remark}`); 18 | return e.message; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/rmrk2.0.0/classes/resadd.ts: -------------------------------------------------------------------------------- 1 | import { validateResadd } from "../tools/validate-remark"; 2 | import { Resource } from "./nft"; 3 | import { getRemarkData } from "../tools/utils"; 4 | import { nanoid } from "nanoid"; 5 | import { Theme } from "./base"; 6 | 7 | export class Resadd { 8 | readonly base?: string; 9 | readonly src?: string; 10 | readonly thumb?: string; 11 | readonly metadata?: string; 12 | readonly slot?: string; 13 | readonly parts?: string[]; 14 | readonly theme?: Theme; 15 | readonly themeId?: string; 16 | id: string; 17 | nftId: string; 18 | pending: boolean; 19 | replace?: string; 20 | 21 | constructor(nftId: string, resource: Resource, replaceId?: string) { 22 | this.base = resource.base; 23 | this.src = resource.src; 24 | this.thumb = resource.thumb; 25 | this.metadata = resource.metadata; 26 | this.slot = resource.slot; 27 | this.parts = resource.parts; 28 | this.theme = resource.theme; 29 | this.themeId = resource.themeId; 30 | this.pending = resource.pending || true; 31 | this.nftId = nftId; 32 | this.id = resource.id || nanoid(8); 33 | this.replace = replaceId; 34 | } 35 | 36 | static fromRemark(remark: string): Resadd | string { 37 | try { 38 | validateResadd(remark); 39 | const [ 40 | _prefix, 41 | _op_type, 42 | _version, 43 | nftId, 44 | resource, 45 | replaceId, 46 | ] = remark.split("::"); 47 | const resourceObj: Resource = getRemarkData(resource); 48 | return new this(nftId, resourceObj, replaceId); 49 | } catch (e: any) { 50 | console.error(e.message); 51 | console.log(`RESADD error: full input was ${remark}`); 52 | return e.message; 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/rmrk2.0.0/classes/send.ts: -------------------------------------------------------------------------------- 1 | import { validateSend } from "../tools/validate-remark"; 2 | import { isValidAddressPolkadotAddress } from "../tools/consolidator/utils"; 3 | import { encodeAddress } from "@polkadot/keyring"; 4 | 5 | export class Send { 6 | recipient: string; 7 | id: string; 8 | 9 | constructor(id: string, recipient: string) { 10 | this.recipient = recipient; 11 | this.id = id; 12 | } 13 | 14 | static fromRemark(remark: string, ss58Format?: number): Send | string { 15 | try { 16 | validateSend(remark); 17 | const [_prefix, _op_type, _version, id, recipient] = remark.split("::"); 18 | let recipientEncoded = recipient; 19 | if (isValidAddressPolkadotAddress(recipient)) { 20 | recipientEncoded = encodeAddress(recipient, ss58Format); 21 | } 22 | return new Send(id, recipientEncoded); 23 | } catch (e: any) { 24 | console.error(e.message); 25 | console.log(`SEND error: full input was ${remark}`); 26 | return e.message; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/rmrk2.0.0/classes/setpriority.ts: -------------------------------------------------------------------------------- 1 | import { validateSetPriority } from "../tools/validate-remark"; 2 | import { getRemarkData } from "../tools/utils"; 3 | 4 | export class Setpriority { 5 | id: string; 6 | priority: string[]; 7 | 8 | constructor(id: string, priority: string[]) { 9 | this.priority = priority; 10 | this.id = id; 11 | } 12 | 13 | static fromRemark(remark: string): Setpriority | string { 14 | try { 15 | validateSetPriority(remark); 16 | const [_prefix, _op_type, _version, id, priority] = remark.split("::"); 17 | const priorityArray: string[] = getRemarkData(priority); 18 | return new this(id, priorityArray); 19 | } catch (e: any) { 20 | console.error(e.message); 21 | console.log(`SETPRIORITY error: full input was ${remark}`); 22 | return e.message; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/rmrk2.0.0/classes/setproperty.ts: -------------------------------------------------------------------------------- 1 | import { validateSetAttribute } from "../tools/validate-remark"; 2 | import { IAttribute } from "../tools/types"; 3 | import { getRemarkData } from "../tools/utils"; 4 | 5 | export class Setproperty { 6 | key: string; 7 | property: Partial; 8 | id: string; 9 | freeze?: "freeze"; 10 | 11 | constructor( 12 | id: string, 13 | key: string, 14 | property: Partial, 15 | freeze?: "freeze" 16 | ) { 17 | this.id = id; 18 | this.property = property; 19 | this.key = key; 20 | this.freeze = freeze; 21 | } 22 | 23 | static fromRemark(remark: string): Setproperty | string { 24 | try { 25 | validateSetAttribute(remark); 26 | const [ 27 | _prefix, 28 | _op_type, 29 | _version, 30 | id, 31 | key, 32 | property, 33 | freeze, 34 | ] = remark.split("::"); 35 | const attributeObj: Partial = getRemarkData(property); 36 | if (freeze && freeze !== "freeze") { 37 | throw new Error(`Not a valid freeze ${freeze}`); 38 | } 39 | return new Setproperty(id, key, attributeObj, freeze as "freeze"); 40 | } catch (e: any) { 41 | console.error(e.message); 42 | console.log(`SETPROPERTY error: full input was ${remark}`); 43 | return e.message; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/rmrk2.0.0/classes/themeadd.ts: -------------------------------------------------------------------------------- 1 | import { validateThemeadd } from "../tools/validate-remark"; 2 | import { VERSION } from "../tools/constants"; 3 | import { Theme } from "./base"; 4 | import { getRemarkData } from "../tools/utils"; 5 | 6 | export class Themeadd { 7 | baseId: string; 8 | themeId: string; 9 | theme: Theme; 10 | static V = VERSION; 11 | 12 | constructor(baseId: string, themeId: string, theme: Theme) { 13 | this.baseId = baseId; 14 | this.themeId = themeId; 15 | this.theme = theme; 16 | } 17 | 18 | static fromRemark(remark: string): Themeadd | string { 19 | try { 20 | validateThemeadd(remark); 21 | const [ 22 | _prefix, 23 | _op_type, 24 | _version, 25 | baseId, 26 | themeId, 27 | theme, 28 | ] = remark.split("::"); 29 | const themeObj: Theme = getRemarkData(theme); 30 | return new Themeadd(baseId, themeId, themeObj); 31 | } catch (e: any) { 32 | console.error(e.message); 33 | console.log(`THEMEADD error: full input was ${remark}`); 34 | return e.message; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/rmrk2.0.0/constants.ts: -------------------------------------------------------------------------------- 1 | export const LATEST_DUMP = "QmVUCSXjuBXC6WJga2yckM3seXJj1MNKgFk6VLB34ExhhW"; // up to 6619194 2 | -------------------------------------------------------------------------------- /src/rmrk2.0.0/index.ts: -------------------------------------------------------------------------------- 1 | import "@polkadot/api-augment"; 2 | export { Consolidator, NFTConsolidated, CollectionConsolidated, BaseConsolidated, ConsolidatorReturnType } from "./tools/consolidator/consolidator"; 3 | export { Collection } from "./classes/collection"; 4 | export { NFT, IResourceConsolidated } from "./classes/nft"; 5 | export { Base } from "./classes/base"; 6 | export { List } from "./classes/list"; 7 | export { Burn } from "./classes/burn"; 8 | export { ChangeIssuer } from "./classes/changeissuer"; 9 | export { Buy } from "./classes/buy"; 10 | export { Send } from "./classes/send"; 11 | export { Emote } from "./classes/emote"; 12 | export { Accept } from "./classes/accept"; 13 | export { Resadd } from "./classes/resadd"; 14 | export { Setproperty } from "./classes/setproperty"; 15 | export { Equip } from "./classes/equip"; 16 | export { Equippable } from "./classes/equippable"; 17 | export { Setpriority } from "./classes/setpriority"; 18 | export { default as fetchRemarks } from "./tools/fetchRemarks"; 19 | export { RemarkListener } from "./listener"; 20 | export { 21 | getLatestFinalizedBlock, 22 | getRemarksFromBlocks, 23 | getBlockCallsFromSignedBlock, 24 | getRemarkData, 25 | } from "./tools/utils"; 26 | export { validateMintNFT } from "./tools/consolidator/interactions/mint"; 27 | export { 28 | validateCreateIds, 29 | getCollectionFromRemark, 30 | } from "./tools/consolidator/interactions/create"; 31 | export { listForSaleInteraction } from "./tools/consolidator/interactions/list"; 32 | export { emoteInteraction } from "./tools/consolidator/interactions/emote"; 33 | export { burnInteraction } from "./tools/consolidator/interactions/burn"; 34 | export { 35 | changeIssuerInteraction, 36 | getChangeIssuerEntity, 37 | } from "./tools/consolidator/interactions/changeIssuer"; 38 | export { buyInteraction } from "./tools/consolidator/interactions/buy"; 39 | export { sendInteraction } from "./tools/consolidator/interactions/send"; 40 | export { resAddInteraction } from "./tools/consolidator/interactions/resadd"; 41 | export { acceptInteraction } from "./tools/consolidator/interactions/accept"; 42 | export { equippableInteraction } from "./tools/consolidator/interactions/equippable"; 43 | export { equipInteraction } from "./tools/consolidator/interactions/equip"; 44 | export * from "./tools/constants"; 45 | export * from "./tools/validate-remark"; 46 | export * from "./tools/validate-metadata"; 47 | export * from "./tools/consolidator/utils"; 48 | export * from "./tools/types"; 49 | -------------------------------------------------------------------------------- /src/rmrk2.0.0/tools/consolidator/adapters/types.ts: -------------------------------------------------------------------------------- 1 | import { NFT } from "../../../classes/nft"; 2 | import { Collection } from "../../../classes/collection"; 3 | import { 4 | BaseConsolidated, 5 | CollectionConsolidated, 6 | NFTConsolidated, 7 | } from "../consolidator"; 8 | import { Base } from "../../../classes/base"; 9 | import { AcceptEntityType } from "../../../classes/accept"; 10 | 11 | export interface IConsolidatorAdapter { 12 | updateNFTEmote(nft: NFT, consolidatedNFT: NFTConsolidated): Promise; 13 | updateNFTList(nft: NFT, consolidatedNFT: NFTConsolidated): Promise; 14 | updateNftResadd(nft: NFT, consolidatedNFT: NFTConsolidated): Promise; 15 | updateEquip(nft: NFT, consolidatedNFT: NFTConsolidated): Promise; 16 | updateNftAccept( 17 | nft: NFT, 18 | consolidatedNFT: NFTConsolidated, 19 | entity: AcceptEntityType 20 | ): Promise; 21 | updateNFTBuy(nft: NFT, consolidatedNFT: NFTConsolidated): Promise; 22 | updateNFTSend(nft: NFT, consolidatedNFT: NFTConsolidated): Promise; 23 | updateNFTBurn( 24 | nft: NFT | NFTConsolidated, 25 | consolidatedNFT: NFTConsolidated 26 | ): Promise; 27 | updateNFTMint(nft: NFT): Promise; 28 | updateSetPriority(nft: NFT, consolidatedNFT: NFTConsolidated): Promise; 29 | updateSetAttribute(nft: NFT, consolidatedNFT: NFTConsolidated): Promise; 30 | updateCollectionMint(collection: CollectionConsolidated): Promise; 31 | updateCollectionDestroy(collection: CollectionConsolidated): Promise; 32 | updateCollectionLock(collection: CollectionConsolidated): Promise; 33 | updateBase(base: Base): Promise; 34 | updateBaseEquippable( 35 | base: Base, 36 | consolidatedBase: BaseConsolidated 37 | ): Promise; 38 | updateBaseThemeAdd( 39 | base: Base, 40 | consolidatedBase: BaseConsolidated 41 | ): Promise; 42 | updateCollectionIssuer( 43 | collection: Collection, 44 | consolidatedCollection: CollectionConsolidated 45 | ): Promise; 46 | updateBaseIssuer( 47 | base: Base, 48 | consolidatedBase: BaseConsolidated 49 | ): Promise; 50 | updateNFTChildrenRootOwner(nft: NFT): Promise; 51 | getNFTById(id: string): Promise; 52 | getCollectionById(id: string): Promise; 53 | getBaseById(id: string): Promise; 54 | getNFTByIdUnique(id: string): Promise; 55 | getNFTsByCollection( 56 | collectionId: string 57 | ): Promise; 58 | getAllNFTs?: () => Promise>; 59 | getAllCollections?: () => Promise>; 60 | getAllBases?: () => Promise>; 61 | } 62 | -------------------------------------------------------------------------------- /src/rmrk2.0.0/tools/consolidator/interactions/accept.ts: -------------------------------------------------------------------------------- 1 | import { Remark } from "../remark"; 2 | import { NFT } from "../../../classes/nft"; 3 | import { OP_TYPES } from "../../constants"; 4 | import { IConsolidatorAdapter } from "../adapters/types"; 5 | import { Accept } from "../../../classes/accept"; 6 | import { findRealOwner } from "../utils"; 7 | 8 | interface ReturnObject { 9 | RESOURCES: string[]; 10 | CHILDREN: string[]; 11 | } 12 | 13 | export const acceptInteraction = async ( 14 | remark: Remark, 15 | acceptEntity: Accept, 16 | dbAdapter: IConsolidatorAdapter, 17 | nft?: NFT 18 | ): Promise => { 19 | if (!nft) { 20 | throw new Error( 21 | `[${OP_TYPES.ACCEPT}] Attempting to accept ${acceptEntity.entity} on a non-existant NFT ${acceptEntity.nftId}` 22 | ); 23 | } 24 | 25 | if (Boolean(nft.burned)) { 26 | throw new Error( 27 | `[${OP_TYPES.ACCEPT}] Attempting to accept ${acceptEntity.entity} on burned NFT ${acceptEntity.nftId}` 28 | ); 29 | } 30 | 31 | // If NFT owner is adding this resource then immediatly accept it 32 | const rootowner = 33 | nft.rootowner || (await findRealOwner(nft.owner, dbAdapter)); 34 | if (rootowner !== remark.caller) { 35 | throw new Error( 36 | `[${OP_TYPES.ACCEPT}] Attempting to accept ${acceptEntity.entity} on non-owned NFT ${acceptEntity.nftId}` 37 | ); 38 | } 39 | 40 | const returnObject: ReturnObject = { 41 | RESOURCES: [], 42 | CHILDREN: [], 43 | }; 44 | 45 | if (acceptEntity.entity === "NFT") { 46 | const pendingNft = await dbAdapter.getNFTById(acceptEntity.id); 47 | if (!pendingNft) { 48 | throw new Error( 49 | `[${OP_TYPES.ACCEPT}] Attempting to accept non-existant child NFT ${acceptEntity.id}` 50 | ); 51 | } 52 | 53 | const childIndex = nft.children.findIndex( 54 | (child) => child.id === acceptEntity.id 55 | ); 56 | if (childIndex > -1) { 57 | nft.children[childIndex].pending = false; 58 | returnObject.CHILDREN.push(acceptEntity.id); 59 | } 60 | 61 | const childNft = await dbAdapter.getNFTById(acceptEntity.id); 62 | if (childNft) { 63 | childNft.pending = false; 64 | } 65 | } else if (acceptEntity.entity === "RES") { 66 | const resourceIndex = nft.resources.findIndex( 67 | (resource) => resource.id === acceptEntity.id 68 | ); 69 | if (resourceIndex > -1 && nft.resources?.[resourceIndex]?.pending) { 70 | nft.resources[resourceIndex].pending = false; 71 | returnObject.RESOURCES.push(acceptEntity.id); 72 | const { replace, ...resource } = nft.resources[resourceIndex]; 73 | 74 | if (!nft.priority.includes(replace || acceptEntity.id)) { 75 | nft.priority.push(replace || acceptEntity.id); 76 | } 77 | 78 | const existingResourceIndex = replace 79 | ? nft.resources.findIndex((res) => res.id === replace) 80 | : -1; 81 | // Replace existing resource 82 | if (existingResourceIndex > -1 && replace) { 83 | nft.resources[existingResourceIndex] = { ...resource, id: replace }; 84 | nft.resources.splice(resourceIndex, 1); 85 | returnObject.RESOURCES.push(replace); 86 | } 87 | } 88 | } 89 | 90 | return returnObject; 91 | }; 92 | -------------------------------------------------------------------------------- /src/rmrk2.0.0/tools/consolidator/interactions/base.ts: -------------------------------------------------------------------------------- 1 | import { Remark } from "../remark"; 2 | import { OP_TYPES } from "../../constants"; 3 | import { Base } from "../../../classes/base"; 4 | 5 | export const getBaseFromRemark = (remark: Remark, ss58Format?: number) => { 6 | const base = Base.fromRemark(remark.remark, remark.block, ss58Format); 7 | if (typeof base === "string") { 8 | throw new Error(`[${OP_TYPES.BASE}] Dead before instantiation: ${base}`); 9 | } 10 | const partIds: string[] = []; 11 | for (let i = 0; i < (base.parts?.length || 0); i++) { 12 | if (base.parts?.[i]?.id && partIds.includes(base.parts?.[i]?.id)) { 13 | throw new Error( 14 | `[${OP_TYPES.BASE}] Duplicate base part id found: ${base.parts?.[i]?.id}` 15 | ); 16 | } else if (base.parts?.[i]?.id) { 17 | partIds.push(base.parts?.[i]?.id); 18 | } 19 | } 20 | return base; 21 | }; 22 | -------------------------------------------------------------------------------- /src/rmrk2.0.0/tools/consolidator/interactions/changeIssuer.ts: -------------------------------------------------------------------------------- 1 | import { Collection } from "../../../classes/collection"; 2 | import { OP_TYPES } from "../../constants"; 3 | import { Change } from "../../../changelog"; 4 | import { Remark } from "../remark"; 5 | import { ChangeIssuer } from "../../../classes/changeissuer"; 6 | import { Base } from "../../../classes/base"; 7 | 8 | export const getChangeIssuerEntity = ( 9 | remark: Remark, 10 | ss58Format?: number 11 | ): ChangeIssuer => { 12 | const changeIssuerEntity = ChangeIssuer.fromRemark(remark.remark, ss58Format); 13 | 14 | if (typeof changeIssuerEntity === "string") { 15 | throw new Error( 16 | `[${OP_TYPES.CHANGEISSUER}] Dead before instantiation: ${changeIssuerEntity}` 17 | ); 18 | } 19 | return changeIssuerEntity; 20 | }; 21 | 22 | export const changeIssuerInteraction = ( 23 | remark: Remark, 24 | changeIssuerEntity: ChangeIssuer, 25 | entity?: Collection | Base 26 | ) => { 27 | const entityType = changeIssuerEntity.id.startsWith("base-") 28 | ? "base" 29 | : "collection"; 30 | if (!entity) { 31 | throw new Error( 32 | `This ${OP_TYPES.CHANGEISSUER} remark is invalid - no such ${entityType} with ID ${changeIssuerEntity.id} found before block ${remark.block}!` 33 | ); 34 | } 35 | 36 | if (remark.caller !== entity.issuer) { 37 | throw new Error( 38 | `Attempting to change issuer of ${entityType} ${changeIssuerEntity.id} when not issuer!` 39 | ); 40 | } 41 | 42 | entity.addChange({ 43 | field: "issuer", 44 | old: entity.issuer, 45 | new: changeIssuerEntity.issuer, 46 | caller: remark.caller, 47 | block: remark.block, 48 | opType: OP_TYPES.CHANGEISSUER, 49 | } as Change); 50 | 51 | entity.issuer = changeIssuerEntity.issuer; 52 | }; 53 | -------------------------------------------------------------------------------- /src/rmrk2.0.0/tools/consolidator/interactions/create.ts: -------------------------------------------------------------------------------- 1 | import { decodeAddress } from "@polkadot/keyring"; 2 | import { Collection } from "../../../classes/collection"; 3 | import { u8aToHex } from "@polkadot/util"; 4 | import { Remark } from "../remark"; 5 | import { OP_TYPES } from "../../constants"; 6 | 7 | export const getCollectionFromRemark = ( 8 | remark: Remark, 9 | ss58Format?: number 10 | ) => { 11 | const collection = Collection.fromRemark( 12 | remark.remark, 13 | remark.block, 14 | ss58Format 15 | ); 16 | if (typeof collection === "string") { 17 | throw new Error( 18 | `[${OP_TYPES.CREATE}] Dead before instantiation: ${collection}` 19 | ); 20 | } 21 | return collection; 22 | }; 23 | 24 | export const validateCreateIds = (collection: Collection, remark: Remark) => { 25 | const pubkey = decodeAddress(remark.caller); 26 | const pubkeyString = u8aToHex(pubkey); 27 | const pubkeyStart = pubkeyString.substr(2, 8); 28 | const pubkeyEnd = pubkeyString.substring(pubkeyString.length - 8); 29 | const id = Collection.generateId(u8aToHex(pubkey), collection.symbol); 30 | const idStart = id.substr(0, 8); 31 | const idEnd = id.substring(pubkeyString.length - 8); 32 | if (idStart === pubkeyStart && idEnd === pubkeyEnd) { 33 | throw new Error( 34 | `Caller's pubkey ${u8aToHex(pubkey)} (${id}) does not match generated ID` 35 | ); 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /src/rmrk2.0.0/tools/consolidator/interactions/destroy.ts: -------------------------------------------------------------------------------- 1 | import { Remark } from "../remark"; 2 | import { OP_TYPES } from "../../constants"; 3 | import { IConsolidatorAdapter } from "../adapters/types"; 4 | import { Collection } from "../../../classes/collection"; 5 | import { Destroy } from "../../../classes/destroy"; 6 | 7 | export const destroyInteraction = async ( 8 | remark: Remark, 9 | destroyEntity: Destroy, 10 | dbAdapter: IConsolidatorAdapter, 11 | collection?: Collection 12 | ): Promise => { 13 | if (!collection) { 14 | throw new Error( 15 | `[${OP_TYPES.DESTROY}] Attempting to destroy a non-existent Collection ${destroyEntity.id}` 16 | ); 17 | } 18 | 19 | // TODO: add unit tests 20 | if (remark.caller !== collection.issuer) { 21 | throw new Error( 22 | `Attempting to destroy collection ${destroyEntity.id} when not issuer!` 23 | ); 24 | } 25 | 26 | const nfts = await dbAdapter.getNFTsByCollection(destroyEntity.id); 27 | const unburnedNfts = nfts ? nfts.filter((nft) => nft.burned === "") : []; 28 | 29 | if (unburnedNfts.length > 0) { 30 | throw new Error( 31 | `[${OP_TYPES.DESTROY}] Collection with unburned nfts cannot be destroyed ${destroyEntity.id}` 32 | ); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /src/rmrk2.0.0/tools/consolidator/interactions/emote.ts: -------------------------------------------------------------------------------- 1 | import { OP_TYPES } from "../../constants"; 2 | import { Remark } from "../remark"; 3 | import { Emote } from "../../../classes/emote"; 4 | import { NFT } from "../../../classes/nft"; 5 | import { Change } from "../../../changelog"; 6 | import { isValidEmoji } from "../../validate-emoji"; 7 | 8 | const addEmoteChange = ( 9 | remark: Remark, 10 | emoteEntity: Emote, 11 | nft: NFT, 12 | removing = false 13 | ) => { 14 | nft.addChange({ 15 | field: "reactions", 16 | old: "", 17 | new: `${removing ? "-" : "+"}${emoteEntity.unicode}`, 18 | caller: remark.caller, 19 | block: remark.block, 20 | opType: OP_TYPES.EMOTE, 21 | } as Change); 22 | }; 23 | 24 | export const emoteInteraction = ( 25 | remark: Remark, 26 | emoteEntity: Emote, 27 | nft?: NFT, 28 | emitEmoteChanges?: boolean 29 | ): void => { 30 | if (!nft) { 31 | throw new Error( 32 | `[${OP_TYPES.EMOTE}] Attempting to emote on non-existant NFT ${emoteEntity.id}` 33 | ); 34 | } 35 | 36 | if (Boolean(nft.burned)) { 37 | throw new Error( 38 | `[${OP_TYPES.EMOTE}] Cannot emote to a burned NFT ${emoteEntity.id}` 39 | ); 40 | } 41 | 42 | if (!isValidEmoji(emoteEntity.unicode)) { 43 | throw new Error( 44 | `Cannot EMOTE on NFT ${nft.getId()} with an invalid emoji unicode ${ 45 | emoteEntity.unicode 46 | }` 47 | ); 48 | } 49 | 50 | if (!nft.reactions[emoteEntity.unicode]) { 51 | nft.reactions[emoteEntity.unicode] = []; 52 | } 53 | const index = nft.reactions[emoteEntity.unicode].indexOf(remark.caller, 0); 54 | 55 | const removing = index > -1; 56 | if (removing) { 57 | nft.reactions[emoteEntity.unicode].splice(index, 1); 58 | } else { 59 | nft.reactions[emoteEntity.unicode].push(remark.caller); 60 | } 61 | 62 | if (emitEmoteChanges) { 63 | addEmoteChange(remark, emoteEntity, nft, removing); 64 | } 65 | }; 66 | -------------------------------------------------------------------------------- /src/rmrk2.0.0/tools/consolidator/interactions/equippable.ts: -------------------------------------------------------------------------------- 1 | import { OP_TYPES } from "../../constants"; 2 | import { Remark } from "../remark"; 3 | import { 4 | collectionRegexPattern, 5 | Equippable, 6 | } from "../../../classes/equippable"; 7 | import { Base } from "../../../classes/base"; 8 | 9 | export const equippableInteraction = ( 10 | remark: Remark, 11 | equippableEntity: Equippable, 12 | base?: Base 13 | ): void => { 14 | if (!base) { 15 | throw new Error( 16 | `[${OP_TYPES.EQUIPPABLE}] Attempting to change equippable on non-existant NFT ${equippableEntity.id}` 17 | ); 18 | } 19 | 20 | if (!base.parts || !equippableEntity.slot) { 21 | throw new Error( 22 | `[${ 23 | OP_TYPES.EQUIPPABLE 24 | }] Attempting to change equippable on base with no parts or slot is not specified ${base.getId()}` 25 | ); 26 | } 27 | 28 | // Check if allowed to issue equippable - if issuer == caller 29 | if (base.issuer != remark.caller) { 30 | throw new Error( 31 | `[${ 32 | OP_TYPES.LIST 33 | }] Attempting to change equippable on non-owned Base ${base.getId()}, real owner: ${ 34 | base.issuer 35 | }` 36 | ); 37 | } 38 | 39 | const equippableChangeMatch = equippableEntity.equippableChange.match( 40 | collectionRegexPattern 41 | ); 42 | if (!equippableChangeMatch) { 43 | return; 44 | } 45 | 46 | const [_, operation, equippableChange] = equippableChangeMatch; 47 | 48 | const partIndex = base.parts.findIndex( 49 | (part) => part.id === equippableEntity.slot 50 | ); 51 | 52 | if (partIndex < 0 || !base.parts[partIndex]) { 53 | throw new Error( 54 | `[${OP_TYPES.EQUIPPABLE}] Attempting to change equippable on non-existant part with a slot id ${equippableEntity.slot}` 55 | ); 56 | } 57 | 58 | if (base.parts[partIndex].type !== "slot") { 59 | throw new Error( 60 | `[${OP_TYPES.EQUIPPABLE}] Attempting to change equippable on base part of type ${base.parts[partIndex].type}` 61 | ); 62 | } 63 | 64 | // Change equippable to allow all collectiones 65 | if (equippableChange === "*") { 66 | base.parts[partIndex].equippable = equippableChange; 67 | return; 68 | } 69 | 70 | const equippableArray = equippableChange.split(","); 71 | if (!operation) { 72 | base.parts[partIndex].equippable = equippableArray; 73 | return; 74 | } 75 | 76 | // Remove NFT classes from equippable list 77 | if (operation === "-") { 78 | const newEquippableArray: string[] = []; 79 | (base.parts[partIndex].equippable as string[]).forEach((equippable) => { 80 | if (!equippableArray.includes(equippable)) { 81 | newEquippableArray.push(equippable); 82 | } 83 | }); 84 | base.parts[partIndex].equippable = newEquippableArray; 85 | return; 86 | } 87 | 88 | // Add NFT classes to equippable list 89 | if (operation === "+") { 90 | const newEquippableArray = [ 91 | ...(base.parts[partIndex].equippable as string[]), 92 | ]; 93 | equippableArray.forEach((newEquippable) => { 94 | if (!newEquippableArray.includes(newEquippable)) { 95 | newEquippableArray.push(newEquippable); 96 | } 97 | }); 98 | 99 | base.parts[partIndex].equippable = newEquippableArray; 100 | } 101 | }; 102 | -------------------------------------------------------------------------------- /src/rmrk2.0.0/tools/consolidator/interactions/list.ts: -------------------------------------------------------------------------------- 1 | import { Remark } from "../remark"; 2 | import { List } from "../../../classes/list"; 3 | import { NFT } from "../../../classes/nft"; 4 | import { OP_TYPES } from "../../constants"; 5 | import { Change } from "../../../changelog"; 6 | import { 7 | consolidatedNFTtoInstance, 8 | findRealOwner, 9 | isValidAddressPolkadotAddress, 10 | validateTransferability, 11 | } from "../utils"; 12 | import { IConsolidatorAdapter } from "../adapters/types"; 13 | 14 | export const listForSaleInteraction = async ( 15 | remark: Remark, 16 | listEntity: List, 17 | dbAdapter: IConsolidatorAdapter, 18 | nft?: NFT 19 | ): Promise => { 20 | if (!nft) { 21 | throw new Error( 22 | `[${OP_TYPES.LIST}] Attempting to list non-existant NFT ${listEntity.id}` 23 | ); 24 | } 25 | 26 | if (Boolean(nft.burned)) { 27 | throw new Error( 28 | `[${OP_TYPES.LIST}] Attempting to list burned NFT ${listEntity.id}` 29 | ); 30 | } 31 | 32 | // Check if allowed to send parent NFT. ( owner is Polkadot address ) 33 | if (isValidAddressPolkadotAddress(nft.owner) && nft.owner != remark.caller) { 34 | throw new Error( 35 | `[${OP_TYPES.LIST}] Attempting to list non-owned NFT ${listEntity.id}, real owner: ${nft.owner}` 36 | ); 37 | } 38 | 39 | const rootowner = 40 | nft.rootowner || (await findRealOwner(nft.owner, dbAdapter)); 41 | // Check if allowed to send child NFT by rootowner and owner is another NFT id 42 | if (!isValidAddressPolkadotAddress(nft.owner) && rootowner != remark.caller) { 43 | throw new Error( 44 | `[${OP_TYPES.LIST}] Attempting to list non-owned NFT ${listEntity.id}, real rootowner: ${rootowner}` 45 | ); 46 | } 47 | 48 | if (listEntity.price !== BigInt(0)) { 49 | validateTransferability(nft, remark, OP_TYPES.LIST); 50 | } 51 | 52 | if (listEntity.price !== nft.forsale) { 53 | nft.addChange({ 54 | field: "forsale", 55 | old: nft.forsale, 56 | new: listEntity.price, 57 | caller: remark.caller, 58 | block: remark.block, 59 | opType: OP_TYPES.LIST, 60 | } as Change); 61 | 62 | nft.forsale = listEntity.price; 63 | } 64 | }; 65 | -------------------------------------------------------------------------------- /src/rmrk2.0.0/tools/consolidator/interactions/lock.ts: -------------------------------------------------------------------------------- 1 | import { Remark } from "../remark"; 2 | import { OP_TYPES } from "../../constants"; 3 | import { IConsolidatorAdapter } from "../adapters/types"; 4 | import { Collection } from "../../../classes/collection"; 5 | import { Lock } from "../../../classes/lock"; 6 | 7 | export const lockInteraction = async ( 8 | remark: Remark, 9 | lockEntity: Lock, 10 | dbAdapter: IConsolidatorAdapter, 11 | collection?: Collection 12 | ): Promise => { 13 | if (!collection) { 14 | throw new Error( 15 | `[${OP_TYPES.LOCK}] Attempting to lock a non-existent Collection ${lockEntity.id}` 16 | ); 17 | } 18 | 19 | // TODO: add unit tests 20 | if (remark.caller !== collection.issuer) { 21 | throw new Error( 22 | `Attempting to lock collection ${lockEntity.id} when not issuer!` 23 | ); 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /src/rmrk2.0.0/tools/consolidator/interactions/mint.ts: -------------------------------------------------------------------------------- 1 | import { Remark } from "../remark"; 2 | import { NFT } from "../../../classes/nft"; 3 | import { OP_TYPES } from "../../constants"; 4 | import { Collection } from "../../../classes/collection"; 5 | import { IConsolidatorAdapter } from "../adapters/types"; 6 | import { findRealOwner, isValidAddressPolkadotAddress } from "../utils"; 7 | import { validateRoyaltiesPropertyValue } from "../../validate-remark"; 8 | 9 | export const validateMintNFT = async ( 10 | remark: Remark, 11 | nft: NFT, 12 | dbAdapter: IConsolidatorAdapter, 13 | nftParentCollection?: Collection 14 | ) => { 15 | if (!nftParentCollection) { 16 | throw new Error( 17 | `NFT referencing non-existant parent collection ${nft.collection}` 18 | ); 19 | } 20 | 21 | if (remark.caller !== nftParentCollection.issuer) { 22 | throw new Error( 23 | `Attempted issue of NFT in non-owned collection. Issuer: ${nftParentCollection.issuer}, caller: ${remark.caller}` 24 | ); 25 | } 26 | 27 | if ( 28 | nftParentCollection.count >= nftParentCollection.max && 29 | nftParentCollection.max !== 0 30 | ) { 31 | throw new Error( 32 | `Attempted to mint into maxed out collection ${nftParentCollection.id}` 33 | ); 34 | } 35 | 36 | if (nft.owner) { 37 | if (!isValidAddressPolkadotAddress(nft.owner)) { 38 | const rootowner = 39 | nft.rootowner || (await findRealOwner(nft.owner, dbAdapter)); 40 | nft.rootowner = rootowner || remark.caller; 41 | 42 | // Add NFT as child of new owner 43 | const newOwner = await dbAdapter.getNFTById(nft.owner); 44 | const childIndex = 45 | (newOwner && 46 | newOwner.children.findIndex((child) => child.id === nft.getId())) || 47 | -1; 48 | if (newOwner && childIndex < 0) { 49 | newOwner.children.push({ 50 | id: nft.getId(), 51 | equipped: "", 52 | pending: rootowner !== remark.caller, 53 | }); 54 | } 55 | 56 | nft.pending = rootowner !== remark.caller; 57 | } 58 | } 59 | 60 | if (nft.properties) { 61 | validateRoyaltiesPropertyValue(nft.properties); 62 | } 63 | 64 | // nft.owner can be already set if mint remark has recipient field that allows to mint directly onto another nft 65 | nft.owner = nft.owner || remark.caller; 66 | nft.rootowner = nft.rootowner || remark.caller; 67 | 68 | if (nft.owner === "") { 69 | throw new Error( 70 | `[${OP_TYPES.MINT}] Somehow this NFT still doesn't have an owner.` 71 | ); 72 | } 73 | }; 74 | -------------------------------------------------------------------------------- /src/rmrk2.0.0/tools/consolidator/interactions/resadd.ts: -------------------------------------------------------------------------------- 1 | import { Remark } from "../remark"; 2 | import { IResourceConsolidated, NFT, Resource } from "../../../classes/nft"; 3 | import { OP_TYPES } from "../../constants"; 4 | import { Resadd } from "../../../classes/resadd"; 5 | import { findRealOwner } from "../utils"; 6 | import { IConsolidatorAdapter } from "../adapters/types"; 7 | 8 | export const resAddInteraction = async ( 9 | remark: Remark, 10 | resaddEntity: Resadd, 11 | dbAdapter: IConsolidatorAdapter, 12 | nft?: NFT 13 | ): Promise => { 14 | if (!nft) { 15 | throw new Error( 16 | `[${OP_TYPES.RESADD}] Attempting to add resource on a non-existant NFT ${resaddEntity.nftId}` 17 | ); 18 | } 19 | 20 | if (Boolean(nft.burned)) { 21 | throw new Error( 22 | `[${OP_TYPES.RESADD}] Attempting to add resource on burned NFT ${resaddEntity.nftId}` 23 | ); 24 | } 25 | 26 | const nftCollection = await dbAdapter.getCollectionById(nft.collection); 27 | if (!nftCollection || nftCollection.issuer !== remark.caller) { 28 | throw new Error( 29 | `[${OP_TYPES.RESADD}] Attempting to add resource to NFT in non-owned collection ${nft.collection}` 30 | ); 31 | } 32 | 33 | for (let i = 0; i < nft.resources.length; i++) { 34 | if (nft.resources[i].id === resaddEntity.id) { 35 | throw new Error( 36 | `[${OP_TYPES.RESADD}] Attempting to add resource with already existing id ${resaddEntity.id} to NFT ${resaddEntity.nftId}` 37 | ); 38 | } 39 | } 40 | 41 | // If NFT owner is adding this resource then immediatly accept it 42 | const rootowner = 43 | nft.rootowner || (await findRealOwner(nft.owner, dbAdapter)); 44 | 45 | const accepted = rootowner === remark.caller; 46 | resaddEntity.pending = !accepted; 47 | const { 48 | pending, 49 | id, 50 | metadata, 51 | base, 52 | src, 53 | slot, 54 | parts, 55 | thumb, 56 | themeId, 57 | theme, 58 | replace, 59 | } = resaddEntity; 60 | 61 | const resource: IResourceConsolidated = { 62 | pending, 63 | id, 64 | metadata, 65 | base, 66 | src, 67 | slot, 68 | parts, 69 | thumb, 70 | themeId, 71 | theme, 72 | replace 73 | }; 74 | 75 | // Remove undefines 76 | Object.keys(resource).forEach((resKey) => { 77 | if (resource[resKey as keyof IResourceConsolidated] === undefined) { 78 | delete resource[resKey as keyof IResourceConsolidated]; 79 | } 80 | }); 81 | 82 | const existingResourceIndex = resaddEntity.replace 83 | ? nft.resources.findIndex((res) => res.id === resaddEntity.replace) 84 | : -1; 85 | // Replace existing resource 86 | if (existingResourceIndex > -1 && accepted && resaddEntity.replace) { 87 | nft.resources[existingResourceIndex] = { 88 | ...resource, 89 | id: resaddEntity.replace, 90 | }; 91 | } else { 92 | nft.resources.push(resource); 93 | } 94 | 95 | // If this is the first resource being added and is immediatly accepted, set default priority array 96 | if (accepted) { 97 | if (!nft.priority.includes(resaddEntity.replace || resaddEntity.id)) { 98 | nft.priority.push(resaddEntity.replace || resaddEntity.id); 99 | } 100 | } 101 | }; 102 | -------------------------------------------------------------------------------- /src/rmrk2.0.0/tools/consolidator/interactions/setpriority.ts: -------------------------------------------------------------------------------- 1 | import { Remark } from "../remark"; 2 | import { NFT } from "../../../classes/nft"; 3 | import { OP_TYPES } from "../../constants"; 4 | import { Setpriority } from "../../../classes/setpriority"; 5 | import { IConsolidatorAdapter } from "../adapters/types"; 6 | import { findRealOwner } from "../utils"; 7 | 8 | export const setPriorityInteraction = async ( 9 | remark: Remark, 10 | setPriorityEntity: Setpriority, 11 | dbAdapter: IConsolidatorAdapter, 12 | nft?: NFT 13 | ): Promise => { 14 | if (!nft) { 15 | throw new Error( 16 | `[${OP_TYPES.SETPRIORITY}] Attempting to set priority on a non-existent NFT ${setPriorityEntity.id}` 17 | ); 18 | } 19 | 20 | if (Boolean(nft.burned)) { 21 | throw new Error( 22 | `[${OP_TYPES.SETPRIORITY}] Attempting to set priority on burned NFT ${setPriorityEntity.id}` 23 | ); 24 | } 25 | 26 | const rootowner = 27 | nft.rootowner || (await findRealOwner(nft.owner, dbAdapter)); 28 | if (rootowner !== remark.caller) { 29 | throw new Error( 30 | `[${OP_TYPES.SETPRIORITY}] Attempting to set priority on non-owned NFT ${setPriorityEntity.id}` 31 | ); 32 | } 33 | 34 | if ( 35 | !nft.resources 36 | .filter((resource) => !resource.pending) 37 | .every((resource) => setPriorityEntity.priority.includes(resource.id)) 38 | ) { 39 | throw new Error( 40 | `[${OP_TYPES.SETPRIORITY}] New priority resource ids are missing some of the resource ids on this NFT ${setPriorityEntity.id}` 41 | ); 42 | } 43 | 44 | const priorityDiff = setPriorityEntity.priority.filter( 45 | (x) => !nft.priority.includes(x) 46 | ); 47 | 48 | if ( 49 | !priorityDiff.every((resourceId) => 50 | Boolean(nft.resources.find((resource) => resource.id === resourceId)) 51 | ) 52 | ) { 53 | throw new Error( 54 | `[${OP_TYPES.SETPRIORITY}] one of the NFT resources doesn't contain resource with id from new priority array` 55 | ); 56 | } 57 | 58 | nft.priority = setPriorityEntity.priority; 59 | }; 60 | -------------------------------------------------------------------------------- /src/rmrk2.0.0/tools/consolidator/interactions/themeadd.ts: -------------------------------------------------------------------------------- 1 | import { Remark } from "../remark"; 2 | import { OP_TYPES } from "../../constants"; 3 | import { Themeadd } from "../../../classes/themeadd"; 4 | import { Base } from "../../../classes/base"; 5 | 6 | export const themeAddInteraction = ( 7 | remark: Remark, 8 | themeaddEntity: Themeadd, 9 | base?: Base 10 | ) => { 11 | if (!base) { 12 | throw new Error( 13 | `[${OP_TYPES.THEMEADD}] Attempting to add a theme to a non-existant Base ${themeaddEntity.baseId}` 14 | ); 15 | } 16 | 17 | if (base.themes?.[themeaddEntity.themeId]) { 18 | throw new Error( 19 | `[${OP_TYPES.THEMEADD}] Attempting to add a theme with an already existing theme key ${themeaddEntity.themeId}` 20 | ); 21 | } 22 | 23 | if (!base.themes) { 24 | base.themes = {}; 25 | } 26 | 27 | base.themes[themeaddEntity.themeId] = themeaddEntity.theme; 28 | }; 29 | -------------------------------------------------------------------------------- /src/rmrk2.0.0/tools/consolidator/remark.ts: -------------------------------------------------------------------------------- 1 | import { BlockCall } from "../types"; 2 | 3 | export type Remark = { 4 | block: number; 5 | interaction_type: string; 6 | caller: string; 7 | version: string; 8 | remark: string; 9 | extra_ex?: BlockCall[]; 10 | }; 11 | 12 | export type Extrinsic = { 13 | module: string; 14 | method: string; 15 | arg: string; 16 | }; 17 | -------------------------------------------------------------------------------- /src/rmrk2.0.0/tools/constants.ts: -------------------------------------------------------------------------------- 1 | export const VERSION = "2.0.0"; 2 | export const PREFIX = "RMRK"; 3 | 4 | export enum OP_TYPES { 5 | BUY = "BUY", 6 | LIST = "LIST", 7 | CREATE = "CREATE", 8 | MINT = "MINT", 9 | SEND = "SEND", 10 | EMOTE = "EMOTE", 11 | CHANGEISSUER = "CHANGEISSUER", 12 | BURN = "BURN", 13 | BASE = "BASE", 14 | EQUIPPABLE = "EQUIPPABLE", 15 | THEMEADD = "THEMEADD", 16 | RESADD = "RESADD", 17 | ACCEPT = "ACCEPT", 18 | EQUIP = "EQUIP", 19 | SETPROPERTY = "SETPROPERTY", 20 | LOCK = "LOCK", 21 | SETPRIORITY = "SETPRIORITY", 22 | DESTROY = "DESTROY", 23 | } 24 | -------------------------------------------------------------------------------- /src/rmrk2.0.0/tools/deriveMultisigAddress.ts: -------------------------------------------------------------------------------- 1 | import { encodeAddress, decodeAddress } from "@polkadot/keyring"; 2 | import { u8aSorted } from "@polkadot/util"; 3 | import { blake2AsU8a } from "@polkadot/util-crypto"; 4 | 5 | type Options = { 6 | addresses: string[]; // array of the addresses. 7 | ss58Prefix: number; // Prefix for the network encoding to use. 8 | threshold: number; // Number of addresses that are needed to approve an action. 9 | }; 10 | 11 | const derivePubkey = (addresses: string[], threshold = 1): Uint8Array => { 12 | const prefix = "modlpy/utilisuba"; 13 | const payload = new Uint8Array(prefix.length + 1 + 32 * addresses.length + 2); 14 | payload.set( 15 | Array.from(prefix).map((c) => c.charCodeAt(0)), 16 | 0 17 | ); 18 | payload[prefix.length] = addresses.length << 2; 19 | const pubkeys = addresses.map((addr) => decodeAddress(addr)); 20 | u8aSorted(pubkeys).forEach((pubkey, idx) => { 21 | payload.set(pubkey, prefix.length + 1 + idx * 32); 22 | }); 23 | payload[prefix.length + 1 + 32 * addresses.length] = threshold; 24 | 25 | return blake2AsU8a(payload); 26 | }; 27 | 28 | export const deriveMultisigAddress = (opts: Options): string => { 29 | const { addresses, ss58Prefix, threshold } = opts; 30 | 31 | if (!addresses) throw new Error("Please provide the addresses option."); 32 | 33 | const addrs = addresses.filter((x) => !!x); 34 | 35 | const pubkey = derivePubkey(addrs, threshold); 36 | const msig = encodeAddress(pubkey, ss58Prefix); 37 | 38 | return msig; 39 | }; 40 | -------------------------------------------------------------------------------- /src/rmrk2.0.0/tools/fetchRemarks.ts: -------------------------------------------------------------------------------- 1 | import { BlockCalls } from "./types"; 2 | import { deeplog, getBlockCallsFromSignedBlock } from "../tools/utils"; 3 | import { ApiPromise } from "@polkadot/api"; 4 | 5 | export default async ( 6 | api: ApiPromise, 7 | from: number, 8 | to: number, 9 | prefixes: string[], 10 | ss58Format = 2 11 | ): Promise => { 12 | const bcs: BlockCalls[] = []; 13 | for (let i = from; i <= to; i++) { 14 | if (i % 1000 === 0) { 15 | const event = new Date(); 16 | console.log(`Block ${i} at time ${event.toTimeString()}`); 17 | if (i % 5000 === 0) { 18 | console.log(`Currently at ${bcs.length} remarks.`); 19 | } 20 | } 21 | 22 | const blockHash = await api.rpc.chain.getBlockHash(i); 23 | const block = await api.rpc.chain.getBlock(blockHash); 24 | 25 | if (block.block === undefined) { 26 | console.error("block.block is undefined for block " + i); 27 | deeplog(block); 28 | continue; 29 | } 30 | 31 | const blockCalls = await getBlockCallsFromSignedBlock( 32 | block, 33 | prefixes, 34 | api, 35 | ss58Format 36 | ); 37 | 38 | if (blockCalls.length) { 39 | bcs.push({ 40 | block: i, 41 | calls: blockCalls, 42 | } as BlockCalls); 43 | } 44 | } 45 | return bcs; 46 | }; 47 | -------------------------------------------------------------------------------- /src/rmrk2.0.0/tools/get-polkadot-api-with-reconnect.ts: -------------------------------------------------------------------------------- 1 | import { ApiPromise, WsProvider } from "@polkadot/api"; 2 | 3 | export const PUBLIC_KUSAMA_WS_ENDPOINTS = [ 4 | "wss://kusama-rpc.polkadot.io", 5 | "wss://kusama.api.onfinality.io/public-ws", 6 | "wss://kusama-rpc.dwellir.com", 7 | ]; 8 | 9 | export const sleep = (ms: number): Promise => { 10 | return new Promise((resolve) => { 11 | setTimeout(() => resolve(), ms); 12 | }); 13 | }; 14 | 15 | const MAX_RETRIES = 5; 16 | const WS_DISCONNECT_TIMEOUT_SECONDS = 20; 17 | 18 | let wsProvider: WsProvider; 19 | let polkadotApi: ApiPromise; 20 | let healthCheckInProgress = false; 21 | 22 | /** 23 | * 24 | * @param wsEndpoints - array of rpc ws endpoints. In the order of their priority 25 | */ 26 | const providerHealthCheck = async (wsEndpoints: string[]) => { 27 | const [primaryEndpoint, secondaryEndpoint, ...otherEndpoints] = wsEndpoints; 28 | console.log( 29 | `Performing ${WS_DISCONNECT_TIMEOUT_SECONDS} seconds health check for WS Provider fro rpc ${primaryEndpoint}.` 30 | ); 31 | healthCheckInProgress = true; 32 | await sleep(WS_DISCONNECT_TIMEOUT_SECONDS * 1000); 33 | if (wsProvider.isConnected) { 34 | console.log(`All good. Connected back to ${primaryEndpoint}`); 35 | healthCheckInProgress = false; 36 | return true; 37 | } else { 38 | console.log( 39 | `rpc endpoint ${primaryEndpoint} still disconnected after ${WS_DISCONNECT_TIMEOUT_SECONDS} seconds. Disconnecting from ${primaryEndpoint} and switching to a backup rpc endpoint ${secondaryEndpoint}` 40 | ); 41 | await wsProvider.disconnect(); 42 | 43 | healthCheckInProgress = false; 44 | throw new Error( 45 | `rpc endpoint ${primaryEndpoint} still disconnected after ${WS_DISCONNECT_TIMEOUT_SECONDS} seconds.` 46 | ); 47 | } 48 | }; 49 | 50 | /** 51 | * 52 | * @param wsEndpoints - array of rpc ws endpoints. In the order of their priority 53 | */ 54 | const getProvider = async (wsEndpoints: string[]) => { 55 | const [primaryEndpoint, ...otherEndpoints] = wsEndpoints; 56 | return await new Promise((resolve, reject) => { 57 | wsProvider = new WsProvider(primaryEndpoint); 58 | wsProvider.on("disconnected", async () => { 59 | console.log(`WS provider for rpc ${primaryEndpoint} disconnected!`); 60 | if (!healthCheckInProgress) { 61 | try { 62 | await providerHealthCheck(wsEndpoints); 63 | resolve(wsProvider); 64 | } catch (error: any) { 65 | reject(error); 66 | } 67 | } 68 | }); 69 | wsProvider.on("connected", () => { 70 | console.log(`WS provider for rpc ${primaryEndpoint} connected`); 71 | resolve(wsProvider); 72 | }); 73 | wsProvider.on("error", async () => { 74 | console.log(`Error thrown for rpc ${primaryEndpoint}`); 75 | if (!healthCheckInProgress) { 76 | try { 77 | await providerHealthCheck(wsEndpoints); 78 | resolve(wsProvider); 79 | } catch (error: any) { 80 | reject(error); 81 | } 82 | } 83 | }); 84 | }); 85 | }; 86 | 87 | /** 88 | * 89 | * @param wsEndpoints - array of rpc ws endpoints. In the order of their priority 90 | * @param retry - retry count 91 | */ 92 | export const getApiWithReconnect = async ( 93 | wsEndpoints: string[] = PUBLIC_KUSAMA_WS_ENDPOINTS, 94 | retry = 0 95 | ): Promise => { 96 | if (wsProvider && polkadotApi && polkadotApi.isConnected) return polkadotApi; 97 | const [primaryEndpoint, secondaryEndpoint, ...otherEndpoints] = wsEndpoints; 98 | 99 | try { 100 | const provider = await getProvider(wsEndpoints); 101 | polkadotApi = await ApiPromise.create({ provider }); 102 | await polkadotApi.isReady; 103 | return polkadotApi; 104 | } catch (error: any) { 105 | if (retry < MAX_RETRIES) { 106 | // If we have reached maximum number of retries on the primaryEndpoint, let's move it to the end of array and try the secondary endpoint 107 | return await getApiWithReconnect( 108 | [secondaryEndpoint, ...otherEndpoints, primaryEndpoint], 109 | retry + 1 110 | ); 111 | } else { 112 | return polkadotApi; 113 | } 114 | } 115 | }; 116 | 117 | // const api = await getPolkadotApi(); 118 | -------------------------------------------------------------------------------- /src/rmrk2.0.0/tools/metadata-to-ipfs.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | // @ts-ignore 3 | import pinataSDK from "@pinata/sdk"; 4 | import { Metadata } from "./types"; 5 | 6 | const pinata = pinataSDK(process.env.PINATA_KEY, process.env.PINATA_SECRET); 7 | 8 | const defaultOptions = { 9 | pinataOptions: { 10 | cidVersion: 1, 11 | }, 12 | }; 13 | 14 | export const pinToIpfs = async (filePath: string, name?: string) => { 15 | const options = { ...defaultOptions, pinataMetadata: { name } }; 16 | try { 17 | const readableStreamForFile = fs.createReadStream(filePath); 18 | const result = await pinata.pinFileToIPFS(readableStreamForFile, options); 19 | return result.IpfsHash; 20 | } catch (error: any) { 21 | console.error(error); 22 | } 23 | }; 24 | 25 | export const uploadRMRKMetadata = async ( 26 | imagePath: string, 27 | metadataFields: Metadata 28 | ): Promise => { 29 | const options = { 30 | ...defaultOptions, 31 | pinataMetadata: { name: metadataFields.name }, 32 | }; 33 | try { 34 | const imageHash = await pinToIpfs(imagePath, metadataFields.name); 35 | const metadata = { ...metadataFields, image: `ipfs://ipfs/${imageHash}` }; 36 | const metadataHashResult = await pinata.pinJSONToIPFS(metadata, options); 37 | return `ipfs://ipfs/${metadataHashResult.IpfsHash}`; 38 | } catch (error: any) { 39 | return ""; 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /src/rmrk2.0.0/tools/polyfill-string-from-codepoint.ts: -------------------------------------------------------------------------------- 1 | const _String = String; 2 | 3 | /** 4 | * Polyfill String.fromCodePoint to parse emoji unicode 5 | */ 6 | export default _String.fromCodePoint || 7 | function stringFromCodePoint(...args) { 8 | const MAX_SIZE = 0x4000; 9 | const codeUnits = []; 10 | let highSurrogate; 11 | let lowSurrogate; 12 | let index = -1; 13 | const length = args.length; 14 | if (!length) { 15 | return ""; 16 | } 17 | let result = ""; 18 | while (++index < length) { 19 | let codePoint = Number(args[index]); 20 | if ( 21 | !isFinite(codePoint) || // `NaN`, `+Infinity`, or `-Infinity` 22 | codePoint < 0 || // not a valid Unicode code point 23 | codePoint > 0x10ffff || // not a valid Unicode code point 24 | Math.floor(codePoint) != codePoint // not an integer 25 | ) { 26 | throw RangeError("Invalid code point: " + codePoint); 27 | } 28 | if (codePoint <= 0xffff) { 29 | // BMP code point 30 | codeUnits.push(codePoint); 31 | } else { 32 | // Astral code point; split in surrogate halves 33 | // http://mathiasbynens.be/notes/javascript-encoding#surrogate-formulae 34 | codePoint -= 0x10000; 35 | highSurrogate = (codePoint >> 10) + 0xd800; 36 | lowSurrogate = (codePoint % 0x400) + 0xdc00; 37 | codeUnits.push(highSurrogate, lowSurrogate); 38 | } 39 | if (index + 1 === length || codeUnits.length > MAX_SIZE) { 40 | result += String.fromCharCode.apply(null, codeUnits); 41 | codeUnits.length = 0; 42 | } 43 | } 44 | return result; 45 | }; 46 | -------------------------------------------------------------------------------- /src/rmrk2.0.0/tools/types.ts: -------------------------------------------------------------------------------- 1 | import { Send } from "../classes/send"; 2 | import { Buy } from "../classes/buy"; 3 | import { List } from "../classes/list"; 4 | import { Emote } from "../classes/emote"; 5 | import { ChangeIssuer } from "../classes/changeissuer"; 6 | import { OP_TYPES } from "./constants"; 7 | 8 | export type IProperties = Record; 9 | 10 | export interface IAttribute { 11 | _mutation?: { 12 | allowed: boolean; 13 | with?: { 14 | opType: OP_TYPES; 15 | condition?: string; 16 | }; 17 | }; 18 | type: 19 | | "array" 20 | | "object" 21 | | "int" 22 | | "float" 23 | | "number" 24 | | "string" 25 | | "boolean" 26 | | "datetime" 27 | | "royalty"; 28 | value: any; 29 | } 30 | 31 | export interface IRoyaltyAttribute extends IAttribute { 32 | type: "royalty"; 33 | value: { 34 | receiver: string; 35 | royaltyPercentFloat: number; 36 | }; 37 | } 38 | 39 | export interface Metadata { 40 | mediaUri?: string; 41 | thumbnailUri?: string; 42 | externalUri?: string; 43 | description?: string; 44 | name?: string; 45 | license?: string; 46 | licenseUri?: string; 47 | type?: string; 48 | locale?: string; 49 | properties?: IProperties; 50 | /** @deprecated deprecated in favour of `externalUri` field */ 51 | external_url?: string; 52 | /** @deprecated deprecated in favour of `mediaUri` or `thumbnailUri` field */ 53 | image?: string; 54 | /** @deprecated */ 55 | image_data?: string; 56 | /** @deprecated deprecated in favour of `mediaUri` field */ 57 | animation_url?: string; 58 | /* Allow any other arbitrary key value pairs */ 59 | [key: string]: any; 60 | } 61 | 62 | export type Options = { 63 | ws: string; 64 | from: string; 65 | to: string; 66 | prefixes: string; 67 | blocks: string; 68 | json: string; 69 | folder: string; 70 | append: string; 71 | remark: string; 72 | }; 73 | 74 | export type BlockCalls = { 75 | block: number; 76 | calls: BlockCall[]; 77 | }; 78 | 79 | export type BlockCall = { 80 | call: string; 81 | value: string; 82 | caller: string; 83 | extras?: BlockCall[]; 84 | }; 85 | 86 | export type BaseType = "svg" | "png" | string; 87 | 88 | export type Interaction = Send | Buy | List | Emote | ChangeIssuer; 89 | -------------------------------------------------------------------------------- /src/rmrk2.0.0/tools/validate-emoji.ts: -------------------------------------------------------------------------------- 1 | import stringFromCodePoint from "./polyfill-string-from-codepoint"; 2 | import emojiRegex from "emoji-regex/text"; 3 | 4 | /** 5 | * Converted dashed emoji unicodes into native emoji unicode with prefix 6 | * @param unified - emoji unicode 7 | */ 8 | export const unifiedToNative = (unified: string) => { 9 | try { 10 | const unicodes = unified 11 | .replace(/([U+]){2}/g, "") 12 | .split(unified.includes("-") ? "-" : " "); 13 | const codePoints = unicodes.map((u) => (u ? `0x${u}` : "")); 14 | 15 | return stringFromCodePoint( 16 | ...codePoints.map((codePoint) => Number(codePoint)) 17 | ); 18 | } catch (error: any) { 19 | return ""; 20 | } 21 | }; 22 | 23 | /** 24 | * Validate emoji 25 | * @param emoji 26 | */ 27 | export const isValidEmoji = (emoji: string) => { 28 | const unified = unifiedToNative(emoji); 29 | const regex = emojiRegex(); 30 | return regex.test(unified); 31 | }; 32 | -------------------------------------------------------------------------------- /src/rmrk2.0.0/tools/validate-metadata.ts: -------------------------------------------------------------------------------- 1 | import { 2 | optional, 3 | pattern, 4 | string, 5 | type, 6 | any, 7 | assert, 8 | object, 9 | enums, 10 | record, 11 | literal, 12 | boolean, 13 | } from "superstruct"; 14 | import { IProperties, Metadata } from "./types"; 15 | import { OP_TYPES } from "./constants"; 16 | 17 | const MetadataStruct = type({ 18 | name: optional(string()), 19 | description: optional(string()), 20 | mediaUri: optional(pattern(string(), new RegExp("^(https?|ipfs)://.*$"))), 21 | thumbnailUri: optional(pattern(string(), new RegExp("^(https?|ipfs)://.*$"))), 22 | image: optional(pattern(string(), new RegExp("^(https?|ipfs)://.*$"))), 23 | properties: any(), 24 | externalUri: optional(pattern(string(), new RegExp("^(https?|ipfs)://.*$"))), 25 | }); 26 | 27 | export const PropertiesStruct = object({ 28 | value: any(), 29 | type: enums([ 30 | "string", 31 | "array", 32 | "object", 33 | "int", 34 | "boolean", 35 | "datetime", 36 | "float", 37 | ]), 38 | _mutation: optional( 39 | object({ 40 | allowed: boolean(), 41 | with: optional( 42 | object({ 43 | opType: enums([ 44 | "BUY", 45 | "LIST", 46 | "CREATE", 47 | "MINT", 48 | "SEND", 49 | "EMOTE", 50 | "CHANGEISSUER", 51 | "BURN", 52 | "BASE", 53 | "EQUIPPABLE", 54 | "THEMEADD", 55 | "RESADD", 56 | "ACCEPT", 57 | "ACCEPT", 58 | "EQUIP", 59 | "SETPROPERTY", 60 | "SETPRIORITY", 61 | ] as OP_TYPES[]), 62 | condition: optional(string()), 63 | }) 64 | ), 65 | }) 66 | ), 67 | }); 68 | 69 | export const validateAttributes = (properties?: IProperties) => { 70 | if (!properties) { 71 | return true; 72 | } 73 | assert(properties, record(string(), PropertiesStruct)); 74 | 75 | Object.values(properties).forEach((property) => { 76 | const { value, type } = property; 77 | if (type === "string") { 78 | if (typeof value !== "string") { 79 | throw new Error("for type 'string' 'value' has to be a string"); 80 | } 81 | } 82 | 83 | if (type === "int" || type === "float" || type === "number" || type === "datetime") { 84 | if (typeof value !== "number") { 85 | throw new Error("for type 'number' 'value' has to be a number"); 86 | } 87 | } 88 | 89 | if (type === "boolean") { 90 | if (typeof value !== "boolean") { 91 | throw new Error("for type 'boolean' 'value' has to be a boolean"); 92 | } 93 | } 94 | 95 | if (type === "array") { 96 | if (!Array.isArray(value)) { 97 | throw new Error("for type 'array' 'value' has to be an array"); 98 | } 99 | } 100 | 101 | if (type === "object") { 102 | if (typeof value !== "object") { 103 | throw new Error("for type 'object' 'value' has to be an Object"); 104 | } 105 | } 106 | }); 107 | 108 | return true; 109 | }; 110 | 111 | /** 112 | * Validate Metadata 113 | * @param metadata 114 | */ 115 | export const validateMetadata = (metadata: Metadata) => { 116 | assert(metadata, MetadataStruct); 117 | 118 | validateAttributes(metadata.properties); 119 | return true; 120 | }; 121 | -------------------------------------------------------------------------------- /test/1.0.0/mocks/metadata-valid.ts: -------------------------------------------------------------------------------- 1 | export const attributesMockBoostNumberValid = [ 2 | { 3 | display_type: "boost_number", 4 | trait_type: "mock", 5 | value: 2, 6 | max_value: 4, 7 | }, 8 | ]; 9 | 10 | export const attributesMockBoostPercentageValid = [ 11 | { 12 | display_type: "boost_percentage", 13 | trait_type: "mock", 14 | value: 2, 15 | }, 16 | ]; 17 | 18 | export const attributesMockNumberValid = [ 19 | { 20 | display_type: "number", 21 | value: 2, 22 | }, 23 | ]; 24 | 25 | export const attributesMockValueValid = [ 26 | { 27 | value: "2", 28 | }, 29 | ]; 30 | 31 | export const attributesMockDateValid = [ 32 | { 33 | display_type: "date", 34 | value: 1620380805485, 35 | }, 36 | ]; 37 | 38 | export const metadataMockAllValid = { 39 | external_url: "https://youtube.com", 40 | image: "ipfs://ipfs/12345", 41 | image_data: "", 42 | description: "Mock description", 43 | name: "Mock 1", 44 | attributes: attributesMockBoostNumberValid, 45 | background_color: "", 46 | animation_url: "ipfs://ipfs/12345", 47 | youtube_url: "https://youtube.com", 48 | }; 49 | 50 | export const metadataMockAllValid2 = { 51 | image: "ipfs://ipfs/12345", 52 | description: "Mock description", 53 | name: "Mock 1", 54 | attributes: attributesMockBoostNumberValid, 55 | background_color: "", 56 | youtube_url: "https://youtube.com", 57 | }; 58 | 59 | export const metadataMockAllValid3 = { 60 | animation_url: "ipfs://ipfs/12345", 61 | }; 62 | 63 | export const metadataMockAllValid4 = { 64 | animation_url: "ipfs://ipfs/12345", 65 | attributes: attributesMockNumberValid, 66 | }; 67 | 68 | export const metadataMockAllValid5 = { 69 | animation_url: "ipfs://ipfs/12345", 70 | attributes: attributesMockBoostPercentageValid, 71 | }; 72 | 73 | export const metadataMockAllValid6 = { 74 | animation_url: "ipfs://ipfs/12345", 75 | attributes: attributesMockValueValid, 76 | }; 77 | 78 | export const metadataMockAllValid7 = { 79 | animation_url: "ipfs://ipfs/12345", 80 | attributes: attributesMockDateValid, 81 | }; 82 | -------------------------------------------------------------------------------- /test/1.0.0/mocks/remark-mocks.ts: -------------------------------------------------------------------------------- 1 | export const mintRemarkValidMocks = [ 2 | 'RMRK::MINT::1.0.0::{"name"%3A"Foo"%2C"max"%3A5%2C"issuer"%3A"5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY"%2C"symbol"%3A"FOO"%2C"id"%3A"d43593c715a56da27d-FOO"%2C"metadata"%3A"https%3A%2F%2Fsome.url"}', 3 | 'RMRK::MINT::1.0.0::{"name"%3A"Test Batch"%2C"max"%3A5%2C"issuer"%3A"5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY"%2C"symbol"%3A"TB"%2C"id"%3A"d43593c715a56da27d-TB"%2C"metadata"%3A"https%3A%2F%2Fsome.url"}', 4 | 'RMRK::MINT::1.0.0::{"name"%3A"Test Batch 2"%2C"max"%3A5%2C"issuer"%3A"5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY"%2C"symbol"%3A"TB2"%2C"id"%3A"d43593c715a56da27d-TB2"%2C"metadata"%3A"https%3A%2F%2Fsome.url"}', 5 | 'RMRK::MINT::1.0.0::{"name"%3A"Bar"%2C"max"%3A5%2C"issuer"%3A"5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY"%2C"symbol"%3A"BAR"%2C"id"%3A"d43593c715a56da27d-BAR"%2C"metadata"%3A"https%3A%2F%2Fsome.url"}', 6 | 'RMRK::MINT::1.0.0::{"name"%3A"Foo"%2C"max"%3A5%2C"issuer"%3A"5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY"%2C"symbol"%3A"FOO"%2C"id"%3A"d43593c715a56da27d-FOO"%2C"metadata"%3A"https%3A%2F%2Fsome.url"}', 7 | ]; 8 | 9 | const changeUserRemarkValidMocks = [ 10 | "RMRK::CHANGEISSUER::1.0.0::d43593c715a56da27d-BAR::5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty", 11 | "RMRK::CHANGEISSUER::1.0.0::d43593c715a56da27d-BAR::5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty", 12 | ]; 13 | 14 | export const validSendRemarkEvent = 15 | "RMRK::SEND::1.0.0::F4677F38191256A73F-TTNKARDS-Celadon Woodash Tankard-0000000000000001::Fksmad33PFxhrQXNYPPJozgWrv82zuFLvXK7Rh8m1xQhe98"; 16 | export const validListRemarkEvent = 17 | "RMRK::LIST::1.0.0::6435603-D4E195CCE7ADB3F876-INVITATION-VIP_INVITATION_1-0000000000000001::1000000000000"; 18 | export const validMintNFTRemarkEvent = 19 | 'RMRK::MINTNFT::1.0.0::{"collection"%3A"D4E195CCE7ADB3F876-INVITATION"%2C"sn"%3A"0000000000000001"%2C"transferable"%3A1%2C"name"%3A"VIP Invitation %231"%2C"metadata"%3A"ipfs%3A%2F%2Fipfs%2FQmQ2Q57PVpaP8QvWvvH9kfn1CdCY49pcv1AaLBjDwS2p4g"%2C"currentOwner"%3A"HPSgWwpjnMe9oyBq4t2dA3dRTU8PwDAU32q6E76xjFDDrEX"%2C"instance"%3A"VIP_INVITATION_1"}'; 20 | export const validBuyRemarkEvent = 21 | "RMRK::BUY::1.0.0::6309833-282781680602E07B32-BIR-BIRTH_1-0000000000000001"; 22 | export const validEmoteRemarkEvent = 23 | "RMRK::EMOTE::1.0.0::6431478-10D77F8B699437BB50-TXT-JUNGLE_TEXTURE-0000000000000001::1F496"; 24 | export const validConsumeRemarkEvent = 25 | "RMRK::CONSUME::1.0.0::6277640-D4E195CCE7ADB3F876-SUPER GIFS-GOODBYE_BULLIES!-0000000000000002"; 26 | -------------------------------------------------------------------------------- /test/1.0.0/utils/validate-base.test.ts: -------------------------------------------------------------------------------- 1 | import { validateBase } from "../../../src/rmrk1.0.0/tools/validate-remark"; 2 | import { OP_TYPES } from "../../../src/rmrk1.0.0/tools/constants"; 3 | 4 | describe("validation: validateBase", () => { 5 | it("should be valid 1", () => { 6 | const remark = 7 | 'RMRK::MINT::1.0.0::{"name"%3A"Foo"%2C"max"%3A5%2C"issuer"%3A"5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY"%2C"symbol"%3A"FOO"%2C"id"%3A"d43593c715a56da27d-FOO"%2C"metadata"%3A"https%3A%2F%2Fsome.url"}'; 8 | 9 | expect(() => validateBase(remark, OP_TYPES.MINT)).not.toThrow(); 10 | }); 11 | 12 | it("should be valid 2", () => { 13 | const remark = "RMRK::MINT::1.0.0"; 14 | expect(() => validateBase(remark, OP_TYPES.MINT)).not.toThrow(); 15 | }); 16 | 17 | it("should be valid 3", () => { 18 | const remark = "RMRK::MINT::1.0.0::FOO::BAR::BAZ"; 19 | expect(() => validateBase(remark, OP_TYPES.MINT)).not.toThrow(); 20 | }); 21 | 22 | it("should throw - does not start with RMRK", () => { 23 | const remark = 24 | 'BRB::MINT::1.0.0::{"name"%3A"Foo"%2C"max"%3A5%2C"issuer"%3A"5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY"%2C"symbol"%3A"FOO"%2C"id"%3A"d43593c715a56da27d-FOO"%2C"metadata"%3A"https%3A%2F%2Fsome.url"}'; 25 | 26 | expect(() => validateBase(remark, OP_TYPES.MINT)).toThrowError( 27 | "Invalid remark - does not start with RMRK" 28 | ); 29 | }); 30 | 31 | it("should throw - The op code needs to be MINT, but it is CLINT", () => { 32 | const remark = 33 | 'RMRK::CLINT::1.0.0::{"name"%3A"Foo"%2C"max"%3A5%2C"issuer"%3A"5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY"%2C"symbol"%3A"FOO"%2C"id"%3A"d43593c715a56da27d-FOO"%2C"metadata"%3A"https%3A%2F%2Fsome.url"}'; 34 | 35 | expect(() => validateBase(remark, OP_TYPES.MINT)).toThrowError( 36 | "The op code needs to be MINT, but it is CLINT" 37 | ); 38 | }); 39 | 40 | it("should throw - wrong version", () => { 41 | const remark = 42 | 'RMRK::MINT::0.0.0::{"name"%3A"Foo"%2C"max"%3A5%2C"issuer"%3A"5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY"%2C"symbol"%3A"FOO"%2C"id"%3A"d43593c715a56da27d-FOO"%2C"metadata"%3A"https%3A%2F%2Fsome.url"}'; 43 | 44 | expect(() => validateBase(remark, OP_TYPES.MINT)).toThrowError( 45 | "This remark was issued under version 0.0.0 instead of 1.0.0" 46 | ); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /test/1.0.0/utils/validate-buy.test.ts: -------------------------------------------------------------------------------- 1 | import { validateBuy } from "../../../src/rmrk1.0.0/tools/validate-remark"; 2 | 3 | describe("validation: validateBuy", () => { 4 | it("should be valid buy 1", () => { 5 | const remark = 6 | "rmrk::BUY::1.0.0::5105000-0aff6865bed3a66b-VALHELLO-POTION_HEAL-0000000000000001"; 7 | 8 | expect(() => validateBuy(remark)).not.toThrow(); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /test/1.0.0/utils/validate-collection.test.ts: -------------------------------------------------------------------------------- 1 | import { validateCollection } from "../../../src/rmrk1.0.0/tools/validate-remark"; 2 | import { mintRemarkValidMocks } from "../mocks/remark-mocks"; 3 | 4 | describe("validation: validateCollection", () => { 5 | mintRemarkValidMocks.forEach((remark) => { 6 | it("should be valid", () => { 7 | expect(() => validateCollection(remark)).not.toThrow(); 8 | }); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /test/1.0.0/utils/validate-mint-ids.test.ts: -------------------------------------------------------------------------------- 1 | import { Collection, validateMintIds } from "../../../src/rmrk1.0.0"; 2 | import { u8aToHex } from "@polkadot/util"; 3 | import { decodeAddress } from "@polkadot/keyring"; 4 | 5 | const remarkMock = { 6 | block: 6707036, 7 | caller: "FqCJeGcPidYSsvvmT17fHVaYdE2nXMYgPsBn3CP9gugvZR5", 8 | extra_ex: undefined, 9 | interaction_type: "MINT", 10 | remark: `RMRK::MINT::1.0.0::{\\"id\\"%3A\\"900D19DC7D3C444E4C-CNR\\"%2C\\"_id\\"%3A\\"\\"%2C\\"symbol\\"%3A\\"CNR\\"%2C\\"issuer\\"%3A\\"FqCJeGcPidYSsvvmT17fHVaYdE2nXMYgPsBn3CP9gugvZR5\\"%2C\\"version\\"%3A\\"1.0.0\\"%2C\\"name\\"%3A\\"CANARY\\"%2C\\"max\\"%3A1%2C\\"metadata\\"%3A\\"ipfs%3A%2F%2Fipfs%2FQmQJGDSd6rxUZuTFDKaCKzVz6nvQpZ7yVLDLnz2dwvvjZs\\"}`, 11 | version: "1.0.0", 12 | }; 13 | 14 | const collextion = new Collection( 15 | 0, 16 | "test", 17 | 0, 18 | "FqCJeGcPidYSsvvmT17fHVaYdE2nXMYgPsBn3CP9gugvZR5", 19 | "CNR", 20 | Collection.generateId(u8aToHex(decodeAddress(remarkMock.caller)), "CNR"), 21 | "http://test" 22 | ); 23 | 24 | describe("validation: validateMintIds", () => { 25 | it("should correctly validate collection id", () => { 26 | expect(() => validateMintIds(collextion, remarkMock)).not.toThrow(); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /test/1.0.0/utils/validate-nft.test.ts: -------------------------------------------------------------------------------- 1 | import { validateNFT } from "../../../src/rmrk1.0.0/tools/validate-remark"; 2 | import { validMintNFTRemarkEvent } from "../mocks/remark-mocks"; 3 | 4 | describe("validation: validateNFT", () => { 5 | it("should be valid NFT 1", () => { 6 | expect(() => validateNFT(validMintNFTRemarkEvent)).not.toThrow(); 7 | }); 8 | 9 | it("should throw - invalid op code", () => { 10 | const remark = 11 | 'RMRK::XXINTNFT::1.0.0::{"collection"%3A"test"%2C"sn"%3A"0000000000000001"%2C"transferable"%3A1%2C"name"%3A"VIP Invitation %231"%2C"metadata"%3A"ipfs%3A%2F%2Fipfs%2FQmQ2Q57PVpaP8QvWvvH9kfn1CdCY49pcv1AaLBjDwS2p4g"%2C"currentOwner"%3A"HPSgWwpjnMe9oyBq4t2dA3dRTU8PwDAU32q6E76xjFDDrEX"%2C"instance"%3A"VIP_INVITATION_1"}'; 12 | 13 | expect(() => validateNFT(remark)).toThrowError( 14 | "The op code needs to be MINTNFT, but it is XXINTNFT" 15 | ); 16 | }); 17 | 18 | it("should throw - invalid collection", () => { 19 | const remark = 20 | 'RMRK::MINTNFT::1.0.0::{"collection"%3A22%2C"sn"%3A"0000000000000001"%2C"transferable"%3A1%2C"name"%3A"VIP Invitation %231"%2C"metadata"%3A"ipfs%3A%2F%2Fipfs%2FQmQ2Q57PVpaP8QvWvvH9kfn1CdCY49pcv1AaLBjDwS2p4g"%2C"currentOwner"%3A"HPSgWwpjnMe9oyBq4t2dA3dRTU8PwDAU32q6E76xjFDDrEX"%2C"instance"%3A"VIP_INVITATION_1"}'; 21 | 22 | expect(() => validateNFT(remark)).toThrowError( 23 | "At path: collection -- Expected a string, but received: 22" 24 | ); 25 | }); 26 | 27 | it("should throw - invalid sn", () => { 28 | const remark = 29 | 'RMRK::MINTNFT::1.0.0::{"collection"%3A"category"%2C"sn"%3A111%2C"transferable"%3A1%2C"name"%3A"VIP Invitation %231"%2C"metadata"%3A"ipfs%3A%2F%2Fipfs%2FQmQ2Q57PVpaP8QvWvvH9kfn1CdCY49pcv1AaLBjDwS2p4g"%2C"currentOwner"%3A"HPSgWwpjnMe9oyBq4t2dA3dRTU8PwDAU32q6E76xjFDDrEX"%2C"instance"%3A"VIP_INVITATION_1"}'; 30 | 31 | expect(() => validateNFT(remark)).toThrowError( 32 | "At path: sn -- Expected a string, but received: 111" 33 | ); 34 | }); 35 | 36 | it("should throw - invalid transferable", () => { 37 | const remark = 38 | 'RMRK::MINTNFT::1.0.0::{"collection"%3A"category"%2C"sn"%3A"0000000000000001"%2C"transferable"%3A"1"%2C"name"%3A"VIP Invitation %231"%2C"metadata"%3A"ipfs%3A%2F%2Fipfs%2FQmQ2Q57PVpaP8QvWvvH9kfn1CdCY49pcv1AaLBjDwS2p4g"%2C"currentOwner"%3A"HPSgWwpjnMe9oyBq4t2dA3dRTU8PwDAU32q6E76xjFDDrEX"%2C"instance"%3A"VIP_INVITATION_1"}'; 39 | 40 | expect(() => validateNFT(remark)).toThrowError( 41 | 'At path: transferable -- Expected a number, but received: "1"' 42 | ); 43 | }); 44 | 45 | it("should throw - invalid sn type", () => { 46 | const remark = 47 | 'RMRK::MINTNFT::1.0.0::{"collection"%3A"category"%2C"foo"%3A"0000000000000001"%2C"transferable"%3A1%2C"name"%3A"VIP Invitation %231"%2C"metadata"%3A"ipfs%3A%2F%2Fipfs%2FQmQ2Q57PVpaP8QvWvvH9kfn1CdCY49pcv1AaLBjDwS2p4g"%2C"currentOwner"%3A"HPSgWwpjnMe9oyBq4t2dA3dRTU8PwDAU32q6E76xjFDDrEX"%2C"instance"%3A"VIP_INVITATION_1"}'; 48 | 49 | expect(() => validateNFT(remark)).toThrowError( 50 | "At path: sn -- Expected a string, but received: undefined" 51 | ); 52 | }); 53 | 54 | it("should throw - invalid metadata string", () => { 55 | const remark = 56 | 'RMRK::MINTNFT::1.0.0::{"collection"%3A"category"%2C"sn"%3A"0000000000000001"%2C"transferable"%3A1%2C"name"%3A"VIP Invitation %231"%2C"metadata"%3A"hpfs%3A%2F%2Fipfs%2FQmQ2Q57PVpaP8QvWvvH9kfn1CdCY49pcv1AaLBjDwS2p4g"%2C"currentOwner"%3A"HPSgWwpjnMe9oyBq4t2dA3dRTU8PwDAU32q6E76xjFDDrEX"%2C"instance"%3A"VIP_INVITATION_1"}'; 57 | 58 | expect(() => validateNFT(remark)).toThrowError( 59 | 'At path: metadata -- Expected a string matching `/^(https?|ipfs):\\/\\/.*$/` but received "hpfs://ipfs/QmQ2Q57PVpaP8QvWvvH9kfn1CdCY49pcv1AaLBjDwS2p4g"' 60 | ); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /test/1.0.0/utils/validate-send.test.ts: -------------------------------------------------------------------------------- 1 | import { validateNFT, validateSend } from "../../../src/rmrk1.0.0/tools/validate-remark"; 2 | 3 | describe("validation: validateSend", () => { 4 | it("should be valid send", () => { 5 | const remark = 6 | "RMRK::SEND::1.0.0::6802213-24d573f4dfa1d7fd33-KAN-KANS-0000000000000001::dfsfsd dfsfd"; 7 | 8 | expect(() => validateSend(remark)).toThrowError( 9 | "Invalid remark - No whitespaces are allowed in recipient" 10 | ); 11 | 12 | expect(() => validateSend(remark.replace(/\s/g, ""))).not.toThrow(); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /test/1.0.0/utils/validate-string-is-a-valid-url.ts: -------------------------------------------------------------------------------- 1 | import { stringIsAValidUrl } from "../../../src/rmrk1.0.0/tools/utils"; 2 | 3 | const testUrl = 'https://rmrk.app/'; 4 | 5 | describe("utils: stringIsAValidUrl", () => { 6 | it("should check if string is a URL and return a boolean", () => { 7 | expect(stringIsAValidUrl(testUrl)).toEqual(true); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /test/2.0.0/__snapshots__/consolidator.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`rmrk2.0.0 Consolidator: CREATE NFT CLASS should correctly create a NFT Collection 1`] = ` 4 | { 5 | "bases": {}, 6 | "collections": { 7 | "d43593c715a56da27d-KANARIABIRDS": Collection { 8 | "block": 2, 9 | "changes": [], 10 | "count": 0, 11 | "id": "d43593c715a56da27d-KANARIABIRDS", 12 | "issuer": "HNZata7iMYWmk5RvZRTiAsSDhV8366zq2YGb3tLH5Upf74F", 13 | "max": 0, 14 | "metadata": "https://some.url", 15 | "symbol": "KANARIABIRDS", 16 | }, 17 | }, 18 | "invalid": [], 19 | "nfts": {}, 20 | } 21 | `; 22 | -------------------------------------------------------------------------------- /test/2.0.0/classes/__snapshots__/base.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`rmrk2.0.0 Base: Add change should match snapshot 1`] = ` 4 | Base { 5 | "block": 0, 6 | "changes": [ 7 | { 8 | "block": 0, 9 | "caller": "", 10 | "field": "", 11 | "new": "", 12 | "old": "", 13 | "opType": "CHANGEISSUER", 14 | }, 15 | ], 16 | "issuer": "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY", 17 | "metadata": undefined, 18 | "parts": [ 19 | { 20 | "equippable": [], 21 | "id": "background", 22 | "type": "slot", 23 | "z": 0, 24 | }, 25 | { 26 | "equippable": [ 27 | "d43593c715a56da27d-KANARIABIRDS", 28 | ], 29 | "id": "backpack", 30 | "type": "slot", 31 | "z": 1, 32 | }, 33 | { 34 | "id": "tail", 35 | "src": "ipfs://ipfs/QmcEuigDVCScMLs2dcrJ8qU4Q265xGisUyKeYdnnGFn6AE/var3_tail.svg", 36 | "type": "fixed", 37 | "z": 2, 38 | }, 39 | ], 40 | "symbol": "KBASE777", 41 | "themes": { 42 | "themeOne": { 43 | "_inherit": true, 44 | "primary": "#fff", 45 | }, 46 | }, 47 | "type": "svg", 48 | } 49 | `; 50 | 51 | exports[`rmrk2.0.0 Base: Base should match snapshot 1`] = `"RMRK::BASE::2.0.0::%7B%22symbol%22%3A%22KBASE777%22%2C%22type%22%3A%22svg%22%2C%22issuer%22%3A%225GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY%22%2C%22parts%22%3A%5B%7B%22id%22%3A%22background%22%2C%22type%22%3A%22slot%22%2C%22equippable%22%3A%5B%5D%2C%22z%22%3A0%7D%2C%7B%22id%22%3A%22backpack%22%2C%22type%22%3A%22slot%22%2C%22equippable%22%3A%5B%22d43593c715a56da27d-KANARIABIRDS%22%5D%2C%22z%22%3A1%7D%2C%7B%22id%22%3A%22tail%22%2C%22type%22%3A%22fixed%22%2C%22src%22%3A%22ipfs%3A%2F%2Fipfs%2FQmcEuigDVCScMLs2dcrJ8qU4Q265xGisUyKeYdnnGFn6AE%2Fvar3_tail.svg%22%2C%22z%22%3A2%7D%5D%2C%22themes%22%3A%7B%22themeOne%22%3A%7B%22_inherit%22%3Atrue%2C%22primary%22%3A%22%23fff%22%7D%7D%7D"`; 52 | 53 | exports[`rmrk2.0.0 Base: Change issuer should match snapshot 1`] = `"RMRK::CHANGEISSUER::2.0.0::base-1-KBASE777::5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty"`; 54 | 55 | exports[`rmrk2.0.0 Base: Equippable should match snapshot 1`] = `"RMRK::EQUIPPABLE::2.0.0::base-1-KBASE777::gemslot2::+d43593c715a56da27d-KANARIAPARTS2"`; 56 | 57 | exports[`rmrk2.0.0 Base: Get changes should match snapshot 1`] = ` 58 | [ 59 | { 60 | "block": 0, 61 | "caller": "", 62 | "field": "", 63 | "new": "", 64 | "old": "", 65 | "opType": "CHANGEISSUER", 66 | }, 67 | ] 68 | `; 69 | 70 | exports[`rmrk2.0.0 Base: Get id should match snapshot 1`] = `"base-1-KBASE777"`; 71 | -------------------------------------------------------------------------------- /test/2.0.0/classes/__snapshots__/nft-class.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`rmrk2.0.0 Collection: Add change should match snapshot 1`] = ` 4 | Collection { 5 | "block": 0, 6 | "changes": [ 7 | { 8 | "block": 0, 9 | "caller": "", 10 | "field": "", 11 | "new": "", 12 | "old": "", 13 | "opType": "CHANGEISSUER", 14 | }, 15 | ], 16 | "count": 0, 17 | "id": "d43593c715a56da27d-KANARIABIRDS", 18 | "issuer": "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY", 19 | "max": 0, 20 | "metadata": "https://some.url", 21 | "symbol": "KANARIABIRDS", 22 | } 23 | `; 24 | 25 | exports[`rmrk2.0.0 Collection: Change issuer should match snapshot 1`] = `"RMRK::CHANGEISSUER::2.0.0::d43593c715a56da27d-KANARIABIRDS::5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty"`; 26 | 27 | exports[`rmrk2.0.0 Collection: Create should match snapshot 1`] = `"RMRK::CREATE::2.0.0::%7B%22max%22%3A0%2C%22issuer%22%3A%225GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY%22%2C%22symbol%22%3A%22KANARIABIRDS%22%2C%22id%22%3A%22d43593c715a56da27d-KANARIABIRDS%22%2C%22metadata%22%3A%22https%3A%2F%2Fsome.url%22%7D"`; 28 | 29 | exports[`rmrk2.0.0 Collection: Get changes should match snapshot 1`] = ` 30 | [ 31 | { 32 | "block": 0, 33 | "caller": "", 34 | "field": "", 35 | "new": "", 36 | "old": "", 37 | "opType": "CHANGEISSUER", 38 | }, 39 | ] 40 | `; 41 | -------------------------------------------------------------------------------- /test/2.0.0/classes/__snapshots__/nft.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`rmrk2.0.0 Nft: Accept should match snapshot 1`] = `"RMRK::ACCEPT::2.0.0::1-d43593c715a56da27d-KANARIABIRDS-KANR-00000777::RES::jXhhR"`; 4 | 5 | exports[`rmrk2.0.0 Nft: Add change should match snapshot 1`] = ` 6 | NFT { 7 | "block": 0, 8 | "burned": "", 9 | "changes": [ 10 | { 11 | "block": 0, 12 | "caller": "", 13 | "field": "", 14 | "new": "", 15 | "old": "", 16 | "opType": "CHANGEISSUER", 17 | }, 18 | ], 19 | "children": [], 20 | "collection": "d43593c715a56da27d-KANARIABIRDS", 21 | "forsale": 0n, 22 | "metadata": undefined, 23 | "owner": "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY", 24 | "pending": false, 25 | "priority": [], 26 | "properties": undefined, 27 | "reactions": {}, 28 | "resources": [], 29 | "rootowner": "", 30 | "sn": "00000777", 31 | "symbol": "KANR", 32 | "transferable": 1, 33 | } 34 | `; 35 | 36 | exports[`rmrk2.0.0 Nft: Buy should match snapshot 1`] = `"RMRK::BUY::2.0.0::1-d43593c715a56da27d-KANARIABIRDS-KANR-00000777::5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty"`; 37 | 38 | exports[`rmrk2.0.0 Nft: Consume should match snapshot 1`] = `"RMRK::BURN::2.0.0::1-d43593c715a56da27d-KANARIABIRDS-KANR-00000777"`; 39 | 40 | exports[`rmrk2.0.0 Nft: Emote should match snapshot 1`] = `"RMRK::EMOTE::2.0.0::RMRK2::1-d43593c715a56da27d-KANARIABIRDS-KANR-00000777::1F600"`; 41 | 42 | exports[`rmrk2.0.0 Nft: Equip should match snapshot 1`] = `"RMRK::EQUIP::2.0.0::1-d43593c715a56da27d-KANARIABIRDS-KANR-00000777::base-test.test"`; 43 | 44 | exports[`rmrk2.0.0 Nft: Get id should match snapshot 1`] = `"1-d43593c715a56da27d-KANARIABIRDS-KANR-00000777"`; 45 | 46 | exports[`rmrk2.0.0 Nft: List should match snapshot 1`] = `"RMRK::LIST::2.0.0::1-d43593c715a56da27d-KANARIABIRDS-KANR-00000777::1000"`; 47 | 48 | exports[`rmrk2.0.0 Nft: Mint should match snapshot 1`] = `"RMRK::MINT::2.0.0::%7B%22collection%22%3A%22d43593c715a56da27d-KANARIABIRDS%22%2C%22symbol%22%3A%22KANR%22%2C%22transferable%22%3A1%2C%22sn%22%3A%2200000777%22%7D::5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty"`; 49 | 50 | exports[`rmrk2.0.0 Nft: Resadd should match snapshot 1`] = `"RMRK::RESADD::2.0.0::1-d43593c715a56da27d-KANARIABIRDS-KANR-00000777::%7B%22base%22%3A%22base-4-KBASE777%22%2C%22id%22%3A%22xXhhR%22%7D"`; 51 | 52 | exports[`rmrk2.0.0 Nft: Send should match snapshot 1`] = `"RMRK::SEND::2.0.0::1-d43593c715a56da27d-KANARIABIRDS-KANR-00000777::5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty"`; 53 | -------------------------------------------------------------------------------- /test/2.0.0/classes/nft-class.test.ts: -------------------------------------------------------------------------------- 1 | import { createCollectionMock, addChangeIssuerMock, getBobKey } from "../mocks"; 2 | import { cryptoWaitReady } from "@polkadot/util-crypto"; 3 | 4 | beforeAll(async () => { 5 | return await cryptoWaitReady(); 6 | }); 7 | 8 | describe("rmrk2.0.0 Collection: Create", () => { 9 | it("should match snapshot", async () => { 10 | const collection = createCollectionMock(); 11 | expect(await collection.create()).toMatchSnapshot(); 12 | }); 13 | }); 14 | 15 | describe("rmrk2.0.0 Collection: Create", () => { 16 | it("should throw error", async () => { 17 | const collection = createCollectionMock(1); 18 | try { 19 | await collection.create(); 20 | } catch (e) { 21 | expect(e.message).toMatch( 22 | "An already existing collection cannot be created!" 23 | ); 24 | } 25 | }); 26 | }); 27 | 28 | describe("rmrk2.0.0 Collection: Change issuer", () => { 29 | it("should match snapshot", async () => { 30 | const collection = createCollectionMock(1); 31 | expect(await collection.change_issuer(getBobKey().address)).toMatchSnapshot(); 32 | }); 33 | }); 34 | 35 | describe("rmrk2.0.0 Collection: Create", () => { 36 | it("should throw error", async () => { 37 | const collection = createCollectionMock(0); 38 | try { 39 | await collection.change_issuer(getBobKey().address); 40 | } catch (e) { 41 | expect(e.message).toMatch( 42 | "This collection is new, so there's no issuer to change. If it has been deployed on chain, load the existing collection as a new instance first, then change issuer." 43 | ); 44 | } 45 | }); 46 | }); 47 | 48 | describe("rmrk2.0.0 Collection: Add change", () => { 49 | it("should match snapshot", async () => { 50 | const collection = createCollectionMock(); 51 | expect(await collection.addChange(addChangeIssuerMock)).toMatchSnapshot(); 52 | }); 53 | }); 54 | 55 | describe("rmrk2.0.0 Collection: Get changes", () => { 56 | it("should match snapshot", async () => { 57 | const collection = createCollectionMock(); 58 | await collection.addChange(addChangeIssuerMock); 59 | expect(await collection.getChanges()).toMatchSnapshot(); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /test/2.0.0/consolidator.test.ts: -------------------------------------------------------------------------------- 1 | import { Consolidator } from "../../src/rmrk2.0.0"; 2 | import { 3 | createCollectionMock, 4 | getBlockCallsMock, 5 | getRemarksFromBlocksMock, 6 | } from "./mocks"; 7 | import { cryptoWaitReady } from "@polkadot/util-crypto"; 8 | 9 | beforeAll(async () => { 10 | return await cryptoWaitReady(); 11 | }); 12 | 13 | describe("rmrk2.0.0 Consolidator: CREATE NFT CLASS", () => { 14 | it("should correctly create a NFT Collection", async () => { 15 | const remarks = getRemarksFromBlocksMock( 16 | getBlockCallsMock(createCollectionMock().create()) 17 | ); 18 | const consolidator = new Consolidator(); 19 | expect(await consolidator.consolidate(remarks)).toMatchSnapshot(); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /test/2.0.0/consolidator/__snapshots__/base.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`rmrk2.0.0 Consolidator: CREATE BASE Should prevent creating a Base with duplicate part ids 1`] = ` 4 | { 5 | "bases": {}, 6 | "collections": {}, 7 | "invalid": [ 8 | { 9 | "block": 2, 10 | "caller": "HNZata7iMYWmk5RvZRTiAsSDhV8366zq2YGb3tLH5Upf74F", 11 | "message": "[BASE] Duplicate base part id found: background", 12 | "object_id": "RMRK::BASE::2.0.0::{"symbol"%3A"KBASE777"%2C"type"%3A"svg"%2C"issuer"%3A"5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY"%2C"parts"%3A[{"id"%3A"background"%2C"type"%3A"slot"%2C"equippable"%3A[]%2C"z"%3A0}%2C{"id"%3A"background"%2C"type"%3A"slot"%2C"equippable"%3A["d43593c715a56da27d-KANARIABIRDS"]%2C"z"%3A1}]%2C"themes"%3A{"themeOne"%3A{"_inherit"%3Atrue%2C"primary"%3A"%23fff"}}}", 13 | "op_type": "BASE", 14 | }, 15 | ], 16 | "nfts": {}, 17 | } 18 | `; 19 | 20 | exports[`rmrk2.0.0 Consolidator: CREATE BASE should correctly create a Base 1`] = ` 21 | { 22 | "bases": { 23 | "base-2-KBASE777": { 24 | "block": 2, 25 | "changes": [], 26 | "id": "base-2-KBASE777", 27 | "issuer": "HNZata7iMYWmk5RvZRTiAsSDhV8366zq2YGb3tLH5Upf74F", 28 | "metadata": undefined, 29 | "parts": [ 30 | { 31 | "equippable": [], 32 | "id": "background", 33 | "type": "slot", 34 | "z": 0, 35 | }, 36 | { 37 | "equippable": [ 38 | "d43593c715a56da27d-KANARIABIRDS", 39 | ], 40 | "id": "backpack", 41 | "type": "slot", 42 | "z": 1, 43 | }, 44 | { 45 | "id": "tail", 46 | "src": "ipfs://ipfs/QmcEuigDVCScMLs2dcrJ8qU4Q265xGisUyKeYdnnGFn6AE/var3_tail.svg", 47 | "type": "fixed", 48 | "z": 2, 49 | }, 50 | ], 51 | "symbol": "KBASE777", 52 | "themes": { 53 | "themeOne": { 54 | "_inherit": true, 55 | "primary": "#fff", 56 | }, 57 | }, 58 | "type": "svg", 59 | }, 60 | }, 61 | "collections": {}, 62 | "invalid": [], 63 | "nfts": {}, 64 | } 65 | `; 66 | -------------------------------------------------------------------------------- /test/2.0.0/consolidator/__snapshots__/destroy.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`rmrk2.0.0 Consolidator: DESTROY Should DESTROY a Collection 1`] = ` 4 | { 5 | "bases": {}, 6 | "collections": {}, 7 | "invalid": [], 8 | "nfts": { 9 | "3-d43593c715a56da27d-KANARIABIRDS-KANR-00000777": { 10 | "block": 3, 11 | "burned": "true", 12 | "changes": [ 13 | { 14 | "block": 4, 15 | "caller": "HNZata7iMYWmk5RvZRTiAsSDhV8366zq2YGb3tLH5Upf74F", 16 | "field": "burned", 17 | "new": "true", 18 | "old": "", 19 | "opType": "BURN", 20 | }, 21 | { 22 | "block": 4, 23 | "caller": "HNZata7iMYWmk5RvZRTiAsSDhV8366zq2YGb3tLH5Upf74F", 24 | "field": "forsale", 25 | "new": 0n, 26 | "old": 0n, 27 | "opType": "BURN", 28 | }, 29 | ], 30 | "children": [], 31 | "collection": "d43593c715a56da27d-KANARIABIRDS", 32 | "equipped": "", 33 | "forsale": 0n, 34 | "id": "3-d43593c715a56da27d-KANARIABIRDS-KANR-00000777", 35 | "metadata": undefined, 36 | "owner": "HNZata7iMYWmk5RvZRTiAsSDhV8366zq2YGb3tLH5Upf74F", 37 | "pending": false, 38 | "priority": [], 39 | "properties": {}, 40 | "reactions": {}, 41 | "resources": [], 42 | "rootowner": "HNZata7iMYWmk5RvZRTiAsSDhV8366zq2YGb3tLH5Upf74F", 43 | "sn": "00000777", 44 | "symbol": "KANR", 45 | "transferable": 1, 46 | }, 47 | }, 48 | } 49 | `; 50 | 51 | exports[`rmrk2.0.0 Consolidator: DESTROY Should not DESTROY a Collection if it has unburned nft 1`] = ` 52 | { 53 | "bases": {}, 54 | "collections": { 55 | "d43593c715a56da27d-KANARIABIRDS": Collection { 56 | "block": 2, 57 | "changes": [], 58 | "count": 1, 59 | "id": "d43593c715a56da27d-KANARIABIRDS", 60 | "issuer": "HNZata7iMYWmk5RvZRTiAsSDhV8366zq2YGb3tLH5Upf74F", 61 | "max": 0, 62 | "metadata": "https://some.url", 63 | "symbol": "KANARIABIRDS", 64 | }, 65 | }, 66 | "invalid": [ 67 | { 68 | "block": 4, 69 | "caller": "HNZata7iMYWmk5RvZRTiAsSDhV8366zq2YGb3tLH5Upf74F", 70 | "message": "[DESTROY] Collection with unburned nfts cannot be destroyed d43593c715a56da27d-KANARIABIRDS", 71 | "object_id": "d43593c715a56da27d-KANARIABIRDS", 72 | "op_type": "DESTROY", 73 | }, 74 | ], 75 | "nfts": { 76 | "3-d43593c715a56da27d-KANARIABIRDS-KANR-00000777": { 77 | "block": 3, 78 | "burned": "", 79 | "changes": [], 80 | "children": [], 81 | "collection": "d43593c715a56da27d-KANARIABIRDS", 82 | "forsale": 0n, 83 | "id": "3-d43593c715a56da27d-KANARIABIRDS-KANR-00000777", 84 | "metadata": undefined, 85 | "owner": "HNZata7iMYWmk5RvZRTiAsSDhV8366zq2YGb3tLH5Upf74F", 86 | "pending": false, 87 | "priority": [], 88 | "properties": {}, 89 | "reactions": {}, 90 | "resources": [], 91 | "rootowner": "HNZata7iMYWmk5RvZRTiAsSDhV8366zq2YGb3tLH5Upf74F", 92 | "sn": "00000777", 93 | "symbol": "KANR", 94 | "transferable": 1, 95 | }, 96 | }, 97 | } 98 | `; 99 | -------------------------------------------------------------------------------- /test/2.0.0/consolidator/__snapshots__/lock.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`rmrk2.0.0 Consolidator: LOCK Should LOCK a Collection 1`] = ` 4 | { 5 | "bases": {}, 6 | "collections": { 7 | "d43593c715a56da27d-KANARIABIRDS": { 8 | "block": 2, 9 | "changes": [], 10 | "count": 1, 11 | "id": "d43593c715a56da27d-KANARIABIRDS", 12 | "issuer": "HNZata7iMYWmk5RvZRTiAsSDhV8366zq2YGb3tLH5Upf74F", 13 | "max": 1, 14 | "metadata": "https://some.url", 15 | "symbol": "KANARIABIRDS", 16 | }, 17 | }, 18 | "invalid": [], 19 | "nfts": { 20 | "3-d43593c715a56da27d-KANARIABIRDS-KANR-00000777": { 21 | "block": 3, 22 | "burned": "true", 23 | "changes": [ 24 | { 25 | "block": 5, 26 | "caller": "HNZata7iMYWmk5RvZRTiAsSDhV8366zq2YGb3tLH5Upf74F", 27 | "field": "burned", 28 | "new": "true", 29 | "old": "", 30 | "opType": "BURN", 31 | }, 32 | { 33 | "block": 5, 34 | "caller": "HNZata7iMYWmk5RvZRTiAsSDhV8366zq2YGb3tLH5Upf74F", 35 | "field": "forsale", 36 | "new": 0n, 37 | "old": 0n, 38 | "opType": "BURN", 39 | }, 40 | ], 41 | "children": [], 42 | "collection": "d43593c715a56da27d-KANARIABIRDS", 43 | "equipped": "", 44 | "forsale": 0n, 45 | "id": "3-d43593c715a56da27d-KANARIABIRDS-KANR-00000777", 46 | "metadata": undefined, 47 | "owner": "HNZata7iMYWmk5RvZRTiAsSDhV8366zq2YGb3tLH5Upf74F", 48 | "pending": false, 49 | "priority": [], 50 | "properties": {}, 51 | "reactions": {}, 52 | "resources": [], 53 | "rootowner": "HNZata7iMYWmk5RvZRTiAsSDhV8366zq2YGb3tLH5Upf74F", 54 | "sn": "00000777", 55 | "symbol": "KANR", 56 | "transferable": 1, 57 | }, 58 | "4-d43593c715a56da27d-KANARIABIRDS-KANR-00000888": { 59 | "block": 4, 60 | "burned": "", 61 | "changes": [], 62 | "children": [], 63 | "collection": "d43593c715a56da27d-KANARIABIRDS", 64 | "forsale": 0n, 65 | "id": "4-d43593c715a56da27d-KANARIABIRDS-KANR-00000888", 66 | "metadata": undefined, 67 | "owner": "HNZata7iMYWmk5RvZRTiAsSDhV8366zq2YGb3tLH5Upf74F", 68 | "pending": false, 69 | "priority": [], 70 | "properties": {}, 71 | "reactions": {}, 72 | "resources": [], 73 | "rootowner": "HNZata7iMYWmk5RvZRTiAsSDhV8366zq2YGb3tLH5Upf74F", 74 | "sn": "00000888", 75 | "symbol": "KANR", 76 | "transferable": 1, 77 | }, 78 | }, 79 | } 80 | `; 81 | -------------------------------------------------------------------------------- /test/2.0.0/consolidator/__snapshots__/themeadd.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`rmrk2.0.0 Consolidator: THEMEADD Add theme to a base 1`] = ` 4 | { 5 | "bases": { 6 | "base-4-KBASE777": { 7 | "block": 4, 8 | "changes": [], 9 | "id": "base-4-KBASE777", 10 | "issuer": "HNZata7iMYWmk5RvZRTiAsSDhV8366zq2YGb3tLH5Upf74F", 11 | "metadata": undefined, 12 | "parts": [ 13 | { 14 | "equippable": [], 15 | "id": "background", 16 | "type": "slot", 17 | "z": 0, 18 | }, 19 | { 20 | "equippable": [ 21 | "d43593c715a56da27d-KANARIABIRDS", 22 | ], 23 | "id": "backpack", 24 | "type": "slot", 25 | "z": 1, 26 | }, 27 | { 28 | "id": "tail", 29 | "src": "ipfs://ipfs/QmcEuigDVCScMLs2dcrJ8qU4Q265xGisUyKeYdnnGFn6AE/var3_tail.svg", 30 | "type": "fixed", 31 | "z": 2, 32 | }, 33 | ], 34 | "symbol": "KBASE777", 35 | "themes": { 36 | "theme2": { 37 | "primaryColor": "#000", 38 | }, 39 | "themeOne": { 40 | "_inherit": true, 41 | "primary": "#fff", 42 | }, 43 | }, 44 | "type": "svg", 45 | }, 46 | }, 47 | "collections": { 48 | "d43593c715a56da27d-KANARIABIRDS": Collection { 49 | "block": 2, 50 | "changes": [], 51 | "count": 1, 52 | "id": "d43593c715a56da27d-KANARIABIRDS", 53 | "issuer": "HNZata7iMYWmk5RvZRTiAsSDhV8366zq2YGb3tLH5Upf74F", 54 | "max": 0, 55 | "metadata": "https://some.url", 56 | "symbol": "KANARIABIRDS", 57 | }, 58 | }, 59 | "invalid": [], 60 | "nfts": { 61 | "3-d43593c715a56da27d-KANARIABIRDS-KANR-00000777": { 62 | "block": 3, 63 | "burned": "", 64 | "changes": [], 65 | "children": [], 66 | "collection": "d43593c715a56da27d-KANARIABIRDS", 67 | "forsale": 0n, 68 | "id": "3-d43593c715a56da27d-KANARIABIRDS-KANR-00000777", 69 | "metadata": undefined, 70 | "owner": "HNZata7iMYWmk5RvZRTiAsSDhV8366zq2YGb3tLH5Upf74F", 71 | "pending": false, 72 | "priority": [], 73 | "properties": {}, 74 | "reactions": {}, 75 | "resources": [], 76 | "rootowner": "HNZata7iMYWmk5RvZRTiAsSDhV8366zq2YGb3tLH5Upf74F", 77 | "sn": "00000777", 78 | "symbol": "KANR", 79 | "transferable": 1, 80 | }, 81 | }, 82 | } 83 | `; 84 | -------------------------------------------------------------------------------- /test/2.0.0/consolidator/accept.test.ts: -------------------------------------------------------------------------------- 1 | import { Consolidator } from "../../../src/rmrk2.0.0"; 2 | import { 3 | createCollectionMock, 4 | getBlockCallsMock, 5 | getBobKey, 6 | getRemarksFromBlocksMock, 7 | mintNftMock, 8 | mintNftMock2, 9 | } from "../mocks"; 10 | import { cryptoWaitReady } from "@polkadot/util-crypto"; 11 | 12 | beforeAll(async () => { 13 | return await cryptoWaitReady(); 14 | }); 15 | 16 | describe("rmrk2.0.0 Consolidator: ACCEPT", () => { 17 | const resid = "jXhhR"; 18 | const getSetupRemarks = () => [ 19 | ...getBlockCallsMock(createCollectionMock().create()), 20 | ...getBlockCallsMock(mintNftMock().mint()), 21 | ...getBlockCallsMock(mintNftMock(3).send(getBobKey().address)), 22 | ...getBlockCallsMock( 23 | mintNftMock(3).resadd({ metadata: "ipfs://ipfs/123", id: resid }) 24 | ), 25 | ]; 26 | 27 | it("Accept a resource on a NFT", async () => { 28 | const remarks = getRemarksFromBlocksMock([ 29 | ...getSetupRemarks(), 30 | ...getBlockCallsMock( 31 | mintNftMock(3).accept(resid, "RES"), 32 | getBobKey().address 33 | ), 34 | ]); 35 | const consolidator = new Consolidator(); 36 | const consolidatedResult = await consolidator.consolidate(remarks); 37 | expect( 38 | consolidatedResult.nfts[mintNftMock(3).getId()].resources[0].pending 39 | ).toBeFalsy(); 40 | }); 41 | 42 | it("Replace a resource on Accept", async () => { 43 | const remarks = getRemarksFromBlocksMock([ 44 | ...getSetupRemarks(), 45 | ...getBlockCallsMock( 46 | mintNftMock(3).accept(resid, "RES"), 47 | getBobKey().address 48 | ), 49 | ...getBlockCallsMock( 50 | mintNftMock(3).resadd( 51 | { 52 | metadata: "ipfs://ipfs/125", 53 | id: "foo", 54 | }, 55 | resid 56 | ) 57 | ), 58 | ...getBlockCallsMock( 59 | mintNftMock(3).accept("foo", "RES"), 60 | getBobKey().address 61 | ), 62 | ]); 63 | const consolidator = new Consolidator(); 64 | const consolidatedResult = await consolidator.consolidate(remarks); 65 | expect( 66 | consolidatedResult.nfts[mintNftMock(3).getId()].resources[0].pending 67 | ).toBeFalsy(); 68 | expect( 69 | consolidatedResult.nfts[mintNftMock(3).getId()].resources[0].id 70 | ).toEqual(resid); 71 | }); 72 | 73 | it("Accept a child NFT on a NFT", async () => { 74 | const remarks = getRemarksFromBlocksMock([ 75 | ...getBlockCallsMock(createCollectionMock().create()), 76 | ...getBlockCallsMock(mintNftMock().mint()), 77 | ...getBlockCallsMock(mintNftMock2().mint(getBobKey().address)), 78 | ...getBlockCallsMock( 79 | mintNftMock2(4).send(mintNftMock(3).getId()), 80 | getBobKey().address 81 | ), 82 | ...getBlockCallsMock( 83 | mintNftMock(3).accept(mintNftMock2(4).getId(), "NFT") 84 | ), 85 | ]); 86 | const consolidator = new Consolidator(); 87 | const consolidatedResult = await consolidator.consolidate(remarks); 88 | expect(consolidatedResult).toMatchSnapshot(); 89 | }); 90 | 91 | it("Should invalidate accept if NFT doesn't exist", async () => { 92 | const remarks = getRemarksFromBlocksMock([ 93 | ...getSetupRemarks(), 94 | ...getBlockCallsMock( 95 | mintNftMock(4).accept(resid, "RES"), 96 | getBobKey().address 97 | ), 98 | ]); 99 | const consolidator = new Consolidator(); 100 | expect(await consolidator.consolidate(remarks)).toMatchSnapshot(); 101 | }); 102 | 103 | it("Should invalidate accept if NFT is burned", async () => { 104 | const remarks = getRemarksFromBlocksMock([ 105 | ...getSetupRemarks(), 106 | ...getBlockCallsMock(mintNftMock(3).burn(), getBobKey().address), 107 | ...getBlockCallsMock( 108 | mintNftMock(3).accept(resid, "RES"), 109 | getBobKey().address 110 | ), 111 | ]); 112 | const consolidator = new Consolidator(); 113 | expect(await consolidator.consolidate(remarks)).toMatchSnapshot(); 114 | }); 115 | }); 116 | -------------------------------------------------------------------------------- /test/2.0.0/consolidator/base.test.ts: -------------------------------------------------------------------------------- 1 | import { Consolidator } from "../../../src/rmrk2.0.0"; 2 | import { 3 | createBaseMock, 4 | createBaseMock2, 5 | getBlockCallsMock, 6 | getRemarksFromBlocksMock, 7 | } from "../mocks"; 8 | import { cryptoWaitReady } from "@polkadot/util-crypto"; 9 | 10 | beforeAll(async () => { 11 | return await cryptoWaitReady(); 12 | }); 13 | 14 | describe("rmrk2.0.0 Consolidator: CREATE BASE", () => { 15 | it("should correctly create a Base", async () => { 16 | const remarks = getRemarksFromBlocksMock( 17 | getBlockCallsMock(createBaseMock().base()) 18 | ); 19 | const consolidator = new Consolidator(); 20 | expect(await consolidator.consolidate(remarks)).toMatchSnapshot(); 21 | }); 22 | 23 | it("Should prevent creating a Base with duplicate part ids", async () => { 24 | const remarks = getRemarksFromBlocksMock( 25 | getBlockCallsMock(createBaseMock2().base()) 26 | ); 27 | const consolidator = new Consolidator(); 28 | expect(await consolidator.consolidate(remarks)).toMatchSnapshot(); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /test/2.0.0/consolidator/burn.test.ts: -------------------------------------------------------------------------------- 1 | import { Consolidator } from "../../../src/rmrk2.0.0"; 2 | import { 3 | createCollectionMock, 4 | getAliceKey, 5 | getBlockCallsMock, 6 | getBobKey, 7 | getRemarksFromBlocksMock, 8 | mintNftMock, 9 | mintNftMock2, 10 | mintNftMock3, 11 | } from "../mocks"; 12 | import { cryptoWaitReady } from "@polkadot/util-crypto"; 13 | 14 | beforeAll(async () => { 15 | return await cryptoWaitReady(); 16 | }); 17 | 18 | describe("rmrk2.0.0 Consolidator: BURN", () => { 19 | const getSetupRemarks = () => [ 20 | ...getBlockCallsMock(createCollectionMock().create()), 21 | ...getBlockCallsMock(mintNftMock().mint()), 22 | ]; 23 | 24 | it("Should BURN an nft", async () => { 25 | const remarks = getRemarksFromBlocksMock([ 26 | ...getSetupRemarks(), 27 | ...getBlockCallsMock(mintNftMock(3).burn(), getAliceKey().address, [ 28 | { 29 | call: "system.remark", 30 | value: "0x484f554f55", 31 | caller: getAliceKey().address, 32 | }, 33 | ]), 34 | ]); 35 | const consolidator = new Consolidator(); 36 | expect(await consolidator.consolidate(remarks)).toMatchSnapshot(); 37 | }); 38 | 39 | it("Should BURN an nft and it's children", async () => { 40 | const remarks = getRemarksFromBlocksMock([ 41 | ...getSetupRemarks(), 42 | ...getBlockCallsMock(mintNftMock2().mint(mintNftMock(3).getId())), 43 | ...getBlockCallsMock(mintNftMock(3).burn(), getAliceKey().address, [ 44 | { 45 | call: "system.remark", 46 | value: "0x484f554f55", 47 | caller: getAliceKey().address, 48 | }, 49 | ]), 50 | ]); 51 | const consolidator = new Consolidator(); 52 | expect(await consolidator.consolidate(remarks)).toMatchSnapshot(); 53 | }); 54 | 55 | it("Should unequip BURNed nft from parent's children array", async () => { 56 | const remarks = getRemarksFromBlocksMock([ 57 | ...getSetupRemarks(), 58 | ...getBlockCallsMock(mintNftMock2().mint()), 59 | ...getBlockCallsMock(mintNftMock(3).send(mintNftMock2(4).getId())), 60 | ...getBlockCallsMock(mintNftMock(3).burn(), getAliceKey().address, [ 61 | { 62 | call: "system.remark", 63 | value: "0x484f554f55", 64 | caller: getAliceKey().address, 65 | }, 66 | ]), 67 | ]); 68 | const consolidator = new Consolidator(); 69 | expect(await consolidator.consolidate(remarks)).toMatchSnapshot(); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /test/2.0.0/consolidator/changeissuer.test.ts: -------------------------------------------------------------------------------- 1 | import { Consolidator } from "../../../src/rmrk2.0.0"; 2 | import { 3 | createBaseMock, 4 | createCollectionMock, 5 | getAliceKey, 6 | getBlockCallsMock, 7 | getBobKey, 8 | getRemarksFromBlocksMock, 9 | mintNftMock, 10 | mintNftMock2, 11 | } from "../mocks"; 12 | import { cryptoWaitReady } from "@polkadot/util-crypto"; 13 | 14 | beforeAll(async () => { 15 | return await cryptoWaitReady(); 16 | }); 17 | 18 | describe("rmrk2.0.0 Consolidator: CHANGEISSUER", () => { 19 | const getSetupRemarks = () => [ 20 | ...getBlockCallsMock(createCollectionMock().create()), 21 | ...getBlockCallsMock(mintNftMock().mint()), 22 | ]; 23 | 24 | it("Should allow to CHANGEISSUER of collection", async () => { 25 | const remarks = getRemarksFromBlocksMock([ 26 | ...getSetupRemarks(), 27 | ...getBlockCallsMock( 28 | createCollectionMock(2).change_issuer(getBobKey().address) 29 | ), 30 | ]); 31 | const consolidator = new Consolidator(); 32 | expect(await consolidator.consolidate(remarks)).toMatchSnapshot(); 33 | }); 34 | 35 | it("Should allow to CHANGEISSUER of base", async () => { 36 | const remarks = getRemarksFromBlocksMock([ 37 | ...getBlockCallsMock(createBaseMock().base()), 38 | ...getBlockCallsMock( 39 | createBaseMock(2).change_issuer(getBobKey().address) 40 | ), 41 | ]); 42 | const consolidator = new Consolidator(); 43 | expect(await consolidator.consolidate(remarks)).toMatchSnapshot(); 44 | }); 45 | 46 | it("Should prevent from CHANGEISSUER of non-existent base or collection", async () => { 47 | const consolidator = new Consolidator(); 48 | expect( 49 | await consolidator.consolidate( 50 | getRemarksFromBlocksMock([ 51 | ...getBlockCallsMock( 52 | createBaseMock(3).change_issuer(getBobKey().address) 53 | ), 54 | ]) 55 | ) 56 | ).toMatchSnapshot(); 57 | 58 | expect( 59 | await consolidator.consolidate( 60 | getRemarksFromBlocksMock([ 61 | ...getBlockCallsMock( 62 | createCollectionMock(3).change_issuer(getBobKey().address) 63 | ), 64 | ]) 65 | ) 66 | ).toMatchSnapshot(); 67 | }); 68 | 69 | it("Should prevent to CHANGEISSUER of non-owned collection", async () => { 70 | const remarks = getRemarksFromBlocksMock([ 71 | ...getBlockCallsMock(createCollectionMock().create()), 72 | ...getBlockCallsMock( 73 | createCollectionMock(2).change_issuer(getBobKey().address), 74 | getBobKey().address 75 | ), 76 | ]); 77 | const consolidator = new Consolidator(); 78 | expect(await consolidator.consolidate(remarks)).toMatchSnapshot(); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /test/2.0.0/consolidator/destroy.test.ts: -------------------------------------------------------------------------------- 1 | import { Consolidator } from "../../../src/rmrk2.0.0"; 2 | import { 3 | createCollectionMock, 4 | getAliceKey, 5 | getBlockCallsMock, 6 | getRemarksFromBlocksMock, 7 | mintNftMock, 8 | } from "../mocks"; 9 | import { cryptoWaitReady } from "@polkadot/util-crypto"; 10 | 11 | beforeAll(async () => { 12 | return await cryptoWaitReady(); 13 | }); 14 | 15 | describe("rmrk2.0.0 Consolidator: DESTROY", () => { 16 | const getSetupRemarks = () => [ 17 | ...getBlockCallsMock(createCollectionMock().create()), 18 | ...getBlockCallsMock(mintNftMock().mint()), 19 | ]; 20 | 21 | it("Should DESTROY a Collection", async () => { 22 | const remarks = getRemarksFromBlocksMock([ 23 | ...getSetupRemarks(), 24 | ...getBlockCallsMock(mintNftMock(3).burn(), getAliceKey().address), 25 | ...getBlockCallsMock( 26 | createCollectionMock(2).destroy(), 27 | getAliceKey().address 28 | ), 29 | ]); 30 | const consolidator = new Consolidator(); 31 | expect(await consolidator.consolidate(remarks)).toMatchSnapshot(); 32 | }); 33 | 34 | it("Should not DESTROY a Collection if it has unburned nft", async () => { 35 | const remarks = getRemarksFromBlocksMock([ 36 | ...getSetupRemarks(), 37 | ...getBlockCallsMock( 38 | createCollectionMock(2).destroy(), 39 | getAliceKey().address 40 | ), 41 | ]); 42 | const consolidator = new Consolidator(); 43 | expect(await consolidator.consolidate(remarks)).toMatchSnapshot(); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /test/2.0.0/consolidator/equippable.test.ts: -------------------------------------------------------------------------------- 1 | import { Consolidator } from "../../../src/rmrk2.0.0"; 2 | import { 3 | createCollectionMock, 4 | getBlockCallsMock, 5 | getBobKey, 6 | getRemarksFromBlocksMock, 7 | createBaseMock, 8 | } from "../mocks"; 9 | import { cryptoWaitReady } from "@polkadot/util-crypto"; 10 | 11 | beforeAll(async () => { 12 | return await cryptoWaitReady(); 13 | }); 14 | 15 | describe("rmrk2.0.0 Consolidator: EQUIPPABLE", () => { 16 | const getSetupRemarks = () => [ 17 | ...getBlockCallsMock(createCollectionMock().create()), 18 | ...getBlockCallsMock(createBaseMock().base()), 19 | ]; 20 | 21 | it("Add new Collection id to Base slot equippable", async () => { 22 | const remarks = getRemarksFromBlocksMock([ 23 | ...getSetupRemarks(), 24 | ...getBlockCallsMock( 25 | createBaseMock(3).equippable({ 26 | slot: "background", 27 | collections: [createCollectionMock().id], 28 | operator: "+", 29 | }) 30 | ), 31 | ]); 32 | const consolidator = new Consolidator(); 33 | expect(await consolidator.consolidate(remarks)).toMatchSnapshot(); 34 | }); 35 | 36 | it("Remove new Collection id to Base slot equippable", async () => { 37 | const remarks = getRemarksFromBlocksMock([ 38 | ...getSetupRemarks(), 39 | ...getBlockCallsMock( 40 | createBaseMock(3).equippable({ 41 | slot: "backpack", 42 | collections: [createCollectionMock().id], 43 | operator: "-", 44 | }) 45 | ), 46 | ]); 47 | const consolidator = new Consolidator(); 48 | expect(await consolidator.consolidate(remarks)).toMatchSnapshot(); 49 | }); 50 | 51 | it("Replace a Collection id to Base slot equippable", async () => { 52 | const remarks = getRemarksFromBlocksMock([ 53 | ...getSetupRemarks(), 54 | ...getBlockCallsMock( 55 | createBaseMock(3).equippable({ 56 | slot: "backpack", 57 | collections: ["*"], 58 | operator: "", 59 | }) 60 | ), 61 | ]); 62 | const consolidator = new Consolidator(); 63 | expect(await consolidator.consolidate(remarks)).toMatchSnapshot(); 64 | }); 65 | 66 | it("Should invalidate EQUIPPABLE if base parts slot is missing", async () => { 67 | const remarks = getRemarksFromBlocksMock([ 68 | ...getSetupRemarks(), 69 | ...getBlockCallsMock( 70 | createBaseMock(3).equippable({ 71 | slot: "test", 72 | collections: [createCollectionMock().id], 73 | operator: "+", 74 | }) 75 | ), 76 | ]); 77 | const consolidator = new Consolidator(); 78 | const consolidatedResult = await consolidator.consolidate(remarks); 79 | expect(consolidatedResult.invalid[0].message).toEqual( 80 | "[EQUIPPABLE] Attempting to change equippable on non-existant part with a slot id test" 81 | ); 82 | }); 83 | 84 | it("Should invalidate EQUIPPABLE if base is missing", async () => { 85 | const remarks = getRemarksFromBlocksMock([ 86 | ...getSetupRemarks(), 87 | ...getBlockCallsMock( 88 | createBaseMock(5).equippable({ 89 | slot: "test", 90 | collections: [createCollectionMock().id], 91 | operator: "+", 92 | }) 93 | ), 94 | ]); 95 | const consolidator = new Consolidator(); 96 | const consolidatedResult = await consolidator.consolidate(remarks); 97 | expect(consolidatedResult.invalid[0].message).toEqual( 98 | "[EQUIPPABLE] Attempting to change equippable on non-existant NFT base-5-KBASE777" 99 | ); 100 | }); 101 | }); 102 | -------------------------------------------------------------------------------- /test/2.0.0/consolidator/lock.test.ts: -------------------------------------------------------------------------------- 1 | import { Consolidator } from "../../../src/rmrk2.0.0"; 2 | import { 3 | createCollectionMock, 4 | getAliceKey, 5 | getBlockCallsMock, 6 | getRemarksFromBlocksMock, 7 | mintNftMock, 8 | mintNftMock2, 9 | } from "../mocks"; 10 | import { cryptoWaitReady } from "@polkadot/util-crypto"; 11 | 12 | beforeAll(async () => { 13 | return await cryptoWaitReady(); 14 | }); 15 | 16 | describe("rmrk2.0.0 Consolidator: LOCK", () => { 17 | const getSetupRemarks = () => [ 18 | ...getBlockCallsMock(createCollectionMock().create()), 19 | ...getBlockCallsMock(mintNftMock().mint()), 20 | ...getBlockCallsMock(mintNftMock2().mint()), 21 | ]; 22 | 23 | it("Should LOCK a Collection", async () => { 24 | const remarks = getRemarksFromBlocksMock([ 25 | ...getSetupRemarks(), 26 | ...getBlockCallsMock(mintNftMock(3).burn(), getAliceKey().address), 27 | ...getBlockCallsMock( 28 | createCollectionMock(2).lock(), 29 | getAliceKey().address 30 | ), 31 | ]); 32 | const consolidator = new Consolidator(); 33 | expect(await consolidator.consolidate(remarks)).toMatchSnapshot(); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /test/2.0.0/consolidator/mint.test.ts: -------------------------------------------------------------------------------- 1 | import { Consolidator, NFT } from "../../../src/rmrk2.0.0"; 2 | import { 3 | createCollectionMock, 4 | getBlockCallsMock, 5 | getAliceKey, 6 | getBobKey, 7 | getRemarksFromBlocksMock, 8 | mintNftMock, 9 | mintNftMock2, 10 | } from "../mocks"; 11 | import { cryptoWaitReady } from "@polkadot/util-crypto"; 12 | 13 | beforeAll(async () => { 14 | return await cryptoWaitReady(); 15 | }); 16 | 17 | describe("rmrk2.0.0 Consolidator: MINT", () => { 18 | const getSetupRemarks = () => [ 19 | ...getBlockCallsMock(createCollectionMock().create()), 20 | ...getBlockCallsMock(mintNftMock().mint()), 21 | ...getBlockCallsMock(mintNftMock2().mint()), 22 | ]; 23 | 24 | it("Should mint a NFT and make another account owner", async () => { 25 | const remarks = getRemarksFromBlocksMock([ 26 | ...getBlockCallsMock(createCollectionMock().create()), 27 | ...getBlockCallsMock(mintNftMock().mint(getBobKey().address)), 28 | ]); 29 | const consolidator = new Consolidator(); 30 | expect(await consolidator.consolidate(remarks)).toMatchSnapshot(); 31 | }); 32 | 33 | it("Should mint a NFT and make another NFT an owner", async () => { 34 | const remarks = getRemarksFromBlocksMock([ 35 | ...getBlockCallsMock(createCollectionMock().create()), 36 | ...getBlockCallsMock(mintNftMock2().mint()), 37 | ...getBlockCallsMock(mintNftMock().mint(mintNftMock2(4).getId())), 38 | ]); 39 | const consolidator = new Consolidator(); 40 | expect(await consolidator.consolidate(remarks)).toMatchSnapshot(); 41 | }); 42 | 43 | it("Should not allow to mint a NFT without a calss", async () => { 44 | const remarks = getRemarksFromBlocksMock([ 45 | ...getBlockCallsMock(mintNftMock().mint()), 46 | ]); 47 | const consolidator = new Consolidator(); 48 | expect(await consolidator.consolidate(remarks)).toMatchSnapshot(); 49 | }); 50 | 51 | it("Should allow to mint NFT with royalties", async () => { 52 | const nft = new NFT({ 53 | block: 0, 54 | collection: createCollectionMock().id, 55 | symbol: "KANR", 56 | sn: "999".padStart(8, "0"), 57 | transferable: 1, 58 | owner: getBobKey().address, 59 | properties: { 60 | royaltyInfo: { 61 | type: "royalty", 62 | value: { 63 | royaltyPercentFloat: 2.3, 64 | receiver: getBobKey().address, 65 | }, 66 | }, 67 | }, 68 | }); 69 | 70 | const remarks = getRemarksFromBlocksMock([ 71 | ...getBlockCallsMock(createCollectionMock().create()), 72 | ...getBlockCallsMock(nft.mint()), 73 | ]); 74 | const consolidator = new Consolidator(); 75 | expect(await consolidator.consolidate(remarks)).toMatchSnapshot(); 76 | }); 77 | 78 | it("Should not allow to mint NFT with invalid royalties", async () => { 79 | const nft = new NFT({ 80 | block: 0, 81 | collection: createCollectionMock().id, 82 | symbol: "KANR", 83 | sn: "999".padStart(8, "0"), 84 | transferable: 1, 85 | owner: getBobKey().address, 86 | properties: { 87 | royaltyInfo: { 88 | type: "royalty", 89 | value: { 90 | royaltyPercentFloat: 101, 91 | receiver: getBobKey().address, 92 | }, 93 | }, 94 | }, 95 | }); 96 | 97 | const remarks = getRemarksFromBlocksMock([ 98 | ...getBlockCallsMock(createCollectionMock().create()), 99 | ...getBlockCallsMock(nft.mint()), 100 | ]); 101 | const consolidator = new Consolidator(); 102 | expect(await consolidator.consolidate(remarks)).toMatchSnapshot(); 103 | }); 104 | 105 | it("Should prevent minting NFT into locked collection", async () => { 106 | const remarks = getRemarksFromBlocksMock([ 107 | ...getSetupRemarks(), 108 | ...getBlockCallsMock( 109 | createCollectionMock(2).lock(), 110 | getAliceKey().address 111 | ), 112 | ...getBlockCallsMock(mintNftMock2().mint()), 113 | ]); 114 | const consolidator = new Consolidator(); 115 | const consolidated = await consolidator.consolidate(remarks); 116 | expect(consolidated.invalid[0].message).toEqual( 117 | "Attempted to mint into maxed out collection d43593c715a56da27d-KANARIABIRDS" 118 | ); 119 | }); 120 | }); 121 | -------------------------------------------------------------------------------- /test/2.0.0/consolidator/setpriority.test.ts: -------------------------------------------------------------------------------- 1 | import { Consolidator } from "../../../src/rmrk2.0.0"; 2 | import { 3 | createCollectionMock, 4 | getBlockCallsMock, 5 | getRemarksFromBlocksMock, 6 | mintNftMock, 7 | } from "../mocks"; 8 | import { cryptoWaitReady } from "@polkadot/util-crypto"; 9 | 10 | beforeAll(async () => { 11 | return await cryptoWaitReady(); 12 | }); 13 | 14 | describe("rmrk2.0.0 Consolidator: SETPRIORITY", () => { 15 | const getSetupRemarks = () => [ 16 | ...getBlockCallsMock(createCollectionMock().create()), 17 | ...getBlockCallsMock(mintNftMock().mint()), 18 | ...getBlockCallsMock( 19 | mintNftMock(3).resadd({ metadata: "ipfs://ipfs/123", id: 'foo' }) 20 | ), 21 | ]; 22 | 23 | it("Should add newly added resource id to priority array", async () => { 24 | const remarks = getRemarksFromBlocksMock([...getSetupRemarks()]); 25 | const consolidator = new Consolidator(); 26 | const consolidatedResult = await consolidator.consolidate(remarks); 27 | expect( 28 | consolidatedResult.nfts[mintNftMock(3).getId()].resources[0].id 29 | ).toEqual(consolidatedResult.nfts[mintNftMock(3).getId()].priority[0]); 30 | }); 31 | 32 | it("Should not allow to set priority of a resource that doesn't exist", async () => { 33 | const remarks = getRemarksFromBlocksMock([ 34 | ...getSetupRemarks(), 35 | ...getBlockCallsMock(mintNftMock(3).setpriority(["bar", "foo"])), 36 | ]); 37 | 38 | const consolidator = new Consolidator(); 39 | const consolidatedResult = await consolidator.consolidate(remarks); 40 | 41 | expect( 42 | consolidatedResult.nfts[mintNftMock(3).getId()].priority.includes("bar") 43 | ).toBeFalsy(); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /test/2.0.0/consolidator/themeadd.test.ts: -------------------------------------------------------------------------------- 1 | import { Consolidator } from "../../../src/rmrk2.0.0"; 2 | import { 3 | createBaseMock, 4 | createCollectionMock, 5 | getBlockCallsMock, 6 | getBobKey, 7 | getRemarksFromBlocksMock, 8 | mintNftMock, 9 | } from "../mocks"; 10 | import { cryptoWaitReady } from "@polkadot/util-crypto"; 11 | 12 | beforeAll(async () => { 13 | return await cryptoWaitReady(); 14 | }); 15 | 16 | describe("rmrk2.0.0 Consolidator: THEMEADD", () => { 17 | const getSetupRemarks = () => [ 18 | ...getBlockCallsMock(createCollectionMock().create()), 19 | ...getBlockCallsMock(mintNftMock().mint()), 20 | ...getBlockCallsMock(createBaseMock().base()), 21 | ]; 22 | 23 | it("Add theme to a base", async () => { 24 | const remarks = getRemarksFromBlocksMock([ 25 | ...getSetupRemarks(), 26 | ...getBlockCallsMock( 27 | createBaseMock(4).themeadd({ 28 | themeId: "theme2", 29 | theme: { primaryColor: "#000" }, 30 | }) 31 | ), 32 | ]); 33 | const consolidator = new Consolidator(); 34 | const consolidatedResult = await consolidator.consolidate(remarks); 35 | expect(consolidatedResult).toMatchSnapshot(); 36 | }); 37 | 38 | // it("should fail to add theme with existing key", async () => { 39 | // const remarks = getRemarksFromBlocksMock([ 40 | // ...getSetupRemarks(), 41 | // ...getBlockCallsMock( 42 | // createBaseMock(4).themeadd({ 43 | // themeId: "themeOne", 44 | // theme: { primaryColor: "#000" }, 45 | // }) 46 | // ), 47 | // ]); 48 | // const consolidator = new Consolidator(); 49 | // const consolidatedResult = await consolidator.consolidate(remarks); 50 | // expect(consolidatedResult).toMatchSnapshot(); 51 | // }); 52 | }); 53 | -------------------------------------------------------------------------------- /test/2.0.0/interactions/equippable.test.ts: -------------------------------------------------------------------------------- 1 | import { equippableInteraction } from "../../../src/rmrk2.0.0/tools/consolidator/interactions/equippable"; 2 | import { Equippable } from "../../../src/rmrk2.0.0/classes/equippable"; 3 | import { OP_TYPES } from "../../../src/rmrk2.0.0/tools/constants"; 4 | import { Base } from "../../../src/rmrk2.0.0/classes/base"; 5 | 6 | const dummyRemark = { 7 | block: 0, 8 | interaction_type: OP_TYPES.EQUIPPABLE, 9 | caller: "12345", 10 | version: "2.0.0", 11 | remark: "RMRK:2.0.0:test", 12 | }; 13 | 14 | const initBaseInstance = () => { 15 | const baseInstance = new Base(1, "id-test", "12345", "svg", [ 16 | { 17 | id: "slot-test", 18 | type: "slot", 19 | equippable: ["collection1", "collection2", "collection3"], 20 | }, 21 | ]); 22 | 23 | return baseInstance; 24 | }; 25 | 26 | describe("2.0.0 interactions: equippableInteraction", () => { 27 | it("should throw if base is missing", () => { 28 | 29 | const equippableEntity = new Equippable("id-test", "slot-test", "*"); 30 | 31 | expect(() => 32 | equippableInteraction(dummyRemark, equippableEntity) 33 | ).toThrow(); 34 | }); 35 | 36 | it("should correctly update equippables on base part", () => { 37 | const baseInstance = initBaseInstance(); 38 | const equippableEntity = new Equippable("id-test", "slot-test", "*"); 39 | equippableInteraction(dummyRemark, equippableEntity, baseInstance); 40 | expect(baseInstance.parts?.[0].equippable).toEqual("*"); 41 | 42 | const baseInstance2 = initBaseInstance(); 43 | const equippableEntity2 = new Equippable("id-test", "slot-test", "+collection4"); 44 | equippableInteraction(dummyRemark, equippableEntity2, baseInstance2); 45 | expect(baseInstance2.parts?.[0].equippable).toEqual([ 46 | "collection1", 47 | "collection2", 48 | "collection3", 49 | "collection4", 50 | ]); 51 | 52 | const baseInstance3 = initBaseInstance(); 53 | const equippableEntity3 = new Equippable("id-test", "slot-test", "-collection2"); 54 | equippableInteraction(dummyRemark, equippableEntity3, baseInstance3); 55 | expect(baseInstance3.parts?.[0].equippable).toEqual(["collection1", "collection3"]); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /test/2.0.0/mocks/metadata-valid.ts: -------------------------------------------------------------------------------- 1 | export const attributesMockBoostNumberValid = { 2 | test: { 3 | type: "string", 4 | value: "mock", 5 | }, 6 | }; 7 | 8 | export const attributesMockNumberValid = { 9 | test: { 10 | type: "int", 11 | value: 2, 12 | }, 13 | date: { 14 | type: "datetime", 15 | value: 1648209269044, 16 | }, 17 | }; 18 | 19 | export const attributesMockFloatValid = { 20 | test: { 21 | type: "float", 22 | value: 2.2, 23 | }, 24 | }; 25 | 26 | export const attributesMockBooleanValid = { 27 | test: { 28 | type: "boolean", 29 | value: true, 30 | }, 31 | }; 32 | 33 | export const attributesMockValueValid = { 34 | test: { 35 | type: "string", 36 | value: "2", 37 | }, 38 | bool: { 39 | type: "boolean", 40 | value: true, 41 | }, 42 | }; 43 | 44 | export const metadataMockAllValid = { 45 | external_url: "https://youtube.com", 46 | image: "ipfs://ipfs/12345", 47 | image_data: "", 48 | description: "Mock description", 49 | name: "Mock 1", 50 | properties: attributesMockBoostNumberValid, 51 | }; 52 | 53 | export const metadataMockAllValid2 = { 54 | image: "ipfs://ipfs/12345", 55 | description: "Mock description", 56 | name: "Mock 1", 57 | properties: attributesMockBoostNumberValid, 58 | }; 59 | 60 | export const metadataMockAllValid4 = { 61 | image: "ipfs://ipfs/12345", 62 | properties: attributesMockNumberValid, 63 | }; 64 | 65 | export const metadataMockAllValid6 = { 66 | image: "ipfs://ipfs/12345", 67 | properties: attributesMockValueValid, 68 | }; 69 | 70 | -------------------------------------------------------------------------------- /test/2.0.0/mocks/remark-mocks.ts: -------------------------------------------------------------------------------- 1 | export const mintRemarkValidMocks = [ 2 | 'RMRK::MINT::1.0.0::{"name"%3A"Foo"%2C"max"%3A5%2C"issuer"%3A"5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY"%2C"symbol"%3A"FOO"%2C"id"%3A"d43593c715a56da27d-FOO"%2C"metadata"%3A"https%3A%2F%2Fsome.url"}', 3 | 'RMRK::MINT::1.0.0::{"name"%3A"Test Batch"%2C"max"%3A5%2C"issuer"%3A"5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY"%2C"symbol"%3A"TB"%2C"id"%3A"d43593c715a56da27d-TB"%2C"metadata"%3A"https%3A%2F%2Fsome.url"}', 4 | 'RMRK::MINT::1.0.0::{"name"%3A"Test Batch 2"%2C"max"%3A5%2C"issuer"%3A"5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY"%2C"symbol"%3A"TB2"%2C"id"%3A"d43593c715a56da27d-TB2"%2C"metadata"%3A"https%3A%2F%2Fsome.url"}', 5 | 'RMRK::MINT::1.0.0::{"name"%3A"Bar"%2C"max"%3A5%2C"issuer"%3A"5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY"%2C"symbol"%3A"BAR"%2C"id"%3A"d43593c715a56da27d-BAR"%2C"metadata"%3A"https%3A%2F%2Fsome.url"}', 6 | 'RMRK::MINT::1.0.0::{"name"%3A"Foo"%2C"max"%3A5%2C"issuer"%3A"5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY"%2C"symbol"%3A"FOO"%2C"id"%3A"d43593c715a56da27d-FOO"%2C"metadata"%3A"https%3A%2F%2Fsome.url"}', 7 | ]; 8 | 9 | const changeUserRemarkValidMocks = [ 10 | "RMRK::CHANGEISSUER::1.0.0::d43593c715a56da27d-BAR::5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty", 11 | "RMRK::CHANGEISSUER::1.0.0::d43593c715a56da27d-BAR::5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty", 12 | ]; 13 | 14 | export const validSendRemarkEvent = 15 | "RMRK::SEND::1.0.0::F4677F38191256A73F-TTNKARDS-Celadon Woodash Tankard-0000000000000001::Fksmad33PFxhrQXNYPPJozgWrv82zuFLvXK7Rh8m1xQhe98"; 16 | export const validListRemarkEvent = 17 | "RMRK::LIST::1.0.0::6435603-D4E195CCE7ADB3F876-INVITATION-VIP_INVITATION_1-0000000000000001::1000000000000"; 18 | export const validMintNFTRemarkEvent = 19 | 'RMRK::MINTNFT::1.0.0::{"collection"%3A"D4E195CCE7ADB3F876-INVITATION"%2C"sn"%3A"0000000000000001"%2C"transferable"%3A1%2C"name"%3A"VIP Invitation %231"%2C"metadata"%3A"ipfs%3A%2F%2Fipfs%2FQmQ2Q57PVpaP8QvWvvH9kfn1CdCY49pcv1AaLBjDwS2p4g"%2C"currentOwner"%3A"HPSgWwpjnMe9oyBq4t2dA3dRTU8PwDAU32q6E76xjFDDrEX"%2C"instance"%3A"VIP_INVITATION_1"}'; 20 | export const validBuyRemarkEvent = 21 | "RMRK::BUY::1.0.0::6309833-282781680602E07B32-BIR-BIRTH_1-0000000000000001"; 22 | export const validEmoteRemarkEvent = 23 | "RMRK::EMOTE::1.0.0::6431478-10D77F8B699437BB50-TXT-JUNGLE_TEXTURE-0000000000000001::1F496"; 24 | export const validConsumeRemarkEvent = 25 | "RMRK::CONSUME::1.0.0::6277640-D4E195CCE7ADB3F876-SUPER GIFS-GOODBYE_BULLIES!-0000000000000002"; 26 | -------------------------------------------------------------------------------- /test/2.0.0/utils/append-json-stream.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | // @ts-ignore 3 | import JSONStream from "JSONStream"; 4 | 5 | export const appendPromise = (appendFilePath: string): Promise => 6 | new Promise((resolve, reject) => { 7 | try { 8 | const appendFileStream: any[] = []; 9 | const readStream = fs.createReadStream(appendFilePath); 10 | const parseStream = JSONStream.parse("*"); 11 | parseStream.on("data", (fileChunk: Record) => { 12 | if (fileChunk) { 13 | appendFileStream.push(fileChunk); 14 | } 15 | }); 16 | 17 | readStream.pipe(parseStream); 18 | 19 | readStream.on("finish", async () => { 20 | resolve(appendFileStream); 21 | }); 22 | 23 | readStream.on("end", async () => { 24 | resolve(appendFileStream); 25 | }); 26 | 27 | readStream.on("error", (error) => { 28 | reject(error); 29 | }); 30 | } catch (error: any) { 31 | console.error(error); 32 | reject(error); 33 | } 34 | }); 35 | -------------------------------------------------------------------------------- /test/2.0.0/utils/validate-emoji.test.ts: -------------------------------------------------------------------------------- 1 | import { isValidEmoji } from "../../../src/rmrk2.0.0/tools/validate-emoji"; 2 | 3 | describe("validation: isValidEmoji", () => { 4 | it("should be valid", () => { 5 | expect(isValidEmoji("1F601")).toBeTruthy(); 6 | expect(isValidEmoji("U+2764-U+FE0F-U+200D-U+1F525")).toBeTruthy(); 7 | expect(isValidEmoji("2764-FE0F-200D-1F525")).toBeTruthy(); 8 | expect(isValidEmoji("U+2764 U+FE0F U+200D U+1F525")).toBeTruthy(); 9 | }); 10 | 11 | it("should be invalid", () => { 12 | expect(isValidEmoji("U+1dff601")).toBeFalsy(); 13 | expect(isValidEmoji("foo")).toBeFalsy(); 14 | expect(isValidEmoji("u+foo")).toBeFalsy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /test/2.0.0/utils/validate-metadata.test.ts: -------------------------------------------------------------------------------- 1 | import { validateMetadata } from "../../../src/rmrk2.0.0/tools/validate-metadata"; 2 | import { 3 | attributesMockBoostNumberValid, 4 | metadataMockAllValid, 5 | metadataMockAllValid2, 6 | metadataMockAllValid4, 7 | metadataMockAllValid6, 8 | } from "../mocks/metadata-valid"; 9 | import {Metadata} from "../../../src/rmrk2.0.0"; 10 | 11 | export const attributesMockDateInvalid = [ 12 | { 13 | display_type: "date", 14 | value: 1620380805485, 15 | }, 16 | ]; 17 | 18 | export const metadataMockAllInvalid = { 19 | external_url: "https://youtube.com", 20 | image: "file://", 21 | image_data: "", 22 | description: "Mock description", 23 | name: "Mock 1", 24 | properties: attributesMockBoostNumberValid, 25 | }; 26 | 27 | describe("validation: validateMetadata with valid mocks", () => { 28 | it("should be valid", () => { 29 | expect(() => 30 | validateMetadata(metadataMockAllValid as Metadata) 31 | ).not.toThrow(); 32 | }); 33 | 34 | it("should be valid2", () => { 35 | expect(() => 36 | validateMetadata(metadataMockAllValid2 as Metadata) 37 | ).not.toThrow(); 38 | }); 39 | 40 | it("should be valid4", () => { 41 | expect(() => 42 | validateMetadata(metadataMockAllValid4 as Metadata) 43 | ).not.toThrow(); 44 | }); 45 | 46 | it("should be valid6", () => { 47 | expect(() => 48 | validateMetadata(metadataMockAllValid6 as Metadata) 49 | ).not.toThrow(); 50 | }); 51 | }); 52 | 53 | describe("validation: validateMetadata with invalid mocks", () => { 54 | it("should be invalid", () => { 55 | expect(() => 56 | validateMetadata({ 57 | mediaUri: "file://dfsdf", 58 | } as Metadata) 59 | ).toThrow(); 60 | 61 | expect(() => 62 | validateMetadata({ 63 | name: 1, 64 | } as Metadata) 65 | ).toThrow(); 66 | 67 | expect(() => 68 | validateMetadata({ 69 | description: 1, 70 | } as Metadata) 71 | ).toThrow(); 72 | 73 | expect(() => 74 | validateMetadata({ 75 | image: 1, 76 | } as Metadata) 77 | ).toThrow(); 78 | 79 | expect(() => 80 | validateMetadata({ 81 | image: "ipfs://dfsdf", 82 | externalUri: "Mock", 83 | } as Metadata) 84 | ).toThrow(); 85 | }); 86 | 87 | it("should be invalid with invalid attributes passed", () => { 88 | expect(() => 89 | validateMetadata({ 90 | image: "ipfs://dfsdf", 91 | properties: { test: { value: 123, type: "string" } }, 92 | } as Metadata) 93 | ).toThrow(); 94 | 95 | expect(() => 96 | validateMetadata({ 97 | image: "ipfs://dfsdf", 98 | properties: { 99 | test: { 100 | type: "int", 101 | value: "123", 102 | }, 103 | }, 104 | } as Metadata) 105 | ).toThrow(); 106 | 107 | expect(() => 108 | validateMetadata({ 109 | image: "ipfs://dfsdf", 110 | properties: { 111 | test: { 112 | type: "string", 113 | value: 123, 114 | }, 115 | }, 116 | } as Metadata) 117 | ).toThrow(); 118 | 119 | expect(() => 120 | validateMetadata({ 121 | image: "ipfs://dfsdf", 122 | properties: { 123 | test: { 124 | type: "int", 125 | value: 123, 126 | }, 127 | }, 128 | } as Metadata) 129 | ).toBeTruthy(); 130 | 131 | expect(() => 132 | validateMetadata({ 133 | image: "ipfs://dfsdf", 134 | properties: { 135 | test: { 136 | type: "float", 137 | value: "123", 138 | }, 139 | }, 140 | } as Metadata) 141 | ).toThrow(); 142 | }); 143 | }); 144 | -------------------------------------------------------------------------------- /test/2.0.0/utils/validate-string-is-a-valid-url.ts: -------------------------------------------------------------------------------- 1 | import { stringIsAValidUrl } from "../../../src/rmrk2.0.0/tools/utils"; 2 | 3 | const testUrl = 'https://rmrk.app/'; 4 | 5 | describe("utils: stringIsAValidUrl", () => { 6 | it("should check if string is a URL and return a boolean", () => { 7 | expect(stringIsAValidUrl(testUrl)).toEqual(true); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /test/seed/default/multiple.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmrk-team/rmrk-tools/05eba7ef4f8e740c916bfebab3b264c3422dd3fc/test/seed/default/multiple.ts -------------------------------------------------------------------------------- /test/seed/default/single.ts: -------------------------------------------------------------------------------- 1 | import { Collection } from "../../../src/rmrk1.0.0/classes/collection"; 2 | import { NFT } from "../../../src/rmrk1.0.0/classes/nft"; 3 | import { u8aToHex } from "@polkadot/util"; 4 | import getKeys from "../devaccs"; 5 | 6 | export default function defineSeeds(): any[] { 7 | const accounts = getKeys(); 8 | 9 | const s = []; 10 | 11 | const c1 = new Collection( 12 | 0, 13 | "Foo", 14 | 5, 15 | accounts[0].address, 16 | "FOO", 17 | Collection.generateId(u8aToHex(accounts[0].publicKey), "FOO"), 18 | "https://some.url" 19 | ); 20 | 21 | const c2 = new Collection( 22 | 0, 23 | "Bar", 24 | 5, 25 | accounts[0].address, 26 | "BAR", 27 | Collection.generateId(u8aToHex(accounts[0].publicKey), "BAR"), 28 | "https://some.url" 29 | ); 30 | 31 | s.push([c1.mint(), accounts[0], `Deploy collection 1: ${c1.name}`]); 32 | s.push([c2.mint(), accounts[0], `Deploy collection 2: ${c2.name}`]); 33 | s.push(5000); 34 | 35 | // Load now minted collection c2 as c2b 36 | const c2b = new Collection( 37 | 1, //block.block.header.number.toNumber(), <= @todo, problem. Block number unfetchable here. 38 | "Bar", 39 | 5, 40 | accounts[0].address, 41 | "BAR", 42 | Collection.generateId(u8aToHex(accounts[0].publicKey), "BAR"), 43 | "https://some.url" 44 | ); 45 | 46 | s.push([ 47 | c2b.change_issuer(accounts[1].address), 48 | accounts[0], 49 | `Change owner of collection ${c2b.name} from ${c2b.issuer} to ${accounts[1].address}`, 50 | ]); 51 | 52 | return s; 53 | } 54 | -------------------------------------------------------------------------------- /test/seed/devaccs.ts: -------------------------------------------------------------------------------- 1 | import { Keyring } from "@polkadot/keyring"; 2 | import { KeyringPair } from "@polkadot/keyring/types"; 3 | 4 | export default (): KeyringPair[] => { 5 | const k = []; 6 | const keyringAlice = new Keyring({ type: "sr25519" }); 7 | const keyringBob = new Keyring({ type: "sr25519" }); 8 | const keyringCharlie = new Keyring({ type: "sr25519" }); 9 | k.push(keyringAlice.addFromUri("//Alice")); 10 | k.push(keyringBob.addFromUri("//Bob")); 11 | k.push(keyringCharlie.addFromUri("//Charlie")); 12 | return k; 13 | }; 14 | -------------------------------------------------------------------------------- /test/seed/types.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmrk-team/rmrk-tools/05eba7ef4f8e740c916bfebab3b264c3422dd3fc/test/seed/types.ts -------------------------------------------------------------------------------- /tsconfig.cli-dist.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.cli", 3 | "compilerOptions": { 4 | "outDir": "dist-cli" 5 | }, 6 | "include": ["cli/*.ts", "src/*.ts", "src/**/*.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig", 3 | "compilerOptions": { 4 | "module":"commonjs" 5 | }, 6 | "include": ["cli/*.ts", "src/*.ts", "src/**/*.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowUnreachableCode": false, 4 | "declaration": true, 5 | "esModuleInterop": true, 6 | "lib": ["dom", "es2019"], 7 | "module": "esnext", 8 | "moduleResolution": "node", 9 | "noFallthroughCasesInSwitch": true, 10 | "noUnusedLocals": false, 11 | "noUnusedParameters": false, 12 | "outDir": "dist", 13 | "pretty": true, 14 | "sourceMap": true, 15 | "strict": true, 16 | "resolveJsonModule": true, 17 | "target": "es2017" 18 | }, 19 | "files": [ 20 | "src/rmrk2.0.0/index.ts" 21 | ] 22 | } 23 | --------------------------------------------------------------------------------