├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── stale.yml └── workflows │ ├── release.yml │ └── test.yml ├── .gitignore ├── .prettierrc ├── .releaserc ├── .vscode └── launch.json ├── CHANGELOG.md ├── Contributing.md ├── LICENSE ├── README.md ├── babel-preset.json ├── babel.config.json ├── doc └── did-method-spec.md ├── eslint.config.mjs ├── esm ├── index.js └── package.json ├── jest.config.mjs ├── package.json ├── renovate.json ├── src ├── __tests__ │ ├── EthereumDIDRegistry-Legacy │ │ └── LegacyEthereumDIDRegistry.json │ ├── config.test.ts │ ├── errors.test.ts │ ├── extended-id.test.ts │ ├── index.test.ts │ ├── networks.integration.test.ts │ ├── nonce.test.ts │ ├── resolve.attribute.test.ts │ ├── resolve.controller.test.ts │ ├── resolve.delegate.test.ts │ ├── resolve.metatx.test.ts │ ├── resolve.overlap.test.ts │ ├── resolve.unregistered.test.ts │ ├── resolver.regression.test.ts │ ├── resolver.versioning.test.ts │ └── testUtils.ts ├── config │ ├── EthereumDIDRegistry.ts │ └── deployments.ts ├── configuration.ts ├── controller.ts ├── helpers.ts ├── index.ts ├── logParser.ts └── resolver.ts ├── tsconfig.json └── yarn.lock /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### Prerequisites 11 | 12 | Please answer the following questions for yourself before submitting an issue. 13 | 14 | - [ ] I am running the latest version 15 | - [ ] I checked the documentation and found no answer 16 | - [ ] I checked to make sure that this issue has not already been filed 17 | 18 | **YOU MAY DELETE THE PREREQUISITES SECTION** if you're sure you checked all the boxes. 19 | 20 | ### Current Behavior 21 | 22 | What is the current behavior? 23 | 24 | ### Expected Behavior 25 | 26 | Please describe the behavior you are expecting 27 | 28 | ### Failure Information 29 | 30 | Please help provide information about the failure. 31 | 32 | #### Steps to Reproduce 33 | 34 | Please provide detailed steps for reproducing the issue. 35 | 36 | 1. step 1 37 | 2. step 2 38 | 3. you get it... 39 | 40 | #### Environment Details 41 | 42 | Please provide any relevant information about your setup. This is important in case the issue is not reproducible except for under certain conditions. 43 | 44 | * node/browser version: 45 | * OS Version: 46 | * Device details: 47 | 48 | #### Failure Logs/Screenshots 49 | 50 | Please include any relevant log snippets or files here. 51 | Create a [GIST](https://gist.github.com) which is a paste of your _full or sanitized_ logs, and link them here. 52 | Please do _NOT_ paste your full logs here, as it will make this issue very long and hard to read! 53 | 54 | #### Alternatives you considered 55 | 56 | Please provide details about an environment where this bug does not occur. 57 | 58 | --- 59 | 60 | > **Don't paste private keys anywhere public!** 61 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '[proposal]' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 70 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 14 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - pinned 8 | - security 9 | - in-progress 10 | - planned-feature 11 | - good-first-issue 12 | - triage 13 | # Label to use when marking an issue as stale 14 | staleLabel: stale 15 | # Comment to post when marking an issue as stale. Set to `false` to disable 16 | markComment: > 17 | This issue has been automatically marked as stale because it has not had 18 | recent activity. It will be closed if no further activity occurs. Thank you 19 | for your contributions. 20 | # Comment to post when closing a stale issue. Set to `false` to disable 21 | closeComment: false 22 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Build, Test and Publish 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: 6 | - 'master' 7 | - 'alpha' 8 | jobs: 9 | build-test-publish: 10 | runs-on: ubuntu-24.04 11 | steps: 12 | - uses: actions/checkout@v4 13 | with: 14 | fetch-depth: 0 15 | token: ${{ secrets.GH_TOKEN }} 16 | 17 | - name: "Setup node with cache" 18 | uses: actions/setup-node@v4 19 | with: 20 | node-version: 18 21 | cache: 'yarn' 22 | 23 | - run: yarn install --frozen-lockfile 24 | - run: yarn run build 25 | 26 | - name: "Setup git coordinates" 27 | run: | 28 | git config user.name ${{ secrets.GH_USER }} 29 | git config user.email ${{ secrets.GH_EMAIL }} 30 | 31 | - name: "Run semantic-release" 32 | env: 33 | GH_TOKEN: ${{secrets.GH_TOKEN}} 34 | NPM_TOKEN: ${{secrets.NPM_TOKEN}} 35 | if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/alpha' 36 | run: yarn run release 37 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Build and Test NODE 2 | on: [pull_request, workflow_dispatch, push] 3 | jobs: 4 | build-test: 5 | runs-on: ubuntu-24.04 6 | strategy: 7 | matrix: 8 | version: [ 18 ] 9 | steps: 10 | - uses: actions/checkout@v4 11 | with: 12 | fetch-depth: 0 13 | 14 | - name: "Setup node with cache" 15 | uses: actions/setup-node@v4 16 | with: 17 | node-version: ${{ matrix.version }} 18 | cache: 'yarn' 19 | 20 | - run: yarn install --frozen-lockfile 21 | - run: yarn run build 22 | - run: yarn run lint 23 | - run: yarn run test:ci 24 | 25 | - name: "Upload coverage reports" 26 | uses: codecov/codecov-action@v5 27 | with: 28 | fail_ci_if_error: true 29 | token: ${{ secrets.CODECOV_TOKEN }} 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib/ 2 | 3 | # Created by https://www.gitignore.io/api/node,linux,macos,windows,intellij 4 | # Edit at https://www.gitignore.io/?templates=node,linux,macos,windows,intellij 5 | 6 | ### Intellij ### 7 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 8 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 9 | 10 | # User-specific stuff 11 | **/.idea/ 12 | .vscode/ 13 | 14 | # CMake 15 | cmake-build-*/ 16 | 17 | # Mongo Explorer plugin 18 | .idea/**/mongoSettings.xml 19 | 20 | # File-based project format 21 | *.iws 22 | 23 | # IntelliJ 24 | out/ 25 | 26 | # mpeltonen/sbt-idea plugin 27 | .idea_modules/ 28 | 29 | # JIRA plugin 30 | atlassian-ide-plugin.xml 31 | 32 | # Cursive Clojure plugin 33 | .idea/replstate.xml 34 | 35 | # Crashlytics plugin (for Android Studio and IntelliJ) 36 | com_crashlytics_export_strings.xml 37 | crashlytics.properties 38 | crashlytics-build.properties 39 | fabric.properties 40 | 41 | # Editor-based Rest Client 42 | .idea/httpRequests 43 | 44 | # Android studio 3.1+ serialized cache file 45 | .idea/caches/build_file_checksums.ser 46 | 47 | ### Intellij Patch ### 48 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 49 | 50 | *.iml 51 | # modules.xml 52 | # .idea/misc.xml 53 | # *.ipr 54 | 55 | # Sonarlint plugin 56 | .idea/**/sonarlint/ 57 | 58 | # SonarQube Plugin 59 | .idea/**/sonarIssues.xml 60 | 61 | # Markdown Navigator plugin 62 | .idea/**/markdown-navigator.xml 63 | .idea/**/markdown-navigator/ 64 | 65 | ### Linux ### 66 | *~ 67 | 68 | # temporary files which can be created if a process still has a handle open of a deleted file 69 | .fuse_hidden* 70 | 71 | # KDE directory preferences 72 | .directory 73 | 74 | # Linux trash folder which might appear on any partition or disk 75 | .Trash-* 76 | 77 | # .nfs files are created when an open file is removed but is still being accessed 78 | .nfs* 79 | 80 | ### macOS ### 81 | # General 82 | .DS_Store 83 | .AppleDouble 84 | .LSOverride 85 | 86 | # Icon must end with two \r 87 | Icon 88 | 89 | # Thumbnails 90 | ._* 91 | 92 | # Files that might appear in the root of a volume 93 | .DocumentRevisions-V100 94 | .fseventsd 95 | .Spotlight-V100 96 | .TemporaryItems 97 | .Trashes 98 | .VolumeIcon.icns 99 | .com.apple.timemachine.donotpresent 100 | 101 | # Directories potentially created on remote AFP share 102 | .AppleDB 103 | .AppleDesktop 104 | Network Trash Folder 105 | Temporary Items 106 | .apdisk 107 | 108 | ### Node ### 109 | # Logs 110 | logs 111 | *.log 112 | npm-debug.log* 113 | yarn-debug.log* 114 | yarn-error.log* 115 | lerna-debug.log* 116 | 117 | # Diagnostic reports (https://nodejs.org/api/report.html) 118 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 119 | 120 | # Runtime data 121 | pids 122 | *.pid 123 | *.seed 124 | *.pid.lock 125 | 126 | # Directory for instrumented libs generated by jscoverage/JSCover 127 | lib-cov 128 | 129 | # Coverage directory used by tools like istanbul 130 | coverage 131 | *.lcov 132 | 133 | # nyc test coverage 134 | .nyc_output 135 | 136 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 137 | .grunt 138 | 139 | # Bower dependency directory (https://bower.io/) 140 | bower_components 141 | 142 | # node-waf configuration 143 | .lock-wscript 144 | 145 | # Compiled binary addons (https://nodejs.org/api/addons.html) 146 | build/Release 147 | 148 | # Dependency directories 149 | node_modules/ 150 | jspm_packages/ 151 | 152 | # TypeScript v1 declaration files 153 | typings/ 154 | 155 | # TypeScript cache 156 | *.tsbuildinfo 157 | 158 | # Optional npm cache directory 159 | .npm 160 | 161 | # Optional eslint cache 162 | .eslintcache 163 | 164 | # Optional REPL history 165 | .node_repl_history 166 | 167 | # Output of 'npm pack' 168 | *.tgz 169 | 170 | # Yarn Integrity file 171 | .yarn-integrity 172 | 173 | # dotenv environment variables file 174 | .env 175 | .env.test 176 | 177 | # parcel-bundler cache (https://parceljs.org/) 178 | .cache 179 | 180 | # next.js build output 181 | .next 182 | 183 | # nuxt.js build output 184 | .nuxt 185 | 186 | # react / gatsby 187 | public/ 188 | 189 | # vuepress build output 190 | .vuepress/dist 191 | 192 | # Serverless directories 193 | .serverless/ 194 | 195 | # FuseBox cache 196 | .fusebox/ 197 | 198 | # DynamoDB Local files 199 | .dynamodb/ 200 | 201 | ### Windows ### 202 | # Windows thumbnail cache files 203 | Thumbs.db 204 | Thumbs.db:encryptable 205 | ehthumbs.db 206 | ehthumbs_vista.db 207 | 208 | # Dump file 209 | *.stackdump 210 | 211 | # Folder config file 212 | [Dd]esktop.ini 213 | 214 | # Recycle Bin used on file shares 215 | $RECYCLE.BIN/ 216 | 217 | # Windows Installer files 218 | *.cab 219 | *.msi 220 | *.msix 221 | *.msm 222 | *.msp 223 | 224 | # Windows shortcuts 225 | *.lnk 226 | 227 | # End of https://www.gitignore.io/api/node,linux,macos,windows,intellij -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "jsxBracketSameLine": false, 3 | "tabWidth": 2, 4 | "printWidth": 120, 5 | "singleQuote": true, 6 | "trailingComma": "es5", 7 | "semi": false 8 | } 9 | -------------------------------------------------------------------------------- /.releaserc: -------------------------------------------------------------------------------- 1 | { 2 | "tagFormat": "${version}", 3 | "plugins": [ 4 | "@semantic-release/commit-analyzer", 5 | "@semantic-release/release-notes-generator", 6 | [ 7 | "@semantic-release/changelog", 8 | { 9 | "changelogFile": "CHANGELOG.md" 10 | } 11 | ], 12 | "@semantic-release/npm", 13 | [ 14 | "@semantic-release/git", 15 | { 16 | "assets": [ 17 | "CHANGELOG.md", 18 | "docs", 19 | "package.json", 20 | "yarn.lock" 21 | ], 22 | "message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}" 23 | } 24 | ], 25 | "@semantic-release/github" 26 | ], 27 | "branches": [ 28 | "master", 29 | "next", 30 | { 31 | "name": "alpha", 32 | "prerelease": true 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Jest All", 11 | "program": "${workspaceFolder}/node_modules/.bin/jest", 12 | "args": ["--runInBand"], 13 | "console": "integratedTerminal", 14 | "internalConsoleOptions": "neverOpen", 15 | "windows": { 16 | "program": "${workspaceFolder}/node_modules/jest/bin/jest" 17 | } 18 | }, 19 | { 20 | "type": "node", 21 | "request": "launch", 22 | "name": "Jest Current File", 23 | "program": "${workspaceFolder}/node_modules/.bin/jest", 24 | "args": ["${relativeFile}", "--detectOpenHandles"], 25 | "console": "integratedTerminal", 26 | "internalConsoleOptions": "neverOpen", 27 | "windows": { 28 | "program": "${workspaceFolder}/node_modules/jest/bin/jest" 29 | } 30 | }, 31 | { 32 | "type": "node", 33 | "request": "launch", 34 | "name": "Launch Program", 35 | "skipFiles": ["/**"], 36 | "program": "${workspaceFolder}/lib/index.js", 37 | "preLaunchTask": "tsc: build - tsconfig.json", 38 | "outFiles": ["${workspaceFolder}/lib/**/*.js"] 39 | }, 40 | { 41 | "type": "node", 42 | "request": "launch", 43 | "name": "test revoker", 44 | "skipFiles": ["/**"], 45 | "program": "${workspaceFolder}/node_modules/.bin/jest", 46 | "args": ["revokerTests"] 47 | }, 48 | { 49 | "type": "node", 50 | "request": "launch", 51 | "name": "test basic", 52 | "skipFiles": ["/**"], 53 | "program": "${workspaceFolder}/node_modules/.bin/jest", 54 | "args": ["basic"] 55 | } 56 | ] 57 | } 58 | -------------------------------------------------------------------------------- /Contributing.md: -------------------------------------------------------------------------------- 1 | # How to contribute to ethr-did-resolver 2 | 3 | We love your input! We want to make contributing to this project as easy and transparent as possible, whether it's: 4 | 5 | - Reporting a bug 6 | - Discussing the current state of the code 7 | - Submitting a fix 8 | - Proposing new features 9 | 10 | ## Report a bug with detail, background and sample code 11 | **Great Bug Reports** tend to have: 12 | 13 | - A quick summary and/or background 14 | - Steps to reproduce 15 | - Be specific! 16 | - Give sample code if you can. 17 | - What you expected would happen 18 | - What actually happens 19 | - Notes (possibly including why you think this might be happening, or stuff you tried that didn't work) 20 | - You get extra kudos if you attach a failing test demonstrating that bug 21 | 22 | ## Submitting improvements 23 | 24 | ### Commit messages 25 | We use github to host code, to track issues and feature requests, as well as accept pull requests. 26 | We Use [semantic-release](https://github.com/semantic-release/semantic-release) and 27 | [commitlint](https://github.com/conventional-changelog/commitlint) to automate our release process. 28 | Versioning, changelogs and publication is all covered by this automation. 29 | Please see some [commit message examples](https://github.com/semantic-release/semantic-release#commit-message-format) 30 | 31 | Commit messages are really important in this process, and the build will fail if your commits don't adhere to this. 32 | 33 | ### Code style 34 | Use the built in code formatter (`npm run format`) before committing code. It makes lives much easier. 35 | 36 | ### Submitting a fix 37 | - Branch off of `master` 38 | - Wherever possible, commit at least one test to demonstrate the bug 39 | - Commit your code to fix that bug 40 | - Create a PR for it 41 | - Mention the issue you're fixing in the PR (Example: __Closes #17__) 42 | 43 | ### Submitting a proposal 44 | We prefer to discuss proposals before accepting them into the codebase. 45 | Open an issue with as much detail and background as possible to make your case. 46 | Small proposals can come in directly as PRs, but it's generally better to discuss before starting work. 47 | 48 | Any contributions you make will be under the Apache-2.0 License 49 | 50 | ### Posting PRs 51 | - Describe your changes in the PR description. 52 | - Mention issues that should be fixed or closed when the PR is merged. 53 | - Make sure any new code has tests associated! 54 | - Make sure the documentation is still valid if your changes get included. 55 | 56 | Thank you for your contribution! -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2018 Consensys AG 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![npm](https://img.shields.io/npm/dt/ethr-did-resolver.svg)](https://www.npmjs.com/package/ethr-did-resolver) 2 | [![npm](https://img.shields.io/npm/v/ethr-did-resolver.svg)](https://www.npmjs.com/package/ethr-did-resolver) 3 | [![codecov](https://codecov.io/gh/decentralized-identity/ethr-did-resolver/branch/develop/graph/badge.svg)](https://codecov.io/gh/decentralized-identity/ethr-did-resolver) 4 | 5 | # ethr DID Resolver 6 | 7 | This library is intended to use ethereum addresses or secp256k1 publicKeys as fully self-managed 8 | [Decentralized Identifiers](https://w3c.github.io/did-core/#identifier) and wrap them in a 9 | [DID Document](https://w3c.github.io/did-core/#did-document-properties) 10 | 11 | It supports the proposed [Decentralized Identifiers](https://w3c.github.io/did-core/#identifier) spec from the 12 | [W3C Credentials Community Group](https://w3c-ccg.github.io). 13 | 14 | It requires the `did-resolver` library, which is the primary interface for resolving DIDs. 15 | 16 | This DID method relies on the [ethr-did-registry](https://github.com/uport-project/ethr-did-registry). 17 | 18 | ## DID method 19 | 20 | To encode a DID for an Ethereum address on the ethereum mainnet, simply prepend `did:ethr:` 21 | 22 | eg: 23 | 24 | `did:ethr:0xf3beac30c498d9e26865f34fcaa57dbb935b0d74` 25 | 26 | Multi-network DIDs are also supported, if the proper configuration is provided during setup. 27 | 28 | For example: 29 | `did:ethr:0x5:0xf3beac30c498d9e26865f34fcaa57dbb935b0d74` gets resolved on the goerli testnet (chainID=0x5), and 30 | represents a distinct identifier than the generic one, with different DID documents and different key rotation history. 31 | 32 | ## DID Document 33 | 34 | The did resolver takes the ethereum address, looks at contract events and builds a DID document based on the ERC1056 35 | Events corresponding to the address. When an identifier is a full `publicKey`, the corresponding `ethereumAddress` is 36 | computed and checked in the same manner. 37 | 38 | The minimal DID document for an ethereum address `0xb9c5714089478a327f09197987f16f9e5d936e8a` with no transactions to 39 | the registry looks like this: 40 | 41 | ```json 42 | { 43 | "@context": [ 44 | "https://www.w3.org/ns/did/v1", 45 | "https://w3id.org/security/suites/secp256k1recovery-2020/v2" 46 | ], 47 | "id": "did:ethr:0xb9c5714089478a327f09197987f16f9e5d936e8a", 48 | "verificationMethod": [ 49 | { 50 | "id": "did:ethr:0xb9c5714089478a327f09197987f16f9e5d936e8a#controller", 51 | "type": "EcdsaSecp256k1RecoveryMethod2020", 52 | "controller": "did:ethr:0xb9c5714089478a327f09197987f16f9e5d936e8a", 53 | "blockchainAccountId": "eip155:1:0xb9c5714089478a327f09197987f16f9e5d936e8a" 54 | } 55 | ], 56 | "authentication": [ 57 | "did:ethr:0xb9c5714089478a327f09197987f16f9e5d936e8a#controller" 58 | ], 59 | "assertionMethod": [ 60 | "did:ethr:0xb9c5714089478a327f09197987f16f9e5d936e8a#controller" 61 | ] 62 | } 63 | ``` 64 | 65 | Note this resolver uses the `EcdsaSecp256k1RecoveryMethod2020` type and an `blockchainAccountId` to represent the 66 | default 67 | `verificationMethod`, `assertionMethod`, and `authentication` entry. Any value from the registry that returns an 68 | ethereum address will be added to the `verificationMethod` array of the DID document with 69 | type `EcdsaSecp256k1RecoveryMethod2020` and an `blockchainAccountId` attribute containing the address. 70 | 71 | ## Building a DID document 72 | 73 | The DID document is not stored as a file, but is built by using read only functions and contract events on 74 | the [ethr-did-registry](https://github.com/uport-project/ethr-did-registry) Ethereum smart contract. 75 | 76 | Please see the [spec](doc/did-method-spec.md) for details of how the DID document and corresponding metadata are 77 | computed. 78 | 79 | ## Resolving a DID document 80 | 81 | The library presents a `resolve()` function that returns a `Promise` returning the DID document. It is not meant to be 82 | used directly but through the [`did-resolver`](https://github.com/decentralized-identity/did-resolver) aggregator. 83 | 84 | You can use the `getResolver(config)` method to produce an entry that can be used with the `Resolver` 85 | constructor: 86 | 87 | ```javascript 88 | import { Resolver } from 'did-resolver' 89 | import { getResolver } from 'ethr-did-resolver' 90 | 91 | const providerConfig = { 92 | // While experimenting, you can set a rpc endpoint to be used by the web3 provider 93 | rpcUrl: 'http://localhost:7545', 94 | // You can also set the address for your own ethr-did-registry (ERC1056) contract 95 | registry: registry.address, 96 | name: 'development' // this becomes did:ethr:development:0x... 97 | } 98 | // It's recommended to use the multi-network configuration when using this in production 99 | // since that allows you to resolve on multiple public and private networks at the same time. 100 | 101 | // getResolver will return an object with a key/value pair of { "ethr": resolver } where resolver is a function used by the generic did resolver. 102 | const ethrDidResolver = getResolver(providerConfig) 103 | const didResolver = new Resolver(ethrDidResolver) 104 | 105 | didResolver 106 | .resolve('did:ethr:development:0xf3beac30c498d9e26865f34fcaa57dbb935b0d74') 107 | .then((result) => console.dir(result, { depth: 3 })) 108 | ``` 109 | 110 | ## Multi-network configuration 111 | 112 | In production, you will most likely want the ability to resolve DIDs that are based in different ethereum networks. To 113 | do this, you need a configuration that sets the network name or chain ID (and even the registry address) for each 114 | network. An example configuration for multi-network DID resolving would look like this: 115 | 116 | ```javascript 117 | const providerConfig = { 118 | networks: [ 119 | { name: "mainnet", provider: web3.currentProvider }, 120 | { name: "0x5", rpcUrl: "https://goerli.infura.io/v3/" }, 121 | { name: "rsk:testnet", chainId: "0x1f", rpcUrl: "https://did.testnet.rsk.co:4444" }, 122 | { name: "development", rpcUrl: "http://localhost:7545", registry: "0xdca7ef03e98e0dc2b855be647c39abe984fcf21b" }, 123 | { name: "myprivatenet", chainId: 123456, rpcUrl: "https://my.private.net.json.rpc.url" } 124 | ] 125 | } 126 | 127 | const ethrDidResolver = getResolver(providerConfig) 128 | ``` 129 | 130 | The configuration from above allows you to resolve ethr-did's of the following formats: 131 | 132 | - `did:ethr:mainnet:0xabcabc03e98e0dc2b855be647c39abe984193675` 133 | - `did:ethr:0xabcabc03e98e0dc2b855be647c39abe984193675` (defaults to mainnet configuration) 134 | - `did:ethr:0x5:0xabcabc03e98e0dc2b855be647c39abe984193675` (refer to the goerli network by chainID) 135 | - `did:ethr:rsk:testnet:0xabcabc03e98e0dc2b855be647c39abe984193675` 136 | - `did:ethr:0x1f:0xabcabc03e98e0dc2b855be647c39abe984193675` (refer to the rsk:testnet by chainID) 137 | - `did:ethr:development:0xabcabc03e98e0dc2b855be647c39abe984193675` 138 | - `did:ethr:myprivatenet:0xabcabc03e98e0dc2b855be647c39abe984193675` 139 | - `did:ethr:0x1e240:0xabcabc03e98e0dc2b855be647c39abe984193675` (refer to `myprivatenet` by chainID) 140 | 141 | For each network you can specify either an `rpcUrl`, a `provider` or a `web3` instance that can be used to access that 142 | particular network. At least one of `name` or `chainId` must be specified per network. 143 | 144 | These providers will have to support `eth_call` and `eth_getLogs` to be able to resolve DIDs specific to that network. 145 | 146 | You can also override the default registry address by specifying a `registry` attribute per network. 147 | -------------------------------------------------------------------------------- /babel-preset.json: -------------------------------------------------------------------------------- 1 | { 2 | "sourceMaps": true, 3 | "plugins": [ 4 | [ 5 | "module-resolver", 6 | { 7 | "alias": { 8 | "crypto": "crypto-browserify" 9 | } 10 | } 11 | ] 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [["@babel/preset-env", { "targets": { "node": "current" } }], "@babel/preset-typescript"] 3 | } 4 | -------------------------------------------------------------------------------- /doc/did-method-spec.md: -------------------------------------------------------------------------------- 1 | # ETHR DID Method Specification 2 | 3 | ## Author 4 | 5 | - Veramo core team: or veramo-hello@mesh.xyz 6 | 7 | ## Preface 8 | 9 | The ethr DID method specification conforms to the requirements specified in 10 | the [DID specification](https://w3c-ccg.github.io/did-core/), currently published by the W3C Credentials Community 11 | Group. For more information about DIDs and DID method specifications, please see 12 | the [DID Primer](https://github.com/WebOfTrustInfo/rebooting-the-web-of-trust-fall2017/blob/master/topics-and-advance-readings/did-primer.md) 13 | 14 | ## Abstract 15 | 16 | Decentralized Identifiers (DIDs, see [1]) are designed to be compatible with any distributed ledger or network. In the 17 | Ethereum community, a pattern known as ERC1056 (see [2]) utilizes a smart contract for a lightweight identifier 18 | management system intended explicitly for off-chain usage. 19 | 20 | The described DID method allows any Ethereum smart contract or key pair account, or any secp256k1 public key to become 21 | a valid identifier. Such an identifier needs no registration. In case that key management or additional attributes such 22 | as "service endpoints" are required, they are resolved using ERC1056 smart contracts deployed on the networks listed in 23 | the [registry repository](https://github.com/uport-project/ethr-did-registry#contract-deployments). 24 | 25 | Most networks use the default registry address: `0xdca7ef03e98e0dc2b855be647c39abe984fcf21b`. 26 | 27 | Since each Ethereum transaction must be funded, there is a growing trend of on-chain transactions that are authenticated 28 | via an externally created signature and not by the actual transaction originator. This allows for 3rd party funding 29 | services, or for receivers to pay without any fundamental changes to the underlying Ethereum architecture. These kinds 30 | of transactions have to be signed by an actual key pair and thus cannot be used to represent smart contract based 31 | Ethereum accounts. ERC1056 proposes a way of a smart contract or regular key pair delegating signing for various 32 | purposes to externally managed key pairs. This allows a smart contract to be represented, both on-chain and 33 | off-chain or in payment channels through temporary or permanent delegates. 34 | 35 | For a reference implementation of this DID method specification see [3]. 36 | 37 | ### Identifier Controller 38 | 39 | By default, each identifier is controlled by itself, or rather by its corresponding Ethereum address. Each identifier 40 | can only be controlled by a single ethereum address at any given time. The controller can replace themselves with any 41 | other Ethereum address, including contracts to allow more advanced models such as multi-signature control. 42 | 43 | ## Target System 44 | 45 | The target system is the Ethereum network where the ERC1056 is deployed. This could either be: 46 | 47 | - Mainnet 48 | - Goerli 49 | - other EVM-compliant blockchains such as private chains, side-chains, or consortium chains. 50 | 51 | ### Advantages 52 | 53 | - No transaction fee for identifier creation 54 | - Identifier creation is private 55 | - Uses Ethereum's built-in account abstraction 56 | - Supports multi-sig (or proxy) wallet for account controller 57 | - Supports secp256k1 public keys as identifiers (on the same infrastructure) 58 | - Decoupling claims data from the underlying identifier 59 | - Supports decoupling Ethereum interaction from the underlying identifier 60 | - Flexibility to use key management 61 | - Flexibility to allow third-party funding service to pay the gas fee if needed (meta-transactions) 62 | - Supports any EVM-compliant blockchain 63 | - Supports verifiable versioning 64 | 65 | ## JSON-LD Context Definition 66 | 67 | Since this DID method still supports `publicKeyHex` and `publicKeyBase64` encodings for verification methods, it 68 | requires a valid JSON-LD context for those entries. 69 | To enable JSON-LD processing, the `@context` used when constructing DID documents for `did:ethr` should be: 70 | 71 | ``` 72 | "@context": [ 73 | "https://www.w3.org/ns/did/v1", 74 | "https://w3id.org/security/suites/secp256k1recovery-2020/v2" 75 | ] 76 | ``` 77 | 78 | You will also need this `@context` if you need to use `EcdsaSecp256k1RecoveryMethod2020` in your apps. 79 | 80 | ## DID Method Name 81 | 82 | The namestring that shall identify this DID method is: `ethr` 83 | 84 | A DID that uses this method MUST begin with the following prefix: `did:ethr`. Per the DID specification, this string 85 | MUST be in lowercase. The remainder of the DID, after the prefix, is specified below. 86 | 87 | ## Method Specific Identifier 88 | 89 | The method specific identifier is represented as the HEX-encoded secp256k1 public key (in compressed form), 90 | or the corresponding HEX-encoded Ethereum address on the target network, prefixed with `0x`. 91 | 92 | ethr-did = "did:ethr:" ethr-specific-identifier 93 | ethr-specific-identifier = [ ethr-network ":" ] ethereum-address / public-key-hex 94 | ethr-network = "mainnet" / "goerli" / network-chain-id 95 | network-chain-id = "0x" *HEXDIG 96 | ethereum-address = "0x" 40*HEXDIG 97 | public-key-hex = "0x" 66*HEXDIG 98 | 99 | The `ethereum-address` or `public-key-hex` are case-insensitive, however, the corresponding `blockchainAccountId` 100 | MAY be represented using 101 | the [mixed case checksum representation described in EIP55](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-55.md) 102 | in the resulting DID document. 103 | 104 | Note, if no public Ethereum network was specified, it is assumed that the DID is anchored on the Ethereum mainnet by 105 | default. This means the following DIDs will resolve to equivalent DID Documents: 106 | 107 | did:ethr:mainnet:0xb9c5714089478a327f09197987f16f9e5d936e8a 108 | did:ethr:0x1:0xb9c5714089478a327f09197987f16f9e5d936e8a 109 | did:ethr:0xb9c5714089478a327f09197987f16f9e5d936e8a 110 | 111 | If the identifier is a `public-key-hex`: 112 | 113 | - it MUST be represented in compressed form (see https://en.bitcoin.it/wiki/Secp256k1) 114 | - the corresponding `blockchainAccountId` entry is also added to the default DID document, unless the `owner` property 115 | has been changed to a different address. 116 | - all Read, Update, and Delete operations MUST be made using the corresponding `blockchainAccountId` and MUST originate 117 | from the correct controller account (ECR1056 `owner`). 118 | 119 | ## Relationship to ERC1056 120 | 121 | The subject of a `did:ethr` is mapped to an `identity` Ethereum address in the ERC1056 contract. When dealing with 122 | public key identifiers, the Ethereum address corresponding to that public key is used to represent the controller. 123 | 124 | The controller address of a `did:ethr` is mapped to the `owner` of an `identity` in the ERC1056. 125 | The controller address is not listed as the [DID `controller`](https://www.w3.org/TR/did-core/#did-controller) property 126 | in the DID document. This is intentional, to simplify the verification burden required by the DID spec. 127 | Rather, this address it is a concept specific to ERC1056 and defines the address that is allowed to perform Update and 128 | Delete operations on the registry on behalf of the `identity` address. 129 | This address MUST be listed with the ID `${did}#controller` in the `verificationMethod` section and also referenced 130 | in all other verification relationships listed in the DID document. 131 | In addition to this, if the identifier is a public key, this public key MUST be listed with the 132 | ID `${did}#controllerKey` in all locations where `#controller` appears. 133 | 134 | ## CRUD Operation Definitions 135 | 136 | ### Create (Register) 137 | 138 | In order to create a `ethr` DID, an Ethereum address, i.e., key pair, needs to be generated. At this point, no 139 | interaction with the target Ethereum network is required. The registration is implicit as it is impossible to brute 140 | force an Ethereum address, i.e., guessing the private key for a given public key on the Koblitz Curve 141 | (secp256k1). The holder of the private key is the entity identified by the DID. 142 | 143 | The default DID document for an `did:ethr` on mainnet, e.g. 144 | `did:ethr:0xf3beac30c498d9e26865f34fcaa57dbb935b0d74` with no transactions to the ERC1056 registry looks like this: 145 | 146 | ```json 147 | { 148 | "@context": [ 149 | "https://www.w3.org/ns/did/v1", 150 | "https://w3id.org/security/suites/secp256k1recovery-2020/v2" 151 | ], 152 | "id": "did:ethr:0xb9c5714089478a327f09197987f16f9e5d936e8a", 153 | "verificationMethod": [ 154 | { 155 | "id": "did:ethr:0xb9c5714089478a327f09197987f16f9e5d936e8a#controller", 156 | "type": "EcdsaSecp256k1RecoveryMethod2020", 157 | "controller": "did:ethr:0xb9c5714089478a327f09197987f16f9e5d936e8a", 158 | "blockchainAccountId": "eip155:1:0xb9c5714089478a327f09197987f16f9e5d936e8a" 159 | } 160 | ], 161 | "authentication": [ 162 | "did:ethr:0xb9c5714089478a327f09197987f16f9e5d936e8a#controller" 163 | ], 164 | "assertionMethod": [ 165 | "did:ethr:0xb9c5714089478a327f09197987f16f9e5d936e8a#controller" 166 | ] 167 | } 168 | ``` 169 | 170 | The minimal DID Document for a `did:ethr:` where there are no corresponding TXs to the ERC1056 registry 171 | looks like this: 172 | 173 | ```json 174 | { 175 | "@context": [ 176 | "https://www.w3.org/ns/did/v1", 177 | "https://w3id.org/security/suites/secp256k1recovery-2020/v2" 178 | ], 179 | "id": "did:ethr:0x0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", 180 | "verificationMethod": [ 181 | { 182 | "id": "did:ethr:0x0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798#controller", 183 | "type": "EcdsaSecp256k1RecoveryMethod2020", 184 | "controller": "did:ethr:0x0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", 185 | "blockchainAccountId": "eip155:1:0xb9c5714089478a327f09197987f16f9e5d936e8a" 186 | }, 187 | { 188 | "id": "did:ethr:0x0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798#controllerKey", 189 | "type": "EcdsaSecp256k1VerificationKey2019", 190 | "controller": "did:ethr:0x0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", 191 | "publicKeyHex": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" 192 | } 193 | ], 194 | "authentication": [ 195 | "did:ethr:0x0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798#controller", 196 | "did:ethr:0x0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798#controllerKey" 197 | ], 198 | "assertionMethod": [ 199 | "did:ethr:0x0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798#controller", 200 | "did:ethr:0x0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798#controllerKey" 201 | ] 202 | } 203 | ``` 204 | 205 | ### Read (Resolve) 206 | 207 | The DID document is built by using read only functions and contract events on the ERC1056 registry. 208 | 209 | Any value from the registry that returns an Ethereum address will be added to the `verificationMethod` array of the 210 | DID document with type `EcdsaSecp256k1RecoveryMethod2020` and a `blockchainAccountId` attribute containing the address 211 | using [CAIP10 Format](https://github.com/ChainAgnostic/CAIPs/blob/master/CAIPs/caip-10.md). 212 | 213 | Other verification relationships and service entries are added or removed by enumerating contract events (see below). 214 | 215 | #### Controller Address 216 | 217 | Each identifier always has a controller address. By default, it is the same as the identifier address, but the resolver 218 | MUST check the read only contract function `identityOwner(address identity)` on the deployed ERC1056 contract. 219 | 220 | This controller address MUST be represented in the DID document as a `verificationMethod` entry with the `id` set as the 221 | DID being resolved and with the fragment `#controller` appended to it. 222 | A reference to it MUST also be added to the `authentication` and `assertionMethod` arrays of the DID document. 223 | 224 | #### Enumerating Contract Events to build the DID Document 225 | 226 | The ERC1056 contract publishes three types of events for each identifier. 227 | 228 | - `DIDOwnerChanged` (indicating a change of `controller`) 229 | - `DIDDelegateChanged` 230 | - `DIDAttributeChanged` 231 | 232 | If a change has ever been made for the Ethereum address of an identifier the block number is stored in the 233 | `changed` mapping of the contract. 234 | 235 | The latest event can be efficiently looked up by checking for one of the 3 above events at that exact block. 236 | 237 | Each ERC1056 event contains a `previousChange` value which contains the block number of the previous change (if any). 238 | 239 | To see all changes in history for an address use the following pseudo-code: 240 | 241 | 1. eth_call `changed(address identity)` on the ERC1056 contract to get the latest block where a change occurred. 242 | 2. If result is `null` return. 243 | 3. Filter for events for all the above types with the contracts address on the specified block. 244 | 4. If event has a previous change then go to 3 245 | 246 | After building the history of events for an address, interpret each event to build the DID document like so: 247 | 248 | ##### Controller changes (`DIDOwnerChanged`) 249 | 250 | When the controller address of a `did:ethr` is changed, a `DIDOwnerChanged` event is emitted. 251 | 252 | ```solidity 253 | event DIDOwnerChanged( 254 | address indexed identity, 255 | address owner, 256 | uint previousChange 257 | ); 258 | ``` 259 | 260 | The event data MUST be used to update the `#controller` entry in the `verificationMethod` array. 261 | When resolving DIDs with publicKey identifiers, if the controller (`owner`) address is different from the corresponding 262 | address of the publicKey, then the `#controllerKey` entry in the `verificationMethod` array MUST be omitted. 263 | 264 | ##### Delegate Keys (`DIDDelegateChanged`) 265 | 266 | Delegate keys are Ethereum addresses that can either be general signing keys or optionally also perform authentication. 267 | 268 | They are also verifiable from Solidity (on-chain). 269 | 270 | When a delegate is added or revoked, a `DIDDelegateChanged` event is published that MUST be used to update the DID 271 | document. 272 | 273 | ```solidity 274 | event DIDDelegateChanged( 275 | address indexed identity, 276 | bytes32 delegateType, 277 | address delegate, 278 | uint validTo, 279 | uint previousChange 280 | ); 281 | ``` 282 | 283 | The only 2 `delegateTypes` that are currently published in the DID document are: 284 | 285 | - `veriKey` which adds a `EcdsaSecp256k1RecoveryMethod2020` to the `verificationMethod` section of the DID document with 286 | the `blockchainAccountId`(`ethereumAddress`) of the delegate, and adds a reference to it in the `assertionMethod` 287 | section. 288 | - `sigAuth` which adds a `EcdsaSecp256k1RecoveryMethod2020` to the `verificationMethod` section of document and a 289 | reference to it in the `authentication` section. 290 | 291 | Note, the `delegateType` is a `bytes32` type for Ethereum gas efficiency reasons and not a `string`. This restricts us 292 | to 32 bytes, which is why we use the shorthand versions above. 293 | 294 | Only events with a `validTo` (measured in seconds) greater or equal to the current time should be included in the DID 295 | document. When resolving an older version (using `versionId` in the didURL query string), the `validTo` entry MUST be 296 | compared to the timestamp of the block of `versionId` height. 297 | 298 | Such valid delegates MUST be added to the `verificationMethod` array as `EcdsaSecp256k1RecoveryMethod2020` entries, with 299 | the `delegate` address listed in the `blockchainAccountId` property and prefixed with `eip155::`, according 300 | to [CAIP10](https://github.com/ChainAgnostic/CAIPs/blob/master/CAIPs/caip-10.md) 301 | 302 | Example: 303 | 304 | ```json 305 | { 306 | "id": "did:ethr:0xf3beac30c498d9e26865f34fcaa57dbb935b0d74#delegate-1", 307 | "type": "EcdsaSecp256k1RecoveryMethod2020", 308 | "controller": "did:ethr:0xf3beac30c498d9e26865f34fcaa57dbb935b0d74", 309 | "blockchainAccountId": "eip155:1:0x12345678c498d9e26865f34fcaa57dbb935b0d74" 310 | } 311 | ``` 312 | 313 | ##### Non-Ethereum Attributes (`DIDAttributeChanged`) 314 | 315 | Non-Ethereum keys, service endpoints etc. can be added using attributes. Attributes only exist on the blockchain as 316 | contract events of type `DIDAttributeChanged` and can thus not be queried from within solidity code. 317 | 318 | ```solidity 319 | event DIDAttributeChanged( 320 | address indexed identity, 321 | bytes32 name, 322 | bytes value, 323 | uint validTo, 324 | uint previousChange 325 | ); 326 | ``` 327 | 328 | Note, the name is a `bytes32` type for Ethereum gas efficiency reasons and not a `string`. This restricts us to 32 329 | bytes, which is why we use the shorthand attribute versions explained below. 330 | 331 | While any attribute can be stored, for the DID document we support adding to each of these sections of the DID document: 332 | 333 | - Public Keys (Verification Methods) 334 | - Service Endpoints 335 | 336 | This design decision is meant to discourage the use of custom attributes in DID documents as they would be too easy to 337 | misuse for storing personal user information on-chain. 338 | 339 | ###### Public Keys 340 | 341 | The name of the attribute added to ERC1056 should follow this format: 342 | `did/pub/(Secp256k1|RSA|Ed25519|X25519)/(veriKey|sigAuth|enc)/(hex|base64|base58)` 343 | 344 | (Essentially `did/pub///`) 345 | Please opt for the `base58` encoding since the other encodings are not spec compliant and will be removed in future 346 | versions of the spec and reference resolver. 347 | 348 | ###### Key purposes 349 | 350 | - `veriKey` adds a verification key to the `verificationMethod` section of document and adds a reference to it in 351 | the `assertionMethod` section of document. 352 | - `sigAuth` adds a verification key to the `verificationMethod` section of document and adds a reference to it in 353 | the `authentication` section of document. 354 | - `enc` adds a key agreement key to the `verificationMethod` section and a corresponding entry to the `keyAgreement` 355 | section. 356 | This is used to perform a Diffie-Hellman key exchange and derive a secret key for encrypting messages to the DID that 357 | lists such a key. 358 | 359 | > **Note** The `` only refers to the key encoding in the resolved DID document. 360 | > Attribute values sent to the ERC1056 registry should always be hex encodings of the raw public key data. 361 | 362 | ###### Example Hex encoded Secp256k1 Verification Key 363 | 364 | A `DIDAttributeChanged` event for the account `0xf3beac30c498d9e26865f34fcaa57dbb935b0d74` with the name 365 | `did/pub/Secp256k1/veriKey/hex` and the value of `0x02b97c30de767f084ce3080168ee293053ba33b235d7116a3263d29f1450936b71` 366 | generates a verification method entry like the following: 367 | 368 | ```json 369 | { 370 | "id": "did:ethr:0xf3beac30c498d9e26865f34fcaa57dbb935b0d74#delegate-1", 371 | "type": "EcdsaSecp256k1VerificationKey2019", 372 | "controller": "did:ethr:0xf3beac30c498d9e26865f34fcaa57dbb935b0d74", 373 | "publicKeyHex": "02b97c30de767f084ce3080168ee293053ba33b235d7116a3263d29f1450936b71" 374 | } 375 | ``` 376 | 377 | ###### Example Base58 encoded Ed25519 Verification Key 378 | 379 | A `DIDAttributeChanged` event for the account `0xf3beac30c498d9e26865f34fcaa57dbb935b0d74` with the name 380 | `did/pub/Ed25519/veriKey/base58` and the value of `0xb97c30de767f084ce3080168ee293053ba33b235d7116a3263d29f1450936b71` 381 | generates a verification method entry like this: 382 | 383 | ```json 384 | { 385 | "id": "did:ethr:0xf3beac30c498d9e26865f34fcaa57dbb935b0d74#delegate-1", 386 | "type": "Ed25519VerificationKey2018", 387 | "controller": "did:ethr:0xf3beac30c498d9e26865f34fcaa57dbb935b0d74", 388 | "publicKeyBase58": "DV4G2kpBKjE6zxKor7Cj21iL9x9qyXb6emqjszBXcuhz" 389 | } 390 | ``` 391 | 392 | ###### Example Base64 encoded X25519 Encryption Key 393 | 394 | A `DIDAttributeChanged` event for the account `0xf3beac30c498d9e26865f34fcaa57dbb935b0d74` with the name 395 | `did/pub/X25519/enc/base64` and the value of 396 | `0x302a300506032b656e032100118557777ffb078774371a52b00fed75561dcf975e61c47553e664a617661052` 397 | generates a verification method entry like this: 398 | 399 | ```json 400 | { 401 | "id": "did:ethr:0xf3beac30c498d9e26865f34fcaa57dbb935b0d74#delegate-1", 402 | "type": "X25519KeyAgreementKey2019", 403 | "controller": "did:ethr:0xf3beac30c498d9e26865f34fcaa57dbb935b0d74", 404 | "publicKeyBase64": "MCowBQYDK2VuAyEAEYVXd3/7B4d0NxpSsA/tdVYdz5deYcR1U+ZkphdmEFI=" 405 | } 406 | ``` 407 | 408 | ###### Service Endpoints 409 | 410 | The name of the attribute should follow this format: 411 | 412 | `did/svc/[ServiceName]` 413 | 414 | Example: 415 | 416 | A `DIDAttributeChanged` event for the account `0xf3beac30c498d9e26865f34fcaa57dbb935b0d74` with the name 417 | `did/svc/HubService` and value of the URL `https://hubs.uport.me` hex encoded as 418 | `0x68747470733a2f2f687562732e75706f72742e6d65` generates a service endpoint entry like the following: 419 | 420 | ```json 421 | { 422 | "id": "did:ethr:0xf3beac30c498d9e26865f34fcaa57dbb935b0d74#service-1", 423 | "type": "HubService", 424 | "serviceEndpoint": "https://hubs.uport.me" 425 | } 426 | ``` 427 | 428 | #### `id` properties of entries 429 | 430 | With the exception of `#controller` and `#controllerKey`, the `id` properties that appear throughout the DID document 431 | MUST be stable across updates. This means that the same key material will be referenced by the same ID after an update. 432 | 433 | * Attribute or delegate changes that result in `verificationMethod` entries MUST set the `id` 434 | `${did}#delegate-${eventIndex}`. 435 | * Attributes that result in `service` entries MUST set the `id` to `${did}#service-${eventIndex}` 436 | 437 | where `eventIndex` is the index of the event that modifies that section of the DID document. 438 | 439 | **Example** 440 | 441 | * add key => `#delegate-1` is added 442 | * add another key => `#delegate-2` is added 443 | * add delegate => `#delegate-3` is added 444 | * add service => `#service-1` ia added 445 | * revoke first key => `#delegate-1` gets removed from the DID document; `#delegate-2` and `#delegte-3` remain. 446 | * add another delegate => `#delegate-5` is added (earlier revocation is counted as an event) 447 | * first delegate expires => `delegate-3` is removed, `#delegate-5` remains intact 448 | 449 | ### Update 450 | 451 | The DID Document may be updated by invoking the relevant smart contract functions as defined by the ERC1056 standard. 452 | This includes changes to the account owner, adding delegates and adding additional attributes. Please find a detailed 453 | description in the [ERC1056 documentation](https://github.com/ethereum/EIPs/issues/1056). 454 | 455 | These functions will trigger the respective Ethereum events which are used to build the DID Document for a given 456 | account as described 457 | in [Enumerating Contract Events to build the DID Document](#Enumerating-Contract-Events-to-build-the-DID-Document). 458 | 459 | Some elements of the DID Document will be revoked automatically when their validity period expires. This includes the 460 | delegates and additional attributes. Please find a detailed description in the 461 | [ERC1056 documentation](https://github.com/ethereum/EIPs/issues/1056). All attribute and delegate functions will trigger 462 | the respective Ethereum events which are used to build the DID Document for a given identifier as described 463 | in [Enumerating Contract Events to build the DID Document](#Enumerating-Contract-Events-to-build-the-DID-Document). 464 | 465 | ### Delete (Revoke) 466 | 467 | The `owner` property of the identifier MUST be set to `0x0`. Although, `0x0` is a valid Ethereum address, this will 468 | indicate the account has no owner which is a common approach for invalidation, e.g., tokens. To detect if the `owner` is 469 | the `null` address, one MUST get the logs of the last change to the account and inspect if the `owner` was set to the 470 | null address (`0x0000000000000000000000000000000000000000`). It is impossible to make any other changes to the DID 471 | document after such a change, therefore all preexisting keys and services MUST be considered revoked. 472 | 473 | If the intention is to revoke all the signatures corresponding to the DID, this option MUST be used. 474 | 475 | The DID resolution result for a deactivated DID has the following shape: 476 | 477 | ```json 478 | { 479 | "didDocumentMetadata": { 480 | "deactivated": true 481 | }, 482 | "didResolutionMetadata": { 483 | "contentType": "application/did+ld+json" 484 | }, 485 | "didDocument": { 486 | "@context": "https://www.w3.org/ns/did/v1", 487 | "id": "", 488 | "verificationMethod": [], 489 | "assertionMethod": [], 490 | "authentication": [] 491 | } 492 | } 493 | ``` 494 | 495 | ## Metadata 496 | 497 | The `resolve` method returns an object with the following properties: `didDocument`, `didDocumentMetadata`, 498 | `didResolutionMetadata`. 499 | 500 | ### DID Document Metadata 501 | 502 | When resolving a DID document that has had updates, the latest update MUST be listed in the `didDocumentMetadata`. 503 | 504 | * `versionId` MUST be the block number of the latest update. 505 | * `updated` MUST be the ISO date string of the block time of the latest update (without sub-second resolution). 506 | 507 | Example: 508 | 509 | ```json 510 | { 511 | "didDocumentMetadata": { 512 | "versionId": "12090175", 513 | "updated": "2021-03-22T18:14:29Z" 514 | } 515 | } 516 | ``` 517 | 518 | ### DID Resolution Metadata 519 | 520 | ```json 521 | { 522 | "didResolutionMetadata": { 523 | "contentType": "application/did+ld+json" 524 | } 525 | } 526 | ``` 527 | 528 | ## Resolving DID URIs with query parameters. 529 | 530 | ### `versionId` query string parameter 531 | 532 | This DID method supports resolving previous versions of the DID document by specifying a `versionId` parameter. 533 | 534 | Example: `did:ethr:0x26bf14321004e770e7a8b080b7a526d8eed8b388?versionId=12090175` 535 | 536 | The `versionId` is the block number at which the DID resolution MUST be performed. 537 | Only ERC1056 events prior to or contained in this block number are to be considered when building the event history. 538 | 539 | If there are any events after that block that mutate the DID, the earliest of them SHOULD be used to populate the 540 | properties of the `didDocumentMetadata`: 541 | 542 | * `nextVersionId` MUST be the block number of the next update to the DID document. 543 | * `nextUpdate` MUST be the ISO date string of the block time of the next update (without sub-second resolution). 544 | 545 | In case the DID has had updates prior to or included in the `versionId` block number, the `updated` and `versionId` 546 | properties of the `didDocumentMetadata` MUST correspond to the latest block prior to the `versionId` query string param. 547 | 548 | Any timestamp comparisons of `validTo` fields of the event history MUST be done against the `versionId` block timestamp. 549 | 550 | Example: 551 | `?versionId=12101682` 552 | 553 | ```json 554 | { 555 | "didDocumentMetadata": { 556 | "versionId": "12090175", 557 | "updated": "2021-03-22T18:14:29Z", 558 | "nextVersionId": "12276565", 559 | "nextUpdate": "2021-04-20T10:48:42Z" 560 | } 561 | } 562 | ``` 563 | 564 | #### Security considerations of DID versioning 565 | 566 | Applications MUST take precautions when using versioned DID URIs. 567 | If a key is compromised and revoked then it can still be used to issue signatures on behalf of the "older" DID URI. 568 | The use of versioned DID URIs is only recommended in some limited situations where the timestamp of signatures can also 569 | be verified, where malicious signatures can be easily revoked, and where applications can afford to check for these 570 | explicit revocations of either keys or signatures. 571 | Wherever versioned DIDs are in use, it SHOULD be made obvious to users that they are dealing with potentially revoked 572 | data. 573 | 574 | ### `initial-state` query string parameter 575 | 576 | TBD 577 | 578 | ## Reference Implementations 579 | 580 | The code at [https://github.com/decentralized-identity/ethr-did-resolver]() is intended to present a reference 581 | implementation of this DID method. 582 | 583 | ## References 584 | 585 | **[1]** 586 | 587 | **[2]** 588 | 589 | **[3]** 590 | 591 | **[4]** 592 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import typescriptEslint from "@typescript-eslint/eslint-plugin"; 2 | import jest from "eslint-plugin-jest"; 3 | import globals from "globals"; 4 | import tsParser from "@typescript-eslint/parser"; 5 | import path from "node:path"; 6 | import { fileURLToPath } from "node:url"; 7 | import js from "@eslint/js"; 8 | import { FlatCompat } from "@eslint/eslintrc"; 9 | 10 | const __filename = fileURLToPath(import.meta.url); 11 | const __dirname = path.dirname(__filename); 12 | const compat = new FlatCompat({ 13 | baseDirectory: __dirname, 14 | recommendedConfig: js.configs.recommended, 15 | allConfig: js.configs.all 16 | }); 17 | 18 | export default [...compat.extends( 19 | "eslint:recommended", 20 | "plugin:@typescript-eslint/recommended", 21 | "plugin:prettier/recommended", 22 | ), { 23 | plugins: { 24 | "@typescript-eslint": typescriptEslint, 25 | jest, 26 | }, 27 | 28 | languageOptions: { 29 | globals: { 30 | ...globals.node, 31 | ...jest.environments.globals.globals, 32 | }, 33 | 34 | parser: tsParser, 35 | ecmaVersion: 2020, 36 | sourceType: "module", 37 | }, 38 | 39 | rules: {}, 40 | }]; -------------------------------------------------------------------------------- /esm/index.js: -------------------------------------------------------------------------------- 1 | import cjsModule from '../lib/index.js' 2 | 3 | const getResolver = cjsModule.getResolver 4 | const deployments = cjsModule.deployments 5 | const REGISTRY = cjsModule.REGISTRY 6 | const bytes32toString = cjsModule.bytes32toString 7 | const stringToBytes32 = cjsModule.stringToBytes32 8 | const EthrDidController = cjsModule.EthrDidController 9 | const verificationMethodTypes = cjsModule.verificationMethodTypes 10 | const identifierMatcher = cjsModule.identifierMatcher 11 | const interpretIdentifier = cjsModule.interpretIdentifier 12 | const Errors = cjsModule.Errors 13 | const EthereumDIDRegistry = cjsModule.EthereumDIDRegistry 14 | 15 | export { 16 | getResolver, 17 | deployments, 18 | REGISTRY, 19 | bytes32toString, 20 | stringToBytes32, 21 | EthrDidController, 22 | verificationMethodTypes, 23 | identifierMatcher, 24 | interpretIdentifier, 25 | Errors, 26 | EthereumDIDRegistry, 27 | cjsModule as default 28 | } 29 | -------------------------------------------------------------------------------- /esm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module" 3 | } 4 | -------------------------------------------------------------------------------- /jest.config.mjs: -------------------------------------------------------------------------------- 1 | import { defaults } from 'jest-config' 2 | 3 | // @type {import('jest-config').InitialOptions} 4 | const config = { 5 | rootDir: './', 6 | moduleFileExtensions: [...defaults.moduleFileExtensions, 'mts'], 7 | collectCoverage: false, 8 | collectCoverageFrom: [ 9 | 'packages/**/src/**/*.ts', 10 | '!**/examples/**', 11 | '!packages/cli/**', 12 | '!**/types/**', 13 | '!**/build/**', 14 | '!**/node_modules/**', 15 | '!packages/test-react-app/**', 16 | '!packages/test-utils/**', 17 | ], 18 | coverageReporters: ['json'], 19 | coverageDirectory: './coverage', 20 | coverageProvider: 'v8', 21 | testMatch: ['**/__tests__/**/*.test.ts'], 22 | automock: false, 23 | // // typescript 5 removes the need to specify relative imports as .js, so we should no longer need this workaround 24 | // // but webpack still requires .js specifiers, so we are keeping it for now 25 | moduleNameMapper: { 26 | '^(\\.{1,2}/.*)\\.js$': '$1', 27 | }, 28 | transform: { 29 | '^.+\\.m?tsx?$': [ 30 | 'ts-jest', 31 | { 32 | useESM: true, 33 | }, 34 | ], 35 | }, 36 | extensionsToTreatAsEsm: ['.ts'], 37 | testEnvironment: 'node', 38 | } 39 | 40 | export default config 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ethr-did-resolver", 3 | "version": "11.0.3", 4 | "description": "Resolve DID documents for ethereum addresses and public keys", 5 | "type": "commonjs", 6 | "source": "./src/index.ts", 7 | "main": "./lib/index.js", 8 | "module": "./esm/index.js", 9 | "types": "./lib/index.d.ts", 10 | "exports": { 11 | ".": { 12 | "types": "./lib/index.d.ts", 13 | "import": "./esm/index.js", 14 | "require": "./lib/index.js" 15 | } 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git@github.com:decentralized-identity/ethr-did-resolver.git" 20 | }, 21 | "files": [ 22 | "lib", 23 | "esm", 24 | "src", 25 | "LICENSE" 26 | ], 27 | "author": "Pelle Braendgaard", 28 | "contributors": [ 29 | "Mircea Nistor" 30 | ], 31 | "license": "Apache-2.0", 32 | "keywords": [ 33 | "did:ethr", 34 | "DID", 35 | "DID document", 36 | "PKI", 37 | "resolver", 38 | "Verifiable Credential", 39 | "W3C", 40 | "ethereum", 41 | "ethereumAddress", 42 | "blockchainAccountId", 43 | "registry", 44 | "EIP1056", 45 | "EcdsaSecp256k1RecoveryMethod2020", 46 | "EcdsaSecp256k1VerificationKey2019", 47 | "Ed25519VerificationKey2018" 48 | ], 49 | "scripts": { 50 | "test": "jest", 51 | "test:ci": "jest --coverage", 52 | "build": "tsc", 53 | "clean": "rm -rf ./lib", 54 | "format": "prettier --write \"src/**/*.[jt]s\"", 55 | "lint": "eslint --ignore-pattern \"src/**/*.test.[jt]s\" \"src/**/*.[jt]s\"", 56 | "prepublishOnly": "yarn test:ci && yarn format && yarn lint", 57 | "release": "semantic-release --debug" 58 | }, 59 | "devDependencies": { 60 | "@babel/core": "7.26.10", 61 | "@babel/preset-env": "7.26.9", 62 | "@babel/preset-typescript": "7.27.0", 63 | "@eslint/eslintrc": "^3.1.0", 64 | "@eslint/js": "^9.11.1", 65 | "@ethers-ext/provider-ganache": "6.0.0-beta.2", 66 | "@semantic-release/changelog": "6.0.3", 67 | "@semantic-release/git": "10.0.1", 68 | "@types/jest": "29.5.14", 69 | "@typescript-eslint/eslint-plugin": "8.29.0", 70 | "@typescript-eslint/parser": "8.29.0", 71 | "babel-jest": "29.7.0", 72 | "eslint": "8.57.1", 73 | "eslint-config-prettier": "9.1.0", 74 | "eslint-plugin-jest": "28.11.0", 75 | "eslint-plugin-prettier": "5.2.6", 76 | "jest": "29.7.0", 77 | "prettier": "3.5.3", 78 | "semantic-release": "22.0.12", 79 | "ts-jest": "^29.2.5", 80 | "typescript": "5.8.2" 81 | }, 82 | "dependencies": { 83 | "did-resolver": "^4.1.0", 84 | "ethers": "^6.8.1" 85 | }, 86 | "packageManager": "yarn@1.22.22" 87 | } 88 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base", 4 | "group:allNonMajor" 5 | ], 6 | "labels": [ 7 | "maintenance" 8 | ], 9 | "automergeType": "branch", 10 | "automerge": true, 11 | "packageRules": [ 12 | { 13 | "matchPackagePatterns": [ 14 | "did" 15 | ], 16 | "matchUpdateTypes": [ 17 | "bump", 18 | "patch", 19 | "minor", 20 | "major" 21 | ], 22 | "groupName": "did-dependencies", 23 | "commitMessagePrefix": "fix(deps):" 24 | }, 25 | { 26 | "matchDepTypes": [ 27 | "devDependencies" 28 | ], 29 | "groupName": "devDeps", 30 | "schedule": [ 31 | "before 5am on Monday" 32 | ] 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /src/__tests__/config.test.ts: -------------------------------------------------------------------------------- 1 | import { InfuraProvider, JsonRpcProvider } from 'ethers' 2 | import { configureResolverWithNetworks } from '../configuration' 3 | 4 | describe('configuration', () => { 5 | it('works with infuraProjectId', () => { 6 | const contracts = configureResolverWithNetworks({ 7 | infuraProjectId: 'blabla', 8 | networks: [{ name: 'dev', rpcUrl: 'test' }], 9 | }) 10 | expect(contracts['mainnet']).toBeDefined() 11 | expect(contracts['0x1']).toBeDefined() 12 | expect(contracts['dev']).toBeDefined() 13 | expect(contracts['linea:goerli']).toBeDefined() 14 | expect(contracts['0xe704']).toBeDefined() 15 | }) 16 | 17 | it('works with infuraProjectId and overrides', () => { 18 | const contracts = configureResolverWithNetworks({ 19 | infuraProjectId: 'blabla', 20 | networks: [{ name: 'mainnet', rpcUrl: 'redefine me' }], 21 | }) 22 | expect((contracts['mainnet'].runner!.provider).projectId).not.toBeDefined() 23 | expect((contracts['mainnet'].runner!.provider)._getConnection().url).toBe('redefine me') 24 | }) 25 | 26 | it('works with named network', async () => { 27 | const contracts = configureResolverWithNetworks({ 28 | networks: [{ name: 'linea:goerli', provider: new JsonRpcProvider('some goerli JSONRPC URL') }], 29 | }) 30 | expect(contracts['linea:goerli']).toBeDefined() 31 | expect(contracts['0xe704']).toBeDefined() 32 | }) 33 | 34 | it('works with single network', async () => { 35 | const contracts = configureResolverWithNetworks({ 36 | name: 'linea:goerli', 37 | provider: new JsonRpcProvider('some goerli JSONRPC URL'), 38 | }) 39 | expect(contracts['linea:goerli']).toBeDefined() 40 | expect(contracts['0xe704']).toBeDefined() 41 | }) 42 | 43 | it('works with single provider', async () => { 44 | const contracts = configureResolverWithNetworks({ 45 | provider: new JsonRpcProvider('some rinkeby JSONRPC URL'), 46 | }) 47 | expect(contracts['']).toBeDefined() 48 | }) 49 | 50 | it('works with only rpcUrl', async () => { 51 | const contracts = configureResolverWithNetworks({ 52 | rpcUrl: 'some rinkeby JSONRPC URL', 53 | }) 54 | expect(contracts['']).toBeDefined() 55 | }) 56 | 57 | it('works with rpc and numbered chainId', async () => { 58 | const contracts = configureResolverWithNetworks({ 59 | rpcUrl: 'some rinkeby JSONRPC URL', 60 | chainId: BigInt(1), 61 | }) 62 | expect(contracts['0x1']).toBeDefined() 63 | }) 64 | 65 | it('throws when no configuration is provided', () => { 66 | expect(() => { 67 | configureResolverWithNetworks() 68 | }).toThrowError('invalid_config: Please make sure to have at least one network') 69 | }) 70 | 71 | it('throws when no relevant configuration is provided for a network', () => { 72 | expect(() => { 73 | configureResolverWithNetworks({ networks: [{ chainId: '0xbad' }] }) 74 | }).toThrowError('invalid_config: No web3 provider could be determined for network') 75 | }) 76 | 77 | it('throws when malformed configuration is provided for a network', () => { 78 | expect(() => { 79 | configureResolverWithNetworks({ networks: [{ web3: '0xbad' }] }) 80 | }).toThrowError('invalid_config: No web3 provider could be determined for network') 81 | }) 82 | }) 83 | -------------------------------------------------------------------------------- /src/__tests__/errors.test.ts: -------------------------------------------------------------------------------- 1 | import { Resolver } from 'did-resolver' 2 | import { getResolver } from '../resolver' 3 | 4 | describe('error handling', () => { 5 | const didResolver = new Resolver( 6 | getResolver({ 7 | networks: [{ name: 'example', rpcUrl: 'example.com' }], 8 | }) 9 | ) 10 | 11 | it('rejects invalid DID', async () => { 12 | expect.assertions(1) 13 | await expect(didResolver.resolve('did:ethr:2nQtiQG6Cgm1GYTBaaKAgr76uY7iSexUkqX')).resolves.toEqual({ 14 | didDocument: null, 15 | didDocumentMetadata: {}, 16 | didResolutionMetadata: { 17 | error: 'invalidDid', 18 | message: 'Not a valid did:ethr: 2nQtiQG6Cgm1GYTBaaKAgr76uY7iSexUkqX', 19 | }, 20 | }) 21 | }) 22 | 23 | it('rejects resolution on unconfigured network', async () => { 24 | expect.assertions(1) 25 | await expect( 26 | didResolver.resolve('did:ethr:zrx:0x03fdd57adec3d438ea237fe46b33ee1e016eda6b585c3e27ea66686c2ea5358479') 27 | ).resolves.toEqual({ 28 | didDocument: null, 29 | didDocumentMetadata: {}, 30 | didResolutionMetadata: { 31 | error: 'unknownNetwork', 32 | message: 'The DID resolver does not have a configuration for network: zrx', 33 | }, 34 | }) 35 | }) 36 | 37 | it('rejects resolution using unsupported `accept` option', async () => { 38 | expect.assertions(1) 39 | const accept = 'application/did+cbor' 40 | await expect( 41 | didResolver.resolve('did:ethr:example:0x03fdd57adec3d438ea237fe46b33ee1e016eda6b585c3e27ea66686c2ea5358479', { 42 | accept, 43 | }) 44 | ).resolves.toEqual({ 45 | didDocument: null, 46 | didDocumentMetadata: {}, 47 | didResolutionMetadata: { 48 | error: 'unsupportedFormat', 49 | message: `The DID resolver does not support the requested 'accept' format: ${accept}`, 50 | }, 51 | }) 52 | }) 53 | }) 54 | -------------------------------------------------------------------------------- /src/__tests__/extended-id.test.ts: -------------------------------------------------------------------------------- 1 | import { identifierMatcher, interpretIdentifier } from '../helpers' 2 | 3 | describe('pattern matcher', () => { 4 | const matcher = identifierMatcher 5 | 6 | describe('matches', () => { 7 | it('blockchainAccountId:', () => { 8 | expect(matcher.test('0xd0dbe9d3698738f899ccd8ee27ff2347a7faa4dd')).toBe(true) 9 | }) 10 | 11 | it('blockchainAccountId: ignoring case', () => { 12 | expect(matcher.test('0x' + 'd0dbe9d3698738f899ccd8ee27ff2347a7faa4dd'.toUpperCase())).toBe(true) 13 | }) 14 | 15 | it('publicKeyHex compressed', () => { 16 | expect(matcher.test('0x02b97c30de767f084ce3080168ee293053ba33b235d7116a3263d29f1450936b71')).toBe(true) 17 | }) 18 | 19 | it('publicKeyHex compressed ignore case', () => { 20 | expect( 21 | matcher.test('0x' + '02b97c30de767f084ce3080168ee293053ba33b235d7116a3263d29f1450936b71'.toUpperCase()) 22 | ).toBe(true) 23 | }) 24 | }) 25 | 26 | describe('rejects', () => { 27 | it('hex strings of smaller size', () => { 28 | expect(matcher.test('0xd0dbe9d3698738f899ccd8ee27ff23')).toBe(false) 29 | }) 30 | 31 | it('hex strings of ambiguous size', () => { 32 | expect(matcher.test('0x02b97c30de767f084ce3080168ee293053ba33b235d7116a3263d29f14')).toBe(false) 33 | }) 34 | 35 | it('hex strings of larger size', () => { 36 | expect(matcher.test('0x02b97c30de767f084ce3080168ee293053ba33b235d7116a3263d29f1450936b71dbe9d369')).toBe(false) 37 | }) 38 | }) 39 | }) 40 | 41 | describe('interpretIdentifier', () => { 42 | const pubKey = '0x02b97c30de767f084ce3080168ee293053ba33b235d7116a3263d29f1450936b71' 43 | const checksumAddress = '0xC662e6c5F91B9FcD22D7FcafC80Cf8b640aed247' 44 | 45 | it('parses ethereumAddress', () => { 46 | const { address, publicKey, network } = interpretIdentifier(checksumAddress.toLowerCase()) 47 | expect(address).toEqual(checksumAddress) 48 | expect(publicKey).toBeUndefined() 49 | expect(network).toBeUndefined() 50 | }) 51 | it('parses ethereumAddress with checksum', () => { 52 | const { address, publicKey, network } = interpretIdentifier(checksumAddress) 53 | expect(address).toEqual(checksumAddress) 54 | expect(publicKey).toBeUndefined() 55 | expect(network).toBeUndefined() 56 | }) 57 | it('parses did:ethr with address', () => { 58 | const { address, publicKey, network } = interpretIdentifier(`did:ethr:${checksumAddress}`) 59 | expect(address).toEqual(checksumAddress) 60 | expect(publicKey).toBeUndefined() 61 | expect(network).toBeUndefined() 62 | }) 63 | it('parses did:ethr with address and version', () => { 64 | const { address, publicKey, network } = interpretIdentifier(`did:ethr:${checksumAddress}?versionId=42`) 65 | expect(address).toEqual(checksumAddress) 66 | expect(publicKey).toBeUndefined() 67 | expect(network).toBeUndefined() 68 | }) 69 | it('parses did:ethr with address and network', () => { 70 | const { address, publicKey, network } = interpretIdentifier(`did:ethr:0x1:${checksumAddress}`) 71 | expect(address).toEqual(checksumAddress) 72 | expect(publicKey).toBeUndefined() 73 | expect(network).toEqual('0x1') 74 | }) 75 | it('parses did:ethr with address and sub-network', () => { 76 | const { address, publicKey, network } = interpretIdentifier(`did:ethr:rsk:testnet:${checksumAddress}`) 77 | expect(address).toEqual(checksumAddress) 78 | expect(publicKey).toBeUndefined() 79 | expect(network).toEqual('rsk:testnet') 80 | }) 81 | it('parses did:ethr with address and sub-network and version', () => { 82 | const { address, publicKey, network } = interpretIdentifier(`did:ethr:rsk:testnet:${checksumAddress}?versionId=42`) 83 | expect(address).toEqual(checksumAddress) 84 | expect(publicKey).toBeUndefined() 85 | expect(network).toEqual('rsk:testnet') 86 | }) 87 | 88 | it('parses publicKey', () => { 89 | const { address, publicKey, network } = interpretIdentifier(pubKey) 90 | expect(address).toEqual(checksumAddress) 91 | expect(publicKey).toEqual(pubKey) 92 | expect(network).toBeUndefined() 93 | }) 94 | it('parses did:ethr with publicKey', () => { 95 | const { address, publicKey, network } = interpretIdentifier(`did:ethr:${pubKey}`) 96 | expect(address).toEqual(checksumAddress) 97 | expect(publicKey).toEqual(pubKey) 98 | expect(network).toBeUndefined() 99 | }) 100 | it('parses did:ethr with publicKey and version', () => { 101 | const { address, publicKey, network } = interpretIdentifier(`did:ethr:${pubKey}?versionId=42`) 102 | expect(address).toEqual(checksumAddress) 103 | expect(publicKey).toEqual(pubKey) 104 | expect(network).toBeUndefined() 105 | }) 106 | it('parses did:ethr with publicKey and network', () => { 107 | const { address, publicKey, network } = interpretIdentifier(`did:ethr:mainnet:${pubKey}`) 108 | expect(address).toEqual(checksumAddress) 109 | expect(publicKey).toEqual(pubKey) 110 | expect(network).toEqual('mainnet') 111 | }) 112 | it('parses did:ethr with publicKey and sub-network', () => { 113 | const { address, publicKey, network } = interpretIdentifier(`did:ethr:not:so:main:net:${pubKey}`) 114 | expect(address).toEqual(checksumAddress) 115 | expect(publicKey).toEqual(pubKey) 116 | expect(network).toEqual('not:so:main:net') 117 | }) 118 | it('parses did:ethr with publicKey and sub-network', () => { 119 | const { address, publicKey, network } = interpretIdentifier(`did:ethr:not:so:main:net:${pubKey}?versionId=42`) 120 | expect(address).toEqual(checksumAddress) 121 | expect(publicKey).toEqual(pubKey) 122 | expect(network).toEqual('not:so:main:net') 123 | }) 124 | }) 125 | -------------------------------------------------------------------------------- /src/__tests__/index.test.ts: -------------------------------------------------------------------------------- 1 | import * as index from '../index' 2 | 3 | test('has export definitions', () => { 4 | expect.assertions(8) 5 | expect(index.REGISTRY).toBeDefined() 6 | expect(index.attrTypes).toBeDefined() 7 | expect(index.bytes32toString).toBeDefined() 8 | expect(index.delegateTypes).toBeDefined() 9 | expect(index.getResolver).toBeDefined() 10 | expect(index.identifierMatcher).toBeDefined() 11 | expect(index.stringToBytes32).toBeDefined() 12 | expect(index.verificationMethodTypes).toBeDefined() 13 | }) 14 | -------------------------------------------------------------------------------- /src/__tests__/networks.integration.test.ts: -------------------------------------------------------------------------------- 1 | import { Resolver } from 'did-resolver' 2 | import { getResolver } from '../resolver' 3 | import { interpretIdentifier } from '../helpers' 4 | 5 | jest.setTimeout(30000) 6 | 7 | describe('ethrResolver (alt-chains)', () => { 8 | const addr = '0xd0dbe9d3698738f899ccd8ee27ff2347a7faa4dd' 9 | const { address } = interpretIdentifier(addr) 10 | const checksumAddr = address 11 | 12 | describe('eth-networks', () => { 13 | it('resolves on mainnet with versionId', async () => { 14 | const resolver = new Resolver(getResolver({ infuraProjectId: '6b734e0b04454df8a6ce234023c04f26' })) 15 | const result = await resolver.resolve('did:ethr:0x26bf14321004e770e7a8b080b7a526d8eed8b388?versionId=12090174') 16 | expect(result).toEqual({ 17 | didDocumentMetadata: { 18 | nextVersionId: '12090175', 19 | nextUpdate: '2021-03-22T18:14:29Z', 20 | }, 21 | didResolutionMetadata: { 22 | contentType: 'application/did+ld+json', 23 | }, 24 | didDocument: { 25 | '@context': expect.anything(), 26 | id: 'did:ethr:0x26bf14321004e770e7a8b080b7a526d8eed8b388', 27 | verificationMethod: [ 28 | { 29 | id: 'did:ethr:0x26bf14321004e770e7a8b080b7a526d8eed8b388#controller', 30 | type: 'EcdsaSecp256k1RecoveryMethod2020', 31 | controller: 'did:ethr:0x26bf14321004e770e7a8b080b7a526d8eed8b388', 32 | blockchainAccountId: 'eip155:1:0x26bF14321004e770E7A8b080b7a526d8eed8b388', 33 | }, 34 | ], 35 | authentication: ['did:ethr:0x26bf14321004e770e7a8b080b7a526d8eed8b388#controller'], 36 | assertionMethod: ['did:ethr:0x26bf14321004e770e7a8b080b7a526d8eed8b388#controller'], 37 | }, 38 | }) 39 | }) 40 | 41 | it('resolves on sepolia when configured', async () => { 42 | const did = 'did:ethr:sepolia:' + addr 43 | const ethr = getResolver({ 44 | infuraProjectId: '6b734e0b04454df8a6ce234023c04f26', 45 | }) 46 | const resolver = new Resolver(ethr) 47 | const result = await resolver.resolve(did) 48 | expect(result).toEqual({ 49 | didDocumentMetadata: {}, 50 | didResolutionMetadata: { contentType: 'application/did+ld+json' }, 51 | didDocument: { 52 | '@context': expect.anything(), 53 | id: did, 54 | verificationMethod: [ 55 | { 56 | id: `${did}#controller`, 57 | type: 'EcdsaSecp256k1RecoveryMethod2020', 58 | controller: did, 59 | blockchainAccountId: `eip155:11155111:${checksumAddr}`, 60 | }, 61 | ], 62 | authentication: [`${did}#controller`], 63 | assertionMethod: [`${did}#controller`], 64 | }, 65 | }) 66 | }) 67 | 68 | // socket hangup 69 | it.skip('resolves on rsk when configured', async () => { 70 | const did = 'did:ethr:rsk:' + addr 71 | const ethr = getResolver({ networks: [{ name: 'rsk', rpcUrl: 'https://public-node.rsk.co' }] }) 72 | const resolver = new Resolver(ethr) 73 | const result = await resolver.resolve(did) 74 | expect(result).toEqual({ 75 | didDocumentMetadata: {}, 76 | didResolutionMetadata: { contentType: 'application/did+ld+json' }, 77 | didDocument: { 78 | '@context': expect.anything(), 79 | id: did, 80 | verificationMethod: [ 81 | { 82 | id: `${did}#controller`, 83 | type: 'EcdsaSecp256k1RecoveryMethod2020', 84 | controller: did, 85 | blockchainAccountId: `eip155:30:${checksumAddr}`, 86 | }, 87 | ], 88 | authentication: [`${did}#controller`], 89 | assertionMethod: [`${did}#controller`], 90 | }, 91 | }) 92 | }) 93 | 94 | // socket hangup 95 | it.skip('resolves on rsk:testnet when configured', async () => { 96 | const did = 'did:ethr:rsk:testnet:' + addr 97 | const ethr = getResolver({ networks: [{ name: 'rsk:testnet', rpcUrl: 'https://public-node.testnet.rsk.co' }] }) 98 | const resolver = new Resolver(ethr) 99 | const result = await resolver.resolve(did) 100 | expect(result).toEqual({ 101 | didDocumentMetadata: {}, 102 | didResolutionMetadata: { contentType: 'application/did+ld+json' }, 103 | didDocument: { 104 | '@context': expect.anything(), 105 | id: did, 106 | verificationMethod: [ 107 | { 108 | id: `${did}#controller`, 109 | type: 'EcdsaSecp256k1RecoveryMethod2020', 110 | controller: did, 111 | blockchainAccountId: `eip155:31:${checksumAddr}`, 112 | }, 113 | ], 114 | authentication: [`${did}#controller`], 115 | assertionMethod: [`${did}#controller`], 116 | }, 117 | }) 118 | }) 119 | 120 | it.skip('resolves public key identifier on rsk when configured', async () => { 121 | const did = 'did:ethr:rsk:0x03fdd57adec3d438ea237fe46b33ee1e016eda6b585c3e27ea66686c2ea5358479' 122 | const ethr = getResolver({ networks: [{ name: 'rsk', rpcUrl: 'https://did.rsk.co:4444' }] }) 123 | const resolver = new Resolver(ethr) 124 | const doc = await resolver.resolve(did) 125 | return expect(doc).toEqual({ 126 | didDocumentMetadata: {}, 127 | didResolutionMetadata: { contentType: 'application/did+ld+json' }, 128 | didDocument: { 129 | '@context': expect.anything(), 130 | id: did, 131 | verificationMethod: [ 132 | { 133 | id: `${did}#controller`, 134 | type: 'EcdsaSecp256k1RecoveryMethod2020', 135 | controller: did, 136 | blockchainAccountId: 'eip155:30:0xF3beAC30C498D9E26865F34fCAa57dBB935b0D74', 137 | }, 138 | { 139 | id: `${did}#controllerKey`, 140 | type: 'EcdsaSecp256k1VerificationKey2019', 141 | controller: did, 142 | publicKeyHex: '03fdd57adec3d438ea237fe46b33ee1e016eda6b585c3e27ea66686c2ea5358479', 143 | }, 144 | ], 145 | authentication: [`${did}#controller`, `${did}#controllerKey`], 146 | assertionMethod: [`${did}#controller`, `${did}#controllerKey`], 147 | }, 148 | }) 149 | }) 150 | 151 | it('resolves public keys and services on aurora when configured', async () => { 152 | const did = 'did:ethr:aurora:0x036d148205e34a8591dcdcea34fb7fed760f5f1eca66d254830833f755ff359ef0' 153 | const ethr = getResolver({ 154 | networks: [ 155 | { 156 | name: 'aurora', 157 | chainId: 1313161554, 158 | rpcUrl: 'https://mainnet.aurora.dev', 159 | registry: '0x63eD58B671EeD12Bc1652845ba5b2CDfBff198e0', 160 | }, 161 | ], 162 | }) 163 | const resolver = new Resolver(ethr) 164 | const doc = await resolver.resolve(did) 165 | return expect(doc).toEqual({ 166 | didDocumentMetadata: { 167 | updated: '2022-01-19T12:20:00Z', 168 | versionId: '57702194', 169 | }, 170 | didResolutionMetadata: { contentType: 'application/did+ld+json' }, 171 | didDocument: { 172 | '@context': expect.anything(), 173 | id: did, 174 | verificationMethod: [ 175 | { 176 | id: `${did}#controller`, 177 | type: 'EcdsaSecp256k1RecoveryMethod2020', 178 | controller: did, 179 | blockchainAccountId: 'eip155:1313161554:0x7a988202a04f00436f73972DF4dEfD80c3A6BD13', 180 | }, 181 | { 182 | id: `${did}#controllerKey`, 183 | type: 'EcdsaSecp256k1VerificationKey2019', 184 | controller: did, 185 | publicKeyHex: '036d148205e34a8591dcdcea34fb7fed760f5f1eca66d254830833f755ff359ef0', 186 | }, 187 | ], 188 | authentication: [`${did}#controller`, `${did}#controllerKey`], 189 | assertionMethod: [`${did}#controller`, `${did}#controllerKey`], 190 | }, 191 | }) 192 | }) 193 | }) 194 | }) 195 | -------------------------------------------------------------------------------- /src/__tests__/nonce.test.ts: -------------------------------------------------------------------------------- 1 | import { Contract, ContractFactory, getBytes, SigningKey } from 'ethers' 2 | import { EthrDidController } from '../controller' 3 | import { default as LegacyEthereumDIDRegistry } from './EthereumDIDRegistry-Legacy/LegacyEthereumDIDRegistry.json' 4 | import { deployRegistry, randomAccount } from './testUtils' 5 | import { GanacheProvider } from '@ethers-ext/provider-ganache' 6 | 7 | jest.setTimeout(30000) 8 | 9 | describe('nonce tracking', () => { 10 | // let registry, accounts, did, identity, controller, delegate1, delegate2, ethr, didResolver 11 | let legacyRegistryContract: Contract, registryContract: Contract, provider: GanacheProvider 12 | 13 | beforeAll(async () => { 14 | let reg = await deployRegistry() 15 | provider = reg.provider 16 | registryContract = reg.registryContract 17 | 18 | const legacyFactory = ContractFactory.fromSolidity(LegacyEthereumDIDRegistry).connect(await provider.getSigner(0)) 19 | legacyRegistryContract = await legacyFactory.deploy() 20 | legacyRegistryContract = await legacyRegistryContract.waitForDeployment() 21 | }) 22 | 23 | describe('new contract', () => { 24 | it('changing owner two times should result in original owner wallet nonce increase only once', async () => { 25 | const { address: originalOwner, privKey: originalOwnerKey } = await randomAccount(provider) 26 | const { address: nextOwner, privKey: nextOwnerKey } = await randomAccount(provider) 27 | const { address: finalOwner } = await randomAccount(provider) 28 | 29 | const identifier = `did:ethr:dev:${originalOwner}` 30 | 31 | const ethrController = new EthrDidController( 32 | identifier, 33 | registryContract, 34 | await provider.getSigner(0), 35 | undefined, 36 | undefined, 37 | undefined, 38 | undefined, 39 | false 40 | ) 41 | 42 | const hash = await ethrController.createChangeOwnerHash(nextOwner) 43 | const signature = originalOwnerKey.sign(hash) 44 | 45 | await ethrController.changeOwnerSigned(nextOwner, { 46 | sigV: signature.v, 47 | sigR: signature.r, 48 | sigS: signature.s, 49 | }) 50 | 51 | const hash2 = await ethrController.createChangeOwnerHash(finalOwner) 52 | const signature2 = nextOwnerKey.sign(hash2) 53 | 54 | await ethrController.changeOwnerSigned(finalOwner, { 55 | sigV: signature2.v, 56 | sigR: signature2.r, 57 | sigS: signature2.s, 58 | }) 59 | 60 | const originalNonce: bigint = await registryContract.nonce(originalOwner) 61 | const signerNonce: bigint = await registryContract.nonce(nextOwner) 62 | expect(originalNonce).toEqual(1n) 63 | expect(signerNonce).toEqual(1n) 64 | }) 65 | 66 | it('set attribute after owner change should result in original owner wallet nonce increase', async () => { 67 | const { address: originalOwner, shortDID: identifier, privKey: originalOwnerKey } = await randomAccount(provider) 68 | const { address: nextOwner, privKey: nextOwnerKey } = await randomAccount(provider) 69 | 70 | const serviceEndpointParams = { uri: 'https://didcomm.example.com', transportType: 'http' } 71 | const attributeName = 'did/svc/testService' 72 | const attributeValue = JSON.stringify(serviceEndpointParams) 73 | const attributeExpiration = 86400 74 | 75 | const ethrController = new EthrDidController( 76 | identifier, 77 | registryContract, 78 | await provider.getSigner(0), 79 | undefined, 80 | undefined, 81 | undefined, 82 | undefined, 83 | false 84 | ) 85 | 86 | const hash = await ethrController.createChangeOwnerHash(nextOwner) 87 | const signature = originalOwnerKey.sign(hash) 88 | 89 | await ethrController.changeOwnerSigned(nextOwner, { 90 | sigV: signature.v, 91 | sigR: signature.r, 92 | sigS: signature.s, 93 | }) 94 | 95 | const hash2 = await ethrController.createSetAttributeHash(attributeName, attributeValue, attributeExpiration) 96 | const signature2 = nextOwnerKey.sign(hash2) 97 | 98 | await ethrController.setAttributeSigned(attributeName, attributeValue, attributeExpiration, { 99 | sigV: signature2.v, 100 | sigR: signature2.r, 101 | sigS: signature2.s, 102 | }) 103 | 104 | const originalNonce = await registryContract.nonce(originalOwner) 105 | const signerNonce = await registryContract.nonce(nextOwner) 106 | expect(originalNonce).toEqual(1n) 107 | expect(signerNonce).toEqual(1n) 108 | }) 109 | }) 110 | describe('legacy contract', () => { 111 | it('changing owner two times should result in original owner wallet nonce increase', async () => { 112 | const { address: originalOwner, privKey: originalOwnerKey } = await randomAccount(provider) 113 | const { address: nextOwner, privKey: nextOwnerKey } = await randomAccount(provider) 114 | const { address: finalOwner } = await randomAccount(provider) 115 | 116 | const identifier = `did:ethr:legacy:${originalOwner}` 117 | 118 | const hash = await new EthrDidController(identifier, legacyRegistryContract).createChangeOwnerHash(nextOwner) 119 | const signature = originalOwnerKey.sign(hash) 120 | 121 | await new EthrDidController(identifier, legacyRegistryContract, await provider.getSigner(0)).changeOwnerSigned( 122 | nextOwner, 123 | { 124 | sigV: signature.v, 125 | sigR: signature.r, 126 | sigS: signature.s, 127 | } 128 | ) 129 | 130 | const hash2 = await new EthrDidController(identifier, legacyRegistryContract).createChangeOwnerHash(finalOwner) 131 | const signature2 = nextOwnerKey.sign(hash2) 132 | 133 | await new EthrDidController(identifier, legacyRegistryContract, await provider.getSigner(0)).changeOwnerSigned( 134 | finalOwner, 135 | { 136 | sigV: signature2.v, 137 | sigR: signature2.r, 138 | sigS: signature2.s, 139 | } 140 | ) 141 | 142 | // Expect the nonce of the original identity to equal 2 as the nonce tracking in the legacy contract is 143 | // done on an identity basis 144 | const originalNonce = await legacyRegistryContract.nonce(originalOwner) 145 | const signerNonce = await legacyRegistryContract.nonce(nextOwner) 146 | expect(originalNonce).toEqual(2n) 147 | expect(signerNonce).toEqual(0n) 148 | }) 149 | 150 | it('set attribute after owner change should result in original owner wallet nonce increase', async () => { 151 | const { address: originalOwner, privKey: originalOwnerKey } = await randomAccount(provider) 152 | const { address: nextOwner, privKey: nextOwnerKey } = await randomAccount(provider) 153 | 154 | const serviceEndpointParams = { uri: 'https://didcomm.example.com', transportType: 'http' } 155 | const attributeName = 'did/svc/testService' 156 | const attributeValue = JSON.stringify(serviceEndpointParams) 157 | const attributeExpiration = 86400 158 | 159 | const identifier = `did:ethr:legacy:${originalOwner}` 160 | 161 | const hash = await new EthrDidController(identifier, legacyRegistryContract).createChangeOwnerHash(nextOwner) 162 | const signature = originalOwnerKey.sign(hash) 163 | 164 | await new EthrDidController(identifier, legacyRegistryContract, await provider.getSigner(0)).changeOwnerSigned( 165 | nextOwner, 166 | { 167 | sigV: signature.v, 168 | sigR: signature.r, 169 | sigS: signature.s, 170 | } 171 | ) 172 | 173 | const hash2 = await new EthrDidController(identifier, legacyRegistryContract).createSetAttributeHash( 174 | attributeName, 175 | attributeValue, 176 | attributeExpiration 177 | ) 178 | const signature2 = nextOwnerKey.sign(hash2) 179 | 180 | await new EthrDidController(identifier, legacyRegistryContract, await provider.getSigner(0)).setAttributeSigned( 181 | attributeName, 182 | attributeValue, 183 | attributeExpiration, 184 | { 185 | sigV: signature2.v, 186 | sigR: signature2.r, 187 | sigS: signature2.s, 188 | } 189 | ) 190 | 191 | const nonce = await legacyRegistryContract.nonce(originalOwner) 192 | 193 | expect(nonce).toEqual(2n) 194 | }) 195 | }) 196 | }) 197 | -------------------------------------------------------------------------------- /src/__tests__/resolve.attribute.test.ts: -------------------------------------------------------------------------------- 1 | import { Contract, ethers, hexlify, toUtf8Bytes } from 'ethers' 2 | import { Resolvable } from 'did-resolver' 3 | 4 | import { GanacheProvider } from '@ethers-ext/provider-ganache' 5 | import { EthrDidController } from '../controller' 6 | import { deployRegistry, randomAccount, sleep } from './testUtils' 7 | import { stringToBytes32 } from '../helpers' 8 | 9 | jest.setTimeout(30000) 10 | 11 | describe('attributes', () => { 12 | let registryContract: Contract, didResolver: Resolvable, provider: GanacheProvider 13 | 14 | beforeAll(async () => { 15 | const reg = await deployRegistry() 16 | registryContract = reg.registryContract 17 | didResolver = reg.didResolver 18 | provider = reg.provider 19 | }) 20 | 21 | describe('invoking createSetAttributeHash', () => { 22 | it('sets the "encodedValue" to the passed hex encoded string (e.g. a public key)', async () => { 23 | expect.assertions(3) 24 | const { address: identity, signer } = await randomAccount(provider) 25 | const { pubKey: attrValue } = await randomAccount(provider) 26 | const attrName = 'did/pub/Secp256k1/veriKey' 27 | const controller = new EthrDidController(identity, registryContract, signer) 28 | const encodeAttributeValueSpy = jest.spyOn(controller, 'encodeAttributeValue') 29 | const ttl = 11111 30 | await controller.createSetAttributeHash(attrName, attrValue, ttl) 31 | expect(encodeAttributeValueSpy).toHaveBeenCalledWith(attrValue) 32 | expect(encodeAttributeValueSpy).toHaveBeenCalledTimes(1) 33 | expect(encodeAttributeValueSpy).toHaveReturnedWith(attrValue) 34 | }) 35 | 36 | it('sets the "encodedValue" to a bytes encoded version of the passed attribute value (e.g. a service endpoint)', async () => { 37 | expect.assertions(3) 38 | const { address: identity, signer } = await randomAccount(provider) 39 | const attrValue = 'https://hubs.uport.me/service-endpoints-are-not-hex' 40 | const attrName = 'did/pub/Secp256k1/veriKey' 41 | const controller = new EthrDidController(identity, registryContract, signer) 42 | const encodeAttributeValueSpy = jest.spyOn(controller, 'encodeAttributeValue') 43 | const ttl = 11111 44 | await controller.createSetAttributeHash(attrName, attrValue, ttl) 45 | expect(encodeAttributeValueSpy).toHaveBeenCalledWith(attrValue) 46 | expect(encodeAttributeValueSpy).toHaveBeenCalledTimes(1) 47 | const expectedEncodedValue = toUtf8Bytes(attrValue) 48 | expect(encodeAttributeValueSpy).toHaveReturnedWith(expectedEncodedValue) 49 | }) 50 | }) 51 | 52 | describe('invoking createRevokeAttributeHash', () => { 53 | beforeEach(() => { 54 | jest.clearAllMocks() 55 | }) 56 | 57 | it('sets the "encodedValue" to the passed hex encoded string (e.g. a public key)', async () => { 58 | expect.assertions(3) 59 | const { address: identity, signer } = await randomAccount(provider) 60 | const { pubKey: attrValue } = await randomAccount(provider) 61 | const attrName = 'did/pub/Secp256k1/veriKey' 62 | const controller = new EthrDidController(identity, registryContract, signer) 63 | const encodeAttributeValueSpy = jest.spyOn(controller, 'encodeAttributeValue') 64 | await controller.createRevokeAttributeHash(attrName, attrValue) 65 | expect(encodeAttributeValueSpy).toHaveBeenCalledWith(attrValue) 66 | expect(encodeAttributeValueSpy).toHaveBeenCalledTimes(1) 67 | expect(encodeAttributeValueSpy).toHaveReturnedWith(attrValue) 68 | }) 69 | 70 | it('sets the "encodedValue" to a bytes encoded version of the passed attribute value (e.g. a service endpoint)', async () => { 71 | expect.assertions(3) 72 | const { address: identity, signer } = await randomAccount(provider) 73 | const attrValue = 'https://hubs.uport.me/service-endpoints-are-not-hex' 74 | const attrName = 'did/pub/Secp256k1/veriKey' 75 | const controller = new EthrDidController(identity, registryContract, signer) 76 | const encodeAttributeValueSpy = jest.spyOn(controller, 'encodeAttributeValue') 77 | await controller.createRevokeAttributeHash(attrName, attrValue) 78 | expect(encodeAttributeValueSpy).toHaveBeenCalledWith(attrValue) 79 | expect(encodeAttributeValueSpy).toHaveBeenCalledTimes(1) 80 | const expectedEncodedValue = toUtf8Bytes(attrValue) 81 | expect(encodeAttributeValueSpy).toHaveReturnedWith(expectedEncodedValue) 82 | }) 83 | }) 84 | 85 | describe('add public keys', () => { 86 | it('add EcdsaSecp256k1VerificationKey2019 signing key', async () => { 87 | expect.assertions(1) 88 | const { address: identity, shortDID: did, signer } = await randomAccount(provider) 89 | const { pubKey } = await randomAccount(provider) 90 | await new EthrDidController(identity, registryContract, signer).setAttribute( 91 | 'did/pub/Secp256k1/veriKey', 92 | pubKey, 93 | 86401 94 | ) 95 | const { didDocument } = await didResolver.resolve(did) 96 | expect(didDocument).toEqual({ 97 | '@context': expect.anything(), 98 | id: did, 99 | verificationMethod: [ 100 | { 101 | id: `${did}#controller`, 102 | type: 'EcdsaSecp256k1RecoveryMethod2020', 103 | controller: did, 104 | blockchainAccountId: `eip155:1337:${identity}`, 105 | }, 106 | { 107 | id: `${did}#delegate-1`, 108 | type: 'EcdsaSecp256k1VerificationKey2019', 109 | controller: did, 110 | publicKeyHex: pubKey.slice(2), 111 | }, 112 | ], 113 | authentication: [`${did}#controller`], 114 | assertionMethod: [`${did}#controller`, `${did}#delegate-1`], 115 | }) 116 | }) 117 | 118 | it('add Bls12381G2Key2020 assertion key', async () => { 119 | expect.assertions(1) 120 | const { address: identity, shortDID: did, signer } = await randomAccount(provider) 121 | const pubKey = hexlify(toUtf8Bytes('public key material here')) // encodes to 0x7075626c6963206b6579206d6174657269616c2068657265 in base16 122 | await new EthrDidController(identity, registryContract, signer).setAttribute( 123 | 'did/pub/Bls12381G2Key2020', // attrName must fit into 32 bytes. Anything extra will be truncated. 124 | pubKey, // There's no limit on the size of the public key material 125 | 86401 126 | ) 127 | const { didDocument } = await didResolver.resolve(did) 128 | expect(didDocument).toEqual({ 129 | '@context': expect.anything(), 130 | id: did, 131 | verificationMethod: [ 132 | { 133 | id: `${did}#controller`, 134 | type: 'EcdsaSecp256k1RecoveryMethod2020', 135 | controller: did, 136 | blockchainAccountId: `eip155:1337:${identity}`, 137 | }, 138 | { 139 | id: `${did}#delegate-1`, 140 | type: 'Bls12381G2Key2020', 141 | controller: did, 142 | publicKeyHex: '7075626c6963206b6579206d6174657269616c2068657265', 143 | }, 144 | ], 145 | authentication: [`${did}#controller`], 146 | assertionMethod: [`${did}#controller`, `${did}#delegate-1`], 147 | }) 148 | }) 149 | 150 | it('add Ed25519VerificationKey2018 authentication key', async () => { 151 | expect.assertions(1) 152 | const { address: identity, shortDID: did, signer } = await randomAccount(provider) 153 | const { pubKey } = await randomAccount(provider) 154 | await new EthrDidController(identity, registryContract, signer).setAttribute( 155 | 'did/pub/Ed25519/sigAuth/base64', 156 | pubKey, 157 | 86402 158 | ) 159 | const { didDocument } = await didResolver.resolve(did) 160 | expect(didDocument).toEqual({ 161 | '@context': expect.anything(), 162 | id: did, 163 | verificationMethod: [ 164 | { 165 | id: `${did}#controller`, 166 | type: 'EcdsaSecp256k1RecoveryMethod2020', 167 | controller: did, 168 | blockchainAccountId: `eip155:1337:${identity}`, 169 | }, 170 | { 171 | id: `${did}#delegate-1`, 172 | type: 'Ed25519VerificationKey2018', 173 | controller: did, 174 | publicKeyBase64: ethers.encodeBase64(pubKey), 175 | }, 176 | ], 177 | authentication: [`${did}#controller`, `${did}#delegate-1`], 178 | assertionMethod: [`${did}#controller`, `${did}#delegate-1`], 179 | }) 180 | }) 181 | 182 | it('add RSAVerificationKey2018 signing key', async () => { 183 | expect.assertions(1) 184 | const { address: identity, shortDID: did, signer } = await randomAccount(provider) 185 | await new EthrDidController(identity, registryContract, signer).setAttribute( 186 | 'did/pub/RSA/veriKey/pem', 187 | '-----BEGIN PUBLIC KEY...END PUBLIC KEY-----\r\n', 188 | 86403 189 | ) 190 | const { didDocument } = await didResolver.resolve(did) 191 | expect(didDocument).toEqual({ 192 | '@context': expect.anything(), 193 | id: did, 194 | verificationMethod: [ 195 | { 196 | id: `${did}#controller`, 197 | type: 'EcdsaSecp256k1RecoveryMethod2020', 198 | controller: did, 199 | blockchainAccountId: `eip155:1337:${identity}`, 200 | }, 201 | { 202 | id: `${did}#delegate-1`, 203 | type: 'RSAVerificationKey2018', 204 | controller: did, 205 | publicKeyPem: '-----BEGIN PUBLIC KEY...END PUBLIC KEY-----\r\n', 206 | }, 207 | ], 208 | authentication: [`${did}#controller`], 209 | assertionMethod: [`${did}#controller`, `${did}#delegate-1`], 210 | }) 211 | }) 212 | 213 | it('add X25519KeyAgreementKey2019 encryption key', async () => { 214 | expect.assertions(1) 215 | const { address: identity, shortDID: did, signer } = await randomAccount(provider) 216 | const pubKeyBase64 = 'MCowBQYDK2VuAyEAEYVXd3/7B4d0NxpSsA/tdVYdz5deYcR1U+ZkphdmEFI=' 217 | await new EthrDidController(did, registryContract, signer).setAttribute( 218 | 'did/pub/X25519/enc/base64', 219 | ethers.hexlify(ethers.decodeBase64(pubKeyBase64)), 220 | 86404 221 | ) 222 | const { didDocument } = await didResolver.resolve(did) 223 | expect(didDocument).toEqual({ 224 | '@context': expect.anything(), 225 | id: did, 226 | verificationMethod: [ 227 | { 228 | id: `${did}#controller`, 229 | type: 'EcdsaSecp256k1RecoveryMethod2020', 230 | controller: did, 231 | blockchainAccountId: `eip155:1337:${identity}`, 232 | }, 233 | { 234 | id: `${did}#delegate-1`, 235 | type: 'X25519KeyAgreementKey2019', 236 | controller: did, 237 | publicKeyBase64: pubKeyBase64, 238 | }, 239 | ], 240 | authentication: [`${did}#controller`], 241 | assertionMethod: [`${did}#controller`], 242 | keyAgreement: [`${did}#delegate-1`], 243 | }) 244 | }) 245 | 246 | it('add an imaginary key type', async () => { 247 | expect.assertions(1) 248 | const { address: identity, shortDID: did, signer } = await randomAccount(provider) 249 | const imaginaryKey = '0x1234567890' 250 | await new EthrDidController(did, registryContract, signer).setAttribute( 251 | 'did/pub/ImaginaryKey2023/veriKey', 252 | imaginaryKey, 253 | 86404 254 | ) 255 | const { didDocument } = await didResolver.resolve(did) 256 | expect(didDocument).toEqual({ 257 | '@context': expect.anything(), 258 | id: did, 259 | verificationMethod: [ 260 | { 261 | id: `${did}#controller`, 262 | type: 'EcdsaSecp256k1RecoveryMethod2020', 263 | controller: did, 264 | blockchainAccountId: `eip155:1337:${identity}`, 265 | }, 266 | { 267 | id: `${did}#delegate-1`, 268 | type: 'ImaginaryKey2023', 269 | controller: did, 270 | publicKeyHex: imaginaryKey.slice(2), 271 | }, 272 | ], 273 | authentication: [`${did}#controller`], 274 | // This is a bug. Encryption keys should not be added to assertionMethod See #184 275 | assertionMethod: [`${did}#controller`, `${did}#delegate-1`], 276 | }) 277 | }) 278 | }) 279 | 280 | describe('add service endpoints', () => { 281 | it('resolves document', async () => { 282 | expect.assertions(1) 283 | const { address: identity, shortDID: did, signer } = await randomAccount(provider) 284 | await new EthrDidController(identity, registryContract, signer).setAttribute( 285 | stringToBytes32('did/svc/HubService'), 286 | 'https://hubs.uport.me', 287 | 86405 288 | ) 289 | const { didDocument } = await didResolver.resolve(did) 290 | expect(didDocument).toEqual({ 291 | '@context': expect.anything(), 292 | id: did, 293 | verificationMethod: [ 294 | { 295 | id: `${did}#controller`, 296 | type: 'EcdsaSecp256k1RecoveryMethod2020', 297 | controller: did, 298 | blockchainAccountId: `eip155:1337:${identity}`, 299 | }, 300 | ], 301 | authentication: [`${did}#controller`], 302 | assertionMethod: [`${did}#controller`], 303 | service: [ 304 | { 305 | id: `${did}#service-1`, 306 | type: 'HubService', 307 | serviceEndpoint: 'https://hubs.uport.me', 308 | }, 309 | ], 310 | }) 311 | }) 312 | }) 313 | 314 | describe('add expanded service endpoints', () => { 315 | it('resolves document', async () => { 316 | expect.assertions(2) 317 | const { address: identity, shortDID: did, signer } = await randomAccount(provider) 318 | await new EthrDidController(identity, registryContract, signer).setAttribute( 319 | stringToBytes32('did/svc/HubService'), 320 | JSON.stringify({ uri: 'https://hubs.uport.me', transportType: 'http' }), 321 | 86405 322 | ) 323 | const { didDocument } = await didResolver.resolve(did) 324 | expect(didDocument).toEqual({ 325 | '@context': expect.anything(), 326 | id: did, 327 | verificationMethod: [ 328 | { 329 | id: `${did}#controller`, 330 | type: 'EcdsaSecp256k1RecoveryMethod2020', 331 | controller: did, 332 | blockchainAccountId: `eip155:1337:${identity}`, 333 | }, 334 | ], 335 | authentication: [expect.anything()], 336 | assertionMethod: [expect.anything()], 337 | service: [ 338 | { 339 | id: `${did}#service-1`, 340 | type: 'HubService', 341 | serviceEndpoint: { uri: 'https://hubs.uport.me', transportType: 'http' }, 342 | }, 343 | ], 344 | }) 345 | 346 | await new EthrDidController(identity, registryContract, signer).setAttribute( 347 | stringToBytes32('did/svc/HubService'), 348 | JSON.stringify([ 349 | { uri: 'https://hubs.uport.me', transportType: 'http' }, 350 | { uri: 'libp2p.star/123', transportType: 'libp2p' }, 351 | ]), 352 | 86405 353 | ) 354 | const { didDocument: updatedDidDocument } = await didResolver.resolve(did) 355 | expect(updatedDidDocument).toEqual({ 356 | '@context': expect.anything(), 357 | id: did, 358 | verificationMethod: [expect.anything()], 359 | authentication: [expect.anything()], 360 | assertionMethod: [expect.anything()], 361 | service: [ 362 | { 363 | id: `${did}#service-1`, 364 | type: 'HubService', 365 | serviceEndpoint: { uri: 'https://hubs.uport.me', transportType: 'http' }, 366 | }, 367 | { 368 | id: `${did}#service-2`, 369 | type: 'HubService', 370 | serviceEndpoint: [ 371 | { uri: 'https://hubs.uport.me', transportType: 'http' }, 372 | { uri: 'libp2p.star/123', transportType: 'libp2p' }, 373 | ], 374 | }, 375 | ], 376 | }) 377 | }) 378 | }) 379 | 380 | describe('revoke attributes', () => { 381 | it('revoke EcdsaSecp256k1VerificationKey2019 signing key', async () => { 382 | expect.assertions(2) 383 | const { address: identity, shortDID: did, signer } = await randomAccount(provider) 384 | const { pubKey } = await randomAccount(provider) 385 | await new EthrDidController(identity, registryContract, signer).setAttribute( 386 | 'did/pub/Secp256k1/veriKey', 387 | pubKey, 388 | 86401 389 | ) 390 | const { didDocument: didDocumentBefore } = await didResolver.resolve(did) 391 | expect(didDocumentBefore).toEqual({ 392 | '@context': expect.anything(), 393 | id: did, 394 | verificationMethod: [ 395 | { 396 | id: `${did}#controller`, 397 | type: 'EcdsaSecp256k1RecoveryMethod2020', 398 | controller: did, 399 | blockchainAccountId: `eip155:1337:${identity}`, 400 | }, 401 | { 402 | id: `${did}#delegate-1`, 403 | type: 'EcdsaSecp256k1VerificationKey2019', 404 | controller: did, 405 | publicKeyHex: pubKey.slice(2), 406 | }, 407 | ], 408 | authentication: [`${did}#controller`], 409 | assertionMethod: [`${did}#controller`, `${did}#delegate-1`], 410 | }) 411 | 412 | await new EthrDidController(identity, registryContract, signer).revokeAttribute( 413 | 'did/pub/Secp256k1/veriKey', 414 | pubKey 415 | ) 416 | 417 | const { didDocument: didDocumentAfter } = await didResolver.resolve(did) 418 | expect(didDocumentAfter).toEqual({ 419 | '@context': expect.anything(), 420 | id: did, 421 | verificationMethod: [ 422 | { 423 | id: `${did}#controller`, 424 | type: 'EcdsaSecp256k1RecoveryMethod2020', 425 | controller: did, 426 | blockchainAccountId: `eip155:1337:${identity}`, 427 | }, 428 | ], 429 | authentication: [`${did}#controller`], 430 | assertionMethod: [`${did}#controller`], 431 | }) 432 | }) 433 | 434 | it('revokes Ed25519VerificationKey2018 authentication key', async () => { 435 | expect.assertions(2) 436 | const { address: identity, shortDID: did, signer } = await randomAccount(provider) 437 | const { pubKey } = await randomAccount(provider) 438 | await new EthrDidController(identity, registryContract, signer).setAttribute( 439 | 'did/pub/Ed25519/sigAuth/base64', 440 | pubKey, 441 | 86402 442 | ) 443 | const { didDocument: didDocumentBefore } = await didResolver.resolve(did) 444 | expect(didDocumentBefore).toEqual({ 445 | '@context': expect.anything(), 446 | id: did, 447 | verificationMethod: [ 448 | { 449 | id: `${did}#controller`, 450 | type: 'EcdsaSecp256k1RecoveryMethod2020', 451 | controller: did, 452 | blockchainAccountId: `eip155:1337:${identity}`, 453 | }, 454 | { 455 | id: `${did}#delegate-1`, 456 | type: 'Ed25519VerificationKey2018', 457 | controller: did, 458 | publicKeyBase64: ethers.encodeBase64(pubKey), 459 | }, 460 | ], 461 | authentication: [`${did}#controller`, `${did}#delegate-1`], 462 | assertionMethod: [`${did}#controller`, `${did}#delegate-1`], 463 | }) 464 | await new EthrDidController(identity, registryContract, signer).revokeAttribute( 465 | 'did/pub/Ed25519/sigAuth/base64', 466 | pubKey 467 | ) 468 | const { didDocument: didDocumentAfter } = await didResolver.resolve(did) 469 | expect(didDocumentAfter).toEqual({ 470 | '@context': expect.anything(), 471 | id: did, 472 | verificationMethod: [ 473 | { 474 | id: `${did}#controller`, 475 | type: 'EcdsaSecp256k1RecoveryMethod2020', 476 | controller: did, 477 | blockchainAccountId: `eip155:1337:${identity}`, 478 | }, 479 | ], 480 | authentication: [`${did}#controller`], 481 | assertionMethod: [`${did}#controller`], 482 | }) 483 | }) 484 | 485 | it('revokes service endpoint', async () => { 486 | expect.assertions(2) 487 | const { address: identity, shortDID: did, signer } = await randomAccount(provider) 488 | await new EthrDidController(identity, registryContract, signer).setAttribute( 489 | stringToBytes32('did/svc/HubService'), 490 | 'https://hubs.uport.me', 491 | 86405 492 | ) 493 | const { didDocument: didDocumentBefore } = await didResolver.resolve(did) 494 | expect(didDocumentBefore).toEqual({ 495 | '@context': expect.anything(), 496 | id: did, 497 | verificationMethod: [ 498 | { 499 | id: `${did}#controller`, 500 | type: 'EcdsaSecp256k1RecoveryMethod2020', 501 | controller: did, 502 | blockchainAccountId: `eip155:1337:${identity}`, 503 | }, 504 | ], 505 | authentication: [`${did}#controller`], 506 | assertionMethod: [`${did}#controller`], 507 | service: [ 508 | { 509 | id: `${did}#service-1`, 510 | type: 'HubService', 511 | serviceEndpoint: 'https://hubs.uport.me', 512 | }, 513 | ], 514 | }) 515 | 516 | await new EthrDidController(identity, registryContract, signer).revokeAttribute( 517 | stringToBytes32('did/svc/HubService'), 518 | 'https://hubs.uport.me' 519 | ) 520 | 521 | const { didDocument: didDocumentAfter } = await didResolver.resolve(did) 522 | expect(didDocumentAfter).toEqual({ 523 | '@context': expect.anything(), 524 | id: did, 525 | verificationMethod: [ 526 | { 527 | id: `${did}#controller`, 528 | type: 'EcdsaSecp256k1RecoveryMethod2020', 529 | controller: did, 530 | blockchainAccountId: `eip155:1337:${identity}`, 531 | }, 532 | ], 533 | authentication: [`${did}#controller`], 534 | assertionMethod: [`${did}#controller`], 535 | }) 536 | }) 537 | 538 | it('expires key automatically', async () => { 539 | expect.assertions(2) 540 | const { address: identity, shortDID: did, signer } = await randomAccount(provider) 541 | const { pubKey } = await randomAccount(provider) 542 | const validitySeconds = 2 543 | await new EthrDidController(identity, registryContract, signer).setAttribute( 544 | 'did/pub/Ed25519/sigAuth', 545 | pubKey, 546 | validitySeconds 547 | ) 548 | const { didDocument: didDocumentBefore } = await didResolver.resolve(did) 549 | expect(didDocumentBefore).toEqual({ 550 | '@context': expect.anything(), 551 | id: did, 552 | verificationMethod: [ 553 | { 554 | id: `${did}#controller`, 555 | type: 'EcdsaSecp256k1RecoveryMethod2020', 556 | controller: did, 557 | blockchainAccountId: `eip155:1337:${identity}`, 558 | }, 559 | { 560 | id: `${did}#delegate-1`, 561 | type: 'Ed25519VerificationKey2018', 562 | controller: did, 563 | publicKeyHex: pubKey.slice(2), 564 | }, 565 | ], 566 | authentication: [`${did}#controller`, `${did}#delegate-1`], 567 | assertionMethod: [`${did}#controller`, `${did}#delegate-1`], 568 | }) 569 | await sleep((validitySeconds + 1) * 1000) 570 | 571 | const { didDocument: didDocumentAfter } = await didResolver.resolve(did) 572 | expect(didDocumentAfter).toEqual({ 573 | '@context': expect.anything(), 574 | id: did, 575 | verificationMethod: [ 576 | { 577 | id: `${did}#controller`, 578 | type: 'EcdsaSecp256k1RecoveryMethod2020', 579 | controller: did, 580 | blockchainAccountId: `eip155:1337:${identity}`, 581 | }, 582 | ], 583 | authentication: [`${did}#controller`], 584 | assertionMethod: [`${did}#controller`], 585 | }) 586 | }) 587 | }) 588 | }) 589 | -------------------------------------------------------------------------------- /src/__tests__/resolve.controller.test.ts: -------------------------------------------------------------------------------- 1 | import { Contract } from 'ethers' 2 | import { Resolvable } from 'did-resolver' 3 | 4 | import { GanacheProvider } from '@ethers-ext/provider-ganache' 5 | import { EthrDidController } from '../controller' 6 | import { deployRegistry, randomAccount } from './testUtils' 7 | 8 | jest.setTimeout(30000) 9 | 10 | describe('change identity owner', () => { 11 | let registryContract: Contract, didResolver: Resolvable, provider: GanacheProvider 12 | 13 | beforeAll(async () => { 14 | const reg = await deployRegistry() 15 | registryContract = reg.registryContract 16 | didResolver = reg.didResolver 17 | provider = reg.provider 18 | }) 19 | 20 | it('resolves document', async () => { 21 | expect.assertions(2) 22 | const { shortDID: did, signer } = await randomAccount(provider) 23 | const { address: newOwner } = await randomAccount(provider) 24 | const blockHeightBeforeChange = (await provider.getBlock('latest'))!.number 25 | await new EthrDidController(did, registryContract, signer).changeOwner(newOwner) 26 | const result = await didResolver.resolve(did) 27 | expect(parseInt(result?.didDocumentMetadata.versionId ?? '')).toBeGreaterThanOrEqual(blockHeightBeforeChange + 1) 28 | expect(result.didDocument).toEqual({ 29 | '@context': expect.anything(), 30 | id: did, 31 | verificationMethod: [ 32 | { 33 | id: `${did}#controller`, 34 | type: 'EcdsaSecp256k1RecoveryMethod2020', 35 | controller: did, 36 | blockchainAccountId: `eip155:1337:${newOwner}`, 37 | }, 38 | ], 39 | authentication: [`${did}#controller`], 40 | assertionMethod: [`${did}#controller`], 41 | }) 42 | }) 43 | 44 | it('changing controller invalidates the publicKey as identifier', async () => { 45 | expect.assertions(1) 46 | const { longDID: pubDID, signer } = await randomAccount(provider) 47 | const { address: newOwner } = await randomAccount(provider) 48 | 49 | await new EthrDidController(pubDID, registryContract, signer).changeOwner(newOwner) 50 | const { didDocument } = await didResolver.resolve(pubDID) 51 | expect(didDocument).toEqual({ 52 | '@context': expect.anything(), 53 | id: pubDID, 54 | verificationMethod: [ 55 | { 56 | id: `${pubDID}#controller`, 57 | type: 'EcdsaSecp256k1RecoveryMethod2020', 58 | controller: pubDID, 59 | blockchainAccountId: `eip155:1337:${newOwner}`, 60 | }, 61 | ], 62 | authentication: [`${pubDID}#controller`], 63 | assertionMethod: [`${pubDID}#controller`], 64 | }) 65 | }) 66 | }) 67 | -------------------------------------------------------------------------------- /src/__tests__/resolve.delegate.test.ts: -------------------------------------------------------------------------------- 1 | import { Contract } from 'ethers' 2 | import { Resolvable } from 'did-resolver' 3 | 4 | import { GanacheProvider } from '@ethers-ext/provider-ganache' 5 | import { EthrDidController } from '../controller' 6 | import { deployRegistry, randomAccount, sleep } from './testUtils' 7 | 8 | jest.setTimeout(30000) 9 | 10 | describe('delegates', () => { 11 | let registryContract: Contract, didResolver: Resolvable, provider: GanacheProvider 12 | 13 | beforeAll(async () => { 14 | const reg = await deployRegistry() 15 | registryContract = reg.registryContract 16 | didResolver = reg.didResolver 17 | provider = reg.provider 18 | }) 19 | 20 | it('add signing delegate', async () => { 21 | expect.assertions(2) 22 | const blockHeightBeforeChange = (await provider.getBlock('latest'))!.number 23 | const { address: identity, shortDID: did, signer } = await randomAccount(provider) 24 | const { address: signingDelegate } = await randomAccount(provider) 25 | 26 | await new EthrDidController(identity, registryContract, signer).addDelegate('veriKey', signingDelegate, 86401) 27 | const result = await didResolver.resolve(did) 28 | expect(parseInt(result?.didDocumentMetadata.versionId ?? '')).toBeGreaterThanOrEqual(blockHeightBeforeChange + 1) 29 | expect(result).toEqual({ 30 | didDocumentMetadata: { versionId: expect.anything(), updated: expect.anything() }, 31 | didResolutionMetadata: { contentType: 'application/did+ld+json' }, 32 | didDocument: { 33 | '@context': expect.anything(), 34 | id: did, 35 | verificationMethod: [ 36 | { 37 | id: `${did}#controller`, 38 | type: 'EcdsaSecp256k1RecoveryMethod2020', 39 | controller: did, 40 | blockchainAccountId: `eip155:1337:${identity}`, 41 | }, 42 | { 43 | id: `${did}#delegate-1`, 44 | type: 'EcdsaSecp256k1RecoveryMethod2020', 45 | controller: did, 46 | blockchainAccountId: `eip155:1337:${signingDelegate}`, 47 | }, 48 | ], 49 | authentication: [`${did}#controller`], 50 | assertionMethod: [`${did}#controller`, `${did}#delegate-1`], 51 | }, 52 | }) 53 | }) 54 | 55 | it('add auth delegate', async () => { 56 | expect.assertions(2) 57 | const { address: identity, shortDID: did, signer } = await randomAccount(provider) 58 | const { address: authDelegate } = await randomAccount(provider) 59 | const blockHeightBeforeChange = (await provider.getBlock('latest'))!.number 60 | await new EthrDidController(identity, registryContract, signer).addDelegate('sigAuth', authDelegate, 2) 61 | const result = await didResolver.resolve(did) 62 | expect(parseInt(result?.didDocumentMetadata.versionId ?? '')).toBeGreaterThanOrEqual(blockHeightBeforeChange + 1) 63 | expect(result).toEqual({ 64 | didDocumentMetadata: { versionId: expect.anything(), updated: expect.anything() }, 65 | didResolutionMetadata: { contentType: 'application/did+ld+json' }, 66 | didDocument: { 67 | '@context': expect.anything(), 68 | id: did, 69 | verificationMethod: [ 70 | { 71 | id: `${did}#controller`, 72 | type: 'EcdsaSecp256k1RecoveryMethod2020', 73 | controller: did, 74 | blockchainAccountId: `eip155:1337:${identity}`, 75 | }, 76 | { 77 | id: `${did}#delegate-1`, 78 | type: 'EcdsaSecp256k1RecoveryMethod2020', 79 | controller: did, 80 | blockchainAccountId: `eip155:1337:${authDelegate}`, 81 | }, 82 | ], 83 | authentication: [`${did}#controller`, `${did}#delegate-1`], 84 | assertionMethod: [`${did}#controller`, `${did}#delegate-1`], 85 | }, 86 | }) 87 | }) 88 | 89 | it('expire delegate automatically', async () => { 90 | expect.assertions(2) 91 | const { address: identity, shortDID: did, signer } = await randomAccount(provider) 92 | const { address: expiringDelegate } = await randomAccount(provider) 93 | const validitySeconds = 2 94 | await new EthrDidController(identity, registryContract, signer).addDelegate( 95 | 'veriKey', 96 | expiringDelegate, 97 | validitySeconds 98 | ) 99 | const result = await didResolver.resolve(did) 100 | expect(result.didDocument).toEqual({ 101 | '@context': expect.anything(), 102 | id: did, 103 | verificationMethod: [ 104 | { 105 | id: `${did}#controller`, 106 | type: 'EcdsaSecp256k1RecoveryMethod2020', 107 | controller: did, 108 | blockchainAccountId: `eip155:1337:${identity}`, 109 | }, 110 | { 111 | id: `${did}#delegate-1`, 112 | type: 'EcdsaSecp256k1RecoveryMethod2020', 113 | controller: did, 114 | blockchainAccountId: `eip155:1337:${expiringDelegate}`, 115 | }, 116 | ], 117 | authentication: [`${did}#controller`], 118 | assertionMethod: [`${did}#controller`, `${did}#delegate-1`], 119 | }) 120 | await sleep((validitySeconds + 1) * 1000) 121 | const result2 = await didResolver.resolve(did) 122 | expect(result2.didDocument).toEqual({ 123 | '@context': expect.anything(), 124 | id: did, 125 | verificationMethod: [ 126 | { 127 | id: `${did}#controller`, 128 | type: 'EcdsaSecp256k1RecoveryMethod2020', 129 | controller: did, 130 | blockchainAccountId: `eip155:1337:${identity}`, 131 | }, 132 | ], 133 | authentication: [`${did}#controller`], 134 | assertionMethod: [`${did}#controller`], 135 | }) 136 | }) 137 | 138 | it('revoke delegate', async () => { 139 | expect.assertions(2) 140 | const { address: identity, shortDID: did, signer } = await randomAccount(provider) 141 | const { address: signingDelegate } = await randomAccount(provider) 142 | await new EthrDidController(identity, registryContract, signer).addDelegate('veriKey', signingDelegate, 86400) 143 | const resultBefore = await didResolver.resolve(did) 144 | expect(resultBefore.didDocument).toEqual({ 145 | '@context': expect.anything(), 146 | id: did, 147 | verificationMethod: [ 148 | { 149 | id: `${did}#controller`, 150 | type: 'EcdsaSecp256k1RecoveryMethod2020', 151 | controller: did, 152 | blockchainAccountId: `eip155:1337:${identity}`, 153 | }, 154 | { 155 | id: `${did}#delegate-1`, 156 | type: 'EcdsaSecp256k1RecoveryMethod2020', 157 | controller: did, 158 | blockchainAccountId: `eip155:1337:${signingDelegate}`, 159 | }, 160 | ], 161 | authentication: [`${did}#controller`], 162 | assertionMethod: [`${did}#controller`, `${did}#delegate-1`], 163 | }) 164 | await new EthrDidController(identity, registryContract, signer).revokeDelegate('veriKey', signingDelegate) 165 | await sleep(1000) 166 | const result = await didResolver.resolve(did) 167 | expect(result.didDocument).toEqual({ 168 | '@context': expect.anything(), 169 | id: did, 170 | verificationMethod: [ 171 | { 172 | id: `${did}#controller`, 173 | type: 'EcdsaSecp256k1RecoveryMethod2020', 174 | controller: did, 175 | blockchainAccountId: `eip155:1337:${identity}`, 176 | }, 177 | ], 178 | authentication: [`${did}#controller`], 179 | assertionMethod: [`${did}#controller`], 180 | }) 181 | }) 182 | 183 | it('re-add auth delegate', async () => { 184 | expect.assertions(1) 185 | const { address: identity, shortDID: did, signer } = await randomAccount(provider) 186 | const { address: authDelegate } = await randomAccount(provider) 187 | await new EthrDidController(identity, registryContract, signer).addDelegate('sigAuth', authDelegate, 300) 188 | await new EthrDidController(identity, registryContract, signer).revokeDelegate('sigAuth', authDelegate) 189 | await new EthrDidController(identity, registryContract, signer).addDelegate('sigAuth', authDelegate, 86402) 190 | const result = await didResolver.resolve(did) 191 | expect(result.didDocument).toEqual({ 192 | '@context': expect.anything(), 193 | id: did, 194 | verificationMethod: [ 195 | { 196 | id: `${did}#controller`, 197 | type: 'EcdsaSecp256k1RecoveryMethod2020', 198 | controller: did, 199 | blockchainAccountId: `eip155:1337:${identity}`, 200 | }, 201 | { 202 | id: `${did}#delegate-3`, 203 | type: 'EcdsaSecp256k1RecoveryMethod2020', 204 | controller: did, 205 | blockchainAccountId: `eip155:1337:${authDelegate}`, 206 | }, 207 | ], 208 | authentication: [`${did}#controller`, `${did}#delegate-3`], 209 | assertionMethod: [`${did}#controller`, `${did}#delegate-3`], 210 | }) 211 | }) 212 | }) 213 | -------------------------------------------------------------------------------- /src/__tests__/resolve.metatx.test.ts: -------------------------------------------------------------------------------- 1 | import { Contract } from 'ethers' 2 | import { Resolvable } from 'did-resolver' 3 | 4 | import { GanacheProvider } from '@ethers-ext/provider-ganache' 5 | import { EthrDidController } from '../controller' 6 | import { deployRegistry, randomAccount, sleep } from './testUtils' 7 | 8 | jest.setTimeout(30000) 9 | 10 | describe('meta transactions', () => { 11 | let registryContract: Contract, didResolver: Resolvable, provider: GanacheProvider 12 | 13 | beforeAll(async () => { 14 | const reg = await deployRegistry() 15 | registryContract = reg.registryContract 16 | didResolver = reg.didResolver 17 | provider = reg.provider 18 | }) 19 | 20 | it('add delegate signed', async () => { 21 | const { address: identity, shortDID: did, privKey } = await randomAccount(provider) 22 | const { address: authDelegate } = await randomAccount(provider) 23 | 24 | const hash = await new EthrDidController(did, registryContract).createAddDelegateHash( 25 | 'sigAuth', 26 | authDelegate, 27 | 86400 28 | ) 29 | const signature = privKey.sign(hash) 30 | 31 | await new EthrDidController(did, registryContract, await provider.getSigner(0)).addDelegateSigned( 32 | 'sigAuth', 33 | authDelegate, 34 | 86400, 35 | { 36 | sigV: signature.v, 37 | sigR: signature.r, 38 | sigS: signature.s, 39 | } 40 | ) 41 | 42 | const result = await didResolver.resolve(did) 43 | expect(result.didDocument).toEqual({ 44 | '@context': expect.anything(), 45 | id: did, 46 | verificationMethod: [ 47 | { 48 | id: expect.anything(), 49 | type: expect.anything(), 50 | controller: expect.anything(), 51 | blockchainAccountId: `eip155:1337:${identity}`, 52 | }, 53 | { 54 | id: expect.anything(), 55 | type: expect.anything(), 56 | controller: expect.anything(), 57 | blockchainAccountId: `eip155:1337:${authDelegate}`, 58 | }, 59 | ], 60 | authentication: [`${did}#controller`, `${did}#delegate-1`], 61 | assertionMethod: [`${did}#controller`, `${did}#delegate-1`], 62 | }) 63 | }) 64 | 65 | it('revoke delegate signed', async () => { 66 | const { address: identity, shortDID: did, privKey, signer } = await randomAccount(provider) 67 | const { address: delegate } = await randomAccount(provider) 68 | 69 | await new EthrDidController(did, registryContract, signer).addDelegate('sigAuth', delegate, 86400) 70 | const resultBefore = await didResolver.resolve(did) 71 | expect(resultBefore?.didDocument?.verificationMethod?.length).toEqual(2) 72 | 73 | const hash = await new EthrDidController(did, registryContract).createRevokeDelegateHash('sigAuth', delegate) 74 | const signature = privKey.sign(hash) 75 | 76 | await new EthrDidController(did, registryContract, await provider.getSigner(0)).revokeDelegateSigned( 77 | 'sigAuth', 78 | delegate, 79 | { 80 | sigV: signature.v, 81 | sigR: signature.r, 82 | sigS: signature.s, 83 | } 84 | ) 85 | await sleep(1000) 86 | 87 | const result = await didResolver.resolve(did) 88 | expect(result.didDocument).toEqual({ 89 | '@context': expect.anything(), 90 | id: did, 91 | verificationMethod: [ 92 | { 93 | id: expect.anything(), 94 | type: expect.anything(), 95 | controller: expect.anything(), 96 | blockchainAccountId: `eip155:1337:${identity}`, 97 | }, 98 | ], 99 | authentication: expect.anything(), 100 | assertionMethod: expect.anything(), 101 | }) 102 | }) 103 | 104 | it('set attribute signed', async () => { 105 | const { shortDID: identifier, privKey, signer } = await randomAccount(provider) 106 | 107 | const serviceEndpointParams = { uri: 'https://didcomm.example.com', transportType: 'http' } 108 | const attributeName = 'did/svc/testService' 109 | const attributeValue = JSON.stringify(serviceEndpointParams) 110 | const attributeExpiration = 86400 111 | 112 | const hash = await new EthrDidController(identifier, registryContract).createSetAttributeHash( 113 | attributeName, 114 | attributeValue, 115 | attributeExpiration 116 | ) 117 | const signature = privKey.sign(hash) 118 | 119 | await new EthrDidController(identifier, registryContract, await provider.getSigner(0)).setAttributeSigned( 120 | attributeName, 121 | attributeValue, 122 | attributeExpiration, 123 | { 124 | sigV: signature.v, 125 | sigR: signature.r, 126 | sigS: signature.s, 127 | } 128 | ) 129 | 130 | const result = await didResolver.resolve(identifier) 131 | expect(result.didDocument).toEqual({ 132 | '@context': expect.anything(), 133 | id: identifier, 134 | verificationMethod: expect.anything(), 135 | authentication: [expect.anything()], 136 | assertionMethod: [expect.anything()], 137 | service: [ 138 | { 139 | id: expect.anything(), 140 | type: 'testService', 141 | serviceEndpoint: { 142 | uri: serviceEndpointParams.uri, 143 | transportType: serviceEndpointParams.transportType, 144 | }, 145 | }, 146 | ], 147 | }) 148 | }) 149 | 150 | it('revoke attribute signed', async () => { 151 | const { address: identity, shortDID: identifier, privKey, signer } = await randomAccount(provider) 152 | 153 | const serviceEndpointParams = { uri: 'https://didcomm.example.com', transportType: 'http' } 154 | const attributeName = 'did/svc/testService' 155 | const attributeValue = JSON.stringify(serviceEndpointParams) 156 | const attributeExpiration = 86400 157 | 158 | await new EthrDidController(identity, registryContract, signer).setAttribute( 159 | attributeName, 160 | attributeValue, 161 | attributeExpiration 162 | ) 163 | 164 | const resultBefore = await didResolver.resolve(identifier) 165 | expect(resultBefore?.didDocument?.service?.length).toEqual(1) 166 | 167 | const hash = await new EthrDidController(identifier, registryContract).createRevokeAttributeHash( 168 | attributeName, 169 | attributeValue 170 | ) 171 | const signature = privKey.sign(hash) 172 | 173 | await new EthrDidController(identifier, registryContract, await provider.getSigner(0)).revokeAttributeSigned( 174 | attributeName, 175 | attributeValue, 176 | { 177 | sigV: signature.v, 178 | sigR: signature.r, 179 | sigS: signature.s, 180 | } 181 | ) 182 | 183 | // Wait for the event to be emitted 184 | await sleep(1000) 185 | 186 | const result = await didResolver.resolve(identifier) 187 | expect(result.didDocument).toEqual({ 188 | '@context': expect.anything(), 189 | id: identifier, 190 | verificationMethod: expect.anything(), 191 | authentication: [expect.anything()], 192 | assertionMethod: [expect.anything()], 193 | }) 194 | }) 195 | 196 | it('change owner signed', async () => { 197 | const { address: identity, shortDID: identifier, privKey: originalPrivKey } = await randomAccount(provider) 198 | const { address: newOwner, privKey: newOwnerKey } = await randomAccount(provider) 199 | 200 | const hash = await new EthrDidController(identifier, registryContract).createChangeOwnerHash(newOwner) 201 | const signature = originalPrivKey.sign(hash) 202 | 203 | await new EthrDidController(identifier, registryContract, await provider.getSigner(0)).changeOwnerSigned(newOwner, { 204 | sigV: signature.v, 205 | sigR: signature.r, 206 | sigS: signature.s, 207 | }) 208 | 209 | const result = await didResolver.resolve(identifier) 210 | expect(result.didDocument).toEqual({ 211 | '@context': expect.anything(), 212 | id: identifier, 213 | verificationMethod: [ 214 | { 215 | id: `${identifier}#controller`, 216 | type: expect.anything(), 217 | controller: expect.anything(), 218 | blockchainAccountId: `eip155:1337:${newOwner}`, 219 | }, 220 | ], 221 | authentication: [expect.anything()], 222 | assertionMethod: [expect.anything()], 223 | }) 224 | }) 225 | 226 | it('set attribute signed (key)', async () => { 227 | const { address: identity, shortDID: identifier, privKey } = await randomAccount(provider) 228 | const { pubKey: signingKey } = await randomAccount(provider) 229 | 230 | const attributeName = 'did/pub/Secp256k1/veriKey/hex' 231 | const attributeValue = signingKey 232 | const attributeExpiration = 86400 233 | 234 | const hash = await new EthrDidController(identifier, registryContract).createSetAttributeHash( 235 | attributeName, 236 | attributeValue, 237 | attributeExpiration 238 | ) 239 | 240 | const signature = privKey.sign(hash) 241 | 242 | await new EthrDidController(identifier, registryContract, await provider.getSigner(0)).setAttributeSigned( 243 | attributeName, 244 | attributeValue, 245 | attributeExpiration, 246 | { 247 | sigV: signature.v, 248 | sigR: signature.r, 249 | sigS: signature.s, 250 | } 251 | ) 252 | const result = await didResolver.resolve(identifier) 253 | expect(result?.didDocument?.verificationMethod?.[1]).toEqual({ 254 | controller: `${identifier}`, 255 | id: `${identifier}#delegate-1`, 256 | publicKeyHex: attributeValue.slice(2), 257 | type: 'EcdsaSecp256k1VerificationKey2019', 258 | }) 259 | }) 260 | }) 261 | -------------------------------------------------------------------------------- /src/__tests__/resolve.overlap.test.ts: -------------------------------------------------------------------------------- 1 | import { Contract } from 'ethers' 2 | import { Resolvable } from 'did-resolver' 3 | 4 | import { GanacheProvider } from '@ethers-ext/provider-ganache' 5 | import { EthrDidController } from '../controller' 6 | import { deployRegistry, randomAccount, sleep, startMining, stopMining } from './testUtils' 7 | import { stringToBytes32 } from '../helpers' 8 | 9 | jest.setTimeout(30000) 10 | 11 | describe('overlapping events', () => { 12 | let registryContract: Contract, didResolver: Resolvable, provider: GanacheProvider 13 | 14 | beforeAll(async () => { 15 | const reg = await deployRegistry() 16 | registryContract = reg.registryContract 17 | didResolver = reg.didResolver 18 | provider = reg.provider 19 | }) 20 | 21 | it('adding the same service in the same block does not result in duplication', async () => { 22 | expect.assertions(2) 23 | 24 | const { address, shortDID: identifier, signer } = await randomAccount(provider) 25 | 26 | const ethrDid = new EthrDidController(identifier, registryContract, signer) 27 | const blockHeightBeforeChange = (await provider.getBlock('latest'))!.number 28 | await stopMining(provider) 29 | const tx1 = ethrDid.setAttribute(stringToBytes32('did/svc/TestService'), 'https://test.uport.me', 86406, { 30 | from: address, 31 | }) 32 | const tx2 = ethrDid.setAttribute(stringToBytes32('did/svc/TestService'), 'https://test.uport.me', 86407, { 33 | from: address, 34 | }) 35 | await sleep(1000) 36 | await startMining(provider) 37 | await tx1 38 | await tx2 39 | 40 | const result = await didResolver.resolve(identifier) 41 | expect(parseInt(result?.didDocumentMetadata.versionId ?? '')).toEqual(blockHeightBeforeChange + 1) 42 | expect(result).toEqual({ 43 | didDocumentMetadata: { versionId: expect.anything(), updated: expect.anything() }, 44 | didResolutionMetadata: expect.anything(), 45 | didDocument: { 46 | '@context': expect.anything(), 47 | id: identifier, 48 | verificationMethod: expect.anything(), 49 | authentication: expect.anything(), 50 | assertionMethod: expect.anything(), 51 | service: [ 52 | { 53 | id: `${identifier}#service-2`, 54 | type: 'TestService', 55 | serviceEndpoint: 'https://test.uport.me', 56 | }, 57 | ], 58 | }, 59 | }) 60 | }) 61 | 62 | it('adding 2 services in 2 consecutive blocks should result in only 2 services appearing in the DID doc (no duplication)', async () => { 63 | expect.assertions(3) 64 | const { address, shortDID: identifier, signer } = await randomAccount(provider) 65 | 66 | const ethrDid = new EthrDidController(identifier, registryContract, signer) 67 | 68 | const blockHeightBeforeChange = (await provider.getBlock('latest'))!.number 69 | 70 | await ethrDid.setAttribute(stringToBytes32('did/svc/TestService1'), 'https://test1.uport.me', 86406) 71 | let result = await didResolver.resolve(identifier) 72 | expect(result.didDocumentMetadata.versionId).toEqual(`${blockHeightBeforeChange + 1}`) 73 | await ethrDid.setAttribute(stringToBytes32('did/svc/TestService2'), 'https://test2.uport.me', 86407) 74 | 75 | result = await didResolver.resolve(identifier) 76 | expect(result.didDocumentMetadata.versionId).toEqual(`${blockHeightBeforeChange + 2}`) 77 | expect(result.didDocument).toEqual({ 78 | '@context': expect.anything(), 79 | id: identifier, 80 | verificationMethod: [expect.anything()], 81 | authentication: [expect.anything()], 82 | assertionMethod: [expect.anything()], 83 | service: [ 84 | { 85 | id: `${identifier}#service-1`, 86 | type: 'TestService1', 87 | serviceEndpoint: 'https://test1.uport.me', 88 | }, 89 | { 90 | id: `${identifier}#service-2`, 91 | type: 'TestService2', 92 | serviceEndpoint: 'https://test2.uport.me', 93 | }, 94 | ], 95 | }) 96 | }) 97 | 98 | it('adding and removing a service in the same block should result in no change to the doc (correct order, same block)', async () => { 99 | expect.assertions(2) 100 | const { address, shortDID: identifier, signer } = await randomAccount(provider) 101 | 102 | const ethrDid = new EthrDidController(identifier, registryContract, signer) 103 | 104 | const blockHeightBeforeChange = (await provider.getBlock('latest'))!.number 105 | 106 | await stopMining(provider) 107 | const tx1 = ethrDid.setAttribute(stringToBytes32('did/svc/TestService1'), 'https://test1.uport.me', 86406) 108 | let result = await didResolver.resolve(identifier) 109 | expect(result.didDocumentMetadata.versionId).not.toBeDefined() 110 | const tx2 = ethrDid.revokeAttribute(stringToBytes32('did/svc/TestService1'), 'https://test1.uport.me') 111 | await sleep(1000).then(() => startMining(provider)) 112 | await tx1 113 | await tx2 114 | 115 | result = await didResolver.resolve(identifier) 116 | expect(result).toEqual({ 117 | didDocumentMetadata: { versionId: `${blockHeightBeforeChange + 1}`, updated: expect.anything() }, 118 | didResolutionMetadata: expect.anything(), 119 | didDocument: { 120 | '@context': expect.anything(), 121 | id: identifier, 122 | verificationMethod: [expect.anything()], 123 | authentication: [expect.anything()], 124 | assertionMethod: [expect.anything()], 125 | service: undefined, 126 | }, 127 | }) 128 | }) 129 | 130 | it('adding and removing a service in 2 consecutive blocks should result in no change to the doc (correct order 2 blocks).', async () => { 131 | expect.assertions(2) 132 | const { shortDID: identifier, signer } = await randomAccount(provider) 133 | 134 | const ethrDid = new EthrDidController(identifier, registryContract, signer) 135 | 136 | const blockHeightBeforeChange = (await provider.getBlock('latest'))!.number 137 | 138 | await ethrDid.setAttribute(stringToBytes32('did/svc/TestService1'), 'https://test1.uport.me', 86406) 139 | let result = await didResolver.resolve(identifier) 140 | expect(result.didDocumentMetadata.versionId).toEqual(`${blockHeightBeforeChange + 1}`) 141 | await ethrDid.revokeAttribute(stringToBytes32('did/svc/TestService1'), 'https://test1.uport.me') 142 | 143 | result = await didResolver.resolve(identifier) 144 | expect(result).toEqual({ 145 | didDocumentMetadata: { versionId: `${blockHeightBeforeChange + 2}`, updated: expect.anything() }, 146 | didResolutionMetadata: expect.anything(), 147 | didDocument: { 148 | '@context': expect.anything(), 149 | id: identifier, 150 | verificationMethod: [expect.anything()], 151 | authentication: [expect.anything()], 152 | assertionMethod: [expect.anything()], 153 | service: undefined, 154 | }, 155 | }) 156 | }) 157 | 158 | it('removing a service and then adding it back in the next block should keep the service visible in the resolved doc (correct order 2 blocks, corner case)', async () => { 159 | expect.assertions(2) 160 | const { shortDID: identifier, signer } = await randomAccount(provider) 161 | const ethrDid = new EthrDidController(identifier, registryContract, signer) 162 | 163 | const blockHeightBeforeChange = (await provider.getBlock('latest'))!.number 164 | 165 | await ethrDid.revokeAttribute(stringToBytes32('did/svc/TestService1'), 'https://test1.uport.me') 166 | let result = await didResolver.resolve(identifier) 167 | expect(result.didDocumentMetadata.versionId).toEqual(`${blockHeightBeforeChange + 1}`) 168 | await ethrDid.setAttribute(stringToBytes32('did/svc/TestService1'), 'https://test1.uport.me', 86406) 169 | 170 | result = await didResolver.resolve(identifier) 171 | expect(result).toEqual({ 172 | didDocumentMetadata: { versionId: `${blockHeightBeforeChange + 2}`, updated: expect.anything() }, 173 | didResolutionMetadata: expect.anything(), 174 | didDocument: { 175 | '@context': expect.anything(), 176 | id: identifier, 177 | verificationMethod: [expect.anything()], 178 | authentication: [expect.anything()], 179 | assertionMethod: [expect.anything()], 180 | service: [ 181 | { 182 | id: `${identifier}#service-2`, 183 | type: 'TestService1', 184 | serviceEndpoint: 'https://test1.uport.me', 185 | }, 186 | ], 187 | }, 188 | }) 189 | }) 190 | }) 191 | -------------------------------------------------------------------------------- /src/__tests__/resolve.unregistered.test.ts: -------------------------------------------------------------------------------- 1 | import { Resolvable } from 'did-resolver' 2 | 3 | import { GanacheProvider } from '@ethers-ext/provider-ganache' 4 | import { deployRegistry, randomAccount } from './testUtils' 5 | 6 | jest.setTimeout(30000) 7 | 8 | describe('unregistered DIDs', () => { 9 | let didResolver: Resolvable, provider: GanacheProvider 10 | 11 | beforeAll(async () => { 12 | const reg = await deployRegistry() 13 | didResolver = reg.didResolver 14 | provider = reg.provider 15 | }) 16 | 17 | it('resolves doc with ethereum address identifier', async () => { 18 | expect.assertions(1) 19 | const { address, shortDID } = await randomAccount(provider) 20 | 21 | await expect(didResolver.resolve(shortDID)).resolves.toEqual({ 22 | didDocumentMetadata: {}, 23 | didResolutionMetadata: { contentType: 'application/did+ld+json' }, 24 | didDocument: { 25 | '@context': expect.anything(), 26 | id: shortDID, 27 | verificationMethod: [ 28 | { 29 | id: `${shortDID}#controller`, 30 | type: 'EcdsaSecp256k1RecoveryMethod2020', 31 | controller: shortDID, 32 | blockchainAccountId: `eip155:1337:${address}`, 33 | }, 34 | ], 35 | authentication: [`${shortDID}#controller`], 36 | assertionMethod: [`${shortDID}#controller`], 37 | }, 38 | }) 39 | }) 40 | 41 | it('resolves document with publicKey identifier', async () => { 42 | expect.assertions(1) 43 | const { address, longDID, pubKey } = await randomAccount(provider) 44 | await expect(didResolver.resolve(longDID)).resolves.toEqual({ 45 | didDocumentMetadata: {}, 46 | didResolutionMetadata: { contentType: 'application/did+ld+json' }, 47 | didDocument: { 48 | '@context': expect.anything(), 49 | id: longDID, 50 | verificationMethod: [ 51 | { 52 | id: `${longDID}#controller`, 53 | type: 'EcdsaSecp256k1RecoveryMethod2020', 54 | controller: longDID, 55 | blockchainAccountId: `eip155:1337:${address}`, 56 | }, 57 | { 58 | id: `${longDID}#controllerKey`, 59 | type: 'EcdsaSecp256k1VerificationKey2019', 60 | controller: longDID, 61 | publicKeyHex: pubKey.slice(2), 62 | }, 63 | ], 64 | authentication: [`${longDID}#controller`, `${longDID}#controllerKey`], 65 | assertionMethod: [`${longDID}#controller`, `${longDID}#controllerKey`], 66 | }, 67 | }) 68 | }) 69 | 70 | it('resolves document with `accept` resolution option = JSON', async () => { 71 | expect.assertions(1) 72 | const { address, longDID: pubdid, pubKey } = await randomAccount(provider) 73 | await expect(didResolver.resolve(pubdid, { accept: 'application/did+json' })).resolves.toEqual({ 74 | didDocumentMetadata: {}, 75 | didResolutionMetadata: { contentType: 'application/did+json' }, 76 | didDocument: { 77 | id: pubdid, 78 | verificationMethod: [ 79 | { 80 | id: `${pubdid}#controller`, 81 | type: 'EcdsaSecp256k1RecoveryMethod2020', 82 | controller: pubdid, 83 | blockchainAccountId: `eip155:1337:${address}`, 84 | }, 85 | { 86 | id: `${pubdid}#controllerKey`, 87 | type: 'EcdsaSecp256k1VerificationKey2019', 88 | controller: pubdid, 89 | publicKeyHex: pubKey.slice(2), 90 | }, 91 | ], 92 | authentication: [`${pubdid}#controller`, `${pubdid}#controllerKey`], 93 | assertionMethod: [`${pubdid}#controller`, `${pubdid}#controllerKey`], 94 | }, 95 | }) 96 | }) 97 | 98 | it('resolves document with `accept` resolution option = application/did+ld+json', async () => { 99 | expect.assertions(1) 100 | const { address, longDID: pubdid, pubKey } = await randomAccount(provider) 101 | await expect(didResolver.resolve(pubdid, { accept: 'application/did+ld+json' })).resolves.toEqual({ 102 | didDocumentMetadata: {}, 103 | didResolutionMetadata: { contentType: 'application/did+ld+json' }, 104 | didDocument: { 105 | id: pubdid, 106 | verificationMethod: [ 107 | { 108 | id: `${pubdid}#controller`, 109 | type: 'EcdsaSecp256k1RecoveryMethod2020', 110 | controller: pubdid, 111 | blockchainAccountId: `eip155:1337:${address}`, 112 | }, 113 | { 114 | id: `${pubdid}#controllerKey`, 115 | type: 'EcdsaSecp256k1VerificationKey2019', 116 | controller: pubdid, 117 | publicKeyHex: pubKey.slice(2), 118 | }, 119 | ], 120 | authentication: [`${pubdid}#controller`, `${pubdid}#controllerKey`], 121 | assertionMethod: [`${pubdid}#controller`, `${pubdid}#controllerKey`], 122 | '@context': [ 123 | 'https://www.w3.org/ns/did/v1', 124 | 'https://w3id.org/security/suites/secp256k1recovery-2020/v2', 125 | 'https://w3id.org/security/v3-unstable', 126 | ], 127 | }, 128 | }) 129 | }) 130 | }) 131 | -------------------------------------------------------------------------------- /src/__tests__/resolver.regression.test.ts: -------------------------------------------------------------------------------- 1 | import { Contract } from 'ethers' 2 | import { Resolvable } from 'did-resolver' 3 | import { EthrDidController } from '../controller' 4 | import { interpretIdentifier, nullAddress } from '../helpers' 5 | import { deployRegistry, randomAccount } from './testUtils' 6 | import { GanacheProvider } from '@ethers-ext/provider-ganache' 7 | 8 | jest.setTimeout(30000) 9 | 10 | describe('regression', () => { 11 | let registryContract: Contract, didResolver: Resolvable, provider: GanacheProvider 12 | 13 | beforeAll(async () => { 14 | const reg = await deployRegistry() 15 | registryContract = reg.registryContract 16 | didResolver = reg.didResolver 17 | provider = reg.provider 18 | }) 19 | 20 | it('resolves same document with case sensitive eth address (https://github.com/decentralized-identity/ethr-did-resolver/issues/105)', async () => { 21 | expect.assertions(3) 22 | const { address, signer } = await randomAccount(provider) 23 | const lowAddress = address.toLowerCase() 24 | const checksumAddress = interpretIdentifier(address).address 25 | const lowDid = `did:ethr:dev:${lowAddress}` 26 | const checksumDid = `did:ethr:dev:${checksumAddress}` 27 | await new EthrDidController(lowAddress, registryContract, signer).setAttribute( 28 | 'did/pub/Secp256k1/veriKey/hex', 29 | '0x02b97c30de767f084ce3080168ee293053ba33b235d7116a3263d29f1450936b71', 30 | 86409 31 | ) 32 | const didDocumentLow = (await didResolver.resolve(lowDid)).didDocument 33 | const didDocumentChecksum = (await didResolver.resolve(checksumDid)).didDocument 34 | expect(lowDid).not.toEqual(checksumDid) 35 | expect(didDocumentLow).toBeDefined() 36 | //we don't care about the actual keys, only about their sameness 37 | expect(JSON.stringify(didDocumentLow).toLowerCase()).toEqual(JSON.stringify(didDocumentChecksum).toLowerCase()) 38 | }) 39 | 40 | it('adds sigAuth to authentication section (https://github.com/decentralized-identity/ethr-did-resolver/issues/95)', async () => { 41 | expect.assertions(1) 42 | const { address, shortDID: identifier, signer } = await randomAccount(provider) 43 | const authPubKey = `31303866356238393330623164633235386162353765386630646362363932353963363162316166` 44 | await new EthrDidController(identifier, registryContract, signer).setAttribute( 45 | 'did/pub/Ed25519/sigAuth/hex', 46 | `0x${authPubKey}`, 47 | 86410 48 | ) 49 | const { didDocument } = await didResolver.resolve(identifier) 50 | expect(didDocument).toEqual({ 51 | '@context': expect.anything(), 52 | id: identifier, 53 | verificationMethod: [ 54 | { 55 | id: `${identifier}#controller`, 56 | controller: identifier, 57 | type: 'EcdsaSecp256k1RecoveryMethod2020', 58 | blockchainAccountId: `eip155:1337:${address}`, 59 | }, 60 | { 61 | id: `${identifier}#delegate-1`, 62 | controller: identifier, 63 | type: `Ed25519VerificationKey2018`, 64 | publicKeyHex: authPubKey, 65 | }, 66 | ], 67 | authentication: [`${identifier}#controller`, `${identifier}#delegate-1`], 68 | assertionMethod: [`${identifier}#controller`, `${identifier}#delegate-1`], 69 | }) 70 | }) 71 | 72 | it('Ed25519VerificationKey2018 in base58 (https://github.com/decentralized-identity/ethr-did-resolver/pull/106)', async () => { 73 | expect.assertions(1) 74 | const { address, shortDID: identifier, signer } = await randomAccount(provider) 75 | const publicKeyHex = `b97c30de767f084ce3080168ee293053ba33b235d7116a3263d29f1450936b71` 76 | const expectedPublicKeyBase58 = 'DV4G2kpBKjE6zxKor7Cj21iL9x9qyXb6emqjszBXcuhz' 77 | await new EthrDidController(identifier, registryContract, signer).setAttribute( 78 | 'did/pub/Ed25519/veriKey/base58', 79 | `0x${publicKeyHex}`, 80 | 86411 81 | ) 82 | const result = await didResolver.resolve(identifier) 83 | expect(result.didDocument).toEqual({ 84 | '@context': expect.anything(), 85 | id: identifier, 86 | verificationMethod: [ 87 | { 88 | id: `${identifier}#controller`, 89 | type: 'EcdsaSecp256k1RecoveryMethod2020', 90 | controller: identifier, 91 | blockchainAccountId: `eip155:1337:${address}`, 92 | }, 93 | { 94 | id: `${identifier}#delegate-1`, 95 | type: 'Ed25519VerificationKey2018', 96 | controller: identifier, 97 | publicKeyBase58: expectedPublicKeyBase58, 98 | }, 99 | ], 100 | authentication: [`${identifier}#controller`], 101 | assertionMethod: [`${identifier}#controller`, `${identifier}#delegate-1`], 102 | }) 103 | }) 104 | 105 | it('can deactivate a DID (https://github.com/decentralized-identity/ethr-did-resolver/issues/83)', async () => { 106 | expect.assertions(2) 107 | const { shortDID: identifier, signer } = await randomAccount(provider) 108 | const blockHeightBeforeChange = (await provider.getBlock('latest'))!.number 109 | await new EthrDidController(identifier, registryContract, signer).changeOwner(nullAddress) 110 | const result = await didResolver.resolve(identifier) 111 | expect(parseInt(result?.didDocumentMetadata.versionId ?? '')).toBeGreaterThanOrEqual(blockHeightBeforeChange + 1) 112 | expect(result).toEqual({ 113 | didDocumentMetadata: { 114 | deactivated: true, 115 | updated: expect.anything(), 116 | versionId: expect.anything(), 117 | }, 118 | didResolutionMetadata: expect.anything(), 119 | didDocument: { 120 | '@context': expect.anything(), 121 | id: identifier, 122 | verificationMethod: [], 123 | authentication: [], 124 | assertionMethod: [], 125 | }, 126 | }) 127 | }) 128 | }) 129 | -------------------------------------------------------------------------------- /src/__tests__/resolver.versioning.test.ts: -------------------------------------------------------------------------------- 1 | import { Contract } from 'ethers' 2 | import { Resolvable } from 'did-resolver' 3 | import { EthrDidController } from '../controller' 4 | import { nullAddress } from '../helpers' 5 | import { deployRegistry, randomAccount, sleep } from './testUtils' 6 | import { GanacheProvider } from '@ethers-ext/provider-ganache' 7 | 8 | jest.setTimeout(30000) 9 | 10 | describe('versioning', () => { 11 | let registryContract: Contract, didResolver: Resolvable, provider: GanacheProvider 12 | 13 | beforeAll(async () => { 14 | const reg = await deployRegistry() 15 | registryContract = reg.registryContract 16 | didResolver = reg.didResolver 17 | provider = reg.provider 18 | }) 19 | 20 | it('can resolve virgin DID with versionId=latest', async () => { 21 | expect.assertions(1) 22 | const { address: virginAddress, shortDID: virginDID } = await randomAccount(provider) 23 | const result = await didResolver.resolve(`${virginDID}?versionId=latest`) 24 | expect(result).toEqual({ 25 | didDocumentMetadata: {}, 26 | didResolutionMetadata: { 27 | contentType: 'application/did+ld+json', 28 | }, 29 | didDocument: { 30 | '@context': expect.anything(), 31 | id: virginDID, 32 | verificationMethod: [ 33 | { 34 | id: `${virginDID}#controller`, 35 | type: 'EcdsaSecp256k1RecoveryMethod2020', 36 | controller: virginDID, 37 | blockchainAccountId: `eip155:1337:${virginAddress}`, 38 | }, 39 | ], 40 | authentication: [`${virginDID}#controller`], 41 | assertionMethod: [`${virginDID}#controller`], 42 | }, 43 | }) 44 | }) 45 | 46 | it('can resolve did with versionId before deactivation', async () => { 47 | expect.assertions(1) 48 | const { address, shortDID: deactivatedDid, signer } = await randomAccount(provider) 49 | await new EthrDidController(deactivatedDid, registryContract, signer).changeOwner(nullAddress) 50 | const { didDocumentMetadata } = await didResolver.resolve(deactivatedDid) 51 | const deactivationBlock = parseInt(didDocumentMetadata.versionId ?? '') 52 | const result = await didResolver.resolve(`${deactivatedDid}?versionId=${deactivationBlock - 1}`) 53 | expect(result).toEqual({ 54 | didDocumentMetadata: { 55 | nextVersionId: `${deactivationBlock}`, 56 | nextUpdate: didDocumentMetadata.updated, 57 | }, 58 | didResolutionMetadata: { contentType: 'application/did+ld+json' }, 59 | didDocument: { 60 | '@context': expect.anything(), 61 | id: deactivatedDid, 62 | verificationMethod: [ 63 | { 64 | id: `${deactivatedDid}#controller`, 65 | type: 'EcdsaSecp256k1RecoveryMethod2020', 66 | controller: deactivatedDid, 67 | blockchainAccountId: `eip155:1337:${address}`, 68 | }, 69 | ], 70 | authentication: [`${deactivatedDid}#controller`], 71 | assertionMethod: [`${deactivatedDid}#controller`], 72 | }, 73 | }) 74 | }) 75 | 76 | it('can resolve modified did with versionId=latest', async () => { 77 | expect.assertions(2) 78 | const { shortDID: identifier, signer } = await randomAccount(provider) 79 | const { address: newOwner } = await randomAccount(provider) 80 | const blockHeightBeforeChange = (await provider.getBlock('latest'))!.number 81 | await new EthrDidController(identifier, registryContract, signer).changeOwner(newOwner) 82 | const result = await didResolver.resolve(`${identifier}?versionId=latest`) 83 | expect(parseInt(result?.didDocumentMetadata.versionId ?? '')).toBeGreaterThanOrEqual(blockHeightBeforeChange + 1) 84 | expect(result).toEqual({ 85 | didDocumentMetadata: { versionId: expect.anything(), updated: expect.anything() }, 86 | didResolutionMetadata: expect.anything(), 87 | didDocument: { 88 | '@context': expect.anything(), 89 | id: identifier, 90 | verificationMethod: [ 91 | { 92 | id: `${identifier}#controller`, 93 | type: 'EcdsaSecp256k1RecoveryMethod2020', 94 | controller: identifier, 95 | blockchainAccountId: `eip155:1337:${newOwner}`, 96 | }, 97 | ], 98 | authentication: [`${identifier}#controller`], 99 | assertionMethod: [`${identifier}#controller`], 100 | }, 101 | }) 102 | }) 103 | 104 | it('can resolve did with versionId before an attribute change', async () => { 105 | expect.assertions(1) 106 | 107 | const { address, shortDID: identifier, signer } = await randomAccount(provider) 108 | 109 | const blockHeightBeforeChange = (await provider.getBlock('latest'))!.number 110 | const ethrDid = new EthrDidController(identifier, registryContract, signer) 111 | await ethrDid.setAttribute('did/pub/Ed25519/veriKey/hex', `0x11111111`, 86411) 112 | await ethrDid.setAttribute('did/pub/Ed25519/veriKey/hex', `0x22222222`, 86412) 113 | 114 | const result = await didResolver.resolve(`${identifier}?versionId=${blockHeightBeforeChange + 1}`) 115 | expect(result).toEqual({ 116 | didDocumentMetadata: { 117 | versionId: `${blockHeightBeforeChange + 1}`, 118 | nextVersionId: `${blockHeightBeforeChange + 2}`, 119 | updated: expect.anything(), 120 | nextUpdate: expect.anything(), 121 | }, 122 | didResolutionMetadata: { contentType: 'application/did+ld+json' }, 123 | didDocument: { 124 | '@context': expect.anything(), 125 | id: identifier, 126 | verificationMethod: [ 127 | { 128 | id: `${identifier}#controller`, 129 | type: 'EcdsaSecp256k1RecoveryMethod2020', 130 | controller: identifier, 131 | blockchainAccountId: `eip155:1337:${address}`, 132 | }, 133 | { 134 | id: `${identifier}#delegate-1`, 135 | type: 'Ed25519VerificationKey2018', 136 | controller: identifier, 137 | publicKeyHex: '11111111', 138 | }, 139 | ], 140 | authentication: [`${identifier}#controller`], 141 | assertionMethod: [`${identifier}#controller`, `${identifier}#delegate-1`], 142 | }, 143 | }) 144 | }) 145 | 146 | it('can resolve did with versionId before a delegate change', async () => { 147 | expect.assertions(1) 148 | const delegateAddress1 = '0x1111111100000000000000000000000000000001' 149 | const delegateAddress2 = '0x2222222200000000000000000000000000000002' 150 | const { address, shortDID: identifier, signer } = await randomAccount(provider) 151 | 152 | const ethrDid = new EthrDidController(identifier, registryContract, signer) 153 | const blockHeightBeforeChange = (await provider.getBlock('latest'))!.number 154 | await ethrDid.addDelegate('veriKey', delegateAddress1, 86401) 155 | await ethrDid.addDelegate('veriKey', delegateAddress2, 86402) 156 | 157 | const result = await didResolver.resolve(`${identifier}?versionId=${blockHeightBeforeChange + 1}`) 158 | expect(result).toEqual({ 159 | didDocumentMetadata: { 160 | versionId: `${blockHeightBeforeChange + 1}`, 161 | nextVersionId: `${blockHeightBeforeChange + 2}`, 162 | updated: expect.anything(), 163 | nextUpdate: expect.anything(), 164 | }, 165 | didResolutionMetadata: expect.anything(), 166 | didDocument: { 167 | '@context': expect.anything(), 168 | id: identifier, 169 | verificationMethod: [ 170 | { 171 | id: `${identifier}#controller`, 172 | type: 'EcdsaSecp256k1RecoveryMethod2020', 173 | controller: identifier, 174 | blockchainAccountId: `eip155:1337:${address}`, 175 | }, 176 | { 177 | id: `${identifier}#delegate-1`, 178 | type: 'EcdsaSecp256k1RecoveryMethod2020', 179 | controller: identifier, 180 | blockchainAccountId: `eip155:1337:${delegateAddress1}`, 181 | }, 182 | ], 183 | authentication: [`${identifier}#controller`], 184 | assertionMethod: [`${identifier}#controller`, `${identifier}#delegate-1`], 185 | }, 186 | }) 187 | }) 188 | 189 | it('can resolve did with versionId before an owner change', async () => { 190 | expect.assertions(1) 191 | const { address: originalOwner, shortDID: identifier, signer } = await randomAccount(provider) 192 | const { address: newOwner, signer: newSigner } = await randomAccount(provider) 193 | 194 | const blockHeightBeforeChange = (await provider.getBlock('latest'))!.number 195 | await new EthrDidController(identifier, registryContract, signer).changeOwner(newOwner) 196 | await new EthrDidController(identifier, registryContract, newSigner).changeOwner(originalOwner) 197 | const result = await didResolver.resolve(`${identifier}?versionId=${blockHeightBeforeChange + 1}`) 198 | expect(result).toEqual({ 199 | didDocumentMetadata: { 200 | versionId: `${blockHeightBeforeChange + 1}`, 201 | nextVersionId: `${blockHeightBeforeChange + 2}`, 202 | updated: expect.anything(), 203 | nextUpdate: expect.anything(), 204 | }, 205 | didResolutionMetadata: expect.anything(), 206 | didDocument: { 207 | '@context': expect.anything(), 208 | id: identifier, 209 | verificationMethod: [ 210 | { 211 | id: `${identifier}#controller`, 212 | type: 'EcdsaSecp256k1RecoveryMethod2020', 213 | controller: identifier, 214 | blockchainAccountId: `eip155:1337:${newOwner}`, 215 | }, 216 | ], 217 | authentication: [`${identifier}#controller`], 218 | assertionMethod: [`${identifier}#controller`], 219 | }, 220 | }) 221 | }) 222 | 223 | it('can resolve did with versionId before an attribute expiration', async () => { 224 | expect.assertions(3) 225 | const delegate = '0xde1E9a7e00000000000000000000000000000001' 226 | const { address, shortDID: identifier, signer } = await randomAccount(provider) 227 | const validitySeconds = 2 228 | await new EthrDidController(identifier, registryContract, signer).addDelegate('sigAuth', delegate, validitySeconds) 229 | let result = await didResolver.resolve(identifier) 230 | // confirm delegate exists 231 | const versionBeforeExpiry = result.didDocumentMetadata.versionId 232 | expect(result?.didDocument?.verificationMethod?.[1]).toEqual({ 233 | id: `${identifier}#delegate-1`, 234 | type: 'EcdsaSecp256k1RecoveryMethod2020', 235 | controller: identifier, 236 | blockchainAccountId: `eip155:1337:${delegate}`, 237 | }) 238 | // await expiry 239 | await sleep((validitySeconds + 1) * 1000) 240 | // confirm delegate was removed after expiry 241 | result = await didResolver.resolve(identifier) 242 | expect(result?.didDocument?.verificationMethod?.length).toEqual(1) 243 | 244 | // resolve DID before expiry 245 | result = await didResolver.resolve(`${identifier}?versionId=${versionBeforeExpiry}`) 246 | expect(result).toEqual({ 247 | didDocumentMetadata: { versionId: `${versionBeforeExpiry}`, updated: expect.anything() }, 248 | didResolutionMetadata: expect.anything(), 249 | didDocument: { 250 | '@context': expect.anything(), 251 | id: identifier, 252 | verificationMethod: [ 253 | { 254 | id: `${identifier}#controller`, 255 | type: 'EcdsaSecp256k1RecoveryMethod2020', 256 | controller: identifier, 257 | blockchainAccountId: `eip155:1337:${address}`, 258 | }, 259 | { 260 | id: `${identifier}#delegate-1`, 261 | type: 'EcdsaSecp256k1RecoveryMethod2020', 262 | controller: identifier, 263 | blockchainAccountId: `eip155:1337:${delegate}`, 264 | }, 265 | ], 266 | authentication: [`${identifier}#controller`, `${identifier}#delegate-1`], 267 | assertionMethod: [`${identifier}#controller`, `${identifier}#delegate-1`], 268 | }, 269 | }) 270 | }) 271 | }) 272 | -------------------------------------------------------------------------------- /src/__tests__/testUtils.ts: -------------------------------------------------------------------------------- 1 | import { Contract, ContractFactory, ethers, SigningKey, Wallet } from 'ethers' 2 | import { GanacheProvider } from '@ethers-ext/provider-ganache' 3 | import { Resolver } from 'did-resolver' 4 | import { getResolver } from '../resolver' 5 | import { EthereumDIDRegistry } from '../config/EthereumDIDRegistry' 6 | 7 | export async function deployRegistry(): Promise<{ 8 | registryContract: Contract 9 | provider: GanacheProvider 10 | didResolver: Resolver 11 | }> { 12 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 13 | const provider = new GanacheProvider({ logging: { quiet: true } } as any) 14 | const factory = ContractFactory.fromSolidity(EthereumDIDRegistry).connect(await provider.getSigner(0)) 15 | 16 | const registryContract: Contract = await (await factory.deploy()).waitForDeployment() 17 | const registry = await registryContract.getAddress() 18 | 19 | const didResolver = new Resolver(getResolver({ name: 'dev', provider: provider, registry })) 20 | 21 | return { registryContract, didResolver, provider } 22 | } 23 | 24 | export async function sleep(milliseconds: number): Promise { 25 | return new Promise((resolve) => setTimeout(resolve, milliseconds)) 26 | } 27 | 28 | export async function stopMining(provider: GanacheProvider): Promise { 29 | return provider.send('miner_stop', []) 30 | } 31 | 32 | export async function startMining(provider: GanacheProvider): Promise { 33 | return provider.send('miner_start', [1]) 34 | } 35 | 36 | export async function randomAccount(provider: GanacheProvider): Promise<{ 37 | privKey: SigningKey 38 | address: string 39 | shortDID: string 40 | longDID: string 41 | pubKey: string 42 | signer: Wallet 43 | }> { 44 | const privKey = new ethers.SigningKey(ethers.randomBytes(32)) 45 | const pubKey = privKey.compressedPublicKey 46 | const signer = new ethers.Wallet(privKey, provider) 47 | const address = await signer.getAddress() 48 | const shortDID = `did:ethr:dev:${address}` 49 | const longDID = `did:ethr:dev:${pubKey}` 50 | await provider.setAccount(address, { 51 | balance: '0x1000000000000000000000', 52 | }) 53 | return { privKey, pubKey, signer, address, shortDID, longDID } 54 | } 55 | -------------------------------------------------------------------------------- /src/config/deployments.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Represents metadata for a deployment of the ERC1056 registry contract. 3 | * 4 | * This can be used to correctly connect DIDs anchored on a particular network to the known registry for that network. 5 | */ 6 | export type EthrDidRegistryDeployment = { 7 | /** 8 | * The chain ID of the ethereum-like network for this deployment. 9 | * 10 | * The HEX encoding of this value gets used to construct DIDs anchored on this network when the `name` property is 11 | * not set. Example: `did:ethr:<0xHexChainId>:0x...` 12 | */ 13 | chainId: number | bigint 14 | /** 15 | * The ERC1056 contract address on this network 16 | */ 17 | registry: string 18 | /** 19 | * The name of the network. 20 | * This is used to construct DIDs on this network: `did:ethr::0x...`. 21 | * If this is omitted, DIDs for this network are constructed using the HEX encoding of the chainID 22 | */ 23 | name?: string 24 | description?: string 25 | /** 26 | * A JSON-RPC endpoint that can be used to broadcast transactions or queries to this network 27 | */ 28 | rpcUrl?: string 29 | /** 30 | * Contracts prior to ethr-did-registry@0.0.3 track nonces differently for meta-transactions 31 | * 32 | * @see https://github.com/decentralized-identity/ethr-did-resolver/pull/164 33 | */ 34 | legacyNonce?: boolean 35 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 36 | [x: string]: any 37 | } 38 | 39 | /** 40 | * Represents the known deployments of the ERC1056 registry contract. 41 | */ 42 | export const deployments: EthrDidRegistryDeployment[] = [ 43 | { chainId: 1, registry: '0xdca7ef03e98e0dc2b855be647c39abe984fcf21b', name: 'mainnet', legacyNonce: true }, 44 | // { chainId: 3, registry: '0xdca7ef03e98e0dc2b855be647c39abe984fcf21b', name: 'ropsten', legacyNonce: true }, 45 | // { chainId: 4, registry: '0xdca7ef03e98e0dc2b855be647c39abe984fcf21b', name: 'rinkeby', legacyNonce: true }, 46 | // { chainId: 5, registry: '0xdca7ef03e98e0dc2b855be647c39abe984fcf21b', name: 'goerli', legacyNonce: true }, 47 | { chainId: 11155111, registry: '0x03d5003bf0e79C5F5223588F347ebA39AfbC3818', name: 'sepolia', legacyNonce: false }, 48 | { chainId: 100, registry: '0x03d5003bf0e79C5F5223588F347ebA39AfbC3818', name: 'gno', legacyNonce: false }, 49 | { chainId: 17000, registry: '0x03d5003bf0e79C5F5223588F347ebA39AfbC3818', name: 'holesky', legacyNonce: false }, 50 | // { chainId: 42, registry: '0xdca7ef03e98e0dc2b855be647c39abe984fcf21b', name: 'kovan', legacyNonce: true }, 51 | // // rsk networks cause socket hang up 52 | // { chainId: 30, registry: '0xdca7ef03e98e0dc2b855be647c39abe984fcf21b', name: 'rsk', legacyNonce: true }, 53 | // { 54 | // chainId: 31, 55 | // registry: '0xdca7ef03e98e0dc2b855be647c39abe984fcf21b', 56 | // name: 'rsk:testnet', 57 | // legacyNonce: true, 58 | // }, 59 | { 60 | chainId: 246, 61 | registry: '0xE29672f34e92b56C9169f9D485fFc8b9A136BCE4', 62 | name: 'ewc', 63 | description: 'energy web chain', 64 | legacyNonce: false, 65 | }, 66 | { 67 | chainId: 73799, 68 | registry: '0xC15D5A57A8Eb0e1dCBE5D88B8f9a82017e5Cc4AF', 69 | name: 'volta', 70 | description: 'energy web testnet', 71 | legacyNonce: false, 72 | }, 73 | { 74 | chainId: 246785, 75 | registry: '0xdCa7EF03e98e0DC2B855bE647C39ABe984fcF21B', 76 | name: 'artis:tau1', 77 | legacyNonce: true, 78 | }, 79 | { 80 | chainId: 246529, 81 | registry: '0xdCa7EF03e98e0DC2B855bE647C39ABe984fcF21B', 82 | name: 'artis:sigma1', 83 | legacyNonce: true, 84 | }, 85 | { chainId: 137, registry: '0xdca7ef03e98e0dc2b855be647c39abe984fcf21b', name: 'polygon', legacyNonce: true }, 86 | { 87 | chainId: 80001, 88 | registry: '0xdca7ef03e98e0dc2b855be647c39abe984fcf21b', 89 | name: 'polygon:test', 90 | legacyNonce: true, 91 | }, 92 | { 93 | chainId: 1313161554, 94 | registry: '0x63eD58B671EeD12Bc1652845ba5b2CDfBff198e0', 95 | name: 'aurora', 96 | legacyNonce: true, 97 | }, 98 | { 99 | chainId: 59140, 100 | registry: '0x03d5003bf0e79C5F5223588F347ebA39AfbC3818', 101 | name: 'linea:goerli', 102 | legacyNonce: false, 103 | }, 104 | ] 105 | -------------------------------------------------------------------------------- /src/configuration.ts: -------------------------------------------------------------------------------- 1 | import { Contract, ContractFactory, JsonRpcProvider, Provider } from 'ethers' 2 | import { DEFAULT_REGISTRY_ADDRESS } from './helpers.js' 3 | import { deployments, EthrDidRegistryDeployment } from './config/deployments.js' 4 | import { EthereumDIDRegistry } from './config/EthereumDIDRegistry.js' 5 | 6 | const infuraNames: Record = { 7 | polygon: 'matic', 8 | 'polygon:test': 'maticmum', 9 | aurora: 'aurora-mainnet', 10 | } 11 | 12 | const knownInfuraNames = ['mainnet', 'aurora', 'linea:goerli', 'sepolia'] 13 | 14 | /** 15 | * A configuration entry for an ethereum network 16 | * It should contain at least one of `name` or `chainId` AND one of `provider`, `web3`, or `rpcUrl` 17 | * 18 | * @example ```js 19 | * { name: 'development', registry: '0x9af37603e98e0dc2b855be647c39abe984fc2445', rpcUrl: 'http://127.0.0.1:8545/' } 20 | * { name: 'sepolia', chainId: 11155111, provider: new InfuraProvider('sepolia') } 21 | * { name: 'goerli', provider: new AlchemyProvider('goerli') } 22 | * { name: 'rsk:testnet', chainId: '0x1f', rpcUrl: 'https://public-node.testnet.rsk.co' } 23 | * ``` 24 | */ 25 | export interface ProviderConfiguration extends Omit { 26 | provider?: Provider | null 27 | chainId?: string | number | bigint 28 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 29 | web3?: any 30 | } 31 | 32 | export interface MultiProviderConfiguration extends ProviderConfiguration { 33 | networks?: ProviderConfiguration[] 34 | } 35 | 36 | export interface InfuraConfiguration { 37 | infuraProjectId: string 38 | } 39 | 40 | export type ConfigurationOptions = MultiProviderConfiguration | InfuraConfiguration 41 | 42 | export type ConfiguredNetworks = Record 43 | 44 | function configureNetworksWithInfura(projectId?: string): ConfiguredNetworks { 45 | if (!projectId) { 46 | return {} 47 | } 48 | 49 | const networks = knownInfuraNames 50 | .map((n) => { 51 | const existingDeployment = deployments.find((d) => d.name === n) 52 | if (existingDeployment && existingDeployment.name) { 53 | const infuraName = infuraNames[existingDeployment.name] || existingDeployment.name 54 | const rpcUrl = `https://${infuraName}.infura.io/v3/${projectId}` 55 | return { ...existingDeployment, rpcUrl } 56 | } 57 | }) 58 | .filter((conf) => !!conf) as ProviderConfiguration[] 59 | 60 | return configureNetworks({ networks }) 61 | } 62 | 63 | export function getContractForNetwork(conf: ProviderConfiguration): Contract { 64 | let provider: Provider = conf.provider || conf.web3?.currentProvider 65 | if (!provider) { 66 | if (conf.rpcUrl) { 67 | const chainIdRaw = conf.chainId ? conf.chainId : deployments.find((d) => d.name === conf.name)?.chainId 68 | const chainId = chainIdRaw ? BigInt(chainIdRaw) : chainIdRaw 69 | provider = new JsonRpcProvider(conf.rpcUrl, chainId || 'any') 70 | } else { 71 | throw new Error(`invalid_config: No web3 provider could be determined for network ${conf.name || conf.chainId}`) 72 | } 73 | } 74 | const contract = ContractFactory.fromSolidity(EthereumDIDRegistry) 75 | .attach(conf.registry || DEFAULT_REGISTRY_ADDRESS) 76 | .connect(provider) 77 | return contract as Contract 78 | } 79 | 80 | function configureNetwork(net: ProviderConfiguration): ConfiguredNetworks { 81 | const networks: ConfiguredNetworks = {} 82 | const chainId = 83 | net.chainId || deployments.find((d) => net.name && (d.name === net.name || d.description === net.name))?.chainId 84 | if (chainId) { 85 | if (net.name) { 86 | networks[net.name] = getContractForNetwork(net) 87 | } 88 | const id = typeof chainId === 'bigint' || typeof chainId === 'number' ? `0x${chainId.toString(16)}` : chainId 89 | networks[id] = getContractForNetwork(net) 90 | } else if (net.provider || net.web3 || net.rpcUrl) { 91 | networks[net.name || ''] = getContractForNetwork(net) 92 | } 93 | return networks 94 | } 95 | 96 | function configureNetworks(conf: MultiProviderConfiguration): ConfiguredNetworks { 97 | return { 98 | ...configureNetwork(conf), 99 | ...conf.networks?.reduce((networks, net) => { 100 | return { ...networks, ...configureNetwork(net) } 101 | }, {}), 102 | } 103 | } 104 | 105 | /** 106 | * Generates a configuration that maps ethereum network names and chainIDs to the respective ERC1056 contracts deployed 107 | * on them. 108 | * @returns a record of ERC1056 `Contract` instances 109 | * @param conf - configuration options for the resolver. An array of network details. 110 | * Each network entry should contain at least one of `name` or `chainId` AND one of `provider`, `web3`, or `rpcUrl` 111 | * For convenience, you can also specify an `infuraProjectId` which will create a mapping for all the networks 112 | * supported by https://infura.io. 113 | * @example ```js 114 | * [ 115 | * { name: 'development', registry: '0x9af37603e98e0dc2b855be647c39abe984fc2445', rpcUrl: 'http://127.0.0.1:8545/' }, 116 | * { name: 'goerli', chainId: 5, provider: new InfuraProvider('goerli') }, 117 | * { name: 'sepolia', provider: new AlchemyProvider('sepolia') }, 118 | * { name: 'rsk:testnet', chainId: '0x1f', rpcUrl: 'https://public-node.testnet.rsk.co' }, 119 | * ] 120 | * ``` 121 | */ 122 | export function configureResolverWithNetworks(conf: ConfigurationOptions = {}): ConfiguredNetworks { 123 | const networks = { 124 | ...configureNetworksWithInfura((conf).infuraProjectId), 125 | ...configureNetworks(conf), 126 | } 127 | if (Object.keys(networks).length === 0) { 128 | throw new Error('invalid_config: Please make sure to have at least one network') 129 | } 130 | return networks 131 | } 132 | -------------------------------------------------------------------------------- /src/controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Addressable, 3 | AddressLike, 4 | BlockTag, 5 | concat, 6 | Contract, 7 | encodeBytes32String, 8 | getBytes, 9 | hexlify, 10 | isHexString, 11 | JsonRpcProvider, 12 | keccak256, 13 | Overrides, 14 | Provider, 15 | Signer, 16 | toBeHex, 17 | toUtf8Bytes, 18 | TransactionReceipt, 19 | zeroPadValue, 20 | } from 'ethers' 21 | import { getContractForNetwork } from './configuration.js' 22 | import { 23 | address, 24 | DEFAULT_REGISTRY_ADDRESS, 25 | interpretIdentifier, 26 | MESSAGE_PREFIX, 27 | MetaSignature, 28 | stringToBytes32, 29 | } from './helpers.js' 30 | 31 | /** 32 | * A class that can be used to interact with the ERC1056 contract on behalf of a local controller key-pair 33 | */ 34 | export class EthrDidController { 35 | private contract: Contract 36 | private readonly signer?: Signer 37 | private readonly address: string 38 | public readonly did: string 39 | private readonly legacyNonce: boolean 40 | 41 | /** 42 | * Creates an EthrDidController instance. 43 | * 44 | * @param identifier - required - a `did:ethr` string or a publicKeyHex or an ethereum address 45 | * @param signer - optional - a Signer that represents the current controller key (owner) of the identifier. If a 46 | * 'signer' is not provided, then a 'contract' with an attached signer can be used. 47 | * @param contract - optional - a Contract instance representing a ERC1056 contract. At least one of `contract`, 48 | * `provider`, or `rpcUrl` is required 49 | * @param chainNameOrId - optional - the network name or chainID, defaults to 'mainnet' 50 | * @param provider - optional - a web3 Provider. At least one of `contract`, `provider`, or `rpcUrl` is required 51 | * @param rpcUrl - optional - a JSON-RPC URL that can be used to connect to an ethereum network. At least one of 52 | * `contract`, `provider`, or `rpcUrl` is required 53 | * @param registry - optional - The ERC1056 registry address. Defaults to 54 | * '0xdca7ef03e98e0dc2b855be647c39abe984fcf21b'. Only used with 'provider' or 'rpcUrl' 55 | * @param legacyNonce - optional - If the legacy nonce tracking method should be accounted for. If lesser version of 56 | * did-ethr-registry contract v1.0.0 is used then this should be true. 57 | */ 58 | constructor( 59 | identifier: string | address, 60 | contract?: Contract, 61 | signer?: Signer, 62 | chainNameOrId = 'mainnet', 63 | provider?: Provider, 64 | rpcUrl?: string, 65 | registry: string = DEFAULT_REGISTRY_ADDRESS, 66 | legacyNonce = true 67 | ) { 68 | this.legacyNonce = legacyNonce 69 | // initialize identifier 70 | const { address, publicKey, network } = interpretIdentifier(identifier) 71 | const net = network || chainNameOrId 72 | // initialize contract connection 73 | if (contract) { 74 | this.contract = contract 75 | } else if (provider || signer?.provider || rpcUrl) { 76 | const prov = provider || signer?.provider 77 | this.contract = getContractForNetwork({ name: net, provider: prov, registry, rpcUrl }) 78 | } else { 79 | throw new Error(' either a contract instance or a provider or rpcUrl is required to initialize') 80 | } 81 | this.signer = signer 82 | this.address = address 83 | let networkString = net ? `${net}:` : '' 84 | if (networkString in ['mainnet:', '0x1:']) { 85 | networkString = '' 86 | } 87 | this.did = publicKey ? `did:ethr:${networkString}${publicKey}` : `did:ethr:${networkString}${address}` 88 | } 89 | 90 | /** 91 | * @returns the encoded attribute value in hex or utf8 bytes 92 | * @param attrValue - the attribute value to encode (e.g. service endpoint, public key, etc.) 93 | * 94 | * @remarks The incoming attribute value may be a hex encoded key, or an utf8 encoded string (like service endpoints) 95 | **/ 96 | encodeAttributeValue(attrValue: string | `0x${string}`): Uint8Array | `0x${string}` { 97 | return isHexString(attrValue) ? attrValue : toUtf8Bytes(attrValue) 98 | } 99 | 100 | async getOwner(address: address, blockTag?: BlockTag): Promise { 101 | return this.contract.identityOwner(address, { blockTag }) 102 | } 103 | 104 | async attachContract(controller?: AddressLike): Promise { 105 | let currentOwner = controller ? await controller : await this.getOwner(this.address, 'latest') 106 | if (typeof currentOwner !== 'string') currentOwner = await (controller as Addressable).getAddress() 107 | let signer 108 | if (this.signer) { 109 | signer = this.signer 110 | } else { 111 | if (!this.contract) throw new Error(`No contract configured`) 112 | if (!this.contract.runner) throw new Error(`No runner configured for contract`) 113 | if (!this.contract.runner.provider) throw new Error(`No provider configured for runner in contract`) 114 | signer = (await (this.contract.runner.provider).getSigner(currentOwner)) || this.contract.signer 115 | } 116 | return this.contract.connect(signer) as Contract // Needed because ethers attach returns a BaseContract 117 | } 118 | 119 | async changeOwner(newOwner: address, options: Overrides = {}): Promise { 120 | // console.log(`changing owner for ${oldOwner} on registry at ${registryContract.address}`) 121 | const overrides = { 122 | gasLimit: 123456, 123 | ...options, 124 | } as Overrides 125 | const contract = await this.attachContract(overrides.from ?? undefined) 126 | delete overrides.from 127 | 128 | const ownerChange = await contract.changeOwner(this.address, newOwner, overrides) 129 | return await ownerChange.wait() 130 | } 131 | 132 | async createChangeOwnerHash(newOwner: address) { 133 | const paddedNonce = await this.getPaddedNonceCompatibility() 134 | 135 | const dataToHash = concat([ 136 | MESSAGE_PREFIX, 137 | await this.contract.getAddress(), 138 | paddedNonce, 139 | this.address, 140 | getBytes(concat([toUtf8Bytes('changeOwner'), newOwner])), 141 | ]) 142 | return keccak256(dataToHash) 143 | } 144 | 145 | async changeOwnerSigned( 146 | newOwner: address, 147 | metaSignature: MetaSignature, 148 | options: Overrides = {} 149 | ): Promise { 150 | const overrides = { 151 | gasLimit: 123456, 152 | ...options, 153 | } 154 | 155 | const contract = await this.attachContract(overrides.from ?? undefined) 156 | delete overrides.from 157 | 158 | const ownerChange = await contract.changeOwnerSigned( 159 | this.address, 160 | metaSignature.sigV, 161 | metaSignature.sigR, 162 | metaSignature.sigS, 163 | newOwner, 164 | overrides 165 | ) 166 | return await ownerChange.wait() 167 | } 168 | 169 | async addDelegate( 170 | delegateType: string, 171 | delegateAddress: address, 172 | exp: number, 173 | options: Overrides = {} 174 | ): Promise { 175 | const overrides = { 176 | gasLimit: 123456, 177 | ...options, 178 | } 179 | const contract = await this.attachContract(overrides.from ?? undefined) 180 | delete overrides.from 181 | 182 | const delegateTypeBytes = stringToBytes32(delegateType) 183 | const addDelegateTx = await contract.addDelegate(this.address, delegateTypeBytes, delegateAddress, exp, overrides) 184 | return await addDelegateTx.wait() 185 | } 186 | 187 | async createAddDelegateHash(delegateType: string, delegateAddress: address, exp: number) { 188 | const paddedNonce = await this.getPaddedNonceCompatibility() 189 | 190 | const dataToHash = concat([ 191 | MESSAGE_PREFIX, 192 | await this.contract.getAddress(), 193 | paddedNonce, 194 | this.address, 195 | concat([ 196 | toUtf8Bytes('addDelegate'), 197 | encodeBytes32String(delegateType), 198 | delegateAddress, 199 | zeroPadValue(toBeHex(exp), 32), 200 | ]), 201 | ]) 202 | return keccak256(dataToHash) 203 | } 204 | 205 | async addDelegateSigned( 206 | delegateType: string, 207 | delegateAddress: address, 208 | exp: number, 209 | metaSignature: MetaSignature, 210 | options: Overrides = {} 211 | ): Promise { 212 | const overrides = { 213 | gasLimit: 123456, 214 | ...options, 215 | } 216 | const contract = await this.attachContract(overrides.from ?? undefined) 217 | delete overrides.from 218 | 219 | const delegateTypeBytes = stringToBytes32(delegateType) 220 | const addDelegateTx = await contract.addDelegateSigned( 221 | this.address, 222 | metaSignature.sigV, 223 | metaSignature.sigR, 224 | metaSignature.sigS, 225 | delegateTypeBytes, 226 | delegateAddress, 227 | exp, 228 | overrides 229 | ) 230 | return await addDelegateTx.wait() 231 | } 232 | 233 | async revokeDelegate( 234 | delegateType: string, 235 | delegateAddress: address, 236 | options: Overrides = {} 237 | ): Promise { 238 | const overrides = { 239 | gasLimit: 123456, 240 | ...options, 241 | } 242 | delegateType = delegateType.startsWith('0x') ? delegateType : stringToBytes32(delegateType) 243 | const contract = await this.attachContract(overrides.from ?? undefined) 244 | delete overrides.from 245 | const addDelegateTx = await contract.revokeDelegate(this.address, delegateType, delegateAddress, overrides) 246 | return await addDelegateTx.wait() 247 | } 248 | 249 | async createRevokeDelegateHash(delegateType: string, delegateAddress: address) { 250 | const paddedNonce = await this.getPaddedNonceCompatibility() 251 | 252 | const dataToHash = concat([ 253 | MESSAGE_PREFIX, 254 | await this.contract.getAddress(), 255 | paddedNonce, 256 | this.address, 257 | getBytes(concat([toUtf8Bytes('revokeDelegate'), encodeBytes32String(delegateType), delegateAddress])), 258 | ]) 259 | return keccak256(dataToHash) 260 | } 261 | 262 | async revokeDelegateSigned( 263 | delegateType: string, 264 | delegateAddress: address, 265 | metaSignature: MetaSignature, 266 | options: Overrides = {} 267 | ): Promise { 268 | const overrides = { 269 | gasLimit: 123456, 270 | ...options, 271 | } 272 | delegateType = delegateType.startsWith('0x') ? delegateType : stringToBytes32(delegateType) 273 | const contract = await this.attachContract(overrides.from ?? undefined) 274 | delete overrides.from 275 | const addDelegateTx = await contract.revokeDelegateSigned( 276 | this.address, 277 | metaSignature.sigV, 278 | metaSignature.sigR, 279 | metaSignature.sigS, 280 | delegateType, 281 | delegateAddress, 282 | overrides 283 | ) 284 | return await addDelegateTx.wait() 285 | } 286 | 287 | async setAttribute( 288 | attrName: string, 289 | attrValue: string, 290 | exp: number, 291 | options: Overrides = {} 292 | ): Promise { 293 | const overrides = { 294 | gasLimit: 123456, 295 | controller: undefined, 296 | ...options, 297 | } 298 | attrName = attrName.startsWith('0x') ? attrName : stringToBytes32(attrName) 299 | attrValue = attrValue.startsWith('0x') ? attrValue : hexlify(toUtf8Bytes(attrValue)) 300 | const contract = await this.attachContract(overrides.from ?? undefined) 301 | delete overrides.from 302 | const setAttrTx = await contract.setAttribute(this.address, attrName, attrValue, exp, overrides) 303 | return await setAttrTx.wait() 304 | } 305 | 306 | async createSetAttributeHash(attrName: string, attrValue: string, exp: number) { 307 | const paddedNonce = await this.getPaddedNonceCompatibility(true) 308 | const encodedValue = this.encodeAttributeValue(attrValue) 309 | const dataToHash = concat([ 310 | MESSAGE_PREFIX, 311 | await this.contract.getAddress(), 312 | paddedNonce, 313 | this.address, 314 | concat([ 315 | toUtf8Bytes('setAttribute'), 316 | encodeBytes32String(attrName), 317 | encodedValue, 318 | zeroPadValue(toBeHex(exp), 32), 319 | ]), 320 | ]) 321 | return keccak256(dataToHash) 322 | } 323 | 324 | async setAttributeSigned( 325 | attrName: string, 326 | attrValue: string, 327 | exp: number, 328 | metaSignature: MetaSignature, 329 | options: Overrides = {} 330 | ): Promise { 331 | const overrides = { 332 | gasLimit: 123456, 333 | controller: undefined, 334 | ...options, 335 | } 336 | attrName = attrName.startsWith('0x') ? attrName : stringToBytes32(attrName) 337 | attrValue = attrValue.startsWith('0x') ? attrValue : hexlify(toUtf8Bytes(attrValue)) 338 | const contract = await this.attachContract(overrides.from ?? undefined) 339 | delete overrides.from 340 | const setAttrTx = await contract.setAttributeSigned( 341 | this.address, 342 | metaSignature.sigV, 343 | metaSignature.sigR, 344 | metaSignature.sigS, 345 | attrName, 346 | attrValue, 347 | exp, 348 | overrides 349 | ) 350 | return await setAttrTx.wait() 351 | } 352 | 353 | async revokeAttribute(attrName: string, attrValue: string, options: Overrides = {}): Promise { 354 | // console.log(`revoking attribute ${attrName}(${attrValue}) for ${identity}`) 355 | const overrides = { 356 | gasLimit: 123456, 357 | ...options, 358 | } 359 | attrName = attrName.startsWith('0x') ? attrName : stringToBytes32(attrName) 360 | attrValue = attrValue.startsWith('0x') ? attrValue : hexlify(toUtf8Bytes(attrValue)) 361 | const contract = await this.attachContract(overrides.from ?? undefined) 362 | delete overrides.from 363 | const revokeAttributeTX = await contract.revokeAttribute(this.address, attrName, attrValue, overrides) 364 | return await revokeAttributeTX.wait() 365 | } 366 | 367 | async createRevokeAttributeHash(attrName: string, attrValue: string) { 368 | const paddedNonce = await this.getPaddedNonceCompatibility(true) 369 | const encodedValue = this.encodeAttributeValue(attrValue) 370 | const dataToHash = concat([ 371 | MESSAGE_PREFIX, 372 | await this.contract.getAddress(), 373 | paddedNonce, 374 | this.address, 375 | getBytes(concat([toUtf8Bytes('revokeAttribute'), encodeBytes32String(attrName), encodedValue])), 376 | ]) 377 | return keccak256(dataToHash) 378 | } 379 | 380 | /** 381 | * The legacy version of the ethr-did-registry contract tracks the nonce as a property of the original owner, and not 382 | * as a property of the signer (current owner). That's why we need to differentiate between deployments here, or 383 | * otherwise our signature will be computed wrong resulting in a failed TX. 384 | * 385 | * Not only that, but the nonce is loaded differently for [set/revoke]AttributeSigned methods. 386 | */ 387 | private async getPaddedNonceCompatibility(attribute = false) { 388 | let nonceKey 389 | if (this.legacyNonce && attribute) { 390 | nonceKey = this.address 391 | } else { 392 | nonceKey = await this.getOwner(this.address) 393 | } 394 | return zeroPadValue(toBeHex(await this.contract.nonce(nonceKey)), 32) 395 | } 396 | 397 | async revokeAttributeSigned( 398 | attrName: string, 399 | attrValue: string, 400 | metaSignature: MetaSignature, 401 | options: Overrides = {} 402 | ): Promise { 403 | // console.log(`revoking attribute ${attrName}(${attrValue}) for ${identity}`) 404 | const overrides = { 405 | gasLimit: 123456, 406 | ...options, 407 | } 408 | attrName = attrName.startsWith('0x') ? attrName : stringToBytes32(attrName) 409 | attrValue = attrValue.startsWith('0x') ? attrValue : hexlify(toUtf8Bytes(attrValue)) 410 | const contract = await this.attachContract(overrides.from ?? undefined) 411 | delete overrides.from 412 | const revokeAttributeTX = await contract.revokeAttributeSigned( 413 | this.address, 414 | metaSignature.sigV, 415 | metaSignature.sigR, 416 | metaSignature.sigS, 417 | attrName, 418 | attrValue, 419 | overrides 420 | ) 421 | return await revokeAttributeTX.wait() 422 | } 423 | } 424 | -------------------------------------------------------------------------------- /src/helpers.ts: -------------------------------------------------------------------------------- 1 | import { VerificationMethod } from 'did-resolver' 2 | import { computeAddress, getAddress, toUtf8Bytes, toUtf8String, zeroPadBytes } from 'ethers' 3 | 4 | export const identifierMatcher = /^(.*)?(0x[0-9a-fA-F]{40}|0x[0-9a-fA-F]{66})$/ 5 | export const nullAddress = '0x0000000000000000000000000000000000000000' 6 | export const DEFAULT_REGISTRY_ADDRESS = '0xdca7ef03e98e0dc2b855be647c39abe984fcf21b' 7 | export const DEFAULT_JSON_RPC = 'http://127.0.0.1:8545/' 8 | export const MESSAGE_PREFIX = '0x1900' 9 | 10 | export type address = string 11 | export type uint256 = bigint 12 | export type bytes32 = string 13 | export type bytes = string 14 | 15 | export interface ERC1056Event { 16 | identity: address 17 | previousChange: uint256 18 | validTo?: bigint 19 | _eventName: string 20 | blockNumber: number 21 | } 22 | 23 | export interface DIDOwnerChanged extends ERC1056Event { 24 | owner: address 25 | } 26 | 27 | export interface DIDAttributeChanged extends ERC1056Event { 28 | name: bytes32 29 | value: bytes 30 | validTo: uint256 31 | } 32 | 33 | export interface DIDDelegateChanged extends ERC1056Event { 34 | delegateType: bytes32 35 | delegate: address 36 | validTo: uint256 37 | } 38 | 39 | export enum verificationMethodTypes { 40 | EcdsaSecp256k1VerificationKey2019 = 'EcdsaSecp256k1VerificationKey2019', 41 | EcdsaSecp256k1RecoveryMethod2020 = 'EcdsaSecp256k1RecoveryMethod2020', 42 | Ed25519VerificationKey2018 = 'Ed25519VerificationKey2018', 43 | RSAVerificationKey2018 = 'RSAVerificationKey2018', 44 | X25519KeyAgreementKey2019 = 'X25519KeyAgreementKey2019', 45 | } 46 | 47 | export enum eventNames { 48 | DIDOwnerChanged = 'DIDOwnerChanged', 49 | DIDAttributeChanged = 'DIDAttributeChanged', 50 | DIDDelegateChanged = 'DIDDelegateChanged', 51 | } 52 | 53 | export interface LegacyVerificationMethod extends VerificationMethod { 54 | /**@deprecated */ 55 | publicKeyHex?: string 56 | /**@deprecated */ 57 | publicKeyBase64?: string 58 | /**@deprecated */ 59 | publicKeyPem?: string 60 | 61 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 62 | [x: string]: any 63 | } 64 | 65 | /** 66 | * Interface for transporting v, r, s signature parameters used in meta transactions 67 | */ 68 | export interface MetaSignature { 69 | sigV: number 70 | sigR: bytes32 71 | sigS: bytes32 72 | } 73 | 74 | export const legacyAttrTypes: Record = { 75 | sigAuth: 'SignatureAuthentication2018', 76 | veriKey: 'VerificationKey2018', 77 | enc: 'KeyAgreementKey2019', 78 | } 79 | 80 | export const legacyAlgoMap: Record = { 81 | /**@deprecated */ 82 | Secp256k1VerificationKey2018: verificationMethodTypes.EcdsaSecp256k1VerificationKey2019, 83 | /**@deprecated */ 84 | Ed25519SignatureAuthentication2018: verificationMethodTypes.Ed25519VerificationKey2018, 85 | /**@deprecated */ 86 | Secp256k1SignatureAuthentication2018: verificationMethodTypes.EcdsaSecp256k1VerificationKey2019, 87 | //keep legacy mapping 88 | RSAVerificationKey2018: verificationMethodTypes.RSAVerificationKey2018, 89 | Ed25519VerificationKey2018: verificationMethodTypes.Ed25519VerificationKey2018, 90 | X25519KeyAgreementKey2019: verificationMethodTypes.X25519KeyAgreementKey2019, 91 | } 92 | 93 | export function strip0x(input: string): string { 94 | return input.startsWith('0x') ? input.slice(2) : input 95 | } 96 | 97 | export function bytes32toString(input: bytes32 | Uint8Array): string { 98 | return toUtf8String(input).replace(/\0+$/, '') 99 | } 100 | 101 | export function stringToBytes32(str: string): string { 102 | const bytes = toUtf8Bytes(str) 103 | return zeroPadBytes(bytes.slice(0, 32), 32) 104 | } 105 | 106 | export function interpretIdentifier(identifier: string): { address: string; publicKey?: string; network?: string } { 107 | let id = identifier 108 | let network = undefined 109 | if (id.startsWith('did:ethr')) { 110 | id = id.split('?')[0] 111 | const components = id.split(':') 112 | id = components[components.length - 1] 113 | if (components.length >= 4) { 114 | network = components.splice(2, components.length - 3).join(':') 115 | } 116 | } 117 | if (id.length > 42) { 118 | return { address: computeAddress(id), publicKey: id, network } 119 | } else { 120 | return { address: getAddress(id), network } // checksum address 121 | } 122 | } 123 | 124 | export enum Errors { 125 | /** 126 | * The resolver has failed to construct the DID document. 127 | * This can be caused by a network issue, a wrong registry address or malformed logs while parsing the registry 128 | * history. Please inspect the `DIDResolutionMetadata.message` to debug further. 129 | */ 130 | notFound = 'notFound', 131 | 132 | /** 133 | * The resolver does not know how to resolve the given DID. Most likely it is not a `did:ethr`. 134 | */ 135 | invalidDid = 'invalidDid', 136 | 137 | /** 138 | * The resolver is misconfigured or is being asked to resolve a `DID` anchored on an unknown network 139 | */ 140 | unknownNetwork = 'unknownNetwork', 141 | 142 | /** 143 | * The resolver does not support the 'accept' format requested with `DIDResolutionOptions` 144 | */ 145 | unsupportedFormat = 'unsupportedFormat', 146 | } 147 | 148 | /** 149 | * Returns true when the argument is defined and not null. 150 | * Usable as array.filter(isDefined) 151 | * @param arg 152 | */ 153 | export function isDefined(arg: T): arg is Exclude { 154 | return arg !== null && typeof arg !== 'undefined' 155 | } 156 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { getResolver } from './resolver.js' 2 | import { EthrDidController } from './controller.js' 3 | import { 4 | bytes32toString, 5 | DEFAULT_REGISTRY_ADDRESS, 6 | Errors, 7 | identifierMatcher, 8 | interpretIdentifier, 9 | legacyAlgoMap, 10 | legacyAttrTypes, 11 | stringToBytes32, 12 | verificationMethodTypes, 13 | MetaSignature, 14 | } from './helpers.js' 15 | 16 | import { EthereumDIDRegistry } from './config/EthereumDIDRegistry.js' 17 | import { deployments, EthrDidRegistryDeployment } from './config/deployments.js' 18 | 19 | export { 20 | DEFAULT_REGISTRY_ADDRESS as REGISTRY, 21 | getResolver, 22 | bytes32toString, 23 | stringToBytes32, 24 | EthrDidController, 25 | /**@deprecated */ 26 | legacyAlgoMap as delegateTypes, 27 | /**@deprecated */ 28 | legacyAttrTypes as attrTypes, 29 | verificationMethodTypes, 30 | identifierMatcher, 31 | interpretIdentifier, 32 | Errors, 33 | EthereumDIDRegistry, 34 | MetaSignature, 35 | deployments, 36 | EthrDidRegistryDeployment, 37 | } 38 | 39 | // workaround for esbuild/vite/hermes issues 40 | // This should not be needed once we move to ESM only build outputs. 41 | // This library now builds as a CommonJS library, with a small ESM wrapper on top. 42 | // This pattern seems to confuse some bundlers, causing errors like `Cannot read 'getResolver' of undefined` 43 | // see https://github.com/decentralized-identity/ethr-did-resolver/issues/186 44 | export default { 45 | REGISTRY: DEFAULT_REGISTRY_ADDRESS, 46 | getResolver, 47 | bytes32toString, 48 | stringToBytes32, 49 | EthrDidController, 50 | verificationMethodTypes, 51 | identifierMatcher, 52 | interpretIdentifier, 53 | Errors, 54 | EthereumDIDRegistry, 55 | deployments, 56 | } 57 | -------------------------------------------------------------------------------- /src/logParser.ts: -------------------------------------------------------------------------------- 1 | import { Contract, Log, LogDescription } from 'ethers' 2 | import { bytes32toString, ERC1056Event, isDefined } from './helpers.js' 3 | 4 | function populateEventMetaClass(logResult: LogDescription, blockNumber: number): ERC1056Event { 5 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 6 | const result: Record = {} 7 | if (logResult.fragment.inputs.length !== logResult.args.length) { 8 | throw new TypeError('malformed event input. wrong number of arguments') 9 | } 10 | logResult.fragment.inputs.forEach((input, index) => { 11 | let val = logResult.args[index] 12 | if (typeof val === 'object') { 13 | val = BigInt(val) 14 | } 15 | if (input.type === 'bytes32') { 16 | val = bytes32toString(val) 17 | } 18 | result[input.name] = val 19 | }) 20 | result._eventName = logResult.name 21 | result.blockNumber = blockNumber 22 | return result as ERC1056Event 23 | } 24 | 25 | export function logDecoder(contract: Contract, logs: Log[]): ERC1056Event[] { 26 | return logs 27 | .map((log: Log) => { 28 | const res = contract.interface.parseLog({ topics: [...log.topics], data: log.data }) 29 | if (!res) return null 30 | return populateEventMetaClass(res, log.blockNumber) 31 | }) 32 | .filter(isDefined) 33 | } 34 | -------------------------------------------------------------------------------- /src/resolver.ts: -------------------------------------------------------------------------------- 1 | import { BlockTag, encodeBase58, encodeBase64, toUtf8String } from 'ethers' 2 | import { ConfigurationOptions, ConfiguredNetworks, configureResolverWithNetworks } from './configuration.js' 3 | import { 4 | DIDDocument, 5 | DIDResolutionOptions, 6 | DIDResolutionResult, 7 | DIDResolver, 8 | ParsedDID, 9 | Resolvable, 10 | Service, 11 | VerificationMethod, 12 | } from 'did-resolver' 13 | import { 14 | DIDAttributeChanged, 15 | DIDDelegateChanged, 16 | DIDOwnerChanged, 17 | ERC1056Event, 18 | Errors, 19 | eventNames, 20 | identifierMatcher, 21 | interpretIdentifier, 22 | legacyAlgoMap, 23 | legacyAttrTypes, 24 | LegacyVerificationMethod, 25 | nullAddress, 26 | strip0x, 27 | verificationMethodTypes, 28 | } from './helpers.js' 29 | import { logDecoder } from './logParser.js' 30 | 31 | export function getResolver(options: ConfigurationOptions): Record { 32 | return new EthrDidResolver(options).build() 33 | } 34 | 35 | export class EthrDidResolver { 36 | private contracts: ConfiguredNetworks 37 | 38 | constructor(options: ConfigurationOptions) { 39 | this.contracts = configureResolverWithNetworks(options) 40 | } 41 | 42 | /** 43 | * Returns the block number with the previous change to a particular address (DID) 44 | * 45 | * @param address - the address (DID) to check for changes 46 | * @param networkId - the EVM network to check 47 | * @param blockTag - the block tag to use for the query (default: 'latest') 48 | */ 49 | async previousChange(address: string, networkId: string, blockTag?: BlockTag): Promise { 50 | return await this.contracts[networkId].changed(address, { blockTag }) 51 | } 52 | 53 | async getBlockMetadata(blockHeight: number, networkId: string): Promise<{ height: string; isoDate: string }> { 54 | const networkContract = this.contracts[networkId] 55 | if (!networkContract) throw new Error(`No contract configured for network ${networkId}`) 56 | if (!networkContract.runner) throw new Error(`No runner configured for contract with network ${networkId}`) 57 | if (!networkContract.runner.provider) 58 | throw new Error(`No provider configured for runner in contract with network ${networkId}`) 59 | const block = await networkContract.runner.provider.getBlock(blockHeight) 60 | if (!block) throw new Error(`Block at height ${blockHeight} not found`) 61 | return { 62 | height: block.number.toString(), 63 | isoDate: new Date(block.timestamp * 1000).toISOString().replace('.000', ''), 64 | } 65 | } 66 | 67 | async changeLog( 68 | identity: string, 69 | networkId: string, 70 | blockTag: BlockTag = 'latest' 71 | ): Promise<{ address: string; history: ERC1056Event[]; controllerKey?: string; chainId: bigint }> { 72 | const contract = this.contracts[networkId] 73 | if (!contract) throw new Error(`No contract configured for network ${networkId}`) 74 | if (!contract.runner) throw new Error(`No runner configured for contract with network ${networkId}`) 75 | if (!contract.runner.provider) 76 | throw new Error(`No provider configured for runner in contract with network ${networkId}`) 77 | const provider = contract.runner.provider 78 | const hexChainId = networkId.startsWith('0x') ? networkId : undefined 79 | //TODO: this can be used to check if the configuration is ok 80 | const chainId = hexChainId ? BigInt(hexChainId) : (await provider.getNetwork()).chainId 81 | const history: ERC1056Event[] = [] 82 | const { address, publicKey } = interpretIdentifier(identity) 83 | const controllerKey = publicKey 84 | let previousChange: bigint | null = await this.previousChange(address, networkId, blockTag) 85 | while (previousChange) { 86 | const blockNumber = previousChange 87 | const logs = await provider.getLogs({ 88 | address: await contract.getAddress(), // networks[networkId].registryAddress, 89 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 90 | topics: [null as any, `0x000000000000000000000000${address.slice(2)}`], 91 | fromBlock: previousChange, 92 | toBlock: previousChange, 93 | }) 94 | const events: ERC1056Event[] = logDecoder(contract, logs) 95 | events.reverse() 96 | previousChange = null 97 | for (const event of events) { 98 | history.unshift(event) 99 | if (event.previousChange < blockNumber) { 100 | previousChange = event.previousChange 101 | } 102 | } 103 | } 104 | return { address, history, controllerKey, chainId } 105 | } 106 | 107 | wrapDidDocument( 108 | did: string, 109 | address: string, 110 | controllerKey: string | undefined, 111 | history: ERC1056Event[], 112 | chainId: bigint, 113 | blockHeight: string | number, 114 | now: bigint 115 | ): { didDocument: DIDDocument; deactivated: boolean; versionId: number; nextVersionId: number } { 116 | const baseDIDDocument: DIDDocument = { 117 | id: did, 118 | verificationMethod: [], 119 | authentication: [], 120 | assertionMethod: [], 121 | } 122 | 123 | let controller = address 124 | 125 | const authentication = [`${did}#controller`] 126 | const assertionMethod = [`${did}#controller`] 127 | 128 | let versionId = 0 129 | let nextVersionId = Number.POSITIVE_INFINITY 130 | let deactivated = false 131 | let delegateCount = 0 132 | let serviceCount = 0 133 | let endpoint = '' 134 | const auth: Record = {} 135 | const keyAgreementRefs: Record = {} 136 | const signingRefs: Record = {} 137 | const pks: Record = {} 138 | const services: Record = {} 139 | if (typeof blockHeight === 'string') { 140 | // latest 141 | blockHeight = -1 142 | } 143 | for (const event of history) { 144 | if (blockHeight !== -1 && event.blockNumber > blockHeight) { 145 | if (nextVersionId > event.blockNumber) { 146 | nextVersionId = event.blockNumber 147 | } 148 | continue 149 | } else { 150 | if (versionId < event.blockNumber) { 151 | versionId = event.blockNumber 152 | } 153 | } 154 | const validTo = event.validTo || BigInt(0) 155 | const eventIndex = `${event._eventName}-${ 156 | (event as DIDDelegateChanged).delegateType || (event as DIDAttributeChanged).name 157 | }-${(event as DIDDelegateChanged).delegate || (event as DIDAttributeChanged).value}` 158 | if (validTo && validTo >= now) { 159 | if (event._eventName === eventNames.DIDDelegateChanged) { 160 | const currentEvent = event as DIDDelegateChanged 161 | delegateCount++ 162 | const delegateType = currentEvent.delegateType //conversion from bytes32 is done in logParser 163 | switch (delegateType) { 164 | case 'sigAuth': 165 | auth[eventIndex] = `${did}#delegate-${delegateCount}` 166 | signingRefs[eventIndex] = `${did}#delegate-${delegateCount}` 167 | // eslint-disable-next-line no-fallthrough 168 | case 'veriKey': 169 | pks[eventIndex] = { 170 | id: `${did}#delegate-${delegateCount}`, 171 | type: verificationMethodTypes.EcdsaSecp256k1RecoveryMethod2020, 172 | controller: did, 173 | blockchainAccountId: `eip155:${chainId}:${currentEvent.delegate}`, 174 | } 175 | signingRefs[eventIndex] = `${did}#delegate-${delegateCount}` 176 | break 177 | } 178 | } else if (event._eventName === eventNames.DIDAttributeChanged) { 179 | const currentEvent = event as DIDAttributeChanged 180 | const name = currentEvent.name //conversion from bytes32 is done in logParser 181 | const match = name.match(/^did\/(pub|svc)\/(\w+)(\/(\w+))?(\/(\w+))?$/) 182 | if (match) { 183 | const section = match[1] 184 | const algorithm = match[2] 185 | const type = legacyAttrTypes[match[4]] || match[4] 186 | const encoding = match[6] 187 | switch (section) { 188 | case 'pub': { 189 | delegateCount++ 190 | const pk: LegacyVerificationMethod = { 191 | id: `${did}#delegate-${delegateCount}`, 192 | type: `${algorithm}${type}`, 193 | controller: did, 194 | } 195 | pk.type = legacyAlgoMap[pk.type] || algorithm 196 | switch (encoding) { 197 | case null: 198 | case undefined: 199 | case 'hex': 200 | pk.publicKeyHex = strip0x(currentEvent.value) 201 | break 202 | case 'base64': 203 | pk.publicKeyBase64 = encodeBase64(currentEvent.value) 204 | break 205 | case 'base58': 206 | pk.publicKeyBase58 = encodeBase58(currentEvent.value) 207 | break 208 | case 'pem': 209 | pk.publicKeyPem = toUtf8String(currentEvent.value) 210 | break 211 | default: 212 | pk.value = strip0x(currentEvent.value) 213 | } 214 | pks[eventIndex] = pk 215 | if (match[4] === 'sigAuth') { 216 | auth[eventIndex] = pk.id 217 | signingRefs[eventIndex] = pk.id 218 | } else if (match[4] === 'enc') { 219 | keyAgreementRefs[eventIndex] = pk.id 220 | } else { 221 | signingRefs[eventIndex] = pk.id 222 | } 223 | break 224 | } 225 | case 'svc': { 226 | serviceCount++ 227 | const encodedService = toUtf8String(currentEvent.value) 228 | try { 229 | endpoint = JSON.parse(encodedService) 230 | } catch { 231 | endpoint = encodedService 232 | } 233 | services[eventIndex] = { 234 | id: `${did}#service-${serviceCount}`, 235 | type: algorithm, 236 | serviceEndpoint: endpoint, 237 | } 238 | break 239 | } 240 | } 241 | } 242 | } 243 | } else if (event._eventName === eventNames.DIDOwnerChanged) { 244 | const currentEvent = event as DIDOwnerChanged 245 | controller = currentEvent.owner 246 | if (currentEvent.owner === nullAddress) { 247 | deactivated = true 248 | break 249 | } 250 | } else { 251 | if ( 252 | event._eventName === eventNames.DIDDelegateChanged || 253 | (event._eventName === eventNames.DIDAttributeChanged && 254 | (event as DIDAttributeChanged).name.match(/^did\/pub\//)) 255 | ) { 256 | delegateCount++ 257 | } else if ( 258 | event._eventName === eventNames.DIDAttributeChanged && 259 | (event as DIDAttributeChanged).name.match(/^did\/svc\//) 260 | ) { 261 | serviceCount++ 262 | } 263 | delete auth[eventIndex] 264 | delete signingRefs[eventIndex] 265 | delete pks[eventIndex] 266 | delete services[eventIndex] 267 | } 268 | } 269 | 270 | const publicKeys: VerificationMethod[] = [ 271 | { 272 | id: `${did}#controller`, 273 | type: verificationMethodTypes.EcdsaSecp256k1RecoveryMethod2020, 274 | controller: did, 275 | blockchainAccountId: `eip155:${chainId}:${controller}`, 276 | }, 277 | ] 278 | 279 | if (controllerKey && controller == address) { 280 | publicKeys.push({ 281 | id: `${did}#controllerKey`, 282 | type: verificationMethodTypes.EcdsaSecp256k1VerificationKey2019, 283 | controller: did, 284 | publicKeyHex: strip0x(controllerKey), 285 | }) 286 | authentication.push(`${did}#controllerKey`) 287 | assertionMethod.push(`${did}#controllerKey`) 288 | } 289 | 290 | const didDocument: DIDDocument = { 291 | ...baseDIDDocument, 292 | verificationMethod: publicKeys.concat(Object.values(pks)), 293 | authentication: authentication.concat(Object.values(auth)), 294 | assertionMethod: assertionMethod.concat(Object.values(signingRefs)), 295 | } 296 | if (Object.values(services).length > 0) { 297 | didDocument.service = Object.values(services) 298 | } 299 | if (Object.values(keyAgreementRefs).length > 0) { 300 | didDocument.keyAgreement = Object.values(keyAgreementRefs) 301 | } 302 | 303 | return deactivated 304 | ? { 305 | didDocument: baseDIDDocument, 306 | deactivated, 307 | versionId, 308 | nextVersionId, 309 | } 310 | : { didDocument, deactivated, versionId, nextVersionId } 311 | } 312 | 313 | async resolve( 314 | did: string, 315 | parsed: ParsedDID, 316 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 317 | _unused: Resolvable, 318 | options: DIDResolutionOptions 319 | ): Promise { 320 | let ldContext = {} 321 | if (options.accept === 'application/did+json') { 322 | ldContext = {} 323 | } else if (options.accept === 'application/did+ld+json' || typeof options.accept !== 'string') { 324 | ldContext = { 325 | '@context': [ 326 | 'https://www.w3.org/ns/did/v1', 327 | 328 | // defines EcdsaSecp256k1RecoveryMethod2020 & blockchainAccountId 329 | 'https://w3id.org/security/suites/secp256k1recovery-2020/v2', 330 | 331 | // defines publicKeyHex & EcdsaSecp256k1VerificationKey2019; v2 does not define publicKeyHex 332 | 'https://w3id.org/security/v3-unstable', 333 | ], 334 | } 335 | } else { 336 | return { 337 | didResolutionMetadata: { 338 | error: Errors.unsupportedFormat, 339 | message: `The DID resolver does not support the requested 'accept' format: ${options.accept}`, 340 | }, 341 | didDocumentMetadata: {}, 342 | didDocument: null, 343 | } 344 | } 345 | 346 | const fullId = parsed.id.match(identifierMatcher) 347 | if (!fullId) { 348 | return { 349 | didResolutionMetadata: { 350 | error: Errors.invalidDid, 351 | message: `Not a valid did:ethr: ${parsed.id}`, 352 | }, 353 | didDocumentMetadata: {}, 354 | didDocument: null, 355 | } 356 | } 357 | const id = fullId[2] 358 | const networkId = !fullId[1] ? 'mainnet' : fullId[1].slice(0, -1) 359 | let blockTag: string | number = options.blockTag || 'latest' 360 | if (typeof parsed.query === 'string') { 361 | const qParams = new URLSearchParams(parsed.query) 362 | blockTag = qParams.get('versionId') ?? blockTag 363 | const parsedBlockTag = Number.parseInt(blockTag as string) 364 | if (!Number.isNaN(parsedBlockTag)) { 365 | blockTag = parsedBlockTag 366 | } else { 367 | blockTag = 'latest' 368 | } 369 | } 370 | 371 | if (!this.contracts[networkId]) { 372 | return { 373 | didResolutionMetadata: { 374 | error: Errors.unknownNetwork, 375 | message: `The DID resolver does not have a configuration for network: ${networkId}`, 376 | }, 377 | didDocumentMetadata: {}, 378 | didDocument: null, 379 | } 380 | } 381 | 382 | let now = BigInt(Math.floor(new Date().getTime() / 1000)) 383 | 384 | if (typeof blockTag === 'number') { 385 | const block = await this.getBlockMetadata(blockTag, networkId) 386 | now = BigInt(Date.parse(block.isoDate) / 1000) 387 | } else { 388 | // 'latest' 389 | } 390 | 391 | const { address, history, controllerKey, chainId } = await this.changeLog(id, networkId, 'latest') 392 | try { 393 | const { didDocument, deactivated, versionId, nextVersionId } = this.wrapDidDocument( 394 | did, 395 | address, 396 | controllerKey, 397 | history, 398 | chainId, 399 | blockTag, 400 | now 401 | ) 402 | const status = deactivated ? { deactivated: true } : {} 403 | let versionMeta = {} 404 | let versionMetaNext = {} 405 | if (versionId !== 0) { 406 | const block = await this.getBlockMetadata(versionId, networkId) 407 | versionMeta = { 408 | versionId: block.height, 409 | updated: block.isoDate, 410 | } 411 | } 412 | if (nextVersionId !== Number.POSITIVE_INFINITY) { 413 | const block = await this.getBlockMetadata(nextVersionId, networkId) 414 | versionMetaNext = { 415 | nextVersionId: block.height, 416 | nextUpdate: block.isoDate, 417 | } 418 | } 419 | 420 | return { 421 | didDocumentMetadata: { ...status, ...versionMeta, ...versionMetaNext }, 422 | didResolutionMetadata: { contentType: options.accept ?? 'application/did+ld+json' }, 423 | didDocument: { 424 | ...didDocument, 425 | ...ldContext, 426 | }, 427 | } 428 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 429 | } catch (e: any) { 430 | return { 431 | didResolutionMetadata: { 432 | error: Errors.notFound, 433 | message: e.toString(), // This is not in spec, nut may be helpful 434 | }, 435 | didDocumentMetadata: {}, 436 | didDocument: null, 437 | } 438 | } 439 | } 440 | 441 | build(): Record { 442 | return { ethr: this.resolve.bind(this) } 443 | } 444 | } 445 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "CommonJS", 5 | "lib": [ 6 | "dom", 7 | "es6", 8 | "es2015.promise" 9 | ], 10 | "declaration": true, 11 | "declarationMap": true, 12 | "sourceMap": true, 13 | "outDir": "lib", 14 | "strict": true, 15 | "skipLibCheck": true, 16 | "noImplicitThis": false, 17 | "moduleResolution": "node", 18 | "allowSyntheticDefaultImports": true, 19 | "resolveJsonModule": true, 20 | "esModuleInterop": true 21 | }, 22 | "exclude": [ 23 | "**/__tests__/*", 24 | "node_modules", 25 | "lib" 26 | ], 27 | "include": [ 28 | "src" 29 | ] 30 | } 31 | --------------------------------------------------------------------------------