├── .eslintrc.js ├── .github ├── ISSUE_TEMPLATE │ ├── bug-report.md │ ├── code-quality.md │ ├── feature-request.md │ └── ui-update.md └── workflows │ ├── build.yml │ ├── main.yml │ └── publish-npm.yml ├── .gitignore ├── .yarnrc.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── bin └── nit.js ├── example ├── hello.js ├── hello.ts └── mocks │ ├── action.json │ ├── assetTree.json │ ├── commit.json │ ├── identityAssetCreator.json │ ├── identityProofProvider.json │ ├── identityServiceProvider.json │ ├── proof.json │ └── proofMetadata.json ├── package-lock.json ├── package.json ├── schema └── dbml.md ├── scripts └── parseTxNid.ts ├── src ├── action.ts ├── commitdb.ts ├── contract.ts ├── http.ts ├── ipfs.ts ├── license.ts ├── nit.ts ├── run.ts └── util.ts ├── tests ├── testAction.ts ├── testHttp.ts ├── testIpfs.ts └── testNit.ts ├── tsconfig.json └── yarn.lock /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"], 7 | "parser": "@typescript-eslint/parser", 8 | "parserOptions": { 9 | "ecmaVersion": "latest", 10 | "sourceType": "module" 11 | }, 12 | "plugins": ["@typescript-eslint"], 13 | "root": true, 14 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | --- 8 | 9 | ## Description 10 | 11 | A clear and concise description of what the bug is. 12 | 13 |  14 | 15 | ## Steps to Reproduce 16 | 17 | 1. Go to '...' 18 | 1. Click on '....' 19 | 1. Scroll down to '....' 20 | 1. See error 21 | 22 | ## Expected Behavior 23 | 24 | - Expected: A clear and concise description of what you expected to happen. 25 | - Actual: A clear and concise description of what actually happened. 26 | 27 | ## Logs 28 | 29 | If applicable, add logs or screenshots to help explain your problem. 30 | 31 | ## Environment 32 | 33 | - Version: _e.g. 1.3.0, beta2, etc._ 34 | - OS: _e.g. Ubuntu 20.04, Android 10, iOS 13, etc._ 35 | - Device: _e.g. HTC Exodus 1_ 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/code-quality.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Code Quality 3 | about: Improve code quality or project architecture 4 | title: '' 5 | labels: code 6 | assignees: '' 7 | --- 8 | 9 | ## Suggestion 10 | 11 | The content of the suggestion. 12 | 13 |  14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | --- 8 | 9 | ## Concept 10 | 11 | If we provide \[feature] 12 | 13 | ## Reason/Hypothesis 14 | 15 | We can help \[who] to \[benefit] or solve \[problem] because we know \[who] are \[faced about our target]. 16 | 17 | ## More Description (if any) 18 | 19 | Put more description here to support the concept. 20 | 21 | ## Suggested Implementation (if any) 22 | 23 | Put some suggested implementation here if you already have some ideas in mind. 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/ui-update.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: UI Update 3 | about: Create a new UI update 4 | title: '' 5 | labels: uiux 6 | assignees: '' 7 | --- 8 | 9 | ## Update Purpose 10 | 11 | Write the purpose of UI update here. 12 | 13 |  14 | 15 | ## UI Suggestion 16 | 17 | Put images or link to the contents of suggestions. 18 | 19 | ## More Description (if any) 20 | 21 | Put more description here to help developers understand. 22 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Build npm package 5 | 6 | on: 7 | push: 8 | branches: 9 | - 'feature-*' 10 | - 'fix-*' 11 | pull_request: 12 | branches: 13 | - 'main' 14 | 15 | jobs: 16 | build: 17 | runs-on: ubuntu-latest 18 | 19 | strategy: 20 | matrix: 21 | node-version: [16.x, 18.x, 20.x] 22 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 23 | 24 | steps: 25 | - uses: actions/checkout@v3 26 | - name: Use Node.js ${{ matrix.node-version }} 27 | uses: actions/setup-node@v3 28 | with: 29 | node-version: ${{ matrix.node-version }} 30 | cache: 'yarn' 31 | - run: yarn install 32 | - run: yarn build 33 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Add Issues to Backlog Project 2 | 3 | on: 4 | issues: 5 | types: 6 | - opened 7 | 8 | jobs: 9 | add-to-project: 10 | name: Add issue to Backlog project 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Add issue to Backlog project 14 | uses: actions/add-to-project@v0.5.0 15 | with: 16 | project-url: https://github.com/orgs/numbersprotocol/projects/8 17 | # OR for repository projects: 18 | # project-url: https://github.com/users/YOUR_USERNAME/projects/YOUR_PROJECT_NUMBER 19 | # OR for user projects: 20 | # project-url: https://github.com/users/YOUR_USERNAME/projects/YOUR_PROJECT_NUMBER 21 | github-token: ${{ secrets.ADD_TO_PROJECT_PAT }} -------------------------------------------------------------------------------- /.github/workflows/publish-npm.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build and publish a package to npmjs when a version release is created 2 | # https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages#publishing-packages-using-yarn 3 | 4 | name: Publish Package to npmjs 5 | 6 | on: 7 | push: 8 | tags: 9 | - 'v[0-9]+.[0-9]+.[0-9]+*' 10 | 11 | env: 12 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 13 | 14 | jobs: 15 | build: 16 | runs-on: ubuntu-latest 17 | permissions: 18 | contents: read 19 | packages: write 20 | steps: 21 | - name: Checkout code 22 | uses: actions/checkout@v3 23 | 24 | - name: Setup .npmrc file to publish to npm 25 | uses: actions/setup-node@v3 26 | with: 27 | node-version: 16 28 | registry-url: 'https://registry.npmjs.org' 29 | # Defaults to the user or organization that owns the workflow file 30 | scope: '@numbersprotocol' 31 | 32 | - name: Install dependencies 33 | run: yarn install 34 | 35 | - name: Build project 36 | run: yarn build 37 | 38 | - name: Publish to npm 39 | run: yarn publish --no-git-tag-version 40 | env: 41 | NODE_AUTH_TOKEN: ${{ env.NODE_AUTH_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | 3 | plugins: 4 | - path: .yarn/plugins/@yarnpkg/plugin-version.cjs 5 | spec: "@yarnpkg/plugin-version" 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | To check the difference between the last releaes and the latest dev status, click the link above. 11 | 12 | ## [1.6.1] - 2024-01-11 13 | 14 | ### Fixed 15 | 16 | 1. Commit will wait until transaction is mined. (#29) 17 | 18 | ## [1.6.0] - 2023-10-03 19 | 20 | ### Added 21 | 22 | 1. Add initial registration action for IOTA. 23 | 24 | ## [1.5.0] - 2023-06-15 25 | 26 | ### Changed 27 | 28 | 1. [PR26](https://github.com/numbersprotocol/nit/pull/26): Remove default license from Asset Tree 29 | 1. [PR27](https://github.com/numbersprotocol/nit/pull/27): Remove default empty abstract without input 30 | 31 | ## [1.4.0] - 2023-05-02 32 | 33 | ### Added 34 | 35 | 1. Add [Action Names](https://docs.numbersprotocol.io/introduction/numbers-protocol/defining-web3-assets/commit#actions) 36 | 37 | ### Changed 38 | 39 | 1. Add IPFS abstract layer to support multiple IPFS backends 40 | 41 | ## [1.3.0] - 2023-02-14 42 | 43 | ### Added 44 | 45 | 1. Add action action-mint-erc721-nft-jade 46 | 2. Add Numbers networks into config template 47 | 2. Add ESLint 48 | 49 | ### Changed 50 | 51 | 1. Remove not-yet-supported networks from the network dictionary 52 | 53 | ## [1.2.9] - 2023-01-28 54 | 55 | ### Fixed 56 | 57 | 1. Change default network in registration action from Avalanche to Jade 58 | 59 | [unreleased]: https://github.com/numbersprotocol/capture-lite/compare/1.6.1...HEAD 60 | [1.6.1]: https://github.com/numbersprotocol/capture-lite/compare/1.6.0...1.6.1 61 | [1.6.0]: https://github.com/numbersprotocol/capture-lite/compare/1.5.0...1.6.0 62 | [1.5.0]: https://github.com/numbersprotocol/capture-lite/compare/1.4.0...1.5.0 63 | [1.4.0]: https://github.com/numbersprotocol/capture-lite/compare/1.3.0...1.4.0 64 | [1.3.0]: https://github.com/numbersprotocol/capture-lite/compare/1.2.9...1.3.0 65 | [1.2.9]: https://github.com/numbersprotocol/capture-lite/releases/tag/1.2.9 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nit 2 | 3 | Nit is git for web3 digital asset and helps user to commit an asset's activities (chronicle) to blockchain. Please note, this repository is for nit open-source tools. If you are looking for accessing nit via API services, please visit [here](https://docs.numbersprotocol.io/developers/commit-asset-history/commit-via-api) for more details. 4 | 5 | ## Why Nit 6 | 7 | To make digital assets trustworthy and traceable, Nit leverages web3 technologies and Git core concepts. Everyone can chronicle their assets by creating on-chain records so that we can productively debate, narrate, and analyse. All asset histories are written on chain and are searchable by asset CID. A sample transaction can be found [here](https://snowtrace.io/tx/0x3ba2c36f7b0aeefc954041899a099c228e052a791a59f9922ab53ef9630f4a87). 8 | 9 | Case study 10 | 11 | 1. [A crypto-based dossier could help prove Russia committed war crimes](https://edition.cnn.com/2022/06/10/tech/ukraine-war-crimes-blockchain/index.html), CNN 12 | 2. [Starling Lab and Hala Systems file Cryptographic Submission of Evidence of War Crimes in Ukraine to the International Criminal Court](https://dornsife.usc.edu/cagr-news/news/2022/06/33571-starling-lab-and-hala-systems-file-cryptographic-submission-evidence-war-crimes), CAGR 13 | 14 | ## Commit & Asset Tree 15 | 16 | nit adopt similar design as git. 17 | 18 | * When there is an update to the asset, such as updating creator information or updating content itself to create a child asset, there should be a new commit attach the asset itself. 19 | 20 | * Every asset has a Tree file in IPFS to describe the property of the asset, including creator, creation time, license, etc. The asset tree CID is included in the on-chain message. Here is the example of the [asset tree](https://bafkreigbl7262jgwykk6ce47gbzvh4udr3rtzkpgd3b465664gzxma6zfi.ipfs.dweb.link/). 21 | 22 | The db diagram can be found [here](https://dbdiagram.io/d/6220e69c54f9ad109a54c3a5). In this diagram, you will find tables of `commit` and `assetTree` with the explanation of each data field 23 | 24 | ## Installation 25 | 26 | ```shell 27 | yarn global add @numbersprotocol/nit 28 | ``` 29 | 30 | ## Example: Initial Registration 31 | 32 | If it's your first time to run `nit`, initialize Nit with default configuration: 33 | 34 | ``` 35 | nit init 36 | ``` 37 | 38 | and provide personal information. The details of the Nit configuration format is in the [Configuration](#configuration) section below. 39 | 40 | ``` 41 | nit config -e 42 | ``` 43 | 44 | The default integrity register (smart contract) is running on Avalanche Fuji (Testnet) and will migrate to Numbers mainnet in the future. You need to get some testing tokens from the [Fuji faucet](https://faucet.avax-test.network) for paying transaction fee. 45 | 46 | Create a working directory for the targeting asset: 47 | 48 | ``` 49 | mkdir ~/temp && cd ~/temp 50 | ``` 51 | 52 | Create the Asset Tree of the targeting asset. Example: 53 | 54 | ``` 55 | nit add <asset-filepath> -m "First registration." 56 | ``` 57 | 58 | To access the file of the CID, you can use browser to open the URL: `https://<cid>.ipfs.dweb.link` 59 | 60 | Double check the Commit information meets your expectation: 61 | 62 | ``` 63 | nit status 64 | ``` 65 | 66 | Register the Commit on blockchain: 67 | 68 | ``` 69 | nit commit -m "commit description" 70 | ``` 71 | 72 | You can check the Commit on blockchain by following the explorer URL returned by Nit. 73 | 74 | ## Get On-Chain Information 75 | 76 | ### Get on-chain block numbers of an Asset 77 | 78 | ```shell 79 | nit log <asset-cid> --blocks 80 | ``` 81 | 82 | Example command of the mockup Asset 83 | 84 | ```shell 85 | nit log aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa --blocks 86 | ``` 87 | 88 | Commit block numbers and indices 89 | 90 | ``` 91 | Total Commit number: 74 92 | Index: 0, Block number: 7236185 93 | Index: 1, Block number: 7236445 94 | ... 95 | Index: 71, Block number: 10849040 96 | Index: 72, Block number: 10849133 97 | Index: 73, Block number: 11040035 98 | ``` 99 | 100 | ### Get Commits of an Asset 101 | 102 | You can specify the starting and ending indices of block numbers: 103 | 104 | ```shell 105 | nit log <asset-cid> --from-index 3 --to-index 5 106 | ``` 107 | 108 | You will get the Commits from the block numbers associated to index 3 & 4. 109 | 110 | Example command of the mockup Asset 111 | 112 | ```shell 113 | nit log aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa --from-index 71 --to-index 73 114 | ``` 115 | 116 | <details> 117 | <summary>Commits in block 71 & 72 (exclude block 73)</summary> 118 | 119 | ```shell 120 | Total Commit number: 74 121 | 122 | block number: 10849040 123 | tx: 0x6d5902173255afe379cc4ae934a6c684ecfd865679286665622de3cf10eddcbe 124 | { 125 | "assetTreeCid": "bafkreifnpykuw5g2m4k5k5wf55zxtzjmcftstzhtsarlkqytimajj3ntlq", 126 | "assetTreeSha256": "ad7e154b74da6715d576c5ef7379e52c116729e4f39022b54313430094edb35c", 127 | "assetTreeSignature": "0x9faf5c9d13b8d90a7a8e88aa6daf089ca89593c28dc241347c4756e83c2f1ea53ed1cb9e189f7ab81c81327527c97595f44ed71dda8e5d78ebe0dccfe9dd27081c", 128 | "author": "bafkreigzixvzu2tbxbvmvwcvlz2zwoagmb6c2q5egaq4lmd5sesyopmmx4", 129 | "committer": "bafkreigzixvzu2tbxbvmvwcvlz2zwoagmb6c2q5egaq4lmd5sesyopmmx4", 130 | "provider": "bafkreigtmno2wacf4ldb6ewbkboe7oojedsp3dky35djytor6guwzhbdpa", 131 | "timestampCreated": 1655720482, 132 | "action": "bafkreiavifzn7ntlb6p2lr55k3oezo6pofwvknecukc5auhng4miowcte4", 133 | "actionResult": "https://bafkreifnpykuw5g2m4k5k5wf55zxtzjmcftstzhtsarlkqytimajj3ntlq.ipfs.dweb.link", 134 | "abstract": "Action action-initial-registration." 135 | } 136 | 137 | block number: 10849133 138 | tx: 0xe383fdc0f4eaf44e8bde4737f33bcd45742dcb846f3beb890976793d7cc9358e 139 | { 140 | "assetTreeCid": "bafkreidptwydheqfybug4mmnzwzdg4rqxjvg4akl2pwjmrfxhqz33qv4tu", 141 | "assetTreeSha256": "6f9db0339205c0686e318dcdb2337230ba6a6e014bd3ec9644b73c33bdc2bc9d", 142 | "assetTreeSignature": "0xef547e124a9904dbdb5a418022ad03c621201b74111a3b4c5fac61b1d82350170766cef8a27737d21ca9b1bd4e04f7cdea460706b68b14e0ed17f2a3de83f9131b", 143 | "author": "bafkreigzixvzu2tbxbvmvwcvlz2zwoagmb6c2q5egaq4lmd5sesyopmmx4", 144 | "committer": "bafkreigzixvzu2tbxbvmvwcvlz2zwoagmb6c2q5egaq4lmd5sesyopmmx4", 145 | "provider": "bafkreigtmno2wacf4ldb6ewbkboe7oojedsp3dky35djytor6guwzhbdpa", 146 | "timestampCreated": 1655720763, 147 | "action": "bafkreiavifzn7ntlb6p2lr55k3oezo6pofwvknecukc5auhng4miowcte4", 148 | "actionResult": "https://bafkreidptwydheqfybug4mmnzwzdg4rqxjvg4akl2pwjmrfxhqz33qv4tu.ipfs.dweb.link", 149 | "abstract": "Action action-initial-registration." 150 | } 151 | ``` 152 | 153 | </details> 154 | 155 | ### Get difference of Commits and Asset Trees of an Asset 156 | 157 | ```shell 158 | nit diff <asset-cid> --from-index 3 --to-index 5 159 | ``` 160 | 161 | Example command of the mockup Asset 162 | 163 | ```shell 164 | nit diff aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa --from-index 71 --to-index 73 165 | ``` 166 | 167 | <details> 168 | <summary>Diff of Commits and Asset Trees in block 71 & 72 (exclude block 73)</summary> 169 | 170 | ```shell 171 | from: block 10849040, tx 0x6d5902173255afe379cc4ae934a6c684ecfd865679286665622de3cf10eddcbe 172 | to: block 10849133, tx 0xe383fdc0f4eaf44e8bde4737f33bcd45742dcb846f3beb890976793d7cc9358e 173 | 174 | Commit difference 175 | { 176 | "abstract": "Action action-initial-registration.", 177 | "action": "bafkreiavifzn7ntlb6p2lr55k3oezo6pofwvknecukc5auhng4miowcte4", 178 | -"actionResult": "https://bafkreifnpykuw5g2m4k5k5wf55zxtzjmcftstzhtsarlkqytimajj3ntlq.ipfs.dweb.link", 179 | -"assetTreeCid": "bafkreifnpykuw5g2m4k5k5wf55zxtzjmcftstzhtsarlkqytimajj3ntlq", 180 | -"assetTreeSha256": "ad7e154b74da6715d576c5ef7379e52c116729e4f39022b54313430094edb35c", 181 | -"assetTreeSignature": "0x9faf5c9d13b8d90a7a8e88aa6daf089ca89593c28dc241347c4756e83c2f1ea53ed1cb9e189f7ab81c81327527c97595f44ed71dda8e5d78ebe0dccfe9dd27081c", 182 | +"actionResult": "https://bafkreidptwydheqfybug4mmnzwzdg4rqxjvg4akl2pwjmrfxhqz33qv4tu.ipfs.dweb.link", 183 | +"assetTreeCid": "bafkreidptwydheqfybug4mmnzwzdg4rqxjvg4akl2pwjmrfxhqz33qv4tu", 184 | +"assetTreeSha256": "6f9db0339205c0686e318dcdb2337230ba6a6e014bd3ec9644b73c33bdc2bc9d", 185 | +"assetTreeSignature": "0xef547e124a9904dbdb5a418022ad03c621201b74111a3b4c5fac61b1d82350170766cef8a27737d21ca9b1bd4e04f7cdea460706b68b14e0ed17f2a3de83f9131b", 186 | "author": "bafkreigzixvzu2tbxbvmvwcvlz2zwoagmb6c2q5egaq4lmd5sesyopmmx4", 187 | "committer": "bafkreigzixvzu2tbxbvmvwcvlz2zwoagmb6c2q5egaq4lmd5sesyopmmx4", 188 | "provider": "bafkreigtmno2wacf4ldb6ewbkboe7oojedsp3dky35djytor6guwzhbdpa", 189 | -"timestampCreated": 1655720482 190 | +"timestampCreated": 1655720763 191 | } 192 | 193 | Asset Tree difference 194 | { 195 | "abstract": "", 196 | "assetCid": "bafybeif3ctgbmiso4oykvwj6jebyrkjxqr26bfrkesla5yr2ypgx47wgle", 197 | "assetCreator": null, 198 | "assetSha256": null, 199 | "assetTimestampCreated": null, 200 | "assetTreeCustomKey1": "foo", 201 | "assetTreeCustomKey2": "bar", 202 | "encodingFormat": "application/zip", 203 | "license": { 204 | "document": "https://starlinglab.org", 205 | "name": "Starling License" 206 | }, 207 | +"nftRecord": "bafkreielfjf7sfxigb4r7tejt7jhl6kthxoujwziywixlwxjjho32may7y" 208 | } 209 | ``` 210 | </details> 211 | 212 | ## Configuration 213 | 214 | The Nit configuration mimics the [Hardhat configuration](https://hardhat.org/config) with additional fields. 215 | 216 | The Nit configuration is at `~/.nitconfig.json`. Linux users can open the default editor by the command below: 217 | 218 | ```shell 219 | nit config -e 220 | ``` 221 | 222 | <details> 223 | <summary>Example of a Nit configuration:</summary> 224 | 225 | ```json 226 | { 227 | // CID of the author's profile of original Assets 228 | "author": "bafkreihkrnjvjeijjhalozcfpgrgb46673dlt4e3qm5bmvzzb4if423wse", 229 | // CID of the committer's profile who creates Asset Trees and Commits 230 | "committer": "bafkreihkrnjvjeijjhalozcfpgrgb46673dlt4e3qm5bmvzzb4if423wse", 231 | // CID of the service provider who hosts the integrity registration service 232 | "provider": "bafkreido4zu743f6isb5wninfkedvbirj2ngb5fkivrpdijh2xtd3s6rnu", 233 | "defaultNetwork": "fuji", 234 | "network": { 235 | "rinkeby": { 236 | "url": "https://eth-rinkeby.alchemyapi.io/v2/UO5kfog_UDJgGCuqeaSJmnE95_gKOnFN", 237 | "gasLimit": 200000, 238 | "accounts": [ 239 | "<private-key>", 240 | ], 241 | // integrity record contract address 242 | "contract": "0x2Aa4e29872DE77E1Bc6cF310d647F9cB0f9a073B", 243 | "explorerBaseUrl": "https://rinkeby.etherscan.io/tx" 244 | }, 245 | "avalanche": { 246 | "chainId": 43114, 247 | "url": "https://api.avax.network/ext/bc/C/rpc", 248 | "accounts": [ 249 | "<private-key>", 250 | ], 251 | "contract": "0x1970aFD0831E9233667fb9484910926c2b18BCb4", 252 | "explorerBaseUrl": "https://snowtrace.io/tx" 253 | }, 254 | "fuji": { 255 | "url": "https://api.avax-test.network/ext/bc/C/rpc", 256 | "chainId": 43113, 257 | "gasLimit": 200000, 258 | "accounts": [ 259 | "<private-key>", 260 | ], 261 | "contract": "0xA2De03bee39Fa637440909abC5621063bC5DA926", 262 | "explorerBaseUrl": "https://testnet.snowtrace.io/tx" 263 | }, 264 | "polygon": { 265 | "url": "https://polygon-rpc.com/", 266 | "gasPrice": 60000000000, 267 | "accounts": [ 268 | "<private-key>", 269 | ], 270 | "contract": "0x2094747c6c870f20E38f701116CBb46845b5E5c1", 271 | "explorerBaseUrl": "https://polygonscan.com/tx" 272 | }, 273 | "moonbase": { 274 | "url": "https://rpc.api.moonbase.moonbeam.network", 275 | "accounts": [ 276 | "<private-key>", 277 | ], 278 | "contract": "0xfbeA33fe2b266697404Dc5D1dC0A4ee9D0eDED23", 279 | "explorerBaseUrl": "https://moonbase.moonscan.io/tx" 280 | }, 281 | "aurora_testnet": { 282 | "url": "https://testnet.aurora.dev/", 283 | "chainId": 1313161555, 284 | "accounts": [ 285 | "<private-key>", 286 | ], 287 | "contract": "0x8e1bF90681C672e25aE880767d57f0552f6F5Cd1", 288 | "explorerBaseUrl": "https://testnet.aurorascan.dev/tx" 289 | } 290 | }, 291 | // For the ipfsadd command. We will support web3.storage soon 292 | "infura": { 293 | "projectId": "aaaaaaaaaaaaaaaaaaaaaaaaaaa", 294 | "projectSecret": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" 295 | } 296 | } 297 | ``` 298 | 299 | </details> 300 | 301 | ## Verification 302 | 303 | Verify the integrity of an Asset Tree 304 | 305 | ```shell 306 | nit verify --integrity-hash <assetTreeSha256> --signature <assetTreeSignature> 307 | ``` 308 | 309 | Example 310 | 311 | ```shell 312 | nit verify --integrity-hash 6f9db0339205c0686e318dcdb2337230ba6a6e014bd3ec9644b73c33bdc2bc9d --signature 0xef547e124a9904dbdb5a418022ad03c621201b74111a3b4c5fac61b1d82350170766cef8a27737d21ca9b1bd4e04f7cdea460706b68b14e0ed17f2a3de83f9131b 313 | ``` 314 | 315 | Verification result 316 | 317 | ```shell 318 | Signer address: 0x63B7076FC0A914Af543C2e5c201df6C29FCC18c5 319 | ``` 320 | 321 | If the signer address is different from the Committer's wallet address, you can treat this Commit as not trustworthy. 322 | -------------------------------------------------------------------------------- /bin/nit.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | process.title = "nit"; 6 | 7 | require("../lib/run"); -------------------------------------------------------------------------------- /example/hello.js: -------------------------------------------------------------------------------- 1 | const nit = require("@numbersprotocol/nit"); 2 | 3 | async function main() { 4 | const configTemplate = nit.nitconfigTemplate; 5 | console.log(`${JSON.stringify(configTemplate)}`); 6 | } 7 | 8 | main() 9 | .then(() => process.exit(0)) 10 | .catch((error) => { 11 | console.error(error); 12 | process.exit(1); 13 | }); 14 | -------------------------------------------------------------------------------- /example/hello.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ts-node 2 | 3 | import * as nit from "@numbersprotocol/nit"; 4 | 5 | async function main() { 6 | const configTemplate = nit.nitconfigTemplate; 7 | console.log(`${JSON.stringify(configTemplate)}`); 8 | } 9 | 10 | main() 11 | .then(() => process.exit(0)) 12 | .catch((error) => { 13 | console.error(error); 14 | process.exit(1); 15 | }); -------------------------------------------------------------------------------- /example/mocks/action.json: -------------------------------------------------------------------------------- 1 | { 2 | "networkAppName": "PiperNet", 3 | "blockchain": "4", 4 | "tokenAddress": "", 5 | "provider": "bafkreig7asqciqtugev26t35dt4d7voo2jmb3yz3472us4vvw7x62674oq", 6 | "abstract": "integrity registration" 7 | } -------------------------------------------------------------------------------- /example/mocks/assetTree.json: -------------------------------------------------------------------------------- 1 | { 2 | "assetCid": "bafybeihfzv2zzffznveiezqien5ejztfukjex7nhlki3hjy5zncsoccxdi", 3 | "assetSha256": "63560bb47ebfabb7ab4262dbc61e4445eaff184b1ee5a70b55aabe4c333bcef1", 4 | "assetCreator": "bafkreihkrnjvjeijjhalozcfpgrgb46673dlt4e3qm5bmvzzb4if423wse", 5 | "assetTimestampCreated": 1401649200, 6 | "license": { 7 | "name": "mit", 8 | "document": "https://opensource.org/licenses/MIT" 9 | }, 10 | "nftRecords": {}, 11 | "integrityCid": "bafkreicq7kqelooegxd6sq4rpwvxbdu36bz6wpgxeivmndxne4t6pxo22q", 12 | "integritySha256": "50faa045b9c435c7e943917dab708e9bf073eb3cd7222ac68eed2727e7dddad4", 13 | "abstract": "Richard's Middle out compression engine, Weissman score of 5.2", 14 | "encodingFormat": "image/jpeg" 15 | } 16 | -------------------------------------------------------------------------------- /example/mocks/commit.json: -------------------------------------------------------------------------------- 1 | { 2 | "assetTreeCid": "bafkreiea2zhpjokxdggtzzusrsfpatyz2gfadlk6obvpbhf24rmtiuqsfy", 3 | "assetTreeSha256": "80d64ef4b957198d3ce6928c8af04f19d18a01ad5e706af09cbae4593452122e", 4 | "author": "bafkreiabcfdxn7rqlzdbuqsdabhvoe2zc772ocy5c7l5pgzlkwgnc3vmza", 5 | "committer": "bafkreiabcfdxn7rqlzdbuqsdabhvoe2zc772ocy5c7l5pgzlkwgnc3vmza", 6 | "action": { 7 | "networkAppName": "networkActionTest", 8 | "blockchain": "4", 9 | "tokenAddress": "", 10 | "provider": "bafkreidcnhojuyk6epomati36v5llzcji3gncznusghs7kdw7krpi6dmwq", 11 | "abstract": "Execute network action test" 12 | }, 13 | "actionResult": { 14 | "txHash": "", 15 | "resultUri": "" 16 | }, 17 | "provider": "bafkreidcnhojuyk6epomati36v5llzcji3gncznusghs7kdw7krpi6dmwq", 18 | "abstract": "Initial commit for asset meimei fried chicken.", 19 | "timestampCreated": 1646731629 20 | } -------------------------------------------------------------------------------- /example/mocks/identityAssetCreator.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Richard Hendricks", 3 | "wallet": "0xRichardHendricksSiliconValleyPiedPiper42", 4 | "profile": "bafkreigx3k2ovitaecq2ln7us6xy5gr3fz5nrqm3r7ue5cu3qoas5o2yqy", 5 | "social": { 6 | "website": "https://web.archive.org/web/20211010022459/http://www.piedpiper.com/", 7 | "facebook": "https://www.facebook.com/SiliconHBO/", 8 | "twitter": "https://twitter.com/siliconhbo", 9 | "instagram": "https://www.instagram.com/siliconhbo" 10 | }, 11 | "type": "creator", 12 | "information": "CEO and founder of Pied Piper and PiperNet" 13 | } 14 | -------------------------------------------------------------------------------- /example/mocks/identityProofProvider.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Capture for Pied Piper", 3 | "wallet": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", 4 | "profile": "", 5 | "social": {}, 6 | "type": "provider", 7 | "information": "Capture beta version for Pied Piper" 8 | } 9 | -------------------------------------------------------------------------------- /example/mocks/identityServiceProvider.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Pied Piper", 3 | "wallet": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", 4 | "profile": "bafkreifal23aejdyfzpmqulxacimddqrod2kh6r2qsbwssh2gkwycjhptm", 5 | "social": { 6 | "website": "https://web.archive.org/web/20211010022459/http://www.piedpiper.com/", 7 | "facebook": "https://www.facebook.com/SiliconHBO/", 8 | "twitter": "https://twitter.com/siliconhbo", 9 | "instagram": "https://www.instagram.com/siliconhbo" 10 | }, 11 | "type": "provider", 12 | "information": "Over the years, Pied Piper has changed many landscapes. Compression. Data. The Internet. Click below to look back at the evolution of Pied Piper. Revolutionizing one landscape at a time." 13 | } 14 | -------------------------------------------------------------------------------- /example/mocks/proof.json: -------------------------------------------------------------------------------- 1 | { 2 | "device.app_build":"180", 3 | "device.app_id":"io.numbersprotocol.capturelite", 4 | "device.app_name":"Capture", 5 | "device.app_version":"0.35.0", 6 | "device.battery_level":0.4699999988079071, 7 | "device.device_name":"EXODUS 1", 8 | "device.disk_free":412057600, 9 | "device.disk_total":4225581056, 10 | "device.is_charging":true, 11 | "device.is_virtual":false, 12 | "device.manufacturer":"HTC", 13 | "device.mem_used":41826536, 14 | "device.operating_system":"android", 15 | "device.os_version":"9", 16 | "device.platform":"android", 17 | "device.uuid":"16eb24e0fa1f8a28", 18 | "geolocation.geolocation_latitude":25.0058495, 19 | "geolocation.geolocation_longitude":121.4586813 20 | } 21 | -------------------------------------------------------------------------------- /example/mocks/proofMetadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "timestampCreated": 1401649200, 3 | "proof": { 4 | "device.app_build":"180", 5 | "device.app_id":"io.numbersprotocol.capturelite", 6 | "device.app_name":"Capture", 7 | "device.app_version":"0.35.0", 8 | "device.battery_level":0.4699999988079071, 9 | "device.device_name":"EXODUS 1", 10 | "device.disk_free":412057600, 11 | "device.disk_total":4225581056, 12 | "device.is_charging":true, 13 | "device.is_virtual":false, 14 | "device.manufacturer":"HTC", 15 | "device.mem_used":41826536, 16 | "device.operating_system":"android", 17 | "device.os_version":"9", 18 | "device.platform":"android", 19 | "device.uuid":"16eb24e0fa1f8a28", 20 | "geolocation.geolocation_latitude":37.781937, 21 | "geolocation.geolocation_longitude":-122.406063 22 | }, 23 | "proofSha256": "fee45b675643240694a801312d1e2a4cecf85fd5c9427a1b102a5fcc298d2751", 24 | "proofSignature": "", 25 | "provider": "bafkreiajfinqk63kbqrsrs7fjwyszqdpdk3jevcs6py66p4i33ooq6rnny" 26 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@numbersprotocol/nit", 3 | "version": "1.6.1", 4 | "description": "git for web3 digital asset", 5 | "main": "lib/nit.js", 6 | "repository": "https://github.com/numbersprotocol/nit", 7 | "author": "Numbers Protocol", 8 | "license": "GPL-3.0-or-later", 9 | "dependencies": { 10 | "@numbersprotocol/estuary-upload": "^1.1.0", 11 | "@types/mime-types": "^2.1.1", 12 | "axios": "1.2.0-alpha.1", 13 | "colors": "^1.4.0", 14 | "command-line-args": "^5.2.1", 15 | "command-line-usage": "^6.1.1", 16 | "crypto-js": "^4.1.1", 17 | "diff": "^5.1.0", 18 | "ethers": "^5.5.4", 19 | "form-data": "^4.0.0", 20 | "got": "^11.8.2", 21 | "launch-editor": "^2.3.0" 22 | }, 23 | "scripts": { 24 | "build": "yarn run clean && tsc", 25 | "clean": "rm -rf lib/", 26 | "dev-global-install": "yarn global add file:$PWD", 27 | "dev-global-uninstall": "yarn global remove @numbersprotocol/nit", 28 | "dev-install": "yarn run build && yarn add file:$PWD", 29 | "dev-uninstall": "yarn remove @numbersprotocol/nit && yarn run clean", 30 | "tests": "mocha -r ts-node/register tests/**/*.ts", 31 | "test": "mocha -r ts-node/register" 32 | }, 33 | "bin": "bin/nit.js", 34 | "devDependencies": { 35 | "@rollup/plugin-commonjs": "^22.0.2", 36 | "@rollup/plugin-json": "^4.1.0", 37 | "@rollup/plugin-node-resolve": "^14.1.0", 38 | "@rollup/plugin-replace": "^4.0.0", 39 | "@types/chai": "^4.3.1", 40 | "@types/command-line-args": "^5.2.0", 41 | "@types/command-line-usage": "^5.0.2", 42 | "@types/crypto-js": "^4.1.1", 43 | "@types/mocha": "^9.1.0", 44 | "@types/node": "^18.16.1", 45 | "@typescript-eslint/eslint-plugin": "^5.51.0", 46 | "@typescript-eslint/parser": "^5.51.0", 47 | "chai": "^4.3.6", 48 | "eslint": "^8.33.0", 49 | "mocha": "^9.2.2", 50 | "rollup-plugin-polyfill-node": "^0.11.0", 51 | "rollup-plugin-sourcemaps": "^0.6.3", 52 | "ts-node": "^10.7.0", 53 | "typescript": "^4.6.3" 54 | }, 55 | "files": [ 56 | "IntegrityRegister.json", 57 | "/example", 58 | "/lib", 59 | "/bin" 60 | ], 61 | "volta": { 62 | "node": "18.16.0" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /schema/dbml.md: -------------------------------------------------------------------------------- 1 | ```dbml 2 | Project IntegrityRecord { 3 | Note: ''' 4 | description: Numbers Protocol Integrity Record spec 5 | version: 4.3.0 6 | date: 2023-05-11 7 | colors: 8 | - purple: on blockchain (updatable) 9 | - blue: files on IPFS (updatable) 10 | - yellow: json 11 | - red: signature related 12 | - grey: centralized server or DID 13 | ''' 14 | } 15 | 16 | Table commit [headercolor: #8e44ad] { 17 | _ none [note: 'placeholder for linking tables'] 18 | assetCid cid [note: 'This is NOT in the commit but the on-chain index'] 19 | assetTreeCid cid [note: 'CID of the asset Tree file'] 20 | assetTreeSha256 sha256 [note: 'sha256sum of the asset Tree file'] 21 | assetTreeSignature signature [note: 'EIP-191 signature signed by author.'] 22 | committer address [note: 'Who registers the commit'] 23 | author address [note: 'Who write the commit'] 24 | action cid [note: 'CID of the action profile describing the action details including actionName.'] 25 | actionName str [note: 'name of the action, unique string'] 26 | actionResult str [note: 'Result uri of this action'] 27 | provider cid [note: 'CID of the commit service provider.'] 28 | attachment str [note: 'the Nid of the attachment file.'] 29 | 30 | Note: ''' 31 | 1. The goal of Integrity Record is to ensure integrity of the raw asset and its derivatives like metadata and maybe the major asset. 32 | 33 | 2. Integrity Wallet will sign sha256sum of Integrity Record. 34 | ''' 35 | } 36 | 37 | Table action [headercolor: #3498db] { 38 | _ none [note: 'placeholder for linking tables'] 39 | networkActionName str [note: 'name of the network action'] 40 | blockchain network 41 | tokenAddress str [note: 'address of the token required for the action payment'] 42 | provider cid [note: 'CID of the network action provider.'] 43 | abstract str [note: 'description of this action including how to read the results'] 44 | type actionType 45 | Note: ''' 46 | Network Actions are the Apps interacting with Assets. Every action will be recorded on blockchain. 47 | ''' 48 | } 49 | 50 | 51 | Table identity [headercolor: #3498db] { 52 | 53 | _ none [note: 'placeholder for linking tables'] 54 | name str [note: 'provider\'s name', default: 'NUMBERSPROTOCOL'] 55 | wallet address [note: 'integrity wallet address, including 0x'] 56 | profile str [note: 'IPFS address of the profile picture'] 57 | social json [note: 'social links'] 58 | type identityType [note: 'the type of this wallet owner'] 59 | information json [note: 'additional info of this individual'] 60 | 61 | Note: ''' 62 | This table should be handled by DID or similar technology, not the core technology Numbers considered 63 | ''' 64 | } 65 | 66 | Table assetTree [headercolor: #3498db] { 67 | _ none [note: 'placeholder for linking tables'] 68 | assetCid cid [note: 'CID of the asset file (blob)'] 69 | assetSha256 sha256 [note: 'sha256sum of the asset file (blob)'] 70 | encodingFormat str [note: 'The asset\'s type expressed using a MIME format.'] 71 | assetCreator str [note: 'Creator\'s name'] 72 | creatorProfile cid [note: 'Creator of the asset file'] 73 | creatorWallet address [note: 'Creator\'s wallet address'] 74 | assetTimestampCreated timestamp [note: 'Creation time of the asset file'] 75 | assetLocationCreated str [note: 'Creation location of the asset file'] 76 | parentAssetCid cid [note: 'Cid of the parent asset'] 77 | generatedBy str [note: 'AI model used to generate the content'] 78 | generatedThrough str [note: 'URL of AI service'] 79 | usedBy str [note: 'URL of the website that uses the asset'] 80 | license license [note: 'license of the asset file'] 81 | nftRecord "nftRecord[]" [note: 'List of NFT records'] 82 | integrityCid cid [note: 'CID of the integrity proof'] 83 | abstract str [note: 'description of this asset'] 84 | custom json [note: 'custom fields'] 85 | 86 | 87 | Note: ''' 88 | EIP-191 Verification 89 | Input: data, signature 90 | Output: signer's wallet address 91 | ''' 92 | } 93 | 94 | Table assetTreeVerification [headercolor: #c0392b] { 95 | _ none [note: 'placeholder for linking tables'] 96 | assetTreeSha256 sha256 [note: 'sha256sum of commit'] 97 | assetTreeSignature str [note: 'commit service provider signs metadata and generates EIP-191 signature.'] 98 | 99 | Note: '''Verify integrity of Asset Tree. 100 | 101 | Author (Asset Creator) creates the integritySignature. 102 | 103 | EIP-191 Verification 104 | Input: data (integrity hash in sha256), signature 105 | Output: Author's wallet address 106 | ''' 107 | } 108 | 109 | Table license [headercolor: #3498db] { 110 | _ none [note: 'placeholder for linking tables'] 111 | name str [note: 'official name of this license'] 112 | document str [note: 'IPFS file for details of this license'] 113 | } 114 | 115 | Table nftRecord [headercolor: #3498db] { 116 | _ none [note: 'placeholder for linking tables'] 117 | network network [note: 'network of the NFT token'] 118 | contractAddress address 119 | tokenId str 120 | } 121 | 122 | Table nft [headercolor: #8e44ad] { 123 | tokenId str 124 | tokenUri str 125 | } 126 | 127 | Table proofVerification [headercolor: #c0392b] { 128 | _ none [note: 'placeholder for linking tables'] 129 | proofSha256 sha256 [note: 'sha256sum of commit'] 130 | proofSignature str [note: 'commit service provider signs metadata and generates EIP-191 signature.'] 131 | 132 | Note: '''Verify integrity of Proof. 133 | 134 | Proof Recorder creates the proofSignature. 135 | 136 | EIP-191 Verification 137 | Input: data (integrity hash in sha256), signature 138 | Output: Proof Recorder's wallet address 139 | ''' 140 | } 141 | 142 | Table proofMetadata [headercolor: #3498db] { 143 | _ none [note: 'placeholder for linking tables'] 144 | //schemaVersion str [note: 'Metadata schema version with semantic versioning format a.b.c'] 145 | 146 | timestampCreated timestamp [note: 'The moment that Metadata is created in Unix timestamp'] 147 | 148 | proof json [note: 'System/Device information including both HW/SW'] 149 | proofSha256 sha256 [note: 'sha256sum of proof'] 150 | proofSignature str [note: '(Optional) Recorder signs proof and generates EIP-191 signature'] 151 | provider cid [note: 'Proof Recorder'] 152 | 153 | Note: ''' 154 | 1. The goal of integrity_metadata is to provide additional asset-related data including who/when/where/ownership. 155 | 156 | 2. Recorder is a SW module collecting the SW and HW system information. 157 | 158 | 3. A trustworthy Recorder generates proof_signature as proof of its credibility. 159 | ''' 160 | } 161 | 162 | 163 | Table proof [headercolor: #f39c12] { 164 | _ none [note: 'placeholder for linking tables'] 165 | _key proofKey 166 | 167 | Note: ''' 168 | The SW and HW system information collected by Recorder 169 | ''' 170 | } 171 | 172 | Enum "network" { //The main blockchain of the network action (bloakchain of the payment wallet) 173 | "ethereum" 174 | "bsc" 175 | "avalanche" 176 | "polygon" 177 | "thundercore" 178 | "cosmos" 179 | } 180 | 181 | Enum "identityType" { 182 | "individual" 183 | "business" 184 | "school" 185 | "ngo" 186 | } 187 | 188 | Enum "actionType" { 189 | "new" [note: 'The action will newly register the initial commit.'] 190 | "update" [note: 'The action will update attribute to asset and will create a new assetTree.'] 191 | "derive" [note: 'The action will modify asset and create child asset.'] 192 | "use" [note: 'The action will use asset to create derived result without modifying the asset.'] 193 | } 194 | 195 | Enum "proofKey" { 196 | "device.app_build" [note: 'ex: 311'] 197 | "device.app_id" [note: 'ex: io.numbersprotocol.capturelite'] 198 | "device.app_name" [note: 'ex: Capture'] 199 | "device.app_version" [note: 'ex: 0.48.1'] 200 | "device.battery_level" [note: 'ex: 0.6700000166893005'] 201 | "device.device_name" [note: 'ex: SM-F9160'] 202 | "device.disk_free" [note: 'ex: 29540352'] 203 | "device.disk_total" [note: 'ex: 5950615552'] 204 | "device.is_charging" [note: 'ex: false'] 205 | "device.is_virtual" [note: 'ex: false'] 206 | "device.manufacturer" [note: 'ex: "samsung'] 207 | "device.mem_used" [note: 'ex: 68584792'] 208 | "device.operating_system" [note: 'ex: android'] 209 | "device.os_version" [note: 'ex: 12'] 210 | "device.platform" [note: 'ex: android'] 211 | "device.uuid" [note: 'ex: 755107ce5510dc60'] 212 | "geolocation.geolocation_latitude" [note: 'ex: 24.9959323'] 213 | "geolocation.geolocation_longitude" [note: 'ex: 121.5104948'] 214 | } 215 | 216 | Ref: "nftRecord"."tokenId" - "nft"."tokenId" 217 | Ref: "assetTree"."license" - "license"."_" 218 | Ref: "assetTree"."integrityCid" - "proofMetadata"."_" 219 | Ref: "assetTree"."creatorProfile" - "identity"."_" 220 | Ref: "assetTree".nftRecord < nftRecord."_" // One asset can link to multiple nft_records 221 | Ref: "commit"."action" - "action"."_" 222 | Ref: "commit"."provider" - "identity"."_" 223 | Ref: "action"."provider" - "identity"."_" 224 | Ref: "proofMetadata"."provider" - "identity"."_" 225 | Ref: "proofMetadata".proof - proof._ 226 | Ref: "commit"."assetTreeCid" - "assetTree"."_" 227 | Ref: "commit"."committer" - "identity"."_" 228 | Ref: "proofMetadata"."proofSha256" - "proofVerification"."proofSha256" 229 | Ref: "proofMetadata"."proofSignature" - "proofVerification"."proofSignature" 230 | Ref: "commit"."assetTreeSha256" - "assetTreeVerification"."assetTreeSha256" 231 | Ref: "commit"."assetTreeSignature" - "assetTreeVerification"."assetTreeSignature" 232 | 233 | Ref: "action"."tokenAddress" < "action"."_" 234 | ``` -------------------------------------------------------------------------------- /scripts/parseTxNid.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ts-node 2 | 3 | /* 4 | Execution: ts-node parseTxNid.ts 5 | Output: Nid: bafybeiajl3iadbz5phbig6ojxrau22a67lzrba2srdqn2jzy3bpl7oxd6u 6 | */ 7 | 8 | import * as ethers from "ethers"; 9 | 10 | async function main() { 11 | const provider = new ethers.providers.JsonRpcProvider("https://mainnetrpc.num.network"); 12 | // Tx Sample: https://mainnet.num.network/transaction/0xca942395e9c36b3217422979978838a45891336ca54bb036d6aecd43f0b56187 13 | const txHash = "0xca942395e9c36b3217422979978838a45891336ca54bb036d6aecd43f0b56187"; 14 | 15 | // parse the transaction with the function signature 16 | const abi = [ "function commit(string memory assetCid, string memory commitData) public returns (uint256 blockNumber)" ]; 17 | const iface = new ethers.utils.Interface(abi); 18 | 19 | const txData = await provider.getTransaction(txHash); 20 | // uncomment the following line to see the raw transaction data 21 | //console.log(txData); 22 | 23 | const decodedTxData = iface.parseTransaction(txData); 24 | // uncomment the following line to see the decoded transaction data 25 | //console.log(decodedTxData); 26 | 27 | console.log(`Nid: ${decodedTxData.args[0]}`); 28 | } 29 | 30 | main() 31 | .then(() => process.exit(0)) 32 | .catch((error) => { 33 | console.error(error); 34 | process.exit(1); 35 | }); 36 | -------------------------------------------------------------------------------- /src/action.ts: -------------------------------------------------------------------------------- 1 | /* Spec of Actions 2 | * https://docs.numbersprotocol.io/introduction/numbers-protocol/defining-web3-assets/commit#actions 3 | */ 4 | 5 | export const ActionSet = { 6 | // default, the same as action-initial-registration-jade 7 | "action-initial-registration": { 8 | nid: "bafkreicptxn6f752c4pvb6gqwro7s7wb336idkzr6wmolkifj3aafhvwii", 9 | content: { 10 | "networkActionName": "initial registration", 11 | "blockchain": "jade", 12 | "tokenAddress": "", 13 | "provider": "bafkreigrt5tepycewppysdwcjccdkdvvc2ztelqv64idgautq52g3vfh4i", 14 | "abstract": "Register asset to the Numbers network", 15 | "type": "new" 16 | } 17 | }, 18 | "action-initial-registration-avalanche": { 19 | nid: "bafkreiavifzn7ntlb6p2lr55k3oezo6pofwvknecukc5auhng4miowcte4", 20 | content: { 21 | "networkActionName": "initial registration", 22 | "blockchain": "avalanche", 23 | "tokenAddress": "", 24 | "provider": "bafkreido4zu743f6isb5wninfkedvbirj2ngb5fkivrpdijh2xtd3s6rnu", 25 | "abstract": "Register asset to the Numbers network", 26 | "type": "new" 27 | } 28 | }, 29 | "action-initial-registration-jade": { 30 | nid: "bafkreicptxn6f752c4pvb6gqwro7s7wb336idkzr6wmolkifj3aafhvwii", 31 | content: { 32 | "networkActionName": "initial registration", 33 | "blockchain": "jade", 34 | "tokenAddress": "", 35 | "provider": "bafkreigrt5tepycewppysdwcjccdkdvvc2ztelqv64idgautq52g3vfh4i", 36 | "abstract": "Register asset to the Numbers network", 37 | "type": "new" 38 | } 39 | }, 40 | "action-initial-registration-iota": { 41 | nid: "bafkreibnyl3ohbx76rzxmdzfyayvztjcqgjv745wh4t3z6a4ung43xd554", 42 | content: { 43 | "networkActionName": "initial registration", 44 | "blockchain": "iota", 45 | "tokenAddress": "", 46 | "provider": "bafkreigrt5tepycewppysdwcjccdkdvvc2ztelqv64idgautq52g3vfh4i", 47 | "abstract": "Register asset to the Numbers network", 48 | "type": "new" 49 | } 50 | }, 51 | 52 | "action-commit": { 53 | nid: "bafkreiceiqfe4w4g4j46rsi4h4tbqu35ivewfxm4q743tfmegscpg7d63m", 54 | content: { 55 | "networkActionName": "commit", 56 | "blockchain": "jade", 57 | "tokenAddress": "", 58 | "provider": "bafkreigrt5tepycewppysdwcjccdkdvvc2ztelqv64idgautq52g3vfh4i", 59 | "abstract": "Commit asset history to Numbers network", 60 | "type": "update" 61 | } 62 | }, 63 | 64 | // default, the same as action-mint-erc721-nft-jade 65 | "action-mint-erc721-nft": { 66 | nid: "bafkreiakk6le5wgbl3zz3acybd2wfc2y32mpmxj4kbjjjskom7cy2akwly", 67 | content: { 68 | "networkActionName": "mint ERC-721 NFT", 69 | "blockchain": "jade", 70 | "tokenAddress": "", 71 | "provider": "bafkreigrt5tepycewppysdwcjccdkdvvc2ztelqv64idgautq52g3vfh4i", 72 | "abstract": "Mint ERC-721 NFT on Jade", 73 | "type": "update" 74 | } 75 | }, 76 | "action-mint-erc721-nft-avalanche": { 77 | nid: "bafkreibro4v4vsibvr47uwhkj6gqpc5rfvvq5ykfzzz4jnzvm23zwoh2gq", 78 | content: { 79 | "networkActionName": "mint ERC-721 NFT", 80 | "blockchain": "avalanche", 81 | "tokenAddress": "", 82 | "provider": "bafkreido4zu743f6isb5wninfkedvbirj2ngb5fkivrpdijh2xtd3s6rnu", 83 | "abstract": "Mint ERC-721 NFT on Avalanche", 84 | "type": "update" 85 | } 86 | }, 87 | "action-mint-erc721-nft-ethereum": { 88 | nid: "bafkreighzhmelvwlntigjq6ushun4mz36x6cq5nztkvcac47bdxz2ipeca", 89 | content: { 90 | "networkActionName": "mint ERC-721 NFT", 91 | "blockchain": "ethereum", 92 | "tokenAddress": "", 93 | "provider": "bafkreido4zu743f6isb5wninfkedvbirj2ngb5fkivrpdijh2xtd3s6rnu", 94 | "abstract": "Mint ERC-721 NFT on Ethereum", 95 | "type": "update" 96 | } 97 | }, 98 | "action-mint-erc721-nft-jade": { 99 | nid: "bafkreiakk6le5wgbl3zz3acybd2wfc2y32mpmxj4kbjjjskom7cy2akwly", 100 | content: { 101 | "networkActionName": "mint ERC-721 NFT", 102 | "blockchain": "jade", 103 | "tokenAddress": "", 104 | "provider": "bafkreigrt5tepycewppysdwcjccdkdvvc2ztelqv64idgautq52g3vfh4i", 105 | "abstract": "Mint ERC-721 NFT on Jade", 106 | "type": "update" 107 | } 108 | }, 109 | "action-mint-erc721-nft-polygon": { 110 | nid: "bafkreigdxfmwhhepusyhaoidyzy6bv7jzpc5liqohyc6numgvvbfwabn6a", 111 | content: { 112 | "networkActionName": "mint ERC-721 NFT", 113 | "blockchain": "polygon", 114 | "tokenAddress": "", 115 | "provider": "bafkreido4zu743f6isb5wninfkedvbirj2ngb5fkivrpdijh2xtd3s6rnu", 116 | "abstract": "Mint ERC-721 NFT on Polygon", 117 | "type": "update" 118 | } 119 | }, 120 | "action-mint-erc721-nft-thundercore": { 121 | nid: "bafkreih7gzxeukcnosbotvpq7hw6htlxoi2dzzm2nnkskloazgw2sbh55i", 122 | content: { 123 | "networkActionName": "mint ERC-721 NFT", 124 | "blockchain": "thundercore", 125 | "tokenAddress": "", 126 | "provider": "bafkreido4zu743f6isb5wninfkedvbirj2ngb5fkivrpdijh2xtd3s6rnu", 127 | "abstract": "Mint ERC-721 NFT on ThunderCore", 128 | "type": "update" 129 | } 130 | }, 131 | }; 132 | 133 | /* DESIGN: To keep backward compatibility, we keep Actions. 134 | */ 135 | 136 | function generateNidDictionary(actionSet) { 137 | const nidDictionary = {}; 138 | 139 | for (const key in actionSet) { 140 | if (actionSet.hasOwnProperty(key)) { 141 | nidDictionary[key] = actionSet[key].nid; 142 | } 143 | } 144 | 145 | return nidDictionary; 146 | } 147 | 148 | export const Actions = generateNidDictionary(ActionSet); 149 | 150 | /* Return the action key if it's valid, otherwise return "commit". */ 151 | export function getValidActionOrDefault(action: string): string { 152 | return Actions.hasOwnProperty(action) ? action : "action-commit"; 153 | } 154 | 155 | export function generateNameDictionary(actionSet) { 156 | const keyDictionary = {}; 157 | 158 | for (const key in actionSet) { 159 | if (actionSet.hasOwnProperty(key)) { 160 | const nid = actionSet[key].nid; 161 | keyDictionary[nid] = key; 162 | } 163 | } 164 | 165 | return keyDictionary; 166 | } 167 | 168 | export function getNameByNid(actionNid) { 169 | const nameDictionary = generateNameDictionary(ActionSet); 170 | 171 | if (nameDictionary.hasOwnProperty(actionNid)) { 172 | return nameDictionary[actionNid]; 173 | } else { 174 | return 'action-commit'; 175 | } 176 | } -------------------------------------------------------------------------------- /src/commitdb.ts: -------------------------------------------------------------------------------- 1 | import { ethers, utils } from "ethers"; 2 | 3 | import axios from 'axios'; 4 | import FormData = require("form-data"); 5 | 6 | import * as ipfs from "./ipfs"; 7 | import * as nit from "./nit"; 8 | import * as util from "./util"; 9 | 10 | interface ExtendedCommit { 11 | commitEventIndex; 12 | blockNumber; 13 | transactionHash; 14 | commit; 15 | assetCid; 16 | assetTree; 17 | author; 18 | authorWalletAddress; 19 | committer; 20 | committerWalletAddress; 21 | provider; 22 | license; 23 | action; 24 | integrity; 25 | assetCreatedIsoTime: string; 26 | commitCreatedIsoTime: string; 27 | blockchain: string; 28 | } 29 | 30 | async function extend(commitEventIndex, commitSummary, blockchainName) { 31 | const blockNumber = commitSummary.blockNumber ; 32 | const transactionHash = commitSummary.transactionHash; 33 | const commitObject = commitSummary.commit; 34 | const commit = JSON.stringify(commitObject); 35 | 36 | const author = await ipfs.cidToJsonString(commitObject.author); 37 | const authorWalletAddress = JSON.parse(author).wallet; 38 | const committer = await ipfs.cidToJsonString(commitObject.committer); 39 | const committerWalletAddress = JSON.parse(committer).wallet; 40 | const provider = await ipfs.cidToJsonString(commitObject.provider); 41 | const action = await ipfs.cidToJsonString(commitObject.action); 42 | const commitCreatedIsoTime = util.timestampToIsoString(commitObject.timestampCreated); 43 | const assetTree = await ipfs.cidToJsonString(commitObject.assetTreeCid); 44 | const assetTreeObject = JSON.parse(assetTree); 45 | const assetCid = assetTreeObject.assetCid; 46 | const license = await ipfs.cidToJsonString(assetTreeObject.provider); 47 | const integrity = await ipfs.cidToJsonString(assetTreeObject.integrityCid); 48 | const assetCreatedIsoTime = util.timestampToIsoString(assetTreeObject.assetTimestampCreated); 49 | const blockchain = blockchainName; 50 | 51 | const extendedCommit: ExtendedCommit = { 52 | "commitEventIndex": commitEventIndex, 53 | "blockNumber": blockNumber, 54 | "transactionHash": transactionHash, 55 | "commit": commit, 56 | "author": author, 57 | "authorWalletAddress": authorWalletAddress, 58 | "committer": committer, 59 | "committerWalletAddress": committerWalletAddress, 60 | "provider": provider, 61 | "action": action, 62 | "commitCreatedIsoTime": commitCreatedIsoTime, 63 | "assetTree": assetTree, 64 | "assetCid": assetCid, 65 | "license": license, 66 | "integrity": integrity, 67 | "assetCreatedIsoTime": assetCreatedIsoTime, 68 | "blockchain": blockchain, 69 | }; 70 | 71 | return extendedCommit; 72 | } 73 | 74 | export async function extendCommits(assetCid, blockchainInfo, fromIndex, toIndex) { 75 | const commitEvents = await nit.iterateCommitEvents(assetCid, blockchainInfo, fromIndex, toIndex); 76 | const commitsSummary = await nit.getCommits(commitEvents); 77 | await nit.showCommits(commitsSummary); 78 | let extendedCommits = []; 79 | for (const summary of commitsSummary) { 80 | const extendedCommit = await extend(assetCid, summary, nit.blockchainNames[blockchainInfo.chainId]) 81 | extendedCommits.push(extendedCommit); 82 | } 83 | return extendedCommits; 84 | } 85 | 86 | // bubble -> calculate how many items to retrieve -> call update -> push extended commints to db -> return updated entry amount 87 | export async function update(assetCid, blockchainInfo, dbEntryAmount, dbEndpointUrl, accessToken=null) { 88 | const onchainCommitAmount = (await nit.getCommitBlockNumbers(assetCid, blockchainInfo)).length; 89 | const updateFromIndex = dbEntryAmount; 90 | const updateToIndex = onchainCommitAmount; 91 | const extendedCommits = await extendCommits(assetCid, blockchainInfo, updateFromIndex, updateToIndex); 92 | 93 | for (const extendedCommit of extendedCommits) { 94 | const r = await httpPost(dbEndpointUrl, extendedCommit, accessToken); 95 | } 96 | 97 | return { 98 | "originalDbEntryAmount": parseInt(dbEntryAmount), 99 | "updateDbEntryAmount": updateToIndex - updateFromIndex, 100 | }; 101 | } 102 | 103 | export async function httpPost(url, data, accessToken=null) { 104 | const formData = new FormData(); 105 | for (const key of Object.keys(data)) { 106 | formData.append(key, data[key] ? data[key] : ""); 107 | } 108 | 109 | //const authBase64 = Buffer.from(`${ProjectId}:${ProjectSecret}`).toString('base64'); 110 | const requestConfig = { 111 | "headers": { 112 | //"Authorization": `Bearer ${authBase64}`, 113 | ...formData.getHeaders(), 114 | }, 115 | } 116 | if (accessToken != null) { 117 | requestConfig.headers.Authorization = `Bearer ${accessToken}`; 118 | } 119 | const r = await axios.post(url, formData, requestConfig); 120 | const returnedData = r.data; 121 | return returnedData; 122 | } 123 | 124 | export async function push(commitDbUpdateUrl: string, assetCid: string, commitDbCommitUrl: string) { 125 | const data = { 126 | "assetCid": assetCid, 127 | "dbEndpointUrl": commitDbCommitUrl, 128 | }; 129 | const r = await httpPost(commitDbUpdateUrl, data); 130 | return r; 131 | } -------------------------------------------------------------------------------- /src/contract.ts: -------------------------------------------------------------------------------- 1 | const artifact = { 2 | "_format": "hh-sol-artifact-1", 3 | "contractName": "IntegrityRegister", 4 | "sourceName": "contracts/IntegrityRegister.sol", 5 | "abi": [ 6 | { 7 | "anonymous": false, 8 | "inputs": [ 9 | { 10 | "indexed": true, 11 | "internalType": "address", 12 | "name": "recorder", 13 | "type": "address" 14 | }, 15 | { 16 | "indexed": true, 17 | "internalType": "string", 18 | "name": "assetCid", 19 | "type": "string" 20 | }, 21 | { 22 | "indexed": false, 23 | "internalType": "string", 24 | "name": "commitData", 25 | "type": "string" 26 | } 27 | ], 28 | "name": "Commit", 29 | "type": "event" 30 | }, 31 | { 32 | "anonymous": false, 33 | "inputs": [ 34 | { 35 | "indexed": true, 36 | "internalType": "address", 37 | "name": "recorder", 38 | "type": "address" 39 | }, 40 | { 41 | "indexed": true, 42 | "internalType": "string", 43 | "name": "assetCid", 44 | "type": "string" 45 | }, 46 | { 47 | "indexed": false, 48 | "internalType": "string", 49 | "name": "integrityData", 50 | "type": "string" 51 | }, 52 | { 53 | "indexed": false, 54 | "internalType": "bytes", 55 | "name": "signature", 56 | "type": "bytes" 57 | } 58 | ], 59 | "name": "Integrity", 60 | "type": "event" 61 | }, 62 | { 63 | "inputs": [ 64 | { 65 | "internalType": "string", 66 | "name": "assetCid", 67 | "type": "string" 68 | }, 69 | { 70 | "internalType": "string", 71 | "name": "commitData", 72 | "type": "string" 73 | } 74 | ], 75 | "name": "commit", 76 | "outputs": [ 77 | { 78 | "internalType": "uint256", 79 | "name": "blockNumber", 80 | "type": "uint256" 81 | } 82 | ], 83 | "stateMutability": "nonpayable", 84 | "type": "function" 85 | }, 86 | { 87 | "inputs": [ 88 | { 89 | "internalType": "string", 90 | "name": "assetCid", 91 | "type": "string" 92 | } 93 | ], 94 | "name": "getCommits", 95 | "outputs": [ 96 | { 97 | "internalType": "uint256[]", 98 | "name": "", 99 | "type": "uint256[]" 100 | } 101 | ], 102 | "stateMutability": "view", 103 | "type": "function" 104 | }, 105 | { 106 | "inputs": [ 107 | { 108 | "internalType": "string", 109 | "name": "assetCid", 110 | "type": "string" 111 | } 112 | ], 113 | "name": "getRecordLogs", 114 | "outputs": [ 115 | { 116 | "internalType": "uint256[]", 117 | "name": "", 118 | "type": "uint256[]" 119 | } 120 | ], 121 | "stateMutability": "view", 122 | "type": "function" 123 | }, 124 | { 125 | "inputs": [], 126 | "name": "initialize", 127 | "outputs": [], 128 | "stateMutability": "nonpayable", 129 | "type": "function" 130 | }, 131 | { 132 | "inputs": [ 133 | { 134 | "internalType": "string", 135 | "name": "", 136 | "type": "string" 137 | }, 138 | { 139 | "internalType": "uint256", 140 | "name": "", 141 | "type": "uint256" 142 | } 143 | ], 144 | "name": "recordLogs", 145 | "outputs": [ 146 | { 147 | "internalType": "uint256", 148 | "name": "", 149 | "type": "uint256" 150 | } 151 | ], 152 | "stateMutability": "view", 153 | "type": "function" 154 | }, 155 | { 156 | "inputs": [ 157 | { 158 | "internalType": "string", 159 | "name": "assetCid", 160 | "type": "string" 161 | }, 162 | { 163 | "internalType": "string", 164 | "name": "integrityData", 165 | "type": "string" 166 | }, 167 | { 168 | "internalType": "bytes", 169 | "name": "signature", 170 | "type": "bytes" 171 | } 172 | ], 173 | "name": "registerIntegrityRecord", 174 | "outputs": [], 175 | "stateMutability": "nonpayable", 176 | "type": "function" 177 | } 178 | ], 179 | "bytecode": "0x608060405234801561001057600080fd5b5061198d806100206000396000f3fe608060405234801561001057600080fd5b50600436106100625760003560e01c80636715cbe6146100675780638129fc1c1461009757806381d14ffc146100a1578063acf64771146100d1578063da33873e146100ed578063ed9b95e21461011d575b600080fd5b610081600480360381019061007c9190610f11565b61014d565b60405161008e9190611365565b60405180910390f35b61009f6101c3565b005b6100bb60048036038101906100b69190610f52565b6102a7565b6040516100c89190611575565b60405180910390f35b6100eb60048036038101906100e69190610fbe565b61039a565b005b61010760048036038101906101029190610f11565b610543565b6040516101149190611365565b60405180910390f35b61013760048036038101906101329190611055565b6105b9565b6040516101449190611575565b60405180910390f35b606060018260405161015f9190611328565b90815260200160405180910390208054806020026020016040519081016040528092919081815260200182805480156101b757602002820191906000526020600020905b8154815260200190600101908083116101a3575b50505050509050919050565b600060019054906101000a900460ff166101eb5760008054906101000a900460ff16156101f4565b6101f3610600565b5b610233576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161022a90611535565b60405180910390fd5b60008060019054906101000a900460ff161590508015610283576001600060016101000a81548160ff02191690831515021790555060016000806101000a81548160ff0219169083151502179055505b80156102a45760008060016101000a81548160ff0219169083151502179055505b50565b6000826040516102b79190611328565b60405180910390203373ffffffffffffffffffffffffffffffffffffffff167f39b5d1808997c4c413695960efbe703ad3d5c8982d2eb21ff57b01e22a6715328460405161030591906113cc565b60405180910390a361034c6040518060400160405280601d81526020017f436f6d6d6974206576656e7420626c6f636b206e756d6265723a20257300000081525043610611565b60018360405161035c9190611328565b908152602001604051809103902043908060018154018082558091505060019003906000526020600020016000909190919091505543905092915050565b6000826040516020016103ad9190611328565b60405160208183030381529060405280519060200120905060006103d182846106ad565b90506104126040518060400160405280601181526020017f56616c6964207369676e61747572653a2000000000000000000000000000000081525082610762565b80610452576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610449906114f5565b60405180910390fd5b846040516104609190611328565b60405180910390203373ffffffffffffffffffffffffffffffffffffffff167ff14cc285ed29e95249df9be8f97c6d27c13da6147ac82ffde11e0e7cbac00e4e86866040516104b092919061144e565b60405180910390a36104f76040518060400160405280602081526020017f496e74656772697479206576656e7420626c6f636b206e756d6265723a20257381525043610611565b6001856040516105079190611328565b90815260200160405180910390204390806001815401808255809150506001900390600052602060002001600090919091909150555050505050565b60606001826040516105559190611328565b90815260200160405180910390208054806020026020016040519081016040528092919081815260200182805480156105ad57602002820191906000526020600020905b815481526020019060010190808311610599575b50505050509050919050565b60018280516020810182018051848252602083016020850120818352809550505050505081815481106105eb57600080fd5b90600052602060002001600091509150505481565b600061060b306107fe565b15905090565b6106a98282604051602401610627929190611485565b6040516020818303038152906040527f9710a9d0000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff8381831617835250505050610811565b5050565b600080836040516020016106c1919061133f565b6040516020818303038152906040528051906020012090506107186040518060400160405280601081526020017f4d6573736167652073656e6465723a20000000000000000000000000000000008152503361083a565b3373ffffffffffffffffffffffffffffffffffffffff1661074284836108d690919063ffffffff16565b73ffffffffffffffffffffffffffffffffffffffff161491505092915050565b6107fa828260405160240161077892919061141e565b6040516020818303038152906040527fc3b55635000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff8381831617835250505050610811565b5050565b600080823b905060008111915050919050565b60008151905060006a636f6e736f6c652e6c6f679050602083016000808483855afa5050505050565b6108d282826040516024016108509291906113ee565b6040516020818303038152906040527f319af333000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff8381831617835250505050610811565b5050565b60008060006108e585856108fd565b915091506108f281610980565b819250505092915050565b60008060418351141561093f5760008060006020860151925060408601519150606086015160001a905061093387828585610cd1565b94509450505050610979565b604083511415610970576000806020850151915060408501519050610965868383610dde565b935093505050610979565b60006002915091505b9250929050565b600060048111156109ba577f4e487b7100000000000000000000000000000000000000000000000000000000600052602160045260246000fd5b8160048111156109f3577f4e487b7100000000000000000000000000000000000000000000000000000000600052602160045260246000fd5b14156109fe57610cce565b60016004811115610a38577f4e487b7100000000000000000000000000000000000000000000000000000000600052602160045260246000fd5b816004811115610a71577f4e487b7100000000000000000000000000000000000000000000000000000000600052602160045260246000fd5b1415610ab2576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610aa9906114b5565b60405180910390fd5b60026004811115610aec577f4e487b7100000000000000000000000000000000000000000000000000000000600052602160045260246000fd5b816004811115610b25577f4e487b7100000000000000000000000000000000000000000000000000000000600052602160045260246000fd5b1415610b66576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610b5d906114d5565b60405180910390fd5b60036004811115610ba0577f4e487b7100000000000000000000000000000000000000000000000000000000600052602160045260246000fd5b816004811115610bd9577f4e487b7100000000000000000000000000000000000000000000000000000000600052602160045260246000fd5b1415610c1a576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610c1190611515565b60405180910390fd5b600480811115610c53577f4e487b7100000000000000000000000000000000000000000000000000000000600052602160045260246000fd5b816004811115610c8c577f4e487b7100000000000000000000000000000000000000000000000000000000600052602160045260246000fd5b1415610ccd576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610cc490611555565b60405180910390fd5b5b50565b6000807f7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a08360001c1115610d0c576000600391509150610dd5565b601b8560ff1614158015610d245750601c8560ff1614155b15610d36576000600491509150610dd5565b600060018787878760405160008152602001604052604051610d5b9493929190611387565b6020604051602081039080840390855afa158015610d7d573d6000803e3d6000fd5b505050602060405103519050600073ffffffffffffffffffffffffffffffffffffffff168173ffffffffffffffffffffffffffffffffffffffff161415610dcc57600060019250925050610dd5565b80600092509250505b94509492505050565b6000806000807f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff85169150601b8560ff1c019050610e1e87828885610cd1565b935093505050935093915050565b6000610e3f610e3a846115b5565b611590565b905082815260208101848484011115610e5757600080fd5b610e628482856116f2565b509392505050565b6000610e7d610e78846115e6565b611590565b905082815260208101848484011115610e9557600080fd5b610ea08482856116f2565b509392505050565b600082601f830112610eb957600080fd5b8135610ec9848260208601610e2c565b91505092915050565b600082601f830112610ee357600080fd5b8135610ef3848260208601610e6a565b91505092915050565b600081359050610f0b81611940565b92915050565b600060208284031215610f2357600080fd5b600082013567ffffffffffffffff811115610f3d57600080fd5b610f4984828501610ed2565b91505092915050565b60008060408385031215610f6557600080fd5b600083013567ffffffffffffffff811115610f7f57600080fd5b610f8b85828601610ed2565b925050602083013567ffffffffffffffff811115610fa857600080fd5b610fb485828601610ed2565b9150509250929050565b600080600060608486031215610fd357600080fd5b600084013567ffffffffffffffff811115610fed57600080fd5b610ff986828701610ed2565b935050602084013567ffffffffffffffff81111561101657600080fd5b61102286828701610ed2565b925050604084013567ffffffffffffffff81111561103f57600080fd5b61104b86828701610ea8565b9150509250925092565b6000806040838503121561106857600080fd5b600083013567ffffffffffffffff81111561108257600080fd5b61108e85828601610ed2565b925050602061109f85828601610efc565b9150509250929050565b60006110b583836112fb565b60208301905092915050565b6110ca81611693565b82525050565b60006110db82611627565b6110e58185611655565b93506110f083611617565b8060005b8381101561112157815161110888826110a9565b975061111383611648565b9250506001810190506110f4565b5085935050505092915050565b611137816116a5565b82525050565b611146816116b1565b82525050565b61115d611158826116b1565b611765565b82525050565b600061116e82611632565b6111788185611666565b9350611188818560208601611701565b6111918161179e565b840191505092915050565b60006111a78261163d565b6111b18185611677565b93506111c1818560208601611701565b6111ca8161179e565b840191505092915050565b60006111e08261163d565b6111ea8185611688565b93506111fa818560208601611701565b80840191505092915050565b6000611213601883611677565b915061121e826117af565b602082019050919050565b6000611236601f83611677565b9150611241826117d8565b602082019050919050565b6000611259601c83611688565b915061126482611801565b601c82019050919050565b600061127c601183611677565b91506112878261182a565b602082019050919050565b600061129f602283611677565b91506112aa82611853565b604082019050919050565b60006112c2602e83611677565b91506112cd826118a2565b604082019050919050565b60006112e5602283611677565b91506112f0826118f1565b604082019050919050565b611304816116db565b82525050565b611313816116db565b82525050565b611322816116e5565b82525050565b600061133482846111d5565b915081905092915050565b600061134a8261124c565b9150611356828461114c565b60208201915081905092915050565b6000602082019050818103600083015261137f81846110d0565b905092915050565b600060808201905061139c600083018761113d565b6113a96020830186611319565b6113b6604083018561113d565b6113c3606083018461113d565b95945050505050565b600060208201905081810360008301526113e6818461119c565b905092915050565b60006040820190508181036000830152611408818561119c565b905061141760208301846110c1565b9392505050565b60006040820190508181036000830152611438818561119c565b9050611447602083018461112e565b9392505050565b60006040820190508181036000830152611468818561119c565b9050818103602083015261147c8184611163565b90509392505050565b6000604082019050818103600083015261149f818561119c565b90506114ae602083018461130a565b9392505050565b600060208201905081810360008301526114ce81611206565b9050919050565b600060208201905081810360008301526114ee81611229565b9050919050565b6000602082019050818103600083015261150e8161126f565b9050919050565b6000602082019050818103600083015261152e81611292565b9050919050565b6000602082019050818103600083015261154e816112b5565b9050919050565b6000602082019050818103600083015261156e816112d8565b9050919050565b600060208201905061158a600083018461130a565b92915050565b600061159a6115ab565b90506115a68282611734565b919050565b6000604051905090565b600067ffffffffffffffff8211156115d0576115cf61176f565b5b6115d98261179e565b9050602081019050919050565b600067ffffffffffffffff8211156116015761160061176f565b5b61160a8261179e565b9050602081019050919050565b6000819050602082019050919050565b600081519050919050565b600081519050919050565b600081519050919050565b6000602082019050919050565b600082825260208201905092915050565b600082825260208201905092915050565b600082825260208201905092915050565b600081905092915050565b600061169e826116bb565b9050919050565b60008115159050919050565b6000819050919050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b6000819050919050565b600060ff82169050919050565b82818337600083830152505050565b60005b8381101561171f578082015181840152602081019050611704565b8381111561172e576000848401525b50505050565b61173d8261179e565b810181811067ffffffffffffffff8211171561175c5761175b61176f565b5b80604052505050565b6000819050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6000601f19601f8301169050919050565b7f45434453413a20696e76616c6964207369676e61747572650000000000000000600082015250565b7f45434453413a20696e76616c6964207369676e6174757265206c656e67746800600082015250565b7f19457468657265756d205369676e6564204d6573736167653a0a333200000000600082015250565b7f496e76616c6964207369676e6174757265000000000000000000000000000000600082015250565b7f45434453413a20696e76616c6964207369676e6174757265202773272076616c60008201527f7565000000000000000000000000000000000000000000000000000000000000602082015250565b7f496e697469616c697a61626c653a20636f6e747261637420697320616c72656160008201527f647920696e697469616c697a6564000000000000000000000000000000000000602082015250565b7f45434453413a20696e76616c6964207369676e6174757265202776272076616c60008201527f7565000000000000000000000000000000000000000000000000000000000000602082015250565b611949816116db565b811461195457600080fd5b5056fea2646970667358221220e37840e081ddeb5ea866e15b83ca8421cf9e3df36bdb2c113f6381d9c62c3b7d64736f6c63430008040033", 180 | "deployedBytecode": "0x608060405234801561001057600080fd5b50600436106100625760003560e01c80636715cbe6146100675780638129fc1c1461009757806381d14ffc146100a1578063acf64771146100d1578063da33873e146100ed578063ed9b95e21461011d575b600080fd5b610081600480360381019061007c9190610f11565b61014d565b60405161008e9190611365565b60405180910390f35b61009f6101c3565b005b6100bb60048036038101906100b69190610f52565b6102a7565b6040516100c89190611575565b60405180910390f35b6100eb60048036038101906100e69190610fbe565b61039a565b005b61010760048036038101906101029190610f11565b610543565b6040516101149190611365565b60405180910390f35b61013760048036038101906101329190611055565b6105b9565b6040516101449190611575565b60405180910390f35b606060018260405161015f9190611328565b90815260200160405180910390208054806020026020016040519081016040528092919081815260200182805480156101b757602002820191906000526020600020905b8154815260200190600101908083116101a3575b50505050509050919050565b600060019054906101000a900460ff166101eb5760008054906101000a900460ff16156101f4565b6101f3610600565b5b610233576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161022a90611535565b60405180910390fd5b60008060019054906101000a900460ff161590508015610283576001600060016101000a81548160ff02191690831515021790555060016000806101000a81548160ff0219169083151502179055505b80156102a45760008060016101000a81548160ff0219169083151502179055505b50565b6000826040516102b79190611328565b60405180910390203373ffffffffffffffffffffffffffffffffffffffff167f39b5d1808997c4c413695960efbe703ad3d5c8982d2eb21ff57b01e22a6715328460405161030591906113cc565b60405180910390a361034c6040518060400160405280601d81526020017f436f6d6d6974206576656e7420626c6f636b206e756d6265723a20257300000081525043610611565b60018360405161035c9190611328565b908152602001604051809103902043908060018154018082558091505060019003906000526020600020016000909190919091505543905092915050565b6000826040516020016103ad9190611328565b60405160208183030381529060405280519060200120905060006103d182846106ad565b90506104126040518060400160405280601181526020017f56616c6964207369676e61747572653a2000000000000000000000000000000081525082610762565b80610452576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610449906114f5565b60405180910390fd5b846040516104609190611328565b60405180910390203373ffffffffffffffffffffffffffffffffffffffff167ff14cc285ed29e95249df9be8f97c6d27c13da6147ac82ffde11e0e7cbac00e4e86866040516104b092919061144e565b60405180910390a36104f76040518060400160405280602081526020017f496e74656772697479206576656e7420626c6f636b206e756d6265723a20257381525043610611565b6001856040516105079190611328565b90815260200160405180910390204390806001815401808255809150506001900390600052602060002001600090919091909150555050505050565b60606001826040516105559190611328565b90815260200160405180910390208054806020026020016040519081016040528092919081815260200182805480156105ad57602002820191906000526020600020905b815481526020019060010190808311610599575b50505050509050919050565b60018280516020810182018051848252602083016020850120818352809550505050505081815481106105eb57600080fd5b90600052602060002001600091509150505481565b600061060b306107fe565b15905090565b6106a98282604051602401610627929190611485565b6040516020818303038152906040527f9710a9d0000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff8381831617835250505050610811565b5050565b600080836040516020016106c1919061133f565b6040516020818303038152906040528051906020012090506107186040518060400160405280601081526020017f4d6573736167652073656e6465723a20000000000000000000000000000000008152503361083a565b3373ffffffffffffffffffffffffffffffffffffffff1661074284836108d690919063ffffffff16565b73ffffffffffffffffffffffffffffffffffffffff161491505092915050565b6107fa828260405160240161077892919061141e565b6040516020818303038152906040527fc3b55635000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff8381831617835250505050610811565b5050565b600080823b905060008111915050919050565b60008151905060006a636f6e736f6c652e6c6f679050602083016000808483855afa5050505050565b6108d282826040516024016108509291906113ee565b6040516020818303038152906040527f319af333000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff8381831617835250505050610811565b5050565b60008060006108e585856108fd565b915091506108f281610980565b819250505092915050565b60008060418351141561093f5760008060006020860151925060408601519150606086015160001a905061093387828585610cd1565b94509450505050610979565b604083511415610970576000806020850151915060408501519050610965868383610dde565b935093505050610979565b60006002915091505b9250929050565b600060048111156109ba577f4e487b7100000000000000000000000000000000000000000000000000000000600052602160045260246000fd5b8160048111156109f3577f4e487b7100000000000000000000000000000000000000000000000000000000600052602160045260246000fd5b14156109fe57610cce565b60016004811115610a38577f4e487b7100000000000000000000000000000000000000000000000000000000600052602160045260246000fd5b816004811115610a71577f4e487b7100000000000000000000000000000000000000000000000000000000600052602160045260246000fd5b1415610ab2576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610aa9906114b5565b60405180910390fd5b60026004811115610aec577f4e487b7100000000000000000000000000000000000000000000000000000000600052602160045260246000fd5b816004811115610b25577f4e487b7100000000000000000000000000000000000000000000000000000000600052602160045260246000fd5b1415610b66576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610b5d906114d5565b60405180910390fd5b60036004811115610ba0577f4e487b7100000000000000000000000000000000000000000000000000000000600052602160045260246000fd5b816004811115610bd9577f4e487b7100000000000000000000000000000000000000000000000000000000600052602160045260246000fd5b1415610c1a576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610c1190611515565b60405180910390fd5b600480811115610c53577f4e487b7100000000000000000000000000000000000000000000000000000000600052602160045260246000fd5b816004811115610c8c577f4e487b7100000000000000000000000000000000000000000000000000000000600052602160045260246000fd5b1415610ccd576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610cc490611555565b60405180910390fd5b5b50565b6000807f7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a08360001c1115610d0c576000600391509150610dd5565b601b8560ff1614158015610d245750601c8560ff1614155b15610d36576000600491509150610dd5565b600060018787878760405160008152602001604052604051610d5b9493929190611387565b6020604051602081039080840390855afa158015610d7d573d6000803e3d6000fd5b505050602060405103519050600073ffffffffffffffffffffffffffffffffffffffff168173ffffffffffffffffffffffffffffffffffffffff161415610dcc57600060019250925050610dd5565b80600092509250505b94509492505050565b6000806000807f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff85169150601b8560ff1c019050610e1e87828885610cd1565b935093505050935093915050565b6000610e3f610e3a846115b5565b611590565b905082815260208101848484011115610e5757600080fd5b610e628482856116f2565b509392505050565b6000610e7d610e78846115e6565b611590565b905082815260208101848484011115610e9557600080fd5b610ea08482856116f2565b509392505050565b600082601f830112610eb957600080fd5b8135610ec9848260208601610e2c565b91505092915050565b600082601f830112610ee357600080fd5b8135610ef3848260208601610e6a565b91505092915050565b600081359050610f0b81611940565b92915050565b600060208284031215610f2357600080fd5b600082013567ffffffffffffffff811115610f3d57600080fd5b610f4984828501610ed2565b91505092915050565b60008060408385031215610f6557600080fd5b600083013567ffffffffffffffff811115610f7f57600080fd5b610f8b85828601610ed2565b925050602083013567ffffffffffffffff811115610fa857600080fd5b610fb485828601610ed2565b9150509250929050565b600080600060608486031215610fd357600080fd5b600084013567ffffffffffffffff811115610fed57600080fd5b610ff986828701610ed2565b935050602084013567ffffffffffffffff81111561101657600080fd5b61102286828701610ed2565b925050604084013567ffffffffffffffff81111561103f57600080fd5b61104b86828701610ea8565b9150509250925092565b6000806040838503121561106857600080fd5b600083013567ffffffffffffffff81111561108257600080fd5b61108e85828601610ed2565b925050602061109f85828601610efc565b9150509250929050565b60006110b583836112fb565b60208301905092915050565b6110ca81611693565b82525050565b60006110db82611627565b6110e58185611655565b93506110f083611617565b8060005b8381101561112157815161110888826110a9565b975061111383611648565b9250506001810190506110f4565b5085935050505092915050565b611137816116a5565b82525050565b611146816116b1565b82525050565b61115d611158826116b1565b611765565b82525050565b600061116e82611632565b6111788185611666565b9350611188818560208601611701565b6111918161179e565b840191505092915050565b60006111a78261163d565b6111b18185611677565b93506111c1818560208601611701565b6111ca8161179e565b840191505092915050565b60006111e08261163d565b6111ea8185611688565b93506111fa818560208601611701565b80840191505092915050565b6000611213601883611677565b915061121e826117af565b602082019050919050565b6000611236601f83611677565b9150611241826117d8565b602082019050919050565b6000611259601c83611688565b915061126482611801565b601c82019050919050565b600061127c601183611677565b91506112878261182a565b602082019050919050565b600061129f602283611677565b91506112aa82611853565b604082019050919050565b60006112c2602e83611677565b91506112cd826118a2565b604082019050919050565b60006112e5602283611677565b91506112f0826118f1565b604082019050919050565b611304816116db565b82525050565b611313816116db565b82525050565b611322816116e5565b82525050565b600061133482846111d5565b915081905092915050565b600061134a8261124c565b9150611356828461114c565b60208201915081905092915050565b6000602082019050818103600083015261137f81846110d0565b905092915050565b600060808201905061139c600083018761113d565b6113a96020830186611319565b6113b6604083018561113d565b6113c3606083018461113d565b95945050505050565b600060208201905081810360008301526113e6818461119c565b905092915050565b60006040820190508181036000830152611408818561119c565b905061141760208301846110c1565b9392505050565b60006040820190508181036000830152611438818561119c565b9050611447602083018461112e565b9392505050565b60006040820190508181036000830152611468818561119c565b9050818103602083015261147c8184611163565b90509392505050565b6000604082019050818103600083015261149f818561119c565b90506114ae602083018461130a565b9392505050565b600060208201905081810360008301526114ce81611206565b9050919050565b600060208201905081810360008301526114ee81611229565b9050919050565b6000602082019050818103600083015261150e8161126f565b9050919050565b6000602082019050818103600083015261152e81611292565b9050919050565b6000602082019050818103600083015261154e816112b5565b9050919050565b6000602082019050818103600083015261156e816112d8565b9050919050565b600060208201905061158a600083018461130a565b92915050565b600061159a6115ab565b90506115a68282611734565b919050565b6000604051905090565b600067ffffffffffffffff8211156115d0576115cf61176f565b5b6115d98261179e565b9050602081019050919050565b600067ffffffffffffffff8211156116015761160061176f565b5b61160a8261179e565b9050602081019050919050565b6000819050602082019050919050565b600081519050919050565b600081519050919050565b600081519050919050565b6000602082019050919050565b600082825260208201905092915050565b600082825260208201905092915050565b600082825260208201905092915050565b600081905092915050565b600061169e826116bb565b9050919050565b60008115159050919050565b6000819050919050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b6000819050919050565b600060ff82169050919050565b82818337600083830152505050565b60005b8381101561171f578082015181840152602081019050611704565b8381111561172e576000848401525b50505050565b61173d8261179e565b810181811067ffffffffffffffff8211171561175c5761175b61176f565b5b80604052505050565b6000819050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6000601f19601f8301169050919050565b7f45434453413a20696e76616c6964207369676e61747572650000000000000000600082015250565b7f45434453413a20696e76616c6964207369676e6174757265206c656e67746800600082015250565b7f19457468657265756d205369676e6564204d6573736167653a0a333200000000600082015250565b7f496e76616c6964207369676e6174757265000000000000000000000000000000600082015250565b7f45434453413a20696e76616c6964207369676e6174757265202773272076616c60008201527f7565000000000000000000000000000000000000000000000000000000000000602082015250565b7f496e697469616c697a61626c653a20636f6e747261637420697320616c72656160008201527f647920696e697469616c697a6564000000000000000000000000000000000000602082015250565b7f45434453413a20696e76616c6964207369676e6174757265202776272076616c60008201527f7565000000000000000000000000000000000000000000000000000000000000602082015250565b611949816116db565b811461195457600080fd5b5056fea2646970667358221220e37840e081ddeb5ea866e15b83ca8421cf9e3df36bdb2c113f6381d9c62c3b7d64736f6c63430008040033", 181 | "linkReferences": {}, 182 | "deployedLinkReferences": {} 183 | } 184 | 185 | export const abi = artifact.abi; -------------------------------------------------------------------------------- /src/http.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | export async function get(url, headers) { 4 | const options = { 5 | headers: headers, 6 | }; 7 | const response = await axios.get(url, options); 8 | return response; 9 | } 10 | 11 | export async function post(url, body, headers) { 12 | const options = { 13 | headers: headers, 14 | }; 15 | const response = await axios.post(url, body, options); 16 | return response; 17 | } -------------------------------------------------------------------------------- /src/ipfs.ts: -------------------------------------------------------------------------------- 1 | import * as FormData from "form-data"; 2 | import * as stream from "stream"; 3 | import got from "got"; 4 | import { Estuary } from "@numbersprotocol/estuary-upload"; 5 | 6 | import * as http from "./http"; 7 | 8 | // FIXME: make Nit config to be a module 9 | let ProjectId = ""; 10 | let ProjectSecret = ""; 11 | 12 | let EstuaryInstance; 13 | let NumbersProtocolCaptureToken = ""; // Store the Capture Token 14 | let NumbersProtocolIpfsGatewayLambda = "https://mjacdgxzla.execute-api.us-east-1.amazonaws.com/default/ipfs-add-with-payment"; 15 | let NumbersProtocolIpfsGateway = "https://ipfs-pin.numbersprotocol.io/ipfs"; 16 | 17 | export async function initInfura(projectId, projectSecret) { 18 | ProjectId = projectId; 19 | ProjectSecret = projectSecret; 20 | } 21 | 22 | export async function infuraAccessToken(projectId, projectSecret) { 23 | return Buffer.from(`${projectId}:${projectSecret}`).toString('base64'); 24 | } 25 | 26 | export async function infuraIpfsAddBytes(bytes) { 27 | const fileReadStream = stream.Readable.from(bytes); 28 | const formData = new FormData(); 29 | formData.append('file', fileReadStream); 30 | 31 | const url = "https://ipfs.infura.io:5001/api/v0/add?cid-version=1"; 32 | const authBase64 = Buffer.from(`${ProjectId}:${ProjectSecret}`).toString('base64'); 33 | const headers = { 34 | "Authorization": `Basic ${authBase64}`, 35 | ...formData.getHeaders(), 36 | }; 37 | const httpResponse = await http.post(url, formData, headers); 38 | const assetCid = httpResponse.data.Hash; 39 | return assetCid; 40 | } 41 | 42 | export async function infuraIpfsCat(cid) { 43 | const url = `https://ipfs.infura.io:5001/api/v0/cat?arg=${cid}`; 44 | const authBase64 = Buffer.from(`${ProjectId}:${ProjectSecret}`).toString('base64'); 45 | const requestConfig = { 46 | "headers": { 47 | "Authorization": `Basic ${authBase64}`, 48 | }, 49 | timeout: { request: 30000 }, 50 | } 51 | /* FIXME: Axios's response.data.lenght is smaller than content length. 52 | * Use Got as a temporary workardound. 53 | * https://github.com/axios/axios/issues/3711 54 | */ 55 | const r = await got.post(url, requestConfig); 56 | return r.rawBody; 57 | } 58 | 59 | export async function w3sIpfsCat(cid) { 60 | const url = `https://${cid}.ipfs.w3s.link`; 61 | const requestConfig = { 62 | timeout: { request: 30000 }, 63 | } 64 | /* FIXME: Axios's response.data.lenght is smaller than content length. 65 | * Use Got as a temporary workardound. 66 | * https://github.com/axios/axios/issues/3711 67 | */ 68 | const r = await got.get(url, requestConfig); 69 | return r.rawBody; 70 | } 71 | 72 | export async function ipfsAddBytes(bytes) { 73 | return await numbersProtocolIpfsAddBytes(bytes); 74 | } 75 | 76 | export async function ipfsCat(cid) { 77 | return await numbersProtocolIpfsCat(cid); 78 | } 79 | 80 | export async function cidToJsonString(cid) { 81 | try { 82 | const cidContentBytes = await ipfsCat(cid); 83 | return cidContentBytes.toString(); 84 | } catch(error) { 85 | console.error(`Failed to download content of CID ${cid}`); 86 | return null; 87 | } 88 | } 89 | 90 | export async function cidToJson(cid) { 91 | try { 92 | const cidContentString = await cidToJsonString(cid); 93 | return JSON.parse(cidContentString ); 94 | } catch(error) { 95 | console.error(`Failed to download content of CID ${cid}`); 96 | return null; 97 | } 98 | } 99 | 100 | export async function initEstuary(apiKey) { 101 | EstuaryInstance = new Estuary(apiKey); 102 | } 103 | 104 | export async function estuaryAdd(bytes) { 105 | // Kudo CIDv1 106 | let cid; 107 | try { 108 | cid = await EstuaryInstance.addFromBuffer(bytes); 109 | return cid; 110 | } catch(error) { 111 | console.error(error); 112 | } 113 | } 114 | 115 | // Update this function to accept and store the Capture Token 116 | export async function initNumbersProtocol(captureToken) { 117 | NumbersProtocolCaptureToken = captureToken; 118 | } 119 | 120 | export async function numbersProtocolIpfsAddBytes(bytes) { 121 | try { 122 | // Create form data with the file 123 | const fileReadStream = stream.Readable.from(bytes); 124 | const formData = new FormData(); 125 | formData.append('file', fileReadStream); 126 | 127 | // Use Numbers Protocol IPFS add API endpoint with Capture Token in header 128 | const url = NumbersProtocolIpfsGatewayLambda; 129 | const headers = { 130 | "Authorization": `token ${NumbersProtocolCaptureToken}`, 131 | ...formData.getHeaders(), 132 | }; 133 | 134 | const httpResponse = await http.post(url, formData, headers); 135 | const assetCid = httpResponse.data.cid; 136 | return assetCid; 137 | } catch(error) { 138 | console.error("Error adding to Numbers Protocol IPFS:", error); 139 | throw error; 140 | } 141 | } 142 | 143 | export async function numbersProtocolIpfsCat(cid) { 144 | try { 145 | // Use Numbers Protocol IPFS cat API endpoint with Capture Token 146 | const url = `${NumbersProtocolIpfsGateway}/${cid}`; 147 | const requestConfig = { 148 | headers: { 149 | "Authorization": `token ${NumbersProtocolCaptureToken}` 150 | }, 151 | timeout: { request: 30000 }, 152 | }; 153 | 154 | const r = await got.get(url, requestConfig); 155 | return r.rawBody; 156 | } catch(error) { 157 | console.error(`Failed to download content of CID ${cid} from Numbers Protocol IPFS:`, error); 158 | throw error; 159 | } 160 | } 161 | 162 | // Add new function to unpin content from Numbers Protocol IPFS with Capture Token 163 | export async function numbersProtocolIpfsUnpin(cid) { 164 | try { 165 | // Use Numbers Protocol IPFS unpin API endpoint with Capture Token 166 | const url = NumbersProtocolIpfsGatewayLambda; 167 | const requestConfig = { 168 | headers: { 169 | "Authorization": `token ${NumbersProtocolCaptureToken}` 170 | }, 171 | timeout: { request: 30000 }, 172 | }; 173 | 174 | const r = await got.delete(url, requestConfig); 175 | return r.statusCode === 200; 176 | } catch(error) { 177 | console.error(`Failed to unpin CID ${cid} from Numbers Protocol IPFS:`, error); 178 | return false; 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/license.ts: -------------------------------------------------------------------------------- 1 | /* References: 2 | * https://spdx.org/licenses/ 3 | * https://pypi.org/classifiers/ 4 | */ 5 | 6 | export const Licenses = { 7 | "cc-by-4.0": { 8 | "name": "CC-BY-4.0", 9 | "document": "https://creativecommons.org/licenses/by/4.0/legalcode" 10 | }, 11 | "cc-by-sa-4.0": { 12 | "name": "CC-BY-SA-4.0", 13 | "document": "https://creativecommons.org/licenses/by-sa/4.0/legalcode" 14 | }, 15 | "cc-by-nd-4.0": { 16 | "name": "CC-BY-ND-4.0", 17 | "document": "https://creativecommons.org/licenses/by-nd/4.0/legalcode" 18 | }, 19 | "cc-by-nc-4.0": { 20 | "name": "CC-BY-NC-4.0", 21 | "document": "https://creativecommons.org/licenses/by-nc/4.0/legalcode" 22 | }, 23 | "cc-by-nc-sa-4.0": { 24 | "name": "CC-BY-NC-SA-4.0", 25 | "document": "https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode" 26 | }, 27 | "cc-by-nc-nd-4.0": { 28 | "name": "CC-BY-NC-ND-4.0", 29 | "document": "https://creativecommons.org/licenses/by-nc-nd/4.0/legalcode" 30 | }, 31 | "mit": { 32 | "name": "MIT", 33 | "document": "https://opensource.org/licenses/MIT" 34 | }, 35 | "public": { 36 | "name": "MIT", 37 | "document": "https://opensource.org/licenses/MIT" 38 | }, 39 | }; 40 | 41 | export const DefaultLicense: string = "cc-by-nc-4.0"; 42 | 43 | export function isSupportedLicense(licenseName: string) { 44 | if (Object.keys(Licenses).indexOf(licenseName) > -1) { 45 | return true; 46 | } else { 47 | return false; 48 | } 49 | } -------------------------------------------------------------------------------- /src/nit.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "ethers"; 2 | import { BytesLike } from "@ethersproject/bytes" 3 | 4 | import * as action from "./action"; 5 | import * as commitdb from "./commitdb"; 6 | import * as integrityContract from "./contract"; 7 | import * as ipfs from "./ipfs"; 8 | import * as license from "./license"; 9 | import * as util from "./util"; 10 | 11 | import * as colors from "colors"; 12 | import * as diff from "diff"; 13 | 14 | colors.enable(); 15 | 16 | /*---------------------------------------------------------------------------- 17 | * Configuration 18 | *----------------------------------------------------------------------------*/ 19 | const privateKeyLength = 64; 20 | const infuraSecretLength = 27; 21 | export const cidv1Length = 59; 22 | export const integrityHashLength = 64; 23 | export const assetCidMock = "a".repeat(cidv1Length); 24 | export const nitconfigTemplate = { 25 | /* Author's Identity CID. */ 26 | "author": "a".repeat(cidv1Length), 27 | /* Committer's Identity CID. */ 28 | "committer": "a".repeat(cidv1Length), 29 | /* Provider's Identity CID. */ 30 | "provider": "a".repeat(cidv1Length), 31 | /* Which network config you want to use. 32 | One of the network names in the "network" section below. 33 | E.g., rinkeby */ 34 | "defaultNetwork": "jade", 35 | "network": { 36 | "jade": { 37 | "url": "https://mainnetrpc.num.network", 38 | "chainId": 10507, 39 | "accounts": [ 40 | "a".repeat(privateKeyLength) 41 | ], 42 | "contract": "0x7EC2F14ABE8b0Ea2F657bBb62b6fEfDe161c9001", 43 | "explorerBaseUrl": "https://mainnet.num.network/tx" 44 | }, 45 | "snow": { 46 | "url": "https://testnetrpc.num.network", 47 | "chainId": 10508, 48 | "accounts": [ 49 | "a".repeat(privateKeyLength) 50 | ], 51 | "contract": "0x02eFA51b583d03342687b585417e5A62cd8273a4", 52 | "explorerBaseUrl": "https://testnet.num.network/tx" 53 | }, 54 | "avalanche": { 55 | "url": "https://api.avax.network/ext/bc/C/rpc", 56 | "chainId": 43113, 57 | "accounts": [ 58 | "a".repeat(privateKeyLength) 59 | ], 60 | "contract": "0x1970aFD0831E9233667fb9484910926c2b18BCb4", 61 | "explorerBaseUrl": "https://snowtrace.io/tx" 62 | }, 63 | "fuji": { 64 | "url": "https://api.avax-test.network/ext/bc/C/rpc", 65 | "chainId": 43114, 66 | "gasLimit": 200000, 67 | "accounts": [ 68 | "a".repeat(privateKeyLength), 69 | ], 70 | "contract": "0xA2De03bee39Fa637440909abC5621063bC5DA926", 71 | "explorerBaseUrl": "https://testnet.snowtrace.io/tx" 72 | } 73 | }, 74 | /* Infura IPFS project ID and secret for "IPFS add". */ 75 | "infura": { 76 | "projectId": "a".repeat(infuraSecretLength), 77 | "projectSecret": "a".repeat(infuraSecretLength) 78 | }, 79 | /* Numbers Protocol IPFS settings */ 80 | "numbersProtocol": { 81 | "captureToken": "" // Capture Token for Numbers Protocol IPFS API 82 | }, 83 | "commitDatabase": { 84 | "updateUrl": "", 85 | "commitUrl": "", 86 | } 87 | }; 88 | 89 | export const blockchainNames = { 90 | 10507: "jade", 91 | 10508: "snow", 92 | 43113: "fuji", 93 | 43114: "avalanche", 94 | }; 95 | 96 | export async function loadBlockchain(config) { 97 | const networkConfig = config.network[config.defaultNetwork]; 98 | const provider = new ethers.providers.JsonRpcProvider(networkConfig.url); 99 | const contractAbi = integrityContract.abi; 100 | 101 | const [signerPrivateKey, user] = networkConfig.accounts; 102 | let signer = new ethers.Wallet(signerPrivateKey); 103 | signer = signer.connect(provider); 104 | 105 | const contract = new ethers.Contract(networkConfig.contract, contractAbi, signer); 106 | 107 | return { 108 | "contract": contract, 109 | "signer": signer, 110 | "chainId": networkConfig.chainId, 111 | "gasLimit": networkConfig.gasLimit, 112 | "gasPrice": networkConfig.gasPrice ? networkConfig.gasPrice : null, 113 | "explorerBaseUrl": networkConfig.explorerBaseUrl, 114 | "provider": provider 115 | }; 116 | } 117 | 118 | const configurableAssetTreeKeys = [ 119 | "assetCreator", 120 | "assetTimestampCreated", 121 | "license", 122 | "nftRecord", 123 | "integrityCid", 124 | "abstract", 125 | "encodingFormat", 126 | ]; 127 | 128 | /*---------------------------------------------------------------------------- 129 | * Commands 130 | *----------------------------------------------------------------------------*/ 131 | async function createAssetTreeBaseRemote(assetByes, assetMimetype) { 132 | let stagingAssetTree: any = {}; 133 | try { 134 | stagingAssetTree.assetCid = await ipfs.ipfsAddBytes(assetByes); 135 | // remove leading 0x to as the same as most sha256 tools 136 | stagingAssetTree.assetSha256 = (await ethers.utils.sha256(assetByes)).substring(2); 137 | stagingAssetTree.encodingFormat = assetMimetype; 138 | } catch(error) { 139 | console.error(`${error}`); 140 | stagingAssetTree = {}; 141 | } 142 | return stagingAssetTree; 143 | } 144 | 145 | async function createAssetTreeBase(assetCid, assetSha256, assetMimetype) { 146 | let stagingAssetTree: any = {}; 147 | try { 148 | stagingAssetTree.assetCid = assetCid; 149 | stagingAssetTree.assetSha256 = assetSha256 150 | stagingAssetTree.encodingFormat = assetMimetype; 151 | } catch(error) { 152 | console.error(`${error}`); 153 | stagingAssetTree = {}; 154 | } 155 | return stagingAssetTree; 156 | } 157 | 158 | async function createCommitBase(signer, assetTree, authorAddress, providerCid) { 159 | let stagingCommit: any = {}; 160 | 161 | const assetTreeBytes = Buffer.from(JSON.stringify(assetTree, null, 2)); 162 | stagingCommit.assetTreeCid = await ipfs.ipfsAddBytes(assetTreeBytes); 163 | stagingCommit.assetTreeSha256 = (await ethers.utils.sha256(Buffer.from(JSON.stringify(assetTree, null, 2)))).substring(2); 164 | // remove leading 0x to as the same as most sha256 tools 165 | stagingCommit.assetTreeSignature = await signIntegrityHash(stagingCommit.assetTreeSha256, signer); 166 | stagingCommit.author = authorAddress; 167 | stagingCommit.committer = signer.address; 168 | stagingCommit.provider = providerCid; 169 | stagingCommit.timestampCreated = Math.floor(Date.now() / 1000); 170 | 171 | return stagingCommit; 172 | } 173 | 174 | /* Hidden rule: add actionName in commitData 175 | * 176 | * Check actionName is sent in commitData, 177 | * if exist, use the actionName defined by the App, 178 | * if not exist, check the given Action Nid (the action field in commitData), 179 | * if valid, use the mapped actionName 180 | * if invalid, use the default actionName "action-commit" 181 | */ 182 | function addActionNameInCommit(commitData: string) { 183 | const commitJson = JSON.parse(commitData); 184 | if (!commitJson.hasOwnProperty("actionName")) { 185 | if (commitJson.hasOwnProperty("action")) { 186 | const nid = commitJson["action"]; 187 | const key = action.getNameByNid(nid); 188 | commitJson["actionName"] = key; 189 | } else { 190 | commitJson["actionName"] = "action-commit"; 191 | } 192 | } 193 | return JSON.stringify(commitJson); 194 | } 195 | 196 | function addLicenseInAssetTree(assetTree: {[key: string]: any}, assetLicense?: string | undefined) { 197 | if (assetLicense) { 198 | if (license.isSupportedLicense(assetLicense)) { 199 | assetTree.license = license.Licenses[assetLicense]; 200 | } else { 201 | console.error(`Get unsupported license: ${assetLicense}\n`); 202 | } 203 | } 204 | } 205 | 206 | function addAbstractInAssetTree(assetTree: {[key: string]: any}, assetAbstract?: string | undefined) { 207 | if (assetAbstract !== undefined) { 208 | assetTree.abstract = assetAbstract; 209 | } 210 | } 211 | 212 | export async function createAssetTreeInitialRegisterRemote(assetBytes, 213 | assetMimetype, 214 | assetTimestampCreated, 215 | assetCreator, 216 | assetLicense: string | undefined = undefined, 217 | assetAbstract: string | undefined = undefined) { 218 | let stagingAssetTree = await createAssetTreeBaseRemote(assetBytes, assetMimetype); 219 | stagingAssetTree.assetTimestampCreated= assetTimestampCreated; 220 | stagingAssetTree.assetCreator = assetCreator; 221 | addLicenseInAssetTree(stagingAssetTree, assetLicense); 222 | addAbstractInAssetTree(stagingAssetTree, assetAbstract); 223 | return stagingAssetTree; 224 | } 225 | 226 | export async function createAssetTreeInitialRegister(assetCid, 227 | assetSha256, 228 | assetMimetype, 229 | assetTimestampCreated, 230 | assetCreator, 231 | assetLicense: string | undefined = undefined, 232 | assetAbstract: string | undefined = undefined) { 233 | let stagingAssetTree = await createAssetTreeBase(assetCid, assetSha256, assetMimetype); 234 | stagingAssetTree.assetTimestampCreated= assetTimestampCreated; 235 | stagingAssetTree.assetCreator = assetCreator; 236 | addLicenseInAssetTree(stagingAssetTree, assetLicense); 237 | addAbstractInAssetTree(stagingAssetTree, assetAbstract); 238 | return stagingAssetTree; 239 | } 240 | 241 | export async function createCommitInitialRegister(signer, 242 | assetTree, 243 | authorAddress, 244 | providerCid, 245 | commitMessage="Action: action-initial-registration.", 246 | commitAction="action-initial-registration-jade") { 247 | let stagingCommit = await createCommitBase(signer, assetTree, authorAddress, providerCid); 248 | stagingCommit.action = action.Actions[commitAction]; 249 | stagingCommit.actionResult = `https://${stagingCommit.assetTreeCid}.ipfs.dweb.link`; 250 | stagingCommit.abstract = commitMessage; 251 | stagingCommit.timestampCreated = Math.floor(Date.now() / 1000); 252 | return stagingCommit; 253 | } 254 | 255 | export async function createCommit(signer, assetTree, authorAddress, providerCid) { 256 | let stagingCommit = await createCommitBase(signer, assetTree, authorAddress, providerCid); 257 | stagingCommit.action = ""; 258 | stagingCommit.actionResult = ""; 259 | stagingCommit.abstract = `Nit Commit created by ${signer.address}.`; 260 | stagingCommit.timestampCreated = Math.floor(Date.now() / 1000); 261 | return stagingCommit; 262 | } 263 | 264 | /* 265 | export async function createCommitMintErc721Nft(signer, authorCid, committerCid, providerCid, actionIndex, actionResult) { 266 | stagingCommit = await createCommitBase(signer, authorCid, committerCid, providerCid); 267 | stagingCommit.action = action.Actions[actionIndex]; 268 | stagingCommit.actionResult = actionResult; 269 | stagingCommit.abstract = "Mint ERC-721 NFT."; 270 | stagingCommit.timestampCreated = Math.floor(Date.now() / 1000); 271 | return stagingCommit; 272 | } 273 | */ 274 | 275 | export async function updateAssetTreeLegacy(assetTree, assetTreeUpdates) { 276 | const assetTreeKeySet = new Set(configurableAssetTreeKeys); 277 | const assetTreeUpdatesKeySet = new Set(Object.keys(assetTreeUpdates)); 278 | 279 | const isSuperset = util.isSuperset(assetTreeKeySet, assetTreeUpdatesKeySet); 280 | 281 | if (isSuperset) { 282 | for (let key of Object.keys(assetTreeUpdates)) { 283 | assetTree[key] = assetTreeUpdates[key]; 284 | } 285 | return assetTree; 286 | } else { 287 | // find illegal assetTreeUpdates, return assetTree directly 288 | console.log(`Asset Tree Updates is not a legal subset`); 289 | return assetTree 290 | } 291 | } 292 | 293 | export async function updateAssetTree(assetTree, assetTreeUpdates) { 294 | /* Extend Asset Tree with given updates. 295 | */ 296 | let assetTreeCopy = util.deepCopy(assetTree); 297 | for (let key of Object.keys(assetTreeUpdates)) { 298 | assetTreeCopy[key] = assetTreeUpdates[key]; 299 | } 300 | return assetTreeCopy; 301 | } 302 | 303 | export async function pull(assetCid: string, blockchainInfo) { 304 | const latestCommit = await getLatestCommitSummary(assetCid, blockchainInfo); 305 | if (latestCommit != null) { 306 | const assetTree = await getAssetTree(latestCommit.commit.assetTreeCid); 307 | return assetTree; 308 | } else { 309 | return null 310 | } 311 | } 312 | 313 | //export async function add(assetCid, assetUpdates, blockchainInfo) { 314 | // const latestAssetTree = await pull(assetCid, blockchainInfo); 315 | // if (latestAssetTree != null) { 316 | // return await updateAssetTree(latestAssetTree, assetUpdates); 317 | // } else { 318 | // // Asset has not been registered 319 | // } 320 | //} 321 | 322 | export async function commit(assetCid: string, commitData: string, blockchainInfo, confirms: number = 1) { 323 | const commitString = addActionNameInCommit(commitData); 324 | let r; 325 | if (blockchainInfo.gasPrice != null) { 326 | console.log(`Gas Price: ${blockchainInfo.gasPrice} Wei`); 327 | r = await blockchainInfo.contract.commit(assetCid, commitString, { gasLimit: blockchainInfo.gasLimit, gasPrice: blockchainInfo.gasPrice }); 328 | } else { 329 | r = await blockchainInfo.contract.commit(assetCid, commitString, { gasLimit: blockchainInfo.gasLimit }); 330 | } 331 | 332 | // Wait for the transaction to be mined 333 | return await r.wait(confirms); 334 | } 335 | 336 | export async function log(assetCid: string, blockchainInfo, fromIndex: number, toIndex: number = null) { 337 | const network = await blockchainInfo.provider.getNetwork(); 338 | 339 | if (toIndex <= fromIndex) { 340 | const commitBlockNumbers = await getCommitBlockNumbers(assetCid, blockchainInfo); 341 | const commitAmount: number = commitBlockNumbers.length; 342 | toIndex = commitAmount; 343 | } 344 | 345 | const commitEvents = await iterateCommitEvents(assetCid, blockchainInfo, fromIndex, toIndex); 346 | const commits = await getCommits(commitEvents); 347 | await showCommits(commits); 348 | } 349 | 350 | export async function getCommit(assetCid: string, blockchainInfo, blockNumber: number) { 351 | const filter = await blockchainInfo.contract.filters.Commit(null, assetCid); 352 | const abi = [ 353 | "event Commit(address indexed recorder, string indexed assetCid, string commitData)" 354 | ]; 355 | const commitEventInterface = new ethers.utils.Interface(abi); 356 | 357 | let events = []; 358 | 359 | filter.fromBlock = blockNumber; 360 | filter.toBlock = blockNumber; 361 | const eventLogs = await blockchainInfo.provider.getLogs(filter); 362 | 363 | for (const eventLog of eventLogs) { 364 | const commitEvent = commitEventInterface.parseLog(eventLog); 365 | // merge eventLog and commitEvent 366 | events.push(Object.assign({}, eventLog, commitEvent)); 367 | } 368 | 369 | const commits = await getCommits(events); 370 | console.log(`${JSON.stringify(commits[0].commit)}`); 371 | 372 | return commits[0].commit; 373 | //await showCommits(commits); 374 | } 375 | 376 | export async function showCommits(commits) { 377 | commits.map(commit => { 378 | console.log(`\nblock number: ${colors.blue(commit.blockNumber)}`); 379 | console.log(`tx: ${colors.green(commit.transactionHash)}`); 380 | console.log(`${JSON.stringify(commit.commit, null, 2)}`); 381 | }); 382 | } 383 | 384 | export async function showCommmitDiff(commitDiff) { 385 | commitDiff.forEach((part) => { 386 | const color = part.added ? 'green' : part.removed ? 'red' : 'grey'; 387 | console.log(`${part.value.replace(/\n$/, "")[color]}`); 388 | }); 389 | } 390 | 391 | export async function difference(assetCid: string, blockchainInfo, fromIndex: number = null, toIndex: number = null) { 392 | const commitBlockNumbers = await getCommitBlockNumbers(assetCid, blockchainInfo); 393 | const commitAmount = commitBlockNumbers.length; 394 | 395 | // show the Commit diff between (latest - 1, latest) 396 | if (fromIndex == null) { 397 | toIndex = commitAmount; 398 | fromIndex = toIndex - 2; 399 | } 400 | 401 | const commitEvents = await iterateCommitEvents(assetCid, blockchainInfo, fromIndex, fromIndex < toIndex ? toIndex : fromIndex + 1); 402 | const commits = await getCommits(commitEvents); 403 | const fromCommit = commits[0]; 404 | const toCommit = commits[commits.length - 1]; 405 | const fromAssetTree = JSON.parse((await ipfs.ipfsCat(fromCommit.commit.assetTreeCid)).toString()); 406 | const toAssetTree = JSON.parse((await ipfs.ipfsCat(toCommit.commit.assetTreeCid)).toString()); 407 | const commitDiff = { 408 | "fromIndex": fromIndex, 409 | "fromBlockNumber": fromCommit.blockNumber, 410 | "fromTransactionHash": fromCommit.transactionHash, 411 | "toIndex": toIndex, 412 | "toBlockNumber": toCommit.blockNumber, 413 | "toTransactionHash": toCommit.transactionHash, 414 | "commitDiff": diff.diffJson(fromCommit.commit, toCommit.commit), 415 | "assetTreeDiff": diff.diffJson(fromAssetTree, toAssetTree), 416 | } 417 | return commitDiff; 418 | } 419 | 420 | export async function getLatestCommitSummary(assetCid, blockchainInfo) { 421 | const commitBlockNumbers = await getCommitBlockNumbers(assetCid, blockchainInfo); 422 | const commitAmount = commitBlockNumbers.length; 423 | const events = await iterateCommitEvents(assetCid, blockchainInfo, commitAmount - 1, commitAmount); 424 | const commitsSummary = await getCommitsSummary(events); 425 | return commitsSummary.pop(); 426 | } 427 | 428 | export async function getAssetTree(assetTreeCid) { 429 | const assetTreeBytes = await ipfs.ipfsCat(assetTreeCid); 430 | const assetTree = JSON.parse(assetTreeBytes.toString()); 431 | return assetTree; 432 | } 433 | 434 | export async function getCommitBlockNumbers(assetCid: string, blockchainInfo) { 435 | const commits = await blockchainInfo.contract.getCommits(assetCid); 436 | return commits.length > 0 ? commits.map(element => element.toNumber()) : []; 437 | } 438 | 439 | export async function filterCommitEvents(assetCid: string, blockchainInfo, fromIndex, toIndex) { 440 | /* Have 3 more keys than eventLog in iterateCommitEvents: event, eventSignature, args 441 | * 442 | * keys: 443 | * blockNumber, blockHash, transactionIndex, removed, address, data, topics 444 | * transactionHash, logIndex, event, eventSignature, args 445 | */ 446 | const commitBlockNumbers = (await getCommitBlockNumbers(assetCid, blockchainInfo)).slice(fromIndex, toIndex); 447 | const commitAmount = commitBlockNumbers.length; 448 | 449 | if (commitAmount == 0) { return []; } 450 | 451 | const filter = await blockchainInfo.contract.filters.Commit(null, assetCid); 452 | filter.fromBlock = commitBlockNumbers[0]; 453 | filter.toBlock = commitBlockNumbers[commitAmount - 1]; 454 | 455 | let events = await blockchainInfo.contract.queryFilter(filter, filter.fromBlock, filter.toBlock); 456 | return events; 457 | } 458 | 459 | export async function iterateCommitEvents(assetCid: string, blockchainInfo, fromIndex, toIndex) { 460 | /* Get Commit events by using low-level event logs. 461 | * 462 | * If a blockchain provider does not support getting events by filter, 463 | * (e.g., Avalanche), you can use this function. 464 | * 465 | * keys in eventLog: 466 | * blockNumber, blockHash, transactionIndex, removed, address, data, topics 467 | * transactionHash, logIndex, 468 | * keys in commitEvent: 469 | * eventFragment, name, signature, args, topic, args 470 | */ 471 | const commitBlockNumbers = (await getCommitBlockNumbers(assetCid, blockchainInfo)).slice(fromIndex, toIndex); 472 | const commitAmount = commitBlockNumbers.length; 473 | 474 | if (commitAmount == 0) { return []; } 475 | 476 | const filter = await blockchainInfo.contract.filters.Commit(null, assetCid); 477 | const abi = [ 478 | "event Commit(address indexed recorder, string indexed assetCid, string commitData)" 479 | ]; 480 | const commitEventInterface = new ethers.utils.Interface(abi); 481 | 482 | let events = []; 483 | 484 | for (const c of commitBlockNumbers) { 485 | filter.fromBlock = c; 486 | filter.toBlock = c; 487 | const eventLogs = await blockchainInfo.provider.getLogs(filter); 488 | 489 | for (const eventLog of eventLogs) { 490 | const commitEvent = commitEventInterface.parseLog(eventLog); 491 | // merge eventLog and commitEvent 492 | events.push(Object.assign({}, eventLog, commitEvent)); 493 | } 494 | } 495 | 496 | return events; 497 | } 498 | 499 | export async function getCommits(events) { 500 | const commitDataIndex = 2; 501 | const commits = events.map(event => { 502 | let commit; 503 | try { 504 | commit = JSON.parse(event.args[commitDataIndex]); 505 | } catch (error) { 506 | console.error(`Cannot get valid Commit from block ${event.blockNumber}`); 507 | commit = {}; 508 | } 509 | return { 510 | "blockNumber": event.blockNumber, 511 | "transactionHash": event.transactionHash, 512 | "commit": commit, 513 | }; 514 | }); 515 | return commits; 516 | } 517 | 518 | export async function getCommitsSummary(events) { 519 | const commitDataIndex = 2; 520 | const commitsSummary = events.map(element => { 521 | let summary = { blockNumber: 0, txHash: "", commit: {} }; 522 | summary.commit = JSON.parse(element.args[commitDataIndex]); 523 | summary.blockNumber = element.blockNumber; 524 | summary.txHash = element.transactionHash; 525 | return summary; 526 | }); 527 | return commitsSummary; 528 | } 529 | 530 | /* TODO: Remove this function in the next feature release. 531 | * 532 | * Query events on Ethereum. The performance is faster than eventLogIteratingQuery. 533 | */ 534 | async function eventLogRangeQuery(assetCid: string, blockchainInfo) { 535 | console.log(`Commit logs of assetCid: ${assetCid}`); 536 | 537 | const commitBlockNumbers = await getCommitBlockNumbers(assetCid, blockchainInfo); 538 | const commitAmount = commitBlockNumbers.length; 539 | console.log(`Commit block numbers (${commitBlockNumbers.length}):`); 540 | if (commitAmount > 0) { 541 | console.log(`${JSON.stringify(commitBlockNumbers)}`); 542 | } else { 543 | return; 544 | } 545 | 546 | // Get events 547 | const filter = await blockchainInfo.contract.filters.Commit(null, assetCid); 548 | filter.fromBlock = commitBlockNumbers[0]; 549 | filter.toBlock = commitBlockNumbers[commitAmount - 1]; 550 | let events = await blockchainInfo.contract.queryFilter(filter, filter.fromBlock, filter.toBlock); 551 | const commitDataIndex = 2; 552 | for (const event of events) { 553 | console.log(`\nBlock ${event.blockNumber}`); 554 | console.log(`${blockchainInfo.explorerBaseUrl}/${event.transactionHash}`) 555 | console.log(`Commit: ${JSON.stringify(JSON.parse(event.args![commitDataIndex]), null, 2)}`); 556 | } 557 | } 558 | 559 | /* TODO: Remove this function in the next feature release. 560 | */ 561 | async function eventLogIteratingQuery(assetCid: string, blockchainInfo) { 562 | console.log(`assetCid: ${assetCid}`); 563 | 564 | const commitBlockNumbers = await getCommitBlockNumbers(assetCid, blockchainInfo); 565 | const commitAmount = commitBlockNumbers.length; 566 | console.log(`Commit block numbers (${commitBlockNumbers.length}):`); 567 | if (commitAmount > 0) { 568 | console.log(`${JSON.stringify(commitBlockNumbers)}`); 569 | } else { 570 | return; 571 | } 572 | 573 | /* WORKAROUND: create filters for every commit because Avalanche 574 | * only supports 2048-block range in an event log query. 575 | * 576 | * The query performance is more worse than eventLogRangeQuery, 577 | * but it's acceptable on Avalanche. 578 | */ 579 | // Create event log filter 580 | let filter = await blockchainInfo.contract.filters.Commit(null, assetCid); 581 | const abi = [ 582 | "event Commit(address indexed recorder, string indexed assetCid, string commitData)" 583 | ]; 584 | console.log(`Filter: ${JSON.stringify(filter, null, 2)}`); 585 | for (const c of commitBlockNumbers) { 586 | // Get event log by filter 587 | filter.fromBlock = c; 588 | filter.toBlock = c; 589 | const eventLogs = await blockchainInfo.provider.getLogs(filter); 590 | 591 | const commitEventInterface = new ethers.utils.Interface(abi); 592 | for (const eventLog of eventLogs) { 593 | console.log(`\nBlock number: ${(eventLog.blockNumber)}`); 594 | console.log(`${blockchainInfo.explorerBaseUrl}/${(eventLog.transactionHash)}`); 595 | 596 | const commitEvent = commitEventInterface.parseLog(eventLog); 597 | console.log(`commitEvent: ${JSON.stringify(commitEvent, null, 2)}`); 598 | 599 | try { 600 | const commitData = JSON.parse(commitEvent.args[2]); 601 | console.log(`Commit: ${JSON.stringify(commitData, null, 2)}`); 602 | } catch (error) { 603 | console.error(`Failed to parse Commit, error: ${error}`); 604 | } 605 | } 606 | } 607 | } 608 | 609 | /*---------------------------------------------------------------------------- 610 | * Ethereum Signature 611 | *----------------------------------------------------------------------------*/ 612 | export async function signIntegrityHash(sha256sum: string, signer) { 613 | let signature = await signer.signMessage(sha256sum); 614 | return signature; 615 | } 616 | 617 | export async function verifyIntegrityHash(sha256sum: string, signature) { 618 | const recoveredAddress = await ethers.utils.verifyMessage(sha256sum, signature); 619 | return recoveredAddress; 620 | } 621 | 622 | export async function getIntegrityHash(assetBytes: BytesLike) { 623 | return await (ethers.utils.sha256(assetBytes)).substring(2); 624 | } 625 | -------------------------------------------------------------------------------- /src/run.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ts-node 2 | 3 | import fs = require("fs"); 4 | import os = require("os"); 5 | 6 | import commandLineArgs = require("command-line-args"); 7 | import commandLineUsage = require("command-line-usage"); 8 | import mime = require("mime-types"); 9 | import sha256 = require("crypto-js/sha256"); 10 | 11 | import got from "got"; 12 | import * as colors from "colors"; 13 | 14 | import * as action from "./action"; 15 | import * as commitdb from "./commitdb"; 16 | import * as ipfs from "./ipfs"; 17 | import * as license from "./license"; 18 | import * as nit from "./nit"; 19 | 20 | const launch = require("launch-editor"); 21 | 22 | colors.enable(); 23 | 24 | /*---------------------------------------------------------------------------- 25 | * Configuration 26 | *----------------------------------------------------------------------------*/ 27 | const configFilepath = `${os.homedir()}/.nitconfig.json`; 28 | const workingDir = `.nit`; 29 | 30 | async function setWorkingAssetCid(assetCid: string) { 31 | if (fs.existsSync(`${workingDir}`) === false) { 32 | console.log(`Create working dir ${workingDir}`); 33 | fs.mkdirSync(`${workingDir}`, { recursive: true }); 34 | } else { 35 | // working dir exists 36 | } 37 | fs.writeFileSync(`${workingDir}/working.json`, JSON.stringify({"assetCid": assetCid}, null, 2)); 38 | } 39 | 40 | async function getWorkingAssetCid() { 41 | const workingConfig = JSON.parse(fs.readFileSync(`${workingDir}/working.json`, "utf-8")); 42 | return workingConfig.assetCid; 43 | } 44 | 45 | async function loadConfig() { 46 | const config = JSON.parse(fs.readFileSync(`${configFilepath}`, "utf-8")); 47 | return config; 48 | } 49 | 50 | async function writeConfig(configData: Object) { 51 | const nitconfig = `${configFilepath}`; 52 | if (fs.existsSync(nitconfig) === false) { 53 | fs.writeFileSync(nitconfig, 54 | JSON.stringify(configData, null, 2), 55 | { flag: "wx+" }); 56 | } else { 57 | console.warn(`Nit config ${nitconfig} exists.`); 58 | } 59 | } 60 | 61 | /*---------------------------------------------------------------------------- 62 | * I/O 63 | *----------------------------------------------------------------------------*/ 64 | async function stage(assetCid, stagedAssetTree, stagedCommit) { 65 | // Create staged dir whose name is assetCid 66 | const commitDir = `${workingDir}/${assetCid}`; 67 | if (fs.existsSync(commitDir) === false) { 68 | fs.mkdirSync(commitDir, { recursive: true }); 69 | } else {} 70 | 71 | fs.writeFileSync(`${commitDir}/assetTree.json`, JSON.stringify(stagedAssetTree, null, 2)); 72 | fs.writeFileSync(`${commitDir}/commit.json`, JSON.stringify(stagedCommit, null, 2)); 73 | await setWorkingAssetCid(assetCid); 74 | } 75 | 76 | async function getStagedCommit(assetCid) { 77 | return JSON.parse(fs.readFileSync(`${workingDir}/${assetCid}/commit.json`, "utf-8")); 78 | } 79 | 80 | async function getStagedAssetTree(assetCid) { 81 | return JSON.parse(fs.readFileSync(`${workingDir}/${assetCid}/assetTree.json`, "utf-8")); 82 | } 83 | 84 | /*---------------------------------------------------------------------------- 85 | * CLI Usage 86 | *----------------------------------------------------------------------------*/ 87 | async function help() { 88 | const logo = ` 89 | ████████████████████████████████████ 90 | ████████████████████████████████████ 91 | ████████████████████████████████████ 92 | ████████████████████████████████████ 93 | ████████▀▀▀▀▀▀████████▀▀▀▀▀▀████████ 94 | ████████ ▄█▌ ▀████▌ ████████ 95 | ████████ Φ███▌ ▀██▌ ████████ 96 | ████████ ╙▀█▌ ╙▀ ████████ 97 | ████████ L ▐█▄ ████████ 98 | ████████ ▐█▄ ▐███▄ ████████ 99 | ████████ ▐███▄ ▐██▀ ████████ 100 | ████████ ▐██████▄▀ ████████ 101 | ████████████████████████████████████ 102 | ████████████████████████████████████ 103 | ████████████████████████████████████ 104 | ████████████████████████████████████ 105 | ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ 106 | `; 107 | const sections = [ 108 | { 109 | content: logo, 110 | raw: true 111 | }, 112 | { 113 | header: "Available Commands", 114 | content: [ 115 | "init Initialize working environment", 116 | "config Edit Nit configuration", 117 | "add Add assetTree", 118 | "status Show current temporary commit", 119 | "commit Generate and register commit to web3", 120 | "sign Create integrity signature", 121 | "verify Verify integrity signature", 122 | "log Show asset's commits", 123 | "diff Show diff between two commits", 124 | "help Show this usage tips", 125 | ] 126 | }, 127 | { 128 | header: 'init', 129 | content: [ 130 | "$ nit init", 131 | ] 132 | }, 133 | { 134 | header: 'config', 135 | content: [ 136 | "$ nit config -e|--edit", 137 | "$ nit config -l|--list", 138 | ] 139 | }, 140 | { 141 | header: 'add', 142 | content: [ 143 | "$ nit add {underline assetFilepath} -m|--message {underline abstract}", 144 | "$ nit add {underline assetFilepath} -m|--message {underline abstract} --nft-record-cid {underline cid}", 145 | "$ nit add {underline assetFilepath} -m|--message {underline abstract} --integrity-cid {underline cid}", 146 | "$ nit add {underline assetFilepath} -m|--message {underline abstract} --license mit", 147 | "$ nit add {underline assetFilepath} -m|--message {underline abstract} --custom-license {underline license-json}", 148 | "$ nit add {underline assetFilepath} -m|--message {underline abstract} --nft-record-cid {underline cid} --integrity-cid {underline cid}", 149 | "$ nit add {underline assetFilepath} -m|--message {underline abstract} --nft-record-cid {underline cid} --integrity-cid {underline cid} --license mit", 150 | ] 151 | }, 152 | { 153 | header: 'addv1', 154 | content: [ 155 | "$ nit addv1 {underline assetTreeFilepath}", 156 | ] 157 | }, 158 | { 159 | header: 'status', 160 | content: [ 161 | "$ nit status", 162 | ] 163 | }, 164 | { 165 | header: 'commit', 166 | content: [ 167 | "$ nit commit -m|--message {underline abstract} -a|--action {underline action} -r|--action-result {underline actionResult}", 168 | "$ nit commit -m|--message {underline abstract} -a|--action {underline action} -r|--action-result {underline actionResult} --dry-run", 169 | "$ nit commit -m|--message {underline abstract} -a|--action {underline action} -r|--action-result {underline actionResult} --mockup", 170 | ] 171 | }, 172 | { 173 | header: 'commit options', 174 | optionList: [ 175 | { 176 | "name": "message", 177 | "description": 'Discription of this commit. The message will be in the "abstract" field in a Commit.', 178 | "alias": "m", 179 | "typeLabel": "{underline commit-description}" 180 | }, 181 | { 182 | "name": "action", 183 | "description": '(Under-development) The action happened on the targeting ditigal asset (addressed by Asset CID). The message will be in the "action" field in a Commit. You can put arbitrary value currently.', 184 | "alias": "a", 185 | "typeLabel": "{underline commit-description}" 186 | }, 187 | { 188 | "name": "action-result", 189 | "description": '(Under-development) The execution result of the action. The message will be in the "actionResult" field in a Commit. You can put arbitrary value currently.', 190 | "alias": "r", 191 | "typeLabel": "{underline commit-description}" 192 | }, 193 | { 194 | "name": "dry-run", 195 | "description": "Only show the Commit content and will not commit to blockchain. The added Asset Tree will not be cleaned." 196 | }, 197 | { 198 | "name": "mockup", 199 | "description": "Use Asset CID mockup (59 'a' chars) as Commit's targeting digital asset." 200 | }, 201 | ] 202 | }, 203 | { 204 | header: 'sign', 205 | content: [ 206 | "$ nit sign -i|--integrity-hash {underline integrityHash}", 207 | ] 208 | }, 209 | { 210 | header: 'verify', 211 | content: [ 212 | "$ nit verify -i|--integrity-hash {underline integrityHash} -s|--signature {underline signature}", 213 | ] 214 | }, 215 | { 216 | header: 'log', 217 | content: [ 218 | "$ nit log {underline assetCid}", 219 | "$ nit log {underline assetCid} --blocks", 220 | "$ nit log {underline assetCid} --from-index|-f {underline blockNumberIndex}", 221 | "$ nit log {underline assetCid} --from-index|-f {underline blockNumberIndex} --to-index|-t {underline blockNumberIndex}", 222 | ] 223 | }, 224 | { 225 | header: 'log options', 226 | optionList: [ 227 | { 228 | "name": "blocks", 229 | "description": 'Return block numbers of Commits and their indices in the block number array.', 230 | "alias": "b", 231 | "typeLabel": "{underline bool}", 232 | }, 233 | { 234 | "name": "from-index", 235 | "description": 'Get log starting from the block number related to this index. Range: [from, to)', 236 | "alias": "f", 237 | "typeLabel": "{underline blockNumberIndex}", 238 | }, 239 | { 240 | "name": "to-index", 241 | "description": 'Get log ending before the block number related to this index. Range: [from, to)', 242 | "alias": "t", 243 | "typeLabel": "{underline blockNumberIndex}", 244 | }, 245 | ] 246 | }, 247 | { 248 | header: 'diff', 249 | content: [ 250 | "$ nit diff {underline assetCid}", 251 | "$ nit diff {underline assetCid} --from|-f {underline blockNumberIndex}", 252 | "$ nit diff {underline assetCid} --from|-f {underline blockNumberIndex} --to|-t {underline blockNumberIndex}", 253 | ] 254 | }, 255 | ] 256 | const usage = commandLineUsage(sections) 257 | console.log(usage) 258 | } 259 | 260 | async function parseArgs() { 261 | const commandDefinitions = [ 262 | { name: "command", defaultOption: true }, 263 | ]; 264 | const commandOptions = commandLineArgs(commandDefinitions, 265 | { stopAtFirstUnknown: true }); 266 | const argv = commandOptions._unknown || []; 267 | 268 | if (commandOptions.command === "ipfsadd") { 269 | const paramDefinitions = [ 270 | { name: "filepath", defaultOption: true }, 271 | ]; 272 | const paramOptions = commandLineArgs(paramDefinitions, 273 | { argv, stopAtFirstUnknown: true }); 274 | return { 275 | "command": "ipfsadd", 276 | "params": { 277 | "fileapth": paramOptions.filepath, 278 | } 279 | } 280 | } else if (commandOptions.command === "init") { 281 | return { 282 | "command": "init", 283 | "params": {} 284 | } 285 | } else if (commandOptions.command === "addv1") { 286 | const paramDefinitions = [ 287 | { name: "filepath", defaultOption: true }, 288 | ]; 289 | const paramOptions = commandLineArgs(paramDefinitions, 290 | { argv, stopAtFirstUnknown: true }); 291 | return { 292 | "command": "addv1", 293 | "params": { 294 | "filepath": paramOptions.filepath, 295 | } 296 | }; 297 | } else if (commandOptions.command === "add") { 298 | const paramDefinitions = [ 299 | { name: "filepath", defaultOption: true }, 300 | { name: "message", alias: "m" }, 301 | { name: "nft-record-cid" }, 302 | { name: "integrity-cid" }, 303 | // FIXME: Support default license when latest Asset Tree 304 | // and staging Asset Tree comparison is ready. 305 | //{ name: "license", defaultValue: license.DefaultLicense }, 306 | { name: "license" }, 307 | { name: "custom-license" }, 308 | { name: "update", alias: "u" }, 309 | { name: "mockup" }, 310 | ]; 311 | const paramOptions = commandLineArgs(paramDefinitions, 312 | { argv, stopAtFirstUnknown: true }); 313 | return { 314 | "command": commandOptions.command, 315 | "params": paramOptions 316 | }; 317 | } else if (commandOptions.command === "commit") { 318 | const paramDefinitions = [ 319 | { name: "message", alias: "m" }, 320 | { name: "action", alias: "a" }, 321 | { name: "action-result", alias: "r" }, 322 | { name: "dry-run" }, 323 | { name: "mockup" }, 324 | ]; 325 | const paramOptions = commandLineArgs(paramDefinitions, { argv }); 326 | return { 327 | "command": commandOptions.command, 328 | "params": paramOptions 329 | } 330 | } else if (commandOptions.command === "status") { 331 | return { 332 | "command": "status", 333 | "params": {} 334 | } 335 | } else if (commandOptions.command === "sign") { 336 | const paramDefinitions = [ 337 | { name: "integrity-hash", alias: "i", defaultValue: "" }, 338 | { name: "filepath", alias: "f", defaultValue: "" }, 339 | ]; 340 | const paramOptions = commandLineArgs(paramDefinitions, { argv }); 341 | return { 342 | "command": "sign", 343 | "params": paramOptions 344 | } 345 | } else if (commandOptions.command === "verify") { 346 | const paramDefinitions = [ 347 | { name: "integrity-hash", alias: "i" }, 348 | { name: "signature", alias: "s" }, 349 | ]; 350 | const paramOptions = commandLineArgs(paramDefinitions, { argv }); 351 | return { 352 | "command": "verify", 353 | "params": paramOptions 354 | } 355 | } else if (commandOptions.command === "log") { 356 | const paramDefinitions = [ 357 | { name: "asset-cid", defaultOption: true }, 358 | { name: "blocks", alias: "b" }, 359 | { name: "from-index", alias: "f", defaultValue: 0 }, 360 | { name: "to-index", alias: "t", defaultValue: -1 }, 361 | ]; 362 | const paramOptions = commandLineArgs(paramDefinitions, 363 | { argv, stopAtFirstUnknown: true }); 364 | return { 365 | "command": "log", 366 | "params": paramOptions 367 | } 368 | } else if (commandOptions.command === "diff") { 369 | const paramDefinitions = [ 370 | { name: "asset-cid", defaultOption: true }, 371 | { name: "from-index", alias: "f", defaultValue: null }, 372 | { name: "to-index", alias: "t", defaultValue: null }, 373 | ]; 374 | const paramOptions = commandLineArgs(paramDefinitions, 375 | { argv, stopAtFirstUnknown: true }); 376 | return { 377 | "command": "diff", 378 | "params": paramOptions 379 | } 380 | } else if (commandOptions.command === "config") { 381 | const paramDefinitions = [ 382 | { name: "edit", alias: "e" }, 383 | { name: "list", alias: "l" }, 384 | ]; 385 | const paramOptions = commandLineArgs(paramDefinitions, { argv }); 386 | return { 387 | "command": "config", 388 | "params": paramOptions 389 | } 390 | } else if (commandOptions.command === "commitdb") { 391 | const paramDefinitions = [ 392 | { name: "asset-cid", defaultOption: true }, 393 | ]; 394 | const paramOptions = commandLineArgs(paramDefinitions, 395 | { argv, stopAtFirstUnknown: true }); 396 | return { 397 | "command": commandOptions.command, 398 | "params": paramOptions 399 | }; 400 | } else { 401 | return { 402 | "command": "help", 403 | "params": {} 404 | } 405 | } 406 | } 407 | 408 | async function assetSourceToBytes(source) { 409 | console.log("call assetSourceToBytes"); 410 | let assetBytes; 411 | if (source.substring(0, 4) === "bafy") { 412 | console.log("source cid"); 413 | assetBytes = await ipfs.ipfsCat(source); 414 | } else if (source.substring(0, 4) === "http") { 415 | console.log("source http"); 416 | assetBytes = (await got.get(source, { timeout: { request: 30000 } })).rawBody; 417 | } else { 418 | console.log("source filepath"); 419 | assetBytes = fs.readFileSync(source); 420 | } 421 | console.log(`${assetBytes.length}`); 422 | return assetBytes; 423 | } 424 | 425 | async function getMimetypeFromBytes(bytes) { 426 | /* The mime-types module relies on filename extension, 427 | * so saving a temporary file will not work, and the MimeType 428 | * will be "false". 429 | * 430 | * To get MimeType based on the magic number in a file, 431 | * file-type module might be a solution. 432 | * 433 | * The problem is that file-type only supports ES-Module currently. 434 | * https://github.com/sindresorhus/file-type/issues/525 435 | */ 436 | } 437 | 438 | async function main() { 439 | const args = await parseArgs(); 440 | 441 | if (args.command === "init") { 442 | await writeConfig(nit.nitconfigTemplate); 443 | await setWorkingAssetCid(""); 444 | console.log('You can run "nit config -e" to set configuration now.'); 445 | return 446 | } else if (fs.existsSync(configFilepath) === false) { 447 | console.log('Please run "nit init" to create config.'); 448 | return 449 | } else { 450 | // config exists 451 | } 452 | 453 | const config = await loadConfig(); 454 | const blockchain = await nit.loadBlockchain(config); 455 | 456 | await ipfs.initInfura(config.infura.projectId, config.infura.projectSecret); 457 | 458 | // Initialize Numbers Protocol IPFS with Capture Token if available 459 | if (config.numbersProtocol && config.numbersProtocol.captureToken) { 460 | await ipfs.initNumbersProtocol(config.numbersProtocol.captureToken); 461 | } 462 | 463 | if (args.command === "ipfsadd") { 464 | const contentBytes = fs.readFileSync(args.params.fileapth); 465 | const assetCid = await ipfs.ipfsAddBytes(contentBytes); 466 | console.log(`Command ipfsadd result (Asset CID): ${assetCid}`); 467 | } else if (args.command === "addv1") { 468 | const assetTreeFileContent = fs.readFileSync(args.params.filepath, "utf-8"); 469 | const assetTree = JSON.parse(assetTreeFileContent); 470 | console.log(`Add assetTree: ${JSON.stringify(assetTree, null, 2)}\n`); 471 | 472 | // Create commit dir whose name is assetCid 473 | const commitDir = `${workingDir}/${assetTree.assetCid}`; 474 | if (fs.existsSync(commitDir) === false) { 475 | fs.mkdirSync(commitDir, { recursive: true }); 476 | } else {} 477 | 478 | // Create staged assetTree file 479 | console.log(`Current assetTree: ${JSON.stringify(assetTree, null, 2)}\n`); 480 | fs.writeFileSync(`${commitDir}/assetTree.json`, JSON.stringify(assetTree, null, 2)); 481 | 482 | // Get assetTreeCid and encodingFormat 483 | const contentBytes = fs.readFileSync(`${commitDir}/assetTree.json`); 484 | const assetCid= await ipfs.ipfsAddBytes(contentBytes); 485 | 486 | // Get assetTreeSha256 487 | const assetTreeSha256 = sha256(assetTreeFileContent); 488 | 489 | const commit = { 490 | "assetTreeCid": assetCid, 491 | "assetTreeSha256": assetTreeSha256.toString(), 492 | "assetTreeSignature": await nit.signIntegrityHash( 493 | assetTreeSha256.toString(), blockchain.signer), 494 | "author": config.author, 495 | "committer": config.committer, 496 | "action": action.Actions["action-initial-registration"], 497 | "actionResult": `https://${assetCid}.ipfs.dweb.link`, 498 | "provider": config.provider, 499 | "abstract": "Initial registration.", 500 | "timestampCreated": Math.floor(Date.now() / 1000), 501 | } 502 | console.log(`Create temporary commit: ${JSON.stringify(commit, null, 2)}\n`); 503 | fs.writeFileSync(`${commitDir}/commit.json`, JSON.stringify(commit, null, 2)); 504 | 505 | // Update current target assetCid 506 | await setWorkingAssetCid(assetTree.assetCid); 507 | } else if (args.command === "add") { 508 | // Create staged AssetTree 509 | const assetBytes = fs.readFileSync(args.params.filepath); 510 | //const assetBytes = await assetSourceToBytes(args.params.filepath); 511 | 512 | let assetCid; 513 | if ("mockup" in args.params === false) { 514 | assetCid = await ipfs.ipfsAddBytes(assetBytes); 515 | } else { 516 | assetCid = "a".repeat(nit.cidv1Length); 517 | } 518 | let assetTree = await nit.pull(assetCid, blockchain); 519 | if (assetTree === null) { 520 | const assetMimetype = mime.lookup(args.params.filepath); 521 | const assetBirthtime = Math.floor(fs.statSync(args.params.filepath).birthtimeMs / 1000); 522 | //const assetMimetype = await getMimetypeFromBytes(assetBytes); 523 | //const assetBirthtime = Math.floor(Date.now() / 1000); 524 | 525 | assetTree = await nit.createAssetTreeInitialRegisterRemote(assetBytes, 526 | assetMimetype, 527 | assetBirthtime, 528 | config.author); 529 | console.log("Asset Tree is from initial registration\n"); 530 | } else { 531 | console.log("Asset Tree is from latest commit\n"); 532 | } 533 | 534 | /* Create Asset Tree updates */ 535 | let assetTreeUpdates: any = {}; 536 | if ("update" in args.params) { 537 | assetTreeUpdates = JSON.parse(args.params.update); 538 | } else { 539 | if ("message" in args.params) { 540 | assetTreeUpdates.abstract = args.params["message"]; 541 | } 542 | if ("nft-record-cid" in args.params) { 543 | assetTreeUpdates.nftRecord = args.params["nft-record-cid"]; 544 | } 545 | if ("integrity-cid" in args.params) { 546 | assetTreeUpdates.integrityCid= args.params["integrity-cid"]; 547 | } 548 | if (license.isSupportedLicense(args.params.license)) { 549 | assetTreeUpdates.license = license.Licenses[args.params.license]; 550 | } else { 551 | console.error(`Get unsupported or default license: ${args.params.license}\n`); 552 | } 553 | // Custom license will override default or specified license 554 | if ("custom-license" in args.params) { 555 | assetTreeUpdates.license = JSON.parse(args.params["custom-license"]); 556 | } 557 | } 558 | console.log(`Current Asset Tree: ${JSON.stringify(assetTree, null, 2)}\n`); 559 | console.log(`Current Asset Tree Updates: ${JSON.stringify(assetTreeUpdates, null, 2)}\n`); 560 | 561 | /* Create staged Asset Tree & staged Commit if there is any Asset Tree update */ 562 | if (Object.keys(assetTreeUpdates).length > 0) { 563 | const updatedAssetTree = await nit.updateAssetTree(assetTree, assetTreeUpdates); 564 | console.log(`Updated Asset Tree: ${JSON.stringify(updatedAssetTree, null, 2)}\n`); 565 | console.log(`Original Asset Tree: ${JSON.stringify(assetTree, null, 2)}\n`); 566 | 567 | const commit = await nit.createCommit(blockchain.signer, updatedAssetTree, config.author, config.provider); 568 | console.log(`Current Commit: ${JSON.stringify(commit, null, 2)}\n`); 569 | 570 | // Stage 571 | await stage(updatedAssetTree.assetCid, updatedAssetTree, commit); 572 | } else { 573 | console.error("No update and skip this command"); 574 | } 575 | } else if (args.command === "commit") { 576 | const assetCid = await getWorkingAssetCid(); 577 | 578 | if (await getWorkingAssetCid() === "") { 579 | console.log("Need to add an Asset before commit"); 580 | return; 581 | } else { 582 | // there is a working asset 583 | } 584 | 585 | let commitData = await getStagedCommit(assetCid); 586 | 587 | if ("message" in args.params) { 588 | commitData.abstract = args.params["message"]; 589 | } 590 | if ("action" in args.params) { 591 | commitData.action = action.Actions[args.params["action"]]; 592 | } 593 | if ("action-result" in args.params) { 594 | commitData.actionResult = args.params["action-result"]; 595 | } 596 | 597 | // Update commit.timestampCreated 598 | commitData.timestampCreated = Math.floor(Date.now() / 1000); 599 | 600 | console.log(`Asset Cid (index): ${assetCid}`); 601 | console.log(`Commit: ${JSON.stringify(commitData, null, 2)}`); 602 | 603 | if ("dry-run" in args.params === false) { 604 | console.debug(`Committing...`); 605 | console.log([ 606 | "Contract Information", 607 | `Signer wallet address: ${blockchain.signer.address}`, 608 | `Contract address: ${blockchain.contract.address}`, 609 | ]); 610 | 611 | let commitEventIndexCid; 612 | if ("mockup" in args.params === false) { 613 | commitEventIndexCid = assetCid; 614 | } else { 615 | commitEventIndexCid = nit.assetCidMock; 616 | } 617 | 618 | try { 619 | const transactionReceipt = await nit.commit(commitEventIndexCid, JSON.stringify(commitData), blockchain); 620 | console.log(`Commit Tx: ${transactionReceipt.transactionHash}`); 621 | console.log(`Commit Explorer: ${blockchain.explorerBaseUrl}/${transactionReceipt.transactionHash}`); 622 | } catch (error) { 623 | console.error(`Transaction error: ${error}`); 624 | } 625 | 626 | // Reset stage 627 | await setWorkingAssetCid(""); 628 | 629 | // Sync Commit Database 630 | if (config.commitDatabase.updateUrl.length > 0 && config.commitDatabase.commitUrl.length > 0) { 631 | // TODO: Asking Commit DB to sync 632 | //const updateCommitDbResult = await commitdb.push(config.commitDatabase.updateUrl, commitEventIndexCid, config.commitDatabase.commitUrl); 633 | //console.log(`Commit Database update: ${JSON.stringify(updateCommitDbResult, null, 2)}`); 634 | } else { 635 | // User does not set up Commit Database. 636 | } 637 | } else { 638 | console.log("This is dry run and Nit does not register this commit to blockchain."); 639 | } 640 | } else if (args.command === "status") { 641 | const workingAssetCid = await getWorkingAssetCid(); 642 | if (workingAssetCid !== "") { 643 | const commitData = await getStagedCommit(workingAssetCid); 644 | const assetTree = await getStagedAssetTree(workingAssetCid); 645 | console.log(`[ Working Asset CID ]\n${workingAssetCid}\n`); 646 | console.log(`[ Staged Commit ]\n${JSON.stringify(commitData, null, 2)}\n`); 647 | console.log(`[ Staged AssetTree ]\n${JSON.stringify(assetTree, null, 2)}\n`); 648 | } else { 649 | console.log("No working Asset"); 650 | } 651 | } else if (args.command === "sign") { 652 | const filepath = args.params.filepath; 653 | let integrityHash: string = args.params["integrity-hash"]; 654 | 655 | if (filepath !== "" && integrityHash !== "") { 656 | console.error("Signing target should be one of asset or integrity hash. You provide them both."); 657 | return; 658 | } 659 | 660 | if (filepath !== "") { 661 | if (fs.existsSync(filepath)) { 662 | const assetBytes = fs.readFileSync(filepath); 663 | integrityHash = await nit.getIntegrityHash(assetBytes); 664 | } else { 665 | console.error(`File does not exist: ${filepath}`); 666 | return; 667 | } 668 | } else { 669 | if (integrityHash === "") { 670 | await help(); 671 | } 672 | 673 | if (integrityHash.length != nit.integrityHashLength) { 674 | console.error(`Invalid integrity hash, length is ${integrityHash.length} but not 64.`); 675 | return; 676 | } 677 | } 678 | 679 | const signature = await nit.signIntegrityHash(integrityHash, blockchain.signer); 680 | console.log(`Signature: ${signature}`); 681 | } else if (args.command === "verify") { 682 | const integrityHash = args.params["integrity-hash"]; 683 | const signature = args.params.signature; 684 | const signerAddress = await nit.verifyIntegrityHash(integrityHash, signature); 685 | console.log(`Signer address: ${signerAddress}`); 686 | } else if (args.command === "log") { 687 | if ("asset-cid" in args.params) { 688 | const commitBlockNumbers = await nit.getCommitBlockNumbers(args.params["asset-cid"], blockchain); 689 | console.log(`Total Commit number: ${colors.cyan(commitBlockNumbers.length)}`); 690 | if ("blocks" in args.params) { 691 | commitBlockNumbers.map((number, index) => { console.log(`Index: ${index}, Block number: ${number}`); }); 692 | //console.log(`${JSON.stringify(commitBlockNumbers)}`); 693 | } else { 694 | await nit.log(args.params["asset-cid"], blockchain, parseInt(args.params["from-index"]), parseInt(args.params["to-index"])); 695 | } 696 | } else { 697 | await help(); 698 | } 699 | } else if (args.command === "diff") { 700 | if ("asset-cid" in args.params) { 701 | const diff = await nit.difference(args.params["asset-cid"], blockchain, args.params["from-index"], args.params["to-index"]); 702 | console.log(`from: block ${diff.fromBlockNumber}, tx ${diff.fromTransactionHash}`); 703 | console.log(` to: block ${diff.toBlockNumber}, tx ${diff.toTransactionHash}`); 704 | 705 | console.log("\nCommit difference"); 706 | await nit.showCommmitDiff(diff.commitDiff); 707 | 708 | console.log("\nAsset Tree difference"); 709 | await nit.showCommmitDiff(diff.assetTreeDiff); 710 | } else { 711 | await help(); 712 | } 713 | } else if (args.command === "config") { 714 | if ("edit" in args.params) { 715 | await launch(`${configFilepath}`); 716 | } else if ("list" in args.params) { 717 | console.log(JSON.stringify(config, null, 2)); 718 | } else { 719 | await help(); 720 | } 721 | } else if (args.command === "commitdb") { 722 | if ("asset-cid" in args.params) { 723 | const assetCid = args.params["asset-cid"]; 724 | const existingEntryAmount: number = (await commitdb.httpPost(config.commitDatabase.amountUrl, { "assetCid": assetCid })).response.commitAmount; 725 | 726 | const updatedAmounts = await commitdb.update( 727 | assetCid, 728 | blockchain, 729 | existingEntryAmount, 730 | config.commitDatabase.commitUrl, 731 | config.commitDatabase.accessToken 732 | ); 733 | 734 | console.log(`Update status of Asset ${colors.green(assetCid)} in Commit database`); 735 | console.log(`existed entries: ${colors.blue(updatedAmounts.originalDbEntryAmount.toString())}`); 736 | console.log(` added entries: ${colors.blue(updatedAmounts.updateDbEntryAmount.toString())}`); 737 | } else { 738 | await help(); 739 | } 740 | } else { 741 | await help(); 742 | } 743 | } 744 | 745 | main() 746 | .then(() => process.exit(0)) 747 | .catch((error) => { 748 | console.error(error); 749 | process.exit(1); 750 | }); -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | export function isSuperset(set: Set<string>, subset: Set<string>) { 2 | for (let elem of Array.from(subset.values())) { 3 | if (!set.has(elem)) { 4 | return false 5 | } 6 | } 7 | return true 8 | } 9 | 10 | /* Deep copy pure data which does not contain any function. 11 | */ 12 | export function deepCopy(data) { 13 | return JSON.parse(JSON.stringify(data)); 14 | } 15 | 16 | export function timestampToIsoString(timestamp): string { 17 | try { 18 | return new Date((parseInt(timestamp) * 1000)).toISOString() 19 | } catch (error) { 20 | console.log(`Invalid timestamp ${timestamp}. ${error}`); 21 | return ""; 22 | } 23 | } -------------------------------------------------------------------------------- /tests/testAction.ts: -------------------------------------------------------------------------------- 1 | /* manual test: yarn run test --timeout 10000 tests/testAction.ts 2 | */ 3 | 4 | import { expect } from "chai"; 5 | import * as action from "../src/action"; 6 | 7 | describe("Action Tests", function() { 8 | it("Check action by providing a valid action", async function () { 9 | const validAction = "action-initial-registration"; 10 | expect(action.getValidActionOrDefault(validAction)).to.be.equal(validAction); 11 | }); 12 | 13 | it("Check action by providing an invalid action", async function () { 14 | const validAction = "action-invalid"; 15 | expect(action.getValidActionOrDefault(validAction)).to.be.equal("action-commit"); 16 | }); 17 | 18 | it("Check action by providing a valid Action Nid", async function () { 19 | const validActionNid = "bafkreicptxn6f752c4pvb6gqwro7s7wb336idkzr6wmolkifj3aafhvwii"; 20 | const expectedActionName = "action-initial-registration-jade"; 21 | expect(action.getNameByNid(validActionNid)).to.be.equal(expectedActionName); 22 | }); 23 | 24 | it("Check action by providing an invalid action", async function () { 25 | const invalidActionNid = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; 26 | const expectedActionName = "action-commit"; 27 | expect(action.getNameByNid(invalidActionNid)).to.be.equal(expectedActionName); 28 | }); 29 | 30 | it("Debugging messages", async function () { 31 | console.log(`action: ${JSON.stringify(action.Actions, null, 2)}`); 32 | console.log(`action names: ${JSON.stringify(action.generateNameDictionary(action.ActionSet), null, 2)}`); 33 | }); 34 | }); -------------------------------------------------------------------------------- /tests/testHttp.ts: -------------------------------------------------------------------------------- 1 | /* manual test: yarn run test --timeout 10000 tests/testHttp.ts 2 | */ 3 | 4 | import fs = require("fs"); 5 | import os = require("os"); 6 | 7 | import { expect } from "chai"; 8 | import * as FormData from "form-data"; 9 | import * as stream from "stream"; 10 | import * as http from "../src/http"; 11 | import * as ipfs from "../src/ipfs"; 12 | 13 | describe("HTTP Functions", function() { 14 | const configFilepath = `${os.homedir()}/.nitconfig.json`; 15 | let config; 16 | let imageUrl; 17 | let imageCid; 18 | 19 | beforeEach(async function () { 20 | config = JSON.parse(fs.readFileSync(`${configFilepath}`, "utf-8")); 21 | imageUrl = "https://assets.website-files.com/6148548ab244696560ef92dd/614ba33bb06974479683baa0_Numbers.svg"; 22 | imageCid = "bafkreica6gtp7mgqtxcmcj2mwa2fzdd6tq3xuzmsoozrijksagqii7elly"; 23 | }); 24 | 25 | it("GET Numbers logo", async function () { 26 | const url = imageUrl; 27 | const headers = {}; 28 | const httpResponse = await http.get(url, headers); 29 | expect(httpResponse.status).to.be.equal(200); 30 | }); 31 | 32 | it("POST Infura IPFS add", async function () { 33 | const url = "https://ipfs.infura.io:5001/api/v0/add?cid-version=1&pin=false"; 34 | const infuraProjectId = config.infura.projectId; 35 | const infuraProjectSecret = config.infura.projectSecret; 36 | const infuraToken = await ipfs.infuraAccessToken(infuraProjectId, infuraProjectSecret); 37 | const imageData = (await http.get(imageUrl, {})).data; 38 | 39 | let formData = new FormData(); 40 | const fileReadStream = stream.Readable.from(Buffer.from(imageData)); 41 | formData.append("file", fileReadStream); 42 | 43 | const headers = { 44 | "Authorization": `Basic ${infuraToken}`, 45 | ...formData.getHeaders(), 46 | }; 47 | /* Infura IPFS add returns 48 | * { 49 | * "Name": "sample-result.json", 50 | * "Hash": "QmSTkR1kkqMuGEeBS49dxVJjgHRMH6cUYa7D3tcHDQ3ea3", 51 | * "Size": "2120" 52 | * } 53 | */ 54 | const httpResponse = await http.post(url, formData, headers); 55 | 56 | expect(httpResponse.status).to.be.equal(200); 57 | expect(httpResponse.data.Hash).to.be.equal(imageCid); 58 | }); 59 | }); -------------------------------------------------------------------------------- /tests/testIpfs.ts: -------------------------------------------------------------------------------- 1 | /* manual test: yarn run test --timeout 10000 tests/testIpfs.ts 2 | */ 3 | 4 | import fs = require("fs"); 5 | import os = require("os"); 6 | 7 | import { expect } from "chai"; 8 | import * as http from "../src/http"; 9 | import * as ipfs from "../src/ipfs"; 10 | 11 | describe("IPFS Functions", function() { 12 | const configFilepath = `${os.homedir()}/.nitconfig.json`; 13 | let config; 14 | let imageUrl; 15 | let imageCid; 16 | 17 | beforeEach(async function () { 18 | config = JSON.parse(fs.readFileSync(`${configFilepath}`, "utf-8")); 19 | imageUrl = "https://assets.website-files.com/6148548ab244696560ef92dd/614ba33bb06974479683baa0_Numbers.svg"; 20 | imageCid = "bafkreica6gtp7mgqtxcmcj2mwa2fzdd6tq3xuzmsoozrijksagqii7elly"; 21 | }); 22 | 23 | it("Estuary Add", async function () { 24 | const imageData = (await http.get(imageUrl, {})).data; 25 | 26 | await ipfs.initEstuary(config.estuary.apiKey); 27 | const cid = await ipfs.estuaryAdd(Buffer.from(imageData)); 28 | 29 | expect(cid).to.be.equal(imageCid); 30 | }); 31 | }); -------------------------------------------------------------------------------- /tests/testNit.ts: -------------------------------------------------------------------------------- 1 | /* manual test: yarn run test tests/testNit.ts 2 | */ 3 | 4 | import fs = require("fs"); 5 | import os = require("os"); 6 | 7 | import { expect } from "chai"; 8 | import * as nit from "../src/nit"; 9 | 10 | describe("Nit", function() { 11 | const configFilepath = `${os.homedir()}/.nitconfig.json`; 12 | let config; 13 | let imageUrl; 14 | let imageCid; 15 | 16 | /* Asset Tree of the generative AI example #1 17 | * https://nftsearch.site/asset-profile?nid=bafkreid4ug5djtm6iq6hptfs337n3k3driqncivegexzpffzlapiaple44 18 | */ 19 | const gaiAssetTree = { 20 | "assetCid": "bafkreid4ug5djtm6iq6hptfs337n3k3driqncivegexzpffzlapiaple44", 21 | "assetSha256": "7ca1ba34cd9e443c77ccb2defeddab638a20d122a4312f9794b9581e803d64e7", 22 | "encodingFormat": "image/jpeg", 23 | "assetTimestampCreated": 1683287179, 24 | "assetCreator": "", 25 | "license": { 26 | "name": null, 27 | "document": null 28 | }, 29 | "abstract": "", 30 | "assetSourceType": "captureUpload", 31 | "creatorWallet": "0x6059DFC1daFb109474aB6fAD87E93A11Bfa5e1D2" 32 | }; 33 | 34 | beforeEach(async function () { 35 | config = JSON.parse(fs.readFileSync(`${configFilepath}`, "utf-8")); 36 | imageUrl = "https://assets.website-files.com/6148548ab244696560ef92dd/614ba33bb06974479683baa0_Numbers.svg"; 37 | imageCid = "bafkreica6gtp7mgqtxcmcj2mwa2fzdd6tq3xuzmsoozrijksagqii7elly"; 38 | }); 39 | 40 | it(".. should add a valid license successfully", async function () { 41 | const stagingAssetTree = await nit.createAssetTreeInitialRegister( 42 | gaiAssetTree.assetCid, 43 | gaiAssetTree.assetSha256, 44 | gaiAssetTree.encodingFormat, 45 | gaiAssetTree.assetTimestampCreated, 46 | gaiAssetTree.assetCreator, 47 | "cc-by-nc-nd-4.0", 48 | gaiAssetTree.abstract 49 | ); 50 | 51 | await expect(stagingAssetTree.license.name).to.be.equal("CC-BY-NC-ND-4.0"); 52 | await expect(stagingAssetTree.license.document).to.be.equal("https://creativecommons.org/licenses/by-nc-nd/4.0/legalcode"); 53 | }); 54 | 55 | /* TODO: For the key with null or undefined, should we keep the key or remove it? 56 | * We remove the key for now. 57 | */ 58 | it(".. should add an invalid license without breaking program", async function () { 59 | const stagingAssetTree = await nit.createAssetTreeInitialRegister( 60 | gaiAssetTree.assetCid, 61 | gaiAssetTree.assetSha256, 62 | gaiAssetTree.encodingFormat, 63 | gaiAssetTree.assetTimestampCreated, 64 | gaiAssetTree.assetCreator, 65 | "cc-by-nc-nd-invalid", 66 | gaiAssetTree.abstract 67 | ); 68 | 69 | await expect(stagingAssetTree.license).to.be.undefined; 70 | }); 71 | 72 | it(".. should add an abstract successfully", async function () { 73 | gaiAssetTree.abstract = ""; 74 | 75 | const stagingAssetTree = await nit.createAssetTreeInitialRegister( 76 | gaiAssetTree.assetCid, 77 | gaiAssetTree.assetSha256, 78 | gaiAssetTree.encodingFormat, 79 | gaiAssetTree.assetTimestampCreated, 80 | gaiAssetTree.assetCreator, 81 | "cc-by-nc-nd-4.0", 82 | gaiAssetTree.abstract 83 | ); 84 | 85 | await expect(stagingAssetTree.abstract).to.be.equal(""); 86 | }); 87 | 88 | /* TODO: For the key with null or undefined, should we keep the key or remove it? 89 | * We remove the key for now. 90 | */ 91 | it(".. should add an invalid abstract without breaking program", async function () { 92 | const stagingAssetTree = await nit.createAssetTreeInitialRegister( 93 | gaiAssetTree.assetCid, 94 | gaiAssetTree.assetSha256, 95 | gaiAssetTree.encodingFormat, 96 | gaiAssetTree.assetTimestampCreated, 97 | gaiAssetTree.assetCreator, 98 | "cc-by-nc-nd-4.0", 99 | undefined 100 | ); 101 | 102 | await expect(stagingAssetTree.abstract).to.be.undefined; 103 | }); 104 | }); -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noImplicitAny": false, 4 | "outDir": "./lib" 5 | }, 6 | "include": [ 7 | "src/**/*.ts" 8 | ], 9 | "exclude": [ 10 | "./node_modules/", 11 | "./dist/" 12 | ] 13 | } --------------------------------------------------------------------------------