├── .github ├── CODE_OF_CONDUCT.md ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md ├── SECURITY.md ├── dependabot.yml └── workflows │ ├── algoid_image.yml │ ├── assets │ └── utils.sh │ ├── ci.yml │ ├── codeql.yml │ ├── maintenance.yml │ ├── publish.yml │ └── resolver_image.yml ├── .gitignore ├── .golangci.yml ├── .goreleaser.yml ├── DRIVER.md ├── Dockerfile ├── Dockerfile.resolver ├── LICENSE ├── Makefile ├── README.md ├── SPEC.md ├── TUTORIAL.md ├── buf.gen.yaml ├── buf.work.yaml ├── client ├── cli │ ├── cmd │ │ ├── completion.go │ │ ├── completion_bash.go │ │ ├── completion_zsh.go │ │ ├── config.go │ │ ├── config_app_id.go │ │ ├── config_view.go │ │ ├── delete.go │ │ ├── deploy.go │ │ ├── doc.go │ │ ├── edit.go │ │ ├── edit_activate.go │ │ ├── edit_deactivate.go │ │ ├── info.go │ │ ├── key.go │ │ ├── key_add.go │ │ ├── key_remove.go │ │ ├── list.go │ │ ├── proof.go │ │ ├── register.go │ │ ├── resolver.go │ │ ├── retrieve.go │ │ ├── root.go │ │ ├── service.go │ │ ├── service_add.go │ │ ├── service_remove.go │ │ ├── sync.go │ │ ├── ui.go │ │ ├── utils.go │ │ ├── version.go │ │ ├── wallet.go │ │ ├── wallet_connect.go │ │ ├── wallet_create.go │ │ ├── wallet_delete.go │ │ ├── wallet_disconnect.go │ │ ├── wallet_export.go │ │ ├── wallet_info.go │ │ ├── wallet_list.go │ │ ├── wallet_pay.go │ │ ├── wallet_rename.go │ │ └── wallet_restore.go │ ├── doc.go │ └── main.go ├── internal │ ├── contracts.go │ ├── contracts │ │ ├── AlgoDID.abi.json │ │ ├── AlgoDID.approval.teal │ │ ├── AlgoDID.clear.teal │ │ ├── AlgoDID.json │ │ └── AlgoDID.src_map.json │ ├── doc.go │ ├── driver.go │ ├── main.go │ └── utils.go ├── store │ ├── doc.go │ └── local.go └── ui │ ├── api.go │ ├── api_test.go │ ├── app.go │ ├── doc.go │ ├── local-app │ ├── .eslintignore │ ├── .eslintrc.cjs │ ├── .gitignore │ ├── .npmrc │ ├── .prettierignore │ ├── .prettierrc.json │ ├── Makefile │ ├── README.md │ ├── dist │ │ ├── algorand.svg │ │ ├── assets │ │ │ ├── index-64b7c841.js │ │ │ └── index-99bc98b1.css │ │ ├── at_logo_128x128.png │ │ ├── at_logo_192x192.png │ │ ├── at_logo_512x512.png │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── favicon.ico │ │ ├── img │ │ │ ├── beams.jpg │ │ │ └── grid.svg │ │ └── index.html │ ├── home_screen.png │ ├── index.html │ ├── package.json │ ├── pnpm-lock.yaml │ ├── postcss.config.cjs │ ├── public │ │ ├── algorand.svg │ │ ├── at_logo_128x128.png │ │ ├── at_logo_192x192.png │ │ ├── at_logo_512x512.png │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── favicon.ico │ │ └── img │ │ │ ├── beams.jpg │ │ │ └── grid.svg │ ├── src │ │ ├── App.svelte │ │ ├── actions │ │ │ └── outclick.ts │ │ ├── app.css │ │ ├── assets │ │ │ └── .gitkeep │ │ ├── context.ts │ │ ├── lib │ │ │ ├── Alert.svelte │ │ │ ├── Connector.svelte │ │ │ ├── IdentifierDetails.svelte │ │ │ ├── IdentifierList.svelte │ │ │ ├── IdentifierListEntry.svelte │ │ │ ├── ManageAddresses.svelte │ │ │ ├── Modal.svelte │ │ │ ├── NewIdentifier.svelte │ │ │ └── Switch.svelte │ │ ├── main.ts │ │ ├── store.ts │ │ ├── types.ts │ │ └── vite-env.d.ts │ ├── svelte.config.js │ ├── tailwind.config.cjs │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts │ ├── provider.go │ └── utils.go ├── go.mod ├── go.sum ├── info ├── doc.go └── version.go ├── proto ├── buf.lock ├── buf.yaml └── did │ └── v1 │ ├── agent_api.pb.go │ ├── agent_api.pb.gw.go │ ├── agent_api.proto │ ├── agent_api.swagger.json │ ├── agent_api_grpc.pb.go │ ├── doc.go │ ├── image.bin │ └── ticket.go ├── resolver-config.yaml ├── sample-config.yaml ├── sample-monitor.yml └── scripts └── install.sh /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | [info@bryk.io](mailto:info@bryk.io?subject=[GitHub]%20CoC%20Report). 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), 118 | version 2.0, available at . 119 | 120 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 121 | enforcement ladder](https://github.com/mozilla/diversity). 122 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Environment (please complete the following information):** 27 | - OS: [e.g. MacOS 10.15.6] 28 | - Browser [e.g. chrome, safari] 29 | - Application version [e.g. 0.5.2] 30 | 31 | **Additional context** 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 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 | 22 | **Related Issues (if none, remove this section)** 23 | - #... 24 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Description 2 | 3 | Please explain the changes you made. 4 | 5 | - [ ] A description of the problem you're trying to solve; clearly explaining if you are fixing a known issue or proposing a new feature 6 | - [ ] An overview of the suggested solution 7 | - [ ] If your code changes current behavior, reasons why your solution is better 8 | 9 | ### Checklist 10 | 11 | Before you submit a pull request, please make sure of the following. 12 | 13 | - [ ] Make sure you are requesting to **pull a topic/feature/bugfix branch** (right side). Don't request your master! 14 | - [ ] Make sure you are making a pull request against the **canary branch** (left side). Also you should start *your branch* off *our canary* 15 | - [ ] Your code compiles correctly 16 | - [ ] All existing (and new) tests passed 17 | - [ ] Your code should contain tests relevant for the problem you are solving 18 | - [ ] Extend or adjust the documentation, if necessary 19 | - [ ] All commit messages are properly formatted and descriptive 20 | - [ ] Code complies with existing style guides 21 | -------------------------------------------------------------------------------- /.github/SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | Please report to us any security issues you find. 4 | 5 | All security bugs should be reported by email to [info@aid.technology](mailto:info@aid.technology?subject=[GitHub]%20Security%20Report). 6 | This mail is delivered to a small security team. Your email will be acknowledged 7 | within 24 hours, and you'll receive a more detailed response to your email within 8 | 72 hours indicating the next steps in handling your report. 9 | 10 | After the initial reply to your report, the security team will endeavor to keep you 11 | informed of the progress being made towards a fix and full announcement. These updates 12 | will be sent at least every five days. In reality, this is more likely to be every 13 | 24-48 hours. 14 | 15 | ## Responsible Disclosure Program 16 | 17 | This project uses the following disclosure process: 18 | 19 | 1. Once the security report is received it is assigned a primary handler. This person 20 | coordinates the fix and release process. 21 | 22 | 2. The issue is confirmed and a list of affected software is determined. 23 | 24 | 3. Code is audited to find any potential similar problems. 25 | 26 | 4. If it is determined, in consultation with the submitter, that a CVE-ID is required, 27 | the primary handler will obtain one. 28 | 29 | 5. Fixes are prepared for, at least, the two most recent major releases and the head/master 30 | revision. 31 | 32 | This process can take some time, especially when coordination is required with maintainers 33 | of other projects. Every effort will be made to handle the bug in as timely a manner as possible, 34 | however it's important that we follow the process described above to ensure that disclosures 35 | are handled consistently and responsibly. 36 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # Go dependencies 4 | - package-ecosystem: "gomod" 5 | # Where to look for the go.mod file 6 | directory: "/" 7 | # Use '0' to disable the opening of pull requests 8 | open-pull-requests-limit: 5 9 | # Add labels to pull requests 10 | labels: 11 | - "dependencies" 12 | schedule: 13 | # how often to look for updates 14 | interval: "monthly" 15 | # what day to use for opening new requests 16 | day: "monday" 17 | # check for npm updates at 0hrs UTC 18 | time: "00:00" 19 | # Only manage direct dependencies 20 | allow: 21 | - dependency-type: "direct" 22 | # Ignore specific dependencies 23 | ignore: 24 | # Autogenerated proto files 25 | - dependency-name: "google.golang.org/genproto" 26 | # Currently using our own fork at "github.com/bryk-io/cfssl" 27 | - dependency-name: "github.com/cloudflare/cfssl" 28 | # Avoid breaking changes with gRPC gateway 29 | - dependency-name: "github.com/grpc-ecosystem/grpc-gateway/v2" 30 | # Avoid breaking changes with gRPC 31 | - dependency-name: "google.golang.org/grpc" 32 | versions: ["v1.x"] 33 | # Avoid breaking changes with unstable OTEL packages 34 | - dependency-name: "go.opentelemetry.io/*" 35 | versions: ["v1.x"] 36 | # Configure commit messages 37 | commit-message: 38 | # Prefix all commit messages with "dependencies" 39 | prefix: "dependencies" 40 | # Github Actions 41 | - package-ecosystem: "github-actions" 42 | directory: "/" 43 | schedule: 44 | interval: "monthly" 45 | ignore: 46 | - dependency-name: "bufbuild/buf-setup-action" 47 | - dependency-name: "bufbuild/buf-lint-action" 48 | - dependency-name: "bufbuild/buf-breaking-action" 49 | - dependency-name: "trufflesecurity/trufflehog" 50 | - dependency-name: "sonatype-nexus-community/nancy-github-action" 51 | -------------------------------------------------------------------------------- /.github/workflows/algoid_image.yml: -------------------------------------------------------------------------------- 1 | # 2 | name: algoid_image 3 | 4 | # Configures this workflow to run every time a change is pushed to the branch called `release`. 5 | on: 6 | push: 7 | branches: ["main"] 8 | tags: 9 | - "*" 10 | 11 | # Defines two custom environment variables for the workflow. These are used for the Container registry domain, and a name for the Docker image that this workflow builds. 12 | env: 13 | REGISTRY: ghcr.io 14 | IMAGE_NAME: ${{ github.repository_owner }}/algoid 15 | 16 | # There is a single job in this workflow. It's configured to run on the latest available version of Ubuntu. 17 | jobs: 18 | build-and-push-image: 19 | runs-on: ubuntu-latest 20 | # Sets the permissions granted to the `GITHUB_TOKEN` for the actions in this job. 21 | permissions: 22 | contents: read 23 | packages: write 24 | # 25 | steps: 26 | - name: Checkout repository 27 | uses: actions/checkout@v4 28 | 29 | # Uses the `docker/login-action` action to log in to the Container registry registry using the account and password that will publish the packages. Once published, the packages are scoped to the account defined here. 30 | - name: Log in to the Container registry 31 | uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1 32 | with: 33 | registry: ${{ env.REGISTRY }} 34 | username: ${{ github.actor }} 35 | password: ${{ secrets.GITHUB_TOKEN }} 36 | 37 | # This step uses [docker/metadata-action](https://github.com/docker/metadata-action#about) to extract tags and labels that will be applied to the specified image. The `id` "meta" allows the output of this step to be referenced in a subsequent step. The `images` value provides the base name for the tags and labels. 38 | - name: Extract metadata (tags, labels) for Docker 39 | id: meta 40 | uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7 41 | with: 42 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 43 | 44 | # Go 45 | - name: Set up Go 46 | uses: actions/setup-go@v4 47 | with: 48 | go-version: 1.21.x 49 | 50 | - name: Build 51 | run: make build 52 | 53 | # This step uses the `docker/build-push-action` action to build the image, based on your repository's `Dockerfile`. If the build succeeds, it pushes the image to GitHub Packages. 54 | # It uses the `context` parameter to define the build's context as the set of files located in the specified path. For more information, see "[Usage](https://github.com/docker/build-push-action#usage)" in the README of the `docker/build-push-action` repository. 55 | # It uses the `tags` and `labels` parameters to tag and label the image with the output from the "meta" step. 56 | - name: Build and push Docker image 57 | uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4 58 | with: 59 | context: . 60 | push: true 61 | tags: ${{ steps.meta.outputs.tags }} 62 | labels: ${{ steps.meta.outputs.labels }} 63 | -------------------------------------------------------------------------------- /.github/workflows/assets/utils.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Setup runner environment 4 | setup() { 5 | # Configure git to access private Go modules using the 6 | # provided personal access token. 7 | # https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token 8 | git config --global \ 9 | url."https://${GITHUB_USER}:${ACCESS_TOKEN}@github.com".insteadOf "https://github.com" 10 | } 11 | 12 | case $1 in 13 | "setup") 14 | setup 15 | ;; 16 | 17 | *) 18 | echo "Invalid target: $1" 19 | ;; 20 | esac 21 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | env: 3 | commit_msg: "" 4 | on: 5 | push: 6 | branches: 7 | - main 8 | pull_request: 9 | branches: 10 | - main 11 | workflow_dispatch: {} 12 | # Inputs are available under: github.event.inputs.{name} 13 | # inputs: 14 | # name: 15 | # description: 'Variable description' 16 | # required: true 17 | # default: 'default value here' 18 | # https://docs.github.com/en/actions/reference/events-that-trigger-workflows#workflow_dispatch 19 | jobs: 20 | # Scan direct Go dependencies for known vulnerabilities 21 | scan: 22 | name: scan for vulnerabilities 23 | runs-on: ubuntu-latest 24 | steps: 25 | # Checkout code 26 | - name: Checkout repository 27 | uses: actions/checkout@v4 28 | 29 | # Configure runner environment 30 | - name: Set up runner environment 31 | run: ./.github/workflows/assets/utils.sh setup 32 | env: 33 | GITHUB_USER: ${{ github.actor }} 34 | ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }} 35 | 36 | # Go 37 | - name: Set up Go 38 | uses: actions/setup-go@v4 39 | with: 40 | go-version: 1.21.x 41 | 42 | # Get commit message 43 | - name: Get commit message 44 | run: | 45 | echo 'commit_msg<> $GITHUB_ENV 46 | git log --format=%B -n 1 ${{ github.sha }} >> $GITHUB_ENV 47 | echo 'EOF' >> $GITHUB_ENV 48 | 49 | # List direct dependencies 50 | - name: List dependencies 51 | run: go list -mod=readonly -f '{{if not .Indirect}}{{.}}{{end}}' -m all > go.list 52 | 53 | # Scan dependencies using Nancy 54 | # Can be excluded if the commit message contains: [scan-deps skip] 55 | # https://github.com/sonatype-nexus-community/nancy-github-action 56 | - name: Scan dependencies 57 | id: scan-deps 58 | if: ${{ !contains(env.commit_msg, '[scan-deps skip]') }} 59 | uses: sonatype-nexus-community/nancy-github-action@v1.0.2 60 | 61 | # Validate the protocol buffer definitions on the project 62 | # using 'buf'. Remove if not required. 63 | protos: 64 | name: validate protobuf definitions 65 | needs: scan 66 | runs-on: ubuntu-latest 67 | steps: 68 | # Checkout code 69 | - name: Checkout repository 70 | uses: actions/checkout@v4 71 | 72 | # Configure runner environment 73 | - name: Set up runner environment 74 | run: ./.github/workflows/assets/utils.sh setup 75 | env: 76 | GITHUB_USER: ${{ github.actor }} 77 | ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }} 78 | 79 | # Go 80 | - name: Set up Go 81 | uses: actions/setup-go@v4 82 | with: 83 | go-version: 1.21.x 84 | 85 | # Get commit message 86 | - name: Get commit message 87 | run: | 88 | echo 'commit_msg<> $GITHUB_ENV 89 | git log --format=%B -n 1 ${{ github.sha }} >> $GITHUB_ENV 90 | echo 'EOF' >> $GITHUB_ENV 91 | 92 | # Setup buf 93 | - name: Setup buf 94 | id: buf-setup 95 | uses: bufbuild/buf-setup-action@v1.29.0 96 | with: 97 | version: 1.29.0 98 | github_token: ${{ github.token }} 99 | 100 | # Static analysis 101 | - name: Static analysis 102 | id: buf-lint 103 | uses: bufbuild/buf-lint-action@v1.0.3 104 | if: ${{ steps.buf-setup.outcome == 'success' }} 105 | 106 | # Detect breaking changes 107 | - name: Detect breaking changes 108 | id: buf-breaking 109 | uses: bufbuild/buf-breaking-action@v1.1.2 110 | if: steps.buf-lint.outcome == 'success' && !contains(env.commit_msg, '[buf-breaking skip]') 111 | with: 112 | against: "https://github.com/${{ github.repository }}.git#branch=${{ github.event.repository.default_branch }}" 113 | env: 114 | BUF_INPUT_HTTPS_USERNAME: ${{ github.actor }} 115 | BUF_INPUT_HTTPS_PASSWORD: ${{ secrets.ACCESS_TOKEN }} 116 | 117 | # Runs on every push and pull request on the selected branches. 118 | # Can also be executed manually. 119 | test: 120 | name: code quality and correctness 121 | needs: protos 122 | strategy: 123 | matrix: 124 | os: [ubuntu-latest] 125 | runs-on: ${{ matrix.os }} 126 | timeout-minutes: 15 127 | steps: 128 | # Checkout code 129 | - name: Checkout repository 130 | uses: actions/checkout@v4 131 | 132 | # Configure runner environment 133 | - name: Set up runner environment 134 | run: ./.github/workflows/assets/utils.sh setup 135 | env: 136 | GITHUB_USER: ${{ github.actor }} 137 | ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }} 138 | 139 | # Go 140 | - name: Set up Go 141 | uses: actions/setup-go@v4 142 | with: 143 | go-version: 1.21.x 144 | 145 | # Get commit message 146 | - name: Get commit message 147 | run: | 148 | echo 'commit_msg<> $GITHUB_ENV 149 | git log --format=%B -n 1 ${{ github.sha }} >> $GITHUB_ENV 150 | echo 'EOF' >> $GITHUB_ENV 151 | 152 | # Style consistency and static analysis using 'golangci-lint' 153 | # https://github.com/marketplace/actions/run-golangci-lint 154 | - name: Static analysis 155 | uses: golangci/golangci-lint-action@v3 156 | with: 157 | version: v1.54.2 158 | 159 | # Run algokit localnet for tests 160 | - name: Algokit localnet 161 | run: pipx install algokit && algokit localnet start 162 | 163 | # Run unit tests 164 | - name: Test 165 | run: make test 166 | 167 | # Ensure project compile and build successfully 168 | - name: Build 169 | run: make build-for os=linux arch=amd64 170 | 171 | # Save artifacts 172 | - name: Save artifacts 173 | uses: actions/upload-artifact@v3 174 | with: 175 | name: assets 176 | path: | 177 | coverage.html 178 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "codeQL" 2 | on: 3 | push: 4 | branches: 5 | - '*' 6 | pull_request: 7 | branches: 8 | - main 9 | workflow_dispatch: {} 10 | jobs: 11 | # Semantic code analysis to discover vulnerabilities in the codebase 12 | # using GitHub's CodeQL. 13 | # https://codeql.github.com/docs/ 14 | analyze: 15 | name: analyze 16 | runs-on: ubuntu-latest 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | language: ['go'] 21 | steps: 22 | # Checkout code 23 | - name: Checkout repository 24 | uses: actions/checkout@v4 25 | 26 | # Prepare runner environment 27 | - name: Set up runner environment 28 | run: ./.github/workflows/assets/utils.sh setup 29 | env: 30 | GITHUB_USER: ${{ github.actor }} 31 | ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }} 32 | 33 | # Initializes the CodeQL tools for scanning. 34 | - name: Initialize CodeQL 35 | uses: github/codeql-action/init@v2 36 | with: 37 | languages: ${{ matrix.language }} 38 | # If you wish to specify custom queries, you can do so here or in a config file. 39 | # By default, queries listed here will override any specified in a config file. 40 | # Prefix the list here with "+" to use these queries and those in the config file. 41 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 42 | 43 | # Auto build attempts to build any compiled languages (C/C++, C#, or Java). 44 | - name: Auto build 45 | uses: github/codeql-action/autobuild@v2 46 | 47 | # Run manual build only if auto-build fails 48 | - name: Manual build 49 | if: ${{ failure() }} 50 | run: | 51 | make bootstrap 52 | make release 53 | 54 | # Run analysis 55 | - name: Perform CodeQL analysis 56 | uses: github/codeql-action/analyze@v2 57 | -------------------------------------------------------------------------------- /.github/workflows/maintenance.yml: -------------------------------------------------------------------------------- 1 | name: "maintenance" 2 | on: 3 | schedule: 4 | # Run every Monday at 00:00 5 | - cron: "0 0 * * 1" 6 | jobs: 7 | stale: 8 | name: "close stale issues and pull requests" 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/stale@v8 12 | with: 13 | # On the 'debug' mode the action will not perform any operation. 14 | # Add the secret ACTIONS_STEP_DEBUG with a value of 'true' in the repository. 15 | debug-only: false 16 | repo-token: ${{ secrets.GITHUB_TOKEN }} 17 | days-before-stale: 45 18 | days-before-close: 5 19 | stale-issue-label: 'stale' 20 | stale-pr-label: 'stale' 21 | exempt-issue-labels: 'help wanted,awaiting approval,work in progress' 22 | exempt-pr-labels: 'help wanted,awaiting approval,work in progress' 23 | stale-issue-message: 'This issue has been marked as **stale** because it has not registered any activity during the last 45 days. If the **stale** label is not removed or no activity is registered, this will be automatically closed in 5 days.' 24 | close-issue-message: 'This issue has been closed automatically after not registering any activity for 50 consecutive days.' 25 | stale-pr-message: 'This pull request has been marked as **stale** because it has not registered any activity during the last 45 days. If the **stale** label is not removed or no activity is registered, this will be automatically closed in 5 days.' 26 | close-pr-message: 'This pull request has been closed automatically after not registering any activity for 50 consecutive days.' 27 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: publish 2 | on: 3 | push: 4 | tags: 5 | - '*' 6 | jobs: 7 | # Publish project package(s) 8 | publish: 9 | name: publish package 10 | runs-on: ubuntu-latest 11 | timeout-minutes: 20 12 | if: startsWith(github.ref, 'refs/tags/') 13 | steps: 14 | # Checkout code 15 | - name: Checkout repository 16 | uses: actions/checkout@v4 17 | # This is required for the changelog to be properly generated 18 | with: 19 | fetch-depth: 0 20 | 21 | # Prepare runner environment 22 | - name: Set up runner environment 23 | run: ./.github/workflows/assets/utils.sh setup 24 | env: 25 | GITHUB_USER: ${{ github.actor }} 26 | ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }} 27 | 28 | # Go 29 | - name: Set up Go 30 | uses: actions/setup-go@v4 31 | with: 32 | go-version: 1.21.x 33 | 34 | # Use goreleaser to create the new release 35 | # https://github.com/goreleaser/goreleaser-action 36 | - name: Create release 37 | uses: goreleaser/goreleaser-action@v5 38 | if: startsWith(github.ref, 'refs/tags/') 39 | with: 40 | version: latest 41 | args: release --clean --skip=validate 42 | env: 43 | # https://docs.github.com/en/free-pro-team@latest/actions/reference/authentication-in-a-workflow 44 | GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }} 45 | # Login of the user that initiated the workflow run 46 | GITHUB_USER: ${{ github.actor }} 47 | -------------------------------------------------------------------------------- /.github/workflows/resolver_image.yml: -------------------------------------------------------------------------------- 1 | # 2 | name: resolver_image 3 | 4 | # Configures this workflow to run every time a change is pushed to the branch called `main`. 5 | on: 6 | push: 7 | branches: ["main"] 8 | tags: 9 | - "*" 10 | 11 | # Defines two custom environment variables for the workflow. These are used for the Container registry domain, and a name for the Docker image that this workflow builds. 12 | env: 13 | REGISTRY: ghcr.io 14 | IMAGE_NAME: ${{ github.repository }} 15 | 16 | # There is a single job in this workflow. It's configured to run on the latest available version of Ubuntu. 17 | jobs: 18 | build-and-push-image: 19 | runs-on: ubuntu-latest 20 | # Sets the permissions granted to the `GITHUB_TOKEN` for the actions in this job. 21 | permissions: 22 | contents: read 23 | packages: write 24 | # 25 | steps: 26 | - name: Checkout repository 27 | uses: actions/checkout@v4 28 | # Uses the `docker/login-action` action to log in to the Container registry registry using the account and password that will publish the packages. Once published, the packages are scoped to the account defined here. 29 | - name: Log in to the Container registry 30 | uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1 31 | with: 32 | registry: ${{ env.REGISTRY }} 33 | username: ${{ github.actor }} 34 | password: ${{ secrets.GITHUB_TOKEN }} 35 | # This step uses [docker/metadata-action](https://github.com/docker/metadata-action#about) to extract tags and labels that will be applied to the specified image. The `id` "meta" allows the output of this step to be referenced in a subsequent step. The `images` value provides the base name for the tags and labels. 36 | - name: Extract metadata (tags, labels) for Docker 37 | id: meta 38 | uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7 39 | with: 40 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 41 | 42 | # Go 43 | - name: Set up Go 44 | uses: actions/setup-go@v4 45 | with: 46 | go-version: 1.21.x 47 | 48 | - name: Build 49 | run: make build 50 | 51 | # This step uses the `docker/build-push-action` action to build the image, based on your repository's `Dockerfile`. If the build succeeds, it pushes the image to GitHub Packages. 52 | # It uses the `context` parameter to define the build's context as the set of files located in the specified path. For more information, see "[Usage](https://github.com/docker/build-push-action#usage)" in the README of the `docker/build-push-action` repository. 53 | # It uses the `tags` and `labels` parameters to tag and label the image with the output from the "meta" step. 54 | - name: Build and push Docker image 55 | uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4 56 | with: 57 | context: . 58 | file: ./Dockerfile.resolver 59 | push: true 60 | tags: ${{ steps.meta.outputs.tags }} 61 | labels: ${{ steps.meta.outputs.labels }} 62 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .test/ 2 | 3 | # Generals 4 | .DS_Store 5 | .idea 6 | .vscode 7 | *.iml 8 | 9 | # Deps 10 | vendor/** 11 | 12 | # Temporary files 13 | tmp/ 14 | 15 | # Build releases 16 | dist 17 | *.tgz 18 | !client/ui/local-app/dist 19 | 20 | # PKI 21 | *.crt 22 | *.csr 23 | *.cer 24 | *.pem 25 | *.key 26 | !**/testdata/*.cer 27 | !**/testdata/*.pem 28 | 29 | # Secret data 30 | secret* 31 | !helm/**/secret.yaml 32 | 33 | # protobuf / RPC 34 | # *.swagger.json 35 | 36 | # Build 37 | /algoid 38 | /algoid-* 39 | 40 | # Sample config files 41 | config.yaml 42 | config-*.yaml 43 | 44 | # Code coverage reports 45 | coverage.* 46 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | deadline: 5m 3 | issues-exit-code: 1 4 | tests: true 5 | build-tags: [] 6 | skip-dirs-use-default: true 7 | skip-files: 8 | - ".*\\.pb\\.go$" 9 | - ".*\\.pb\\.gw\\.go$" 10 | - ".*\\.validator\\.pb\\.go$" 11 | modules-download-mode: readonly 12 | output: 13 | format: colored-line-number 14 | print-issued-lines: true 15 | print-linter-name: true 16 | linters: 17 | enable: 18 | - errcheck 19 | - gosimple 20 | - govet 21 | - revive 22 | - gofmt 23 | - ineffassign 24 | - staticcheck 25 | - typecheck 26 | - gocyclo 27 | - goconst 28 | - misspell 29 | - lll 30 | - nakedret 31 | - prealloc 32 | - gosec 33 | - bodyclose 34 | - stylecheck 35 | - unparam 36 | - durationcheck 37 | - unconvert 38 | - asciicheck 39 | - errorlint 40 | - exhaustive 41 | - forcetypeassert 42 | - godot 43 | - noctx 44 | - predeclared 45 | - exportloopref 46 | - whitespace 47 | - nestif 48 | - funlen 49 | # Deprecated linters 50 | # - wrapcheck 51 | # - ifshort 52 | # - varcheck 53 | disable: 54 | - deadcode 55 | - unused 56 | - dupl 57 | - depguard 58 | # https://github.com/golangci/golangci-lint/issues/2649 59 | - structcheck 60 | - wastedassign 61 | issues: 62 | exclude-use-default: false 63 | exclude-rules: 64 | - path: _test\.go 65 | linters: 66 | - gocyclo 67 | - errcheck 68 | - dupl 69 | - gosec 70 | - lll 71 | - nakedret 72 | - funlen 73 | - nestif 74 | - noctx 75 | linters-settings: 76 | errcheck: 77 | check-type-assertions: true 78 | check-blank: false 79 | govet: 80 | check-shadowing: false 81 | enable: 82 | - atomic 83 | - atomicalign 84 | - buildtag 85 | - cgocall 86 | - composites 87 | - copylocks 88 | - httpresponse 89 | - loopclosure 90 | - lostcancel 91 | - nilfunc 92 | - shift 93 | - structtag 94 | - unmarshal 95 | - unreachable 96 | - unusedresult 97 | - tests 98 | #- fieldalignment 99 | gofmt: 100 | simplify: true 101 | gocyclo: 102 | min-complexity: 18 103 | goconst: 104 | min-len: 3 105 | min-occurrences: 5 106 | misspell: 107 | locale: US 108 | ignore-words: [] 109 | lll: 110 | line-length: 120 111 | tab-width: 2 112 | nakedret: 113 | max-func-lines: 30 114 | prealloc: 115 | simple: true 116 | range-loops: true 117 | for-loops: false 118 | dupl: 119 | threshold: 150 120 | exhaustive: 121 | default-signifies-exhaustive: true 122 | funlen: 123 | lines: 90 124 | statements: 70 125 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | # main project name value 2 | # https://goreleaser.com/customization/project/ 3 | project_name: did-algo 4 | # produced artifacts are stored in this folder 5 | dist: dist 6 | # customize execution 7 | before: 8 | hooks: 9 | - go mod download 10 | - go generate ./... 11 | - docker login ghcr.io -u {{ .Env.GITHUB_USER }} -p {{ .Env.GITHUB_TOKEN }} 12 | # artifacts to produce 13 | # https://goreleaser.com/customization/build/ 14 | builds: 15 | # a single project can support/require several build targets 16 | - id: did-algo 17 | # main binary name 18 | binary: algoid 19 | # code entrypoint 20 | main: ./client/cli/main.go 21 | # CLI flags for the 'go build' command 22 | flags: 23 | - -v 24 | ldflags: 25 | - -s -w 26 | - -X github.com/algorandfoundation/did-algo/info.CoreVersion={{.Version}} 27 | - -X github.com/algorandfoundation/did-algo/info.BuildCode={{.Commit}} 28 | - -X github.com/algorandfoundation/did-algo/info.BuildTimestamp={{.CommitDate}} 29 | # set the modified timestamp on the output binary to ensure a 30 | # reproducible build 31 | mod_timestamp: "{{ .CommitTimestamp }}" 32 | # disable CGO since it's not supported 33 | env: 34 | - CGO_ENABLED=0 35 | # supported OSs 36 | goos: 37 | - linux 38 | - windows 39 | - darwin 40 | # supported architectures 41 | goarch: 42 | - "386" 43 | - amd64 44 | - arm64 45 | ignore: 46 | - goos: darwin 47 | goarch: "386" 48 | - goos: windows 49 | goarch: arm64 50 | # archives to produce 51 | # https://goreleaser.com/customization/archive/ 52 | archives: 53 | - name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}" 54 | builds: 55 | # artifacts to pack 56 | - did-algo 57 | format_overrides: 58 | # use zip file instead of 'tar.gz' on Windows systems 59 | - goos: windows 60 | format: zip 61 | files: 62 | # include documentation files in the package for distribution 63 | - README.md 64 | - LICENSE* 65 | - src: "*.md" 66 | dst: docs 67 | strip_parent: true 68 | # generate integrity checksums 69 | # https://goreleaser.com/customization/checksum/ 70 | checksum: 71 | name_template: "{{ .ProjectName }}_{{ .Version }}_checksums.txt" 72 | algorithm: sha256 73 | # Include source code package on the release 74 | # https://goreleaser.com/customization/source/ 75 | source: 76 | enabled: true 77 | # produce test releases 78 | # https://goreleaser.com/customization/snapshots/ 79 | snapshot: 80 | name_template: "{{ .Version }}-next" 81 | # build and publish docker images 82 | # https://goreleaser.com/customization/docker/ 83 | dockers: 84 | - # Build IDs to gather the binaries from 85 | ids: 86 | - did-algo 87 | # GOOS of the built binary that should be used 88 | goos: linux 89 | # GOARCH of the built binary that should be used 90 | goarch: amd64 91 | # Dockerfile location 92 | dockerfile: Dockerfile 93 | # OCI image tags 94 | build_flag_templates: 95 | - "--pull" 96 | - "--label=org.opencontainers.image.title={{ .ProjectName }}" 97 | - "--label=org.opencontainers.image.revision={{ .FullCommit }}" 98 | - "--label=org.opencontainers.image.version={{ .Version }}" 99 | - "--label=org.opencontainers.image.created={{ .CommitDate }}" 100 | - "--label=org.opencontainers.image.source=https://github.com/algorandfoundation/{{ .ProjectName }}" 101 | # Registries to push the image to 102 | image_templates: 103 | - "ghcr.io/algorandfoundation/{{ .ProjectName }}:{{ .Version }}" 104 | - "ghcr.io/algorandfoundation/{{ .ProjectName }}:latest" 105 | # Skips the docker push if there is an indicator for prerelease 106 | # in the tag e.g. v1.0.0-rc1 (auto) 107 | skip_push: auto 108 | # Additional files to add/copy into the container image 109 | extra_files: [] 110 | # configure the 'CHANGELOG.md' file produced 111 | # https://goreleaser.com/customization/release/#customize-the-changelog 112 | # linux packages 113 | # https://goreleaser.com/customization/nfpm/ 114 | nfpms: 115 | - file_name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}" 116 | builds: 117 | - did-algo 118 | vendor: Algorand Foundation 119 | homepage: https://github.com/algorandfoundation/did-algo 120 | maintainer: Joe Polny 121 | description: Decentralized Identifiers for the Algorand blockchain 122 | license: BSD-3-Clause 123 | formats: 124 | # Alpine 125 | - apk 126 | # Debian based distributions 127 | - deb 128 | # RedHat based distributions 129 | - rpm 130 | # Binary location 131 | bindir: /usr/local/bin 132 | # Additional files managed by the installer 133 | contents: 134 | - src: sample-config.yaml 135 | dst: /etc/algoid/config.yaml 136 | type: "config|noreplace" 137 | changelog: 138 | # Sorts the changelog commit messages (asc, desc or '') 139 | sort: "" 140 | # Remove certain commit messages from the changelog 141 | filters: 142 | # Standard commit messages can help to produce better changelogs 143 | # https://www.conventionalcommits.org/en/v1.0.0/ 144 | exclude: 145 | - "^docs:" 146 | - "^test:" 147 | - "^chore:" 148 | - "^typo:" 149 | # Produce homebrew formulas for the project artifacts 150 | # https://goreleaser.com/customization/homebrew/ 151 | brews: 152 | - # Formula name 153 | name: algoid 154 | # Push the formula to the tap repository 155 | skip_upload: "true" 156 | # TAP repository 157 | repository: 158 | owner: bryk-io 159 | name: homebrew-tap 160 | # Use 'github-actions' as commit author 161 | # https://github.community/t/github-actions-bot-email-address/17204 162 | commit_author: 163 | name: github-actions 164 | email: 41898282+github-actions[bot]@users.noreply.github.com 165 | # Project details 166 | homepage: "https://github.com/algorandfoundation/did-algo" 167 | description: | 168 | Reference client implementation for the 'algo' DID method. The platform allows 169 | entities to fully manage Decentralized Identifiers as described on the version 170 | v1.0 of the specification. 171 | install: | 172 | bin.install "algoid" 173 | output = Utils.popen_read("#{bin}/algoid completion bash") 174 | (bash_completion/"algoid").write output 175 | output = Utils.popen_read("#{bin}/algoid completion zsh") 176 | (zsh_completion/"algoid").write output 177 | prefix.install_metafiles 178 | test: | 179 | system "#{bin}/algoid version" 180 | -------------------------------------------------------------------------------- /DRIVER.md: -------------------------------------------------------------------------------- 1 | # DIF Universal Resolver Driver 2 | 3 | The driver implementation resolves Decentralized Identifiers (DIDs) for the 4 | `did:algo` method, based on the [W3C DID Core 1.0](https://www.w3.org/TR/did-core/) 5 | and [DID Resolution](https://w3c-ccg.github.io/did-resolution/) specifications. 6 | 7 | ## Driver Interface 8 | 9 | The driver can be invoked via HTTP GET requests of the form: 10 | 11 | `/1.0/identifiers/did:algo:` 12 | 13 | The driver can, optionally, receive an `Accept` header that will affect the result 14 | returned in the HTTP body and the `Content-Type` header. 15 | 16 | If the `Accept` header provided is `application/ld+json;profile="https://w3id.org/did-resolution"` 17 | the resolver with return a DID Resolution Result structure by default with the content type 18 | `application/ld+json;profile="https://w3id.org/did-resolution";charset=utf-8`. This is also the 19 | default behavior when no `Accept` header is provided. 20 | 21 | Request: 22 | 23 | ```shell 24 | curl -X GET 25 | ``` 26 | 27 | Response: 28 | 29 | ```json 30 | { 31 | "@context": [ 32 | "https://www.w3.org/ns/did/v1", 33 | "https://w3id.org/security/suites/ed25519-2020/v1", 34 | "https://w3id.org/security/suites/x25519-2020/v1" 35 | ], 36 | "id": "did:algo:mainnet:app:1845671812:da490f2d15a625459bf970a3d55e1a646dfd3a956d011546e953e945d39fdada", 37 | "verificationMethod": [ 38 | { 39 | "id": "did:algo:mainnet:app:1845671812:da490f2d15a625459bf970a3d55e1a646dfd3a956d011546e953e945d39fdada#master", 40 | "type": "Ed25519VerificationKey2020", 41 | "controller": "did:algo:mainnet:app:1845671812:da490f2d15a625459bf970a3d55e1a646dfd3a956d011546e953e945d39fdada", 42 | "publicKeyMultibase": "zFh6VYTmcxGD2vTNMSTHhtMSa7TkGCded9ofBX5C6CxYq" 43 | } 44 | ], 45 | "authentication": [ 46 | "did:algo:mainnet:app:1845671812:da490f2d15a625459bf970a3d55e1a646dfd3a956d011546e953e945d39fdada#master" 47 | ] 48 | } 49 | ``` 50 | 51 | The following `Accept` values will instruct the resolver to return a DID Document with 52 | `Content-Type` set as `application/did+ld+json;charset=utf-8`. 53 | 54 | - `application/json` 55 | - `application/ld+json` 56 | - `application/did+ld+json` 57 | 58 | Request: 59 | 60 | ```shell 61 | curl -X GET \ 62 | --header "Accept: application/did+ld+json" \ 63 | 64 | ``` 65 | 66 | Response: 67 | 68 | ```json 69 | { 70 | "@context": [ 71 | "https://www.w3.org/ns/did/v1", 72 | "https://w3id.org/security/suites/ed25519-2020/v1", 73 | "https://w3id.org/security/suites/x25519-2020/v1" 74 | ], 75 | "id": "did:algo:mainnet:app:1845671812:da490f2d15a625459bf970a3d55e1a646dfd3a956d011546e953e945d39fdada", 76 | "verificationMethod": [ 77 | { 78 | "id": "did:algo:mainnet:app:1845671812:da490f2d15a625459bf970a3d55e1a646dfd3a956d011546e953e945d39fdada#master", 79 | "type": "Ed25519VerificationKey2020", 80 | "controller": "did:algo:mainnet:app:1845671812:da490f2d15a625459bf970a3d55e1a646dfd3a956d011546e953e945d39fdada", 81 | "publicKeyMultibase": "zFh6VYTmcxGD2vTNMSTHhtMSa7TkGCded9ofBX5C6CxYq" 82 | } 83 | ], 84 | "authentication": [ 85 | "did:algo:mainnet:app:1845671812:da490f2d15a625459bf970a3d55e1a646dfd3a956d011546e953e945d39fdada#master" 86 | ] 87 | } 88 | ``` 89 | 90 | ## Custom Representations 91 | 92 | All other `Accept` values will instruct the resolver to process a "Resolve Representation" 93 | request. If the mime type corresponds to an encoder registered in the resolver instance, it 94 | will be used to generate the representation and return it in the response body. 95 | 96 | If the mime type requested is not available in the resolver instance a `representationNotSupported` 97 | error will be returned. 98 | 99 | Request: 100 | 101 | ```shell 102 | curl -X GET \ 103 | --header "Accept: application/did+cbor" \ 104 | 105 | ``` 106 | 107 | Response: 108 | 109 | ```json 110 | { 111 | "@context": ["https://w3id.org/did-resolution/v1"], 112 | "didResolutionMetadata": { 113 | "contentType": "application/did+cbor", 114 | "retrieved": "2024-03-01T01:39:03Z", 115 | "error": "representationNotSupported" 116 | } 117 | } 118 | ``` 119 | 120 | Custom encoders can be provided by 3rd parties by compling with the interface. 121 | 122 | ```go 123 | type Encoder interface { 124 | // Encode an existing DID document to a valid representation. 125 | // If an error is returned is must be a valid error code as 126 | // defined in the spec. 127 | Encode(doc *did.Document) ([]byte, error) 128 | } 129 | ``` 130 | 131 | More information, and resolver package source code, is available here 132 | . 133 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM zhield/shell:stable 2 | 3 | EXPOSE 9090/tcp 4 | 5 | VOLUME ["/etc/algoid"] 6 | 7 | COPY algoid /usr/bin/algoid 8 | ENTRYPOINT ["/usr/bin/algoid"] 9 | -------------------------------------------------------------------------------- /Dockerfile.resolver: -------------------------------------------------------------------------------- 1 | FROM zhield/shell:stable 2 | 3 | EXPOSE 9091/tcp 4 | 5 | COPY algoid /usr/bin/algoid 6 | 7 | COPY resolver-config.yaml /etc/algoid/config.yaml 8 | 9 | ENTRYPOINT ["/usr/bin/algoid", "resolver"] 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2021; The Algorand Foundation 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are 4 | permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this list of 7 | conditions and the following disclaimer. 8 | 9 | 2. Redistributions in binary form must reproduce the above copyright notice, this list 10 | of conditions and the following disclaimer in the documentation and/or other materials 11 | provided with the distribution. 12 | 13 | 3. Neither the name of the copyright holder nor the names of its contributors may be used 14 | to endorse or promote products derived from this software without specific prior written 15 | permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS 18 | OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY 19 | AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER 20 | OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 21 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 23 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR 24 | OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 25 | POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: * 2 | .DEFAULT_GOAL:=help 3 | 4 | # Project setup 5 | BINARY_NAME=algoid 6 | OWNER=algorandfoundation 7 | REPO=did-algo 8 | PROJECT_REPO=github.com/$(OWNER)/$(REPO) 9 | DOCKER_IMAGE=ghcr.io/$(OWNER)/$(BINARY_NAME) 10 | MAINTAINERS='Joe Polny ' 11 | 12 | # State values 13 | GIT_COMMIT_DATE=$(shell TZ=UTC git log -n1 --pretty=format:'%cd' --date='format-local:%Y-%m-%dT%H:%M:%SZ') 14 | GIT_COMMIT_HASH=$(shell git log -n1 --pretty=format:'%H') 15 | GIT_TAG=$(shell git describe --tags --always --abbrev=0 | cut -c 1-7) 16 | 17 | # Linker tags 18 | # https://golang.org/cmd/link/ 19 | LD_FLAGS += -s -w 20 | LD_FLAGS += -X $(PROJECT_REPO)/info.CoreVersion=$(GIT_TAG:v%=%) 21 | LD_FLAGS += -X $(PROJECT_REPO)/info.BuildCode=$(GIT_COMMIT_HASH) 22 | LD_FLAGS += -X $(PROJECT_REPO)/info.BuildTimestamp=$(GIT_COMMIT_DATE) 23 | 24 | # For commands that require a specific package path, default to all local 25 | # subdirectories if no value is provided. 26 | pkg?="..." 27 | 28 | # "buf" is used to manage protocol buffer definitions, if not installed 29 | # locally we fallback to use a builder image. 30 | buf:=buf 31 | ifeq (, $(shell which buf)) 32 | buf=docker run --platform linux/amd64 --rm -it -v $(shell pwd):/workdir ghcr.io/bryk-io/buf-builder:1.29.0 buf 33 | endif 34 | 35 | help: 36 | @echo "Commands available" 37 | @sed -n 's/^##//p' ${MAKEFILE_LIST} | column -t -s ':' | sed -e 's/^/ /' | sort 38 | 39 | ## bench: Run benchmarks 40 | bench: 41 | go test -run=XXX -bench=. -benchmem ./... 42 | 43 | ## build: Build for the current architecture in use, intended for development 44 | build: 45 | # Build CLI application 46 | go build -v -ldflags '$(LD_FLAGS)' -o $(BINARY_NAME) ./client/cli 47 | 48 | ## build-for: Build the available binaries for the specified 'os' and 'arch' 49 | # make build-for os=linux arch=amd64 50 | build-for: 51 | CGO_ENABLED=0 GOOS=$(os) GOARCH=$(arch) \ 52 | go build -v -ldflags '$(LD_FLAGS)' \ 53 | -o $(BINARY_NAME)_$(os)_$(arch)$(suffix) \ 54 | ./client/cli 55 | 56 | ## ca-roots: Generate the list of valid CA certificates 57 | ca-roots: 58 | @docker run -dit --rm --name ca-roots debian:stable-slim 59 | @docker exec --privileged ca-roots sh -c "apt update" 60 | @docker exec --privileged ca-roots sh -c "apt install -y ca-certificates" 61 | @docker exec --privileged ca-roots sh -c "cat /etc/ssl/certs/* > /ca-roots.crt" 62 | @docker cp ca-roots:/ca-roots.crt ca-roots.crt 63 | @docker stop ca-roots 64 | 65 | ## deps: Verify dependencies and remove intermediary products 66 | deps: 67 | go mod tidy 68 | go clean 69 | 70 | ## docker: Build docker image 71 | # https://github.com/opencontainers/image-spec/blob/master/annotations.md 72 | docker: 73 | make build-for os=linux arch=amd64 74 | mv $(BINARY_NAME)_linux_amd64 $(BINARY_NAME) 75 | @-docker rmi $(DOCKER_IMAGE):$(GIT_TAG:v%=%) 76 | @docker build \ 77 | "--label=org.opencontainers.image.title=$(BINARY_NAME)" \ 78 | "--label=org.opencontainers.image.authors=$(MAINTAINERS)" \ 79 | "--label=org.opencontainers.image.created=$(GIT_COMMIT_DATE)" \ 80 | "--label=org.opencontainers.image.revision=$(GIT_COMMIT_HASH)" \ 81 | "--label=org.opencontainers.image.version=$(GIT_TAG:v%=%)" \ 82 | --rm -t $(DOCKER_IMAGE):$(GIT_TAG:v%=%) . 83 | @docker build \ 84 | "--label=org.opencontainers.image.title=$(BINARY_NAME)-resolver" \ 85 | "--label=org.opencontainers.image.authors=$(MAINTAINERS)" \ 86 | "--label=org.opencontainers.image.created=$(GIT_COMMIT_DATE)" \ 87 | "--label=org.opencontainers.image.revision=$(GIT_COMMIT_HASH)" \ 88 | "--label=org.opencontainers.image.version=$(GIT_TAG:v%=%)" \ 89 | --rm -f Dockerfile.resolver -t $(DOCKER_IMAGE)-resolver:$(GIT_TAG:v%=%) . 90 | @rm $(BINARY_NAME) 91 | 92 | ## install: Install the binary to GOPATH and keep cached all compiled artifacts 93 | install: 94 | @go build -v -ldflags '$(LD_FLAGS)' -o ${GOPATH}/bin/$(BINARY_NAME) ./client/cli 95 | mv ${BINARY_NAME} ${GOBIN}/. 96 | 97 | ## lint: Static analysis 98 | lint: 99 | # Code 100 | golangci-lint run -v ./$(pkg) 101 | 102 | # Helm charts 103 | helm lint helm/* 104 | 105 | ## protos: Compile all protobuf definitions and RPC services 106 | protos: 107 | # Verify PB definitions 108 | make proto-test 109 | 110 | # Build package image 111 | $(buf) build --output proto/did/v1/image.bin --path proto/did/v1 112 | 113 | # Generate package code using buf.gen.yaml 114 | $(buf) generate --output proto --path proto/did/v1 115 | 116 | # Add compiler version to generated files 117 | @-sed -i.bak 's/(unknown)/buf-v$(shell buf --version)/g' proto/did/v1/*.pb.go 118 | 119 | # Remove package comment added by the gateway generator to avoid polluting 120 | # the package documentation. 121 | @-sed -i.bak '/\/\*/,/*\//d' proto/did/v1/*.pb.gw.go 122 | 123 | # Remove non-required dependencies. "protoc-gen-validate" don't have runtime 124 | # dependencies but the generated code includes the package by the default =/. 125 | @-sed -i.bak '/protoc-gen-validate/d' proto/did/v1/*.pb.go 126 | 127 | # Remove in-place edit backup files 128 | @-rm proto/did/v1/*.bak 129 | 130 | # Style adjustments (required for consistency) 131 | gofmt -s -w proto/did/v1 132 | goimports -w proto/did/v1 133 | 134 | ## proto-test: Verify protobuf definitions 135 | proto-test: 136 | # Verify style and consistency 137 | $(buf) lint --path proto/did/v1 138 | 139 | # Verify breaking changes. This fails if no image is already present, 140 | # use `buf build --o proto/did/v1/image.bin --path proto/did/v1` to generate it. 141 | $(buf) breaking --against proto/did/v1/image.bin 142 | 143 | ## release: Prepare artifacts for a new tagged release 144 | release: 145 | goreleaser release --skip-validate --skip-publish --clean 146 | 147 | ## scan-deps: Look for known vulnerabilities in the project dependencies 148 | # https://github.com/sonatype-nexus-community/nancy 149 | scan-deps: 150 | @go list -json -deps ./... | nancy sleuth --skip-update-check 151 | 152 | ## test: Run all tests excluding the vendor dependencies 153 | test: 154 | # Unit tests 155 | # -count=1 -p=1 (disable cache and parallel execution) 156 | go test -race -v -failfast -coverprofile=coverage.report ./$(pkg) 157 | go tool cover -html coverage.report -o coverage.html 158 | 159 | ## updates: List available updates for direct dependencies 160 | # https://github.com/golang/go/wiki/Modules#how-to-upgrade-and-downgrade-dependencies 161 | updates: 162 | @GOWORK=off go list -u -f '{{if (and (not (or .Main .Indirect)) .Update)}}{{.Path}}: [{{.Version}} -> {{.Update.Version}}]{{end}}' -mod=mod -m all 2> /dev/null 163 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # did:algo 2 | 3 | The repository contains the universal resolver driver for the `did:algo` method and all related software. 4 | 5 | The full spec for `did-algo` can be read in [SPEC.md](./SPEC.md) 6 | -------------------------------------------------------------------------------- /buf.gen.yaml: -------------------------------------------------------------------------------- 1 | version: v1 2 | # https://docs.buf.build/generate/managed-mode 3 | managed: 4 | enabled: true 5 | optimize_for: SPEED 6 | cc_enable_arenas: false 7 | java_multiple_files: true 8 | go_package_prefix: 9 | # same value as `module` in go.mod 10 | default: github.com/algorandfoundation/did-algo 11 | # don't build/rewrite import paths for external dependencies 12 | except: 13 | - buf.build/googleapis/googleapis 14 | - buf.build/bufbuild/protovalidate 15 | - buf.build/grpc-ecosystem/grpc-gateway 16 | # For each plugin: 17 | # - plugin: buf will look for a remote plugin 18 | # - name: buf generate will look for a binary named protoc-gen-NAME 19 | # - out: path relative to the output directory 20 | # - opt: options to provide to the plugin 21 | plugins: 22 | - plugin: buf.build/protocolbuffers/go:v1.32.0 23 | out: . 24 | opt: 25 | - paths=source_relative 26 | - plugin: buf.build/grpc/go:v1.3.0 27 | out: . 28 | opt: 29 | - paths=source_relative 30 | - require_unimplemented_servers=true 31 | - plugin: buf.build/grpc-ecosystem/openapiv2:v2.19.1 32 | out: . 33 | opt: 34 | - logtostderr=true 35 | - plugin: buf.build/grpc-ecosystem/gateway:v2.19.1 36 | out: . 37 | opt: 38 | - paths=source_relative 39 | - logtostderr=true 40 | # - plugin: buf.build/bufbuild/validate-go:v1.0.4 41 | # out: . 42 | # opt: 43 | # - paths=source_relative 44 | # - name: go-drpc 45 | # out: . 46 | # opt: 47 | # - paths=source_relative 48 | -------------------------------------------------------------------------------- /buf.work.yaml: -------------------------------------------------------------------------------- 1 | version: v1 2 | directories: 3 | - proto 4 | -------------------------------------------------------------------------------- /client/cli/cmd/completion.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import "github.com/spf13/cobra" 4 | 5 | var completionCmd = &cobra.Command{ 6 | Use: "completion", 7 | Short: "Generate autocompletion for commonly used shells", 8 | Long: ` 9 | Completion is used to output completion code for bash and zsh shells. 10 | Before using completion features, you have to source completion code 11 | from your .profile or .bashrc/.zshrc file. This is done by adding 12 | following line to one of above files: 13 | 14 | source <(algoid completion SHELL) 15 | 16 | Bash users can as well save it to the file and copy it to: 17 | /etc/bash_completion.d/ 18 | 19 | Correct arguments for SHELL are: "bash" and "zsh". 20 | 21 | Notes: 22 | 1) zsh completions requires zsh 5.2 or newer. 23 | 24 | 2) macOS users have to install bash-completion framework to utilize 25 | completion features. This can be done using homebrew: 26 | brew install bash-completion 27 | 28 | Once installed, you must load bash_completion by adding following 29 | line to your .profile or .bashrc/.zshrc: 30 | source $(brew --prefix)/etc/bash_completion 31 | `, 32 | } 33 | 34 | func init() { 35 | rootCmd.AddCommand(completionCmd) 36 | } 37 | -------------------------------------------------------------------------------- /client/cli/cmd/completion_bash.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | var bashCmd = &cobra.Command{ 10 | Use: "bash", 11 | Short: "Generates bash completion scripts", 12 | RunE: func(_ *cobra.Command, _ []string) error { 13 | return rootCmd.GenBashCompletion(os.Stdout) 14 | }, 15 | } 16 | 17 | func init() { 18 | completionCmd.AddCommand(bashCmd) 19 | } 20 | -------------------------------------------------------------------------------- /client/cli/cmd/completion_zsh.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | var zshCmd = &cobra.Command{ 10 | Use: "zsh", 11 | Short: "Generates zsh completion scripts", 12 | RunE: func(_ *cobra.Command, _ []string) error { 13 | return rootCmd.GenZshCompletion(os.Stdout) 14 | }, 15 | } 16 | 17 | func init() { 18 | completionCmd.AddCommand(zshCmd) 19 | } 20 | -------------------------------------------------------------------------------- /client/cli/cmd/config.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/algorandfoundation/did-algo/client/internal" 7 | "github.com/spf13/cobra" 8 | "github.com/spf13/viper" 9 | "gopkg.in/yaml.v3" 10 | ) 11 | 12 | var configCmd = &cobra.Command{ 13 | Use: "config", 14 | Aliases: []string{"cfg", "conf", "settings"}, 15 | Short: "Adjust configuration settings for the CLI client", 16 | } 17 | 18 | func init() { 19 | rootCmd.AddCommand(configCmd) 20 | } 21 | 22 | type appConf struct { 23 | Home string `json:"home" yaml:"home" mapstructure:"home"` 24 | Network *internal.ClientSettings `json:"network" yaml:"network" mapstructure:"network"` 25 | } 26 | 27 | func (ac *appConf) isProfileAvailable(name string) bool { 28 | for _, p := range ac.Network.Profiles { 29 | if p.Name == name { 30 | return true 31 | } 32 | } 33 | return false 34 | } 35 | 36 | func (ac *appConf) setAppID(network string, appID uint) { 37 | for _, p := range ac.Network.Profiles { 38 | if p.Name == network { 39 | p.AppID = appID 40 | } 41 | } 42 | } 43 | 44 | func (ac *appConf) save() error { 45 | file := viper.ConfigFileUsed() 46 | output, _ := yaml.Marshal(ac) 47 | return os.WriteFile(file, output, 0644) // nolint 48 | } 49 | -------------------------------------------------------------------------------- /client/cli/cmd/config_app_id.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | 7 | "github.com/spf13/cobra" 8 | "github.com/spf13/viper" 9 | ) 10 | 11 | var configAppIDCmd = &cobra.Command{ 12 | Use: "app-id", 13 | Example: "algoid config app-id [app-id] [network]", 14 | Short: "Adjust the `app-id` setting for the active network profile", 15 | RunE: func(_ *cobra.Command, args []string) error { 16 | if len(args) == 0 { 17 | return fmt.Errorf("appID name is required") 18 | } 19 | 20 | if len(args) == 1 { 21 | return fmt.Errorf("network name is required") 22 | } 23 | 24 | conf := new(appConf) 25 | if err := viper.Unmarshal(&conf); err != nil { 26 | return err 27 | } 28 | appID, err := strconv.Atoi(args[0]) 29 | if err != nil { 30 | return fmt.Errorf("invalid app-id: '%s'", args[0]) 31 | } 32 | conf.setAppID(args[1], uint(appID)) 33 | err = conf.save() 34 | if err == nil { 35 | log.Info("configuration updated") 36 | } 37 | return err 38 | }, 39 | } 40 | 41 | func init() { 42 | configCmd.AddCommand(configAppIDCmd) 43 | } 44 | -------------------------------------------------------------------------------- /client/cli/cmd/config_view.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | "github.com/spf13/viper" 8 | "gopkg.in/yaml.v3" 9 | ) 10 | 11 | var configViewCmd = &cobra.Command{ 12 | Use: "view", 13 | Example: "algoid config view", 14 | Short: "View current configuration settings", 15 | RunE: func(_ *cobra.Command, _ []string) error { 16 | conf := new(appConf) 17 | if err := viper.Unmarshal(&conf); err != nil { 18 | return err 19 | } 20 | output, _ := yaml.Marshal(conf) 21 | log.Info("configuration file: ", viper.ConfigFileUsed()) 22 | fmt.Printf("%s\n", output) 23 | return nil 24 | }, 25 | } 26 | 27 | func init() { 28 | configCmd.AddCommand(configViewCmd) 29 | } 30 | -------------------------------------------------------------------------------- /client/cli/cmd/delete.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/kennygrant/sanitize" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | var deleteCmd = &cobra.Command{ 12 | Use: "delete", 13 | Short: "Permanently delete a local identifier", 14 | Example: "algoid delete [DID reference name]", 15 | Aliases: []string{"del", "rm", "remove"}, 16 | RunE: func(_ *cobra.Command, args []string) error { 17 | if len(args) != 1 { 18 | return errors.New("you must specify a DID reference name") 19 | } 20 | 21 | // Get store handler 22 | st, err := getClientStore() 23 | if err != nil { 24 | return err 25 | } 26 | 27 | // Get user confirmation 28 | confirmation, err := readValue("this action cannot be undone, are you sure (y/N)") 29 | if err != nil { 30 | return err 31 | } 32 | if confirmation != "y" { 33 | return errors.New("invalid confirmation value, canceling operation") 34 | } 35 | 36 | // Delete identifier 37 | name := sanitize.Name(args[0]) 38 | if err = st.Delete(name); err != nil { 39 | return fmt.Errorf("failed to remove entry: %w", err) 40 | } 41 | log.Infof("identifier successfully deleted: %s", name) 42 | return nil 43 | }, 44 | } 45 | 46 | func init() { 47 | rootCmd.AddCommand(deleteCmd) 48 | } 49 | -------------------------------------------------------------------------------- /client/cli/cmd/deploy.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | ac "github.com/algorand/go-algorand-sdk/v2/crypto" 8 | "github.com/algorand/go-algorand-sdk/v2/mnemonic" 9 | "github.com/kennygrant/sanitize" 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | var deployContractCmd = &cobra.Command{ 14 | Use: "deploy", 15 | Aliases: []string{"deploy-contract"}, 16 | Short: "Deploy the AlgoDID storage smart contract", 17 | Example: "algoid deploy [wallet-name] [network]", 18 | RunE: runDeployContractCmd, 19 | } 20 | 21 | func init() { 22 | rootCmd.AddCommand(deployContractCmd) 23 | } 24 | 25 | func runDeployContractCmd(_ *cobra.Command, args []string) error { 26 | // Get parameters 27 | if len(args) != 2 { 28 | return errors.New("missing required parameters") 29 | } 30 | name := sanitize.Name(args[0]) 31 | wp, err := readSecretValue("enter wallet's passphrase") 32 | if err != nil { 33 | return err 34 | } 35 | 36 | // Get local store handler 37 | store, err := getClientStore() 38 | if err != nil { 39 | return err 40 | } 41 | 42 | // Decrypt wallet 43 | seed, err := store.OpenWallet(name, wp) 44 | if err != nil { 45 | return err 46 | } 47 | 48 | // Restore account handler 49 | key, err := mnemonic.ToPrivateKey(seed) 50 | if err != nil { 51 | return err 52 | } 53 | account, err := ac.AccountFromPrivateKey(key) 54 | if err != nil { 55 | return err 56 | } 57 | 58 | // Get network client 59 | cl, err := getAlgoClient() 60 | if err != nil { 61 | return err 62 | } 63 | 64 | network := args[1] 65 | // Deploy contract 66 | appID, err := cl.Networks[network].DeployContract(&account) 67 | if err != nil { 68 | return err 69 | } 70 | log.WithField("app_id", appID).Info(fmt.Sprintf("storage contract deployed successfully to %s", network)) 71 | return nil 72 | } 73 | -------------------------------------------------------------------------------- /client/cli/cmd/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package cmd provides a CLI application to manage DIDs. 3 | */ 4 | package cmd 5 | -------------------------------------------------------------------------------- /client/cli/cmd/edit.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import "github.com/spf13/cobra" 4 | 5 | var editCmd = &cobra.Command{ 6 | Use: "edit", 7 | Short: "Edit local DIDs", 8 | } 9 | 10 | func init() { 11 | rootCmd.AddCommand(editCmd) 12 | } 13 | -------------------------------------------------------------------------------- /client/cli/cmd/edit_activate.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/kennygrant/sanitize" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | var activateCmd = &cobra.Command{ 13 | Use: "activate", 14 | Short: "Mark a DID as active", 15 | Example: "algoid edit activate [DID reference name]", 16 | RunE: func(_ *cobra.Command, args []string) error { 17 | if len(args) != 1 { 18 | return errors.New("you must specify a DID reference name") 19 | } 20 | 21 | // Get store handler 22 | st, err := getClientStore() 23 | if err != nil { 24 | return err 25 | } 26 | 27 | // Get identifier 28 | name := sanitize.Name(args[0]) 29 | log.Debugf("retrieving entry with reference name: %s", name) 30 | id, err := st.Get(name) 31 | if err != nil { 32 | return fmt.Errorf("no available record under the provided reference name: %s", name) 33 | } 34 | 35 | // Update metadata 36 | log.Info("updating metadata") 37 | md := id.GetMetadata() 38 | md.Deactivated = false 39 | md.Updated = time.Now().UTC().Format(time.RFC3339) 40 | if err := id.AddMetadata(md); err != nil { 41 | return err 42 | } 43 | 44 | // Update record 45 | log.Info("updating local record") 46 | return st.Update(name, id) 47 | }, 48 | } 49 | 50 | func init() { 51 | editCmd.AddCommand(activateCmd) 52 | } 53 | -------------------------------------------------------------------------------- /client/cli/cmd/edit_deactivate.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/kennygrant/sanitize" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | var deactivateCmd = &cobra.Command{ 13 | Use: "deactivate", 14 | Short: "Mark a DID as inactive", 15 | Example: "algoid edit activate [DID reference name]", 16 | RunE: func(_ *cobra.Command, args []string) error { 17 | if len(args) != 1 { 18 | return errors.New("you must specify a DID reference name") 19 | } 20 | 21 | // Get store handler 22 | st, err := getClientStore() 23 | if err != nil { 24 | return err 25 | } 26 | 27 | // Get identifier 28 | name := sanitize.Name(args[0]) 29 | log.Debugf("retrieving entry with reference name: %s", name) 30 | id, err := st.Get(name) 31 | if err != nil { 32 | return fmt.Errorf("no available record under the provided reference name: %s", name) 33 | } 34 | 35 | // Update metadata 36 | log.Info("updating metadata") 37 | md := id.GetMetadata() 38 | md.Deactivated = true 39 | md.Updated = time.Now().UTC().Format(time.RFC3339) 40 | if err := id.AddMetadata(md); err != nil { 41 | return err 42 | } 43 | 44 | // Update record 45 | log.Info("updating local record") 46 | return st.Update(name, id) 47 | }, 48 | } 49 | 50 | func init() { 51 | editCmd.AddCommand(deactivateCmd) 52 | } 53 | -------------------------------------------------------------------------------- /client/cli/cmd/info.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | 8 | "github.com/algorandfoundation/did-algo/client/store" 9 | "github.com/kennygrant/sanitize" 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | var didDetailsCmd = &cobra.Command{ 14 | Use: "info", 15 | Short: "Display the current information available on an existing DID", 16 | Example: "algoid info [DID reference name]", 17 | Aliases: []string{"details", "inspect", "view", "doc"}, 18 | RunE: runDidDetailsCmd, 19 | } 20 | 21 | func init() { 22 | rootCmd.AddCommand(didDetailsCmd) 23 | } 24 | 25 | func runDidDetailsCmd(_ *cobra.Command, args []string) error { 26 | if len(args) != 1 { 27 | return errors.New("you must specify a DID reference name") 28 | } 29 | 30 | // Get store handler 31 | st, err := getClientStore() 32 | if err != nil { 33 | return err 34 | } 35 | 36 | // Retrieve identifier 37 | name := sanitize.Name(args[0]) 38 | id, err := st.Get(name) 39 | if err != nil { 40 | return fmt.Errorf("no available record under the provided reference name: %s", name) 41 | } 42 | 43 | // Present its LD document as output 44 | md := id.GetMetadata() 45 | if md != nil { 46 | log.Infof("created: %s", md.Created) 47 | log.Infof("updated: %s", md.Updated) 48 | log.Infof("active: %v", !md.Deactivated) 49 | } 50 | info, _ := json.MarshalIndent(store.IdentifierRecord{ 51 | Document: id.Document(true), 52 | }, "", " ") 53 | fmt.Printf("%s\n", info) 54 | return nil 55 | } 56 | -------------------------------------------------------------------------------- /client/cli/cmd/key.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import "github.com/spf13/cobra" 4 | 5 | var keyCmd = &cobra.Command{ 6 | Use: "key", 7 | Short: "Manage cryptographic keys associated with the DID", 8 | } 9 | 10 | func init() { 11 | editCmd.AddCommand(keyCmd) 12 | } 13 | -------------------------------------------------------------------------------- /client/cli/cmd/key_add.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/kennygrant/sanitize" 9 | "github.com/spf13/cobra" 10 | "github.com/spf13/viper" 11 | "go.bryk.io/pkg/cli" 12 | "go.bryk.io/pkg/did" 13 | ) 14 | 15 | var addKeyCmd = &cobra.Command{ 16 | Use: "add", 17 | Short: "Add a new cryptographic key for the DID", 18 | Example: "algoid edit key add [DID reference name] --name my-new-key --type ed --authentication", 19 | RunE: runAddKeyCmd, 20 | } 21 | 22 | func init() { 23 | params := []cli.Param{ 24 | { 25 | Name: "name", 26 | Usage: "name to be assigned to the newly added key", 27 | FlagKey: "key-add.name", 28 | ByDefault: "key-#", 29 | Short: "n", 30 | }, 31 | { 32 | Name: "type", 33 | Usage: "type of cryptographic key: RSA (rsa), Ed25519 (ed) or secp256k1 (koblitz)", 34 | FlagKey: "key-add.type", 35 | ByDefault: "ed", 36 | Short: "t", 37 | }, 38 | { 39 | Name: "authentication", 40 | Usage: "enable this key for authentication purposes", 41 | FlagKey: "key-add.authentication", 42 | ByDefault: false, 43 | Short: "a", 44 | }, 45 | } 46 | if err := cli.SetupCommandParams(addKeyCmd, params, viper.GetViper()); err != nil { 47 | panic(err) 48 | } 49 | keyCmd.AddCommand(addKeyCmd) 50 | } 51 | 52 | // nolint: gocyclo 53 | func runAddKeyCmd(_ *cobra.Command, args []string) error { 54 | if len(args) != 1 { 55 | return errors.New("you must specify a DID reference name") 56 | } 57 | 58 | // Get store handler 59 | st, err := getClientStore() 60 | if err != nil { 61 | return err 62 | } 63 | 64 | // Get identifier 65 | name := sanitize.Name(args[0]) 66 | log.Info("adding new key") 67 | log.Debugf("retrieving entry with reference name: %s", name) 68 | id, err := st.Get(name) 69 | if err != nil { 70 | return fmt.Errorf("no available record under the provided reference name: %s", name) 71 | } 72 | 73 | // Sanitize key name 74 | log.Debug("validating parameters") 75 | keyName := viper.GetString("key-add.name") 76 | if strings.Count(keyName, "#") > 1 { 77 | return errors.New("invalid key name") 78 | } 79 | if strings.Count(keyName, "#") == 1 { 80 | keyName = strings.Replace(keyName, "#", fmt.Sprintf("%d", len(id.VerificationMethods())+1), 1) 81 | } 82 | keyName = sanitize.Name(keyName) 83 | 84 | // Set key type 85 | var keyType did.KeyType 86 | switch viper.GetString("key-add.type") { 87 | case "ed": 88 | keyType = did.KeyTypeEd 89 | case "rsa": 90 | keyType = did.KeyTypeRSA 91 | case "koblitz": 92 | keyType = did.KeyTypeSecp256k1 93 | default: 94 | return errors.New("invalid key type") 95 | } 96 | 97 | // Add key 98 | log.Debugf("adding new key with name: %s", keyName) 99 | if err = id.AddNewVerificationMethod(keyName, keyType); err != nil { 100 | return fmt.Errorf("failed to add new key: %w", err) 101 | } 102 | if viper.GetBool("key-add.authentication") { 103 | log.Info("setting new key as authentication mechanism") 104 | if err = id.AddVerificationRelationship(id.GetReference(keyName), did.AuthenticationVM); err != nil { 105 | return fmt.Errorf("failed to establish key for authentication purposes: %w", err) 106 | } 107 | } 108 | 109 | // Update record 110 | log.Info("updating local record") 111 | return st.Update(name, id) 112 | } 113 | -------------------------------------------------------------------------------- /client/cli/cmd/key_remove.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/kennygrant/sanitize" 8 | "github.com/spf13/cobra" 9 | "go.bryk.io/pkg/did" 10 | ) 11 | 12 | var removeKeyCmd = &cobra.Command{ 13 | Use: "remove", 14 | Short: "Remove an existing cryptographic key for the DID", 15 | Example: "algoid edit key remove [DID reference name] [key name]", 16 | Aliases: []string{"rm"}, 17 | RunE: runRemoveKeyCmd, 18 | } 19 | 20 | func init() { 21 | keyCmd.AddCommand(removeKeyCmd) 22 | } 23 | 24 | func runRemoveKeyCmd(_ *cobra.Command, args []string) error { 25 | if len(args) != 2 { 26 | return errors.New("you must specify [DID reference name] [key name]") 27 | } 28 | 29 | // Get store handler 30 | st, err := getClientStore() 31 | if err != nil { 32 | return err 33 | } 34 | 35 | // Get identifier 36 | name := sanitize.Name(args[0]) 37 | keyName := sanitize.Name(args[1]) 38 | log.Info("removing existing key") 39 | log.Debugf("retrieving entry with reference name: %s", name) 40 | id, err := st.Get(name) 41 | if err != nil { 42 | return fmt.Errorf("no available record under the provided reference name: %s", name) 43 | } 44 | 45 | // Remove key 46 | log.Debug("validating parameters") 47 | if len(id.VerificationMethods()) >= 2 { 48 | _ = id.RemoveVerificationRelationship(id.GetReference(keyName), did.AuthenticationVM) 49 | } 50 | if err = id.RemoveVerificationMethod(keyName); err != nil { 51 | return fmt.Errorf("failed to remove key: %s", keyName) 52 | } 53 | 54 | // Update record 55 | log.Debug("key removed") 56 | log.Info("updating local record") 57 | return st.Update(name, id) 58 | } 59 | -------------------------------------------------------------------------------- /client/cli/cmd/list.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "text/tabwriter" 7 | 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | var listCmd = &cobra.Command{ 12 | Use: "list", 13 | Short: "List registered DIDs", 14 | Example: "algoid list", 15 | Aliases: []string{"ls"}, 16 | RunE: runListCmd, 17 | } 18 | 19 | func init() { 20 | rootCmd.AddCommand(listCmd) 21 | } 22 | 23 | func runListCmd(_ *cobra.Command, _ []string) error { 24 | // Get store handler 25 | st, err := getClientStore() 26 | if err != nil { 27 | return err 28 | } 29 | 30 | // Get list of entries 31 | list := st.List() 32 | if len(list) == 0 { 33 | log.Warning("no DIDs registered for the moment") 34 | return nil 35 | } 36 | 37 | // Show list of registered entries 38 | table := tabwriter.NewWriter(os.Stdout, 8, 0, 4, ' ', tabwriter.TabIndent) 39 | _, _ = fmt.Fprintf(table, "%s\t%s\n", "Name", "DID") 40 | for k, id := range list { 41 | _, _ = fmt.Fprintf(table, "%s\t%s\n", k, id.DID()) 42 | } 43 | return table.Flush() 44 | } 45 | -------------------------------------------------------------------------------- /client/cli/cmd/proof.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | 8 | "github.com/kennygrant/sanitize" 9 | "github.com/spf13/cobra" 10 | "github.com/spf13/viper" 11 | "go.bryk.io/pkg/cli" 12 | ) 13 | 14 | var proofCmd = &cobra.Command{ 15 | Use: "proof", 16 | Short: "Produce a linked digital proof document", 17 | Example: "algoid proof [DID reference name] --input \"contents to sign\"", 18 | Aliases: []string{"sign"}, 19 | RunE: runProofCmd, 20 | } 21 | 22 | func init() { 23 | params := []cli.Param{ 24 | { 25 | Name: "input", 26 | Usage: "contents to sign", 27 | FlagKey: "proof.input", 28 | ByDefault: "", 29 | Short: "i", 30 | }, 31 | { 32 | Name: "key", 33 | Usage: "key to use to produce the proof", 34 | FlagKey: "proof.key", 35 | ByDefault: "master", 36 | Short: "k", 37 | }, 38 | { 39 | Name: "domain", 40 | Usage: "domain value to use", 41 | FlagKey: "proof.domain", 42 | ByDefault: "", 43 | Short: "d", 44 | }, 45 | { 46 | Name: "purpose", 47 | Usage: "specific intent for the proof", 48 | FlagKey: "proof.purpose", 49 | ByDefault: "authentication", 50 | Short: "p", 51 | }, 52 | } 53 | if err := cli.SetupCommandParams(proofCmd, params, viper.GetViper()); err != nil { 54 | panic(err) 55 | } 56 | rootCmd.AddCommand(proofCmd) 57 | } 58 | 59 | func runProofCmd(_ *cobra.Command, args []string) error { 60 | if len(args) != 1 { 61 | return errors.New("you must specify a DID reference name") 62 | } 63 | 64 | // Get input, CLI takes precedence, from standard input otherwise 65 | input := []byte(viper.GetString("proof.input")) 66 | if len(input) == 0 { 67 | input, _ = cli.ReadPipedInput(maxPipeInputSize) 68 | } 69 | if len(input) == 0 { 70 | return errors.New("no input passed in to sign") 71 | } 72 | 73 | // Get store handler 74 | st, err := getClientStore() 75 | if err != nil { 76 | return err 77 | } 78 | 79 | // Retrieve identifier 80 | name := sanitize.Name(args[0]) 81 | id, err := st.Get(name) 82 | if err != nil { 83 | return fmt.Errorf("no available record under the provided reference name: %s", name) 84 | } 85 | 86 | // Get key 87 | key := id.VerificationMethod(viper.GetString("proof.key")) 88 | if key == nil { 89 | return fmt.Errorf("selected key is not available on the DID: %s", viper.GetString("proof.key")) 90 | } 91 | 92 | // Produce proof 93 | purpose := viper.GetString("proof.purpose") 94 | domain := viper.GetString("proof.domain") 95 | pld, err := key.ProduceProof(input, purpose, domain) 96 | if err != nil { 97 | return fmt.Errorf("failed to produce proof: %w", err) 98 | } 99 | js, err := json.MarshalIndent(pld, "", " ") 100 | if err != nil { 101 | return fmt.Errorf("failed to produce proof: %w", err) 102 | } 103 | fmt.Printf("%s\n", js) 104 | return nil 105 | } 106 | -------------------------------------------------------------------------------- /client/cli/cmd/register.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | ac "github.com/algorand/go-algorand-sdk/v2/crypto" 8 | "github.com/algorand/go-algorand-sdk/v2/mnemonic" 9 | "github.com/kennygrant/sanitize" 10 | "github.com/spf13/cobra" 11 | "github.com/spf13/viper" 12 | "go.bryk.io/pkg/cli" 13 | "go.bryk.io/pkg/did" 14 | xlog "go.bryk.io/pkg/log" 15 | ) 16 | 17 | var registerCmd = &cobra.Command{ 18 | Use: "register", 19 | Short: "Creates a new DID locally", 20 | Example: "algoid register [wallet-name] [network]", 21 | Aliases: []string{"create", "new"}, 22 | RunE: runRegisterCmd, 23 | } 24 | 25 | func init() { 26 | params := []cli.Param{} 27 | if err := cli.SetupCommandParams(registerCmd, params, viper.GetViper()); err != nil { 28 | panic(err) 29 | } 30 | rootCmd.AddCommand(registerCmd) 31 | } 32 | 33 | func runRegisterCmd(_ *cobra.Command, args []string) error { 34 | if len(args) != 2 { 35 | return errors.New("missing required parameters") 36 | } 37 | name := sanitize.Name(args[0]) 38 | wp, err := readSecretValue("enter wallet's passphrase") 39 | if err != nil { 40 | return err 41 | } 42 | 43 | // Get local store handler 44 | st, err := getClientStore() 45 | if err != nil { 46 | return err 47 | } 48 | 49 | // Decrypt wallet 50 | seed, err := st.OpenWallet(name, wp) 51 | if err != nil { 52 | return err 53 | } 54 | 55 | // Restore account handler 56 | key, err := mnemonic.ToPrivateKey(seed) 57 | if err != nil { 58 | return err 59 | } 60 | account, err := ac.AccountFromPrivateKey(key) 61 | if err != nil { 62 | return err 63 | } 64 | 65 | network := args[1] 66 | // Get storage application identifier 67 | appID, err := getStorageAppID(network) 68 | if err != nil { 69 | return err 70 | } 71 | 72 | // Check for duplicates 73 | dup, _ := st.Get(name) 74 | if dup != nil { 75 | return fmt.Errorf("there's already a DID with reference name: %s", name) 76 | } 77 | 78 | // Generate base identifier instance 79 | subject := fmt.Sprintf("%s:app:%d:%x", network, appID, account.PublicKey) 80 | method := "algo" 81 | log.WithFields(xlog.Fields{ 82 | "subject": subject, 83 | "method": method, 84 | }).Info("generating new identifier") 85 | id, err := did.NewIdentifier(method, subject) 86 | if err != nil { 87 | return err 88 | } 89 | log.Debug("adding master key") 90 | if err = id.AddVerificationMethod("master", key, did.KeyTypeEd); err != nil { 91 | return err 92 | } 93 | log.Debug("setting master key as authentication mechanism") 94 | if err = id.AddVerificationRelationship(id.GetReference("master"), did.AuthenticationVM); err != nil { 95 | return err 96 | } 97 | 98 | // Save instance in the store 99 | log.Info("adding entry to local store") 100 | return st.Save(name, id) 101 | } 102 | -------------------------------------------------------------------------------- /client/cli/cmd/resolver.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "net/http" 5 | "os" 6 | "syscall" 7 | 8 | "github.com/algorandfoundation/did-algo/info" 9 | "github.com/spf13/cobra" 10 | "github.com/spf13/viper" 11 | "go.bryk.io/pkg/cli" 12 | "go.bryk.io/pkg/did/resolver" 13 | xHttp "go.bryk.io/pkg/net/http" 14 | mwHeaders "go.bryk.io/pkg/net/middleware/headers" 15 | ) 16 | 17 | var resolverCmd = &cobra.Command{ 18 | Use: "resolver", 19 | Short: "Run a DID resolver service for the Algorand network", 20 | RunE: runResolverCmd, 21 | } 22 | 23 | func init() { 24 | params := []cli.Param{ 25 | { 26 | Name: "port", 27 | Usage: "TCP port to use for the server", 28 | FlagKey: "resolver.port", 29 | ByDefault: 9091, 30 | Short: "p", 31 | }, 32 | { 33 | Name: "proxy-protocol", 34 | Usage: "enable support for PROXY protocol", 35 | FlagKey: "resolver.proxy_protocol", 36 | ByDefault: false, 37 | Short: "P", 38 | }, 39 | } 40 | if err := cli.SetupCommandParams(resolverCmd, params, viper.GetViper()); err != nil { 41 | panic(err) 42 | } 43 | rootCmd.AddCommand(resolverCmd) 44 | } 45 | 46 | func runResolverCmd(_ *cobra.Command, _ []string) error { 47 | // Network client 48 | cl, err := getAlgoClient() 49 | if err != nil { 50 | return err 51 | } 52 | 53 | rslv, err := resolver.New(resolver.WithProvider("algo", cl)) 54 | if err != nil { 55 | return err 56 | } 57 | 58 | mux := http.NewServeMux() 59 | mux.HandleFunc("/1.0/ping", func(w http.ResponseWriter, _ *http.Request) { 60 | _, _ = w.Write([]byte("pong")) 61 | }) 62 | mux.HandleFunc("/1.0/ready", func(w http.ResponseWriter, _ *http.Request) { 63 | _, _ = w.Write([]byte("ok")) 64 | }) 65 | mux.HandleFunc("/1.0/identifiers/", rslv.ResolutionHandler) 66 | 67 | // start server 68 | srvOpts := []xHttp.Option{ 69 | xHttp.WithHandler(mux), 70 | xHttp.WithPort(viper.GetInt("resolver.port")), 71 | xHttp.WithMiddleware(mwHeaders.Handler(map[string]string{ 72 | "x-resolver-version": info.CoreVersion, 73 | "x-resolver-build": info.BuildCode, 74 | })), 75 | } 76 | srv, err := xHttp.NewServer(srvOpts...) 77 | if err != nil { 78 | return err 79 | } 80 | go srv.Start() // nolint:errcheck 81 | 82 | // wait for system signals 83 | log.Info("waiting for incoming requests") 84 | <-cli.SignalsHandler([]os.Signal{ 85 | syscall.SIGHUP, 86 | syscall.SIGINT, 87 | syscall.SIGTERM, 88 | syscall.SIGQUIT, 89 | }) 90 | 91 | // stop server 92 | log.Info("closing resolver server") 93 | return srv.Stop(true) 94 | } 95 | -------------------------------------------------------------------------------- /client/cli/cmd/retrieve.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | 8 | "github.com/spf13/cobra" 9 | "go.bryk.io/pkg/did" 10 | ) 11 | 12 | var retrieveCmd = &cobra.Command{ 13 | Use: "retrieve", 14 | Short: "Retrieve the DID document of an existing identifier", 15 | Example: "algoid retrieve [existing DID]", 16 | Aliases: []string{"get", "resolve"}, 17 | RunE: runRetrieveCmd, 18 | } 19 | 20 | func init() { 21 | rootCmd.AddCommand(retrieveCmd) 22 | } 23 | 24 | func runRetrieveCmd(_ *cobra.Command, args []string) error { 25 | // Check params 26 | if len(args) != 1 { 27 | return errors.New("you must specify a DID to retrieve") 28 | } 29 | 30 | // Verify the provided value is a valid DID string 31 | _, err := did.Parse(args[0]) 32 | if err != nil { 33 | return err 34 | } 35 | 36 | // Get network client 37 | cl, err := getAlgoClient() 38 | if err != nil { 39 | return err 40 | } 41 | 42 | // Retrieve record 43 | log.Info("retrieving record") 44 | doc, err := cl.Resolve(args[0]) 45 | if err != nil { 46 | return err 47 | } 48 | 49 | // Pretty-print retrieved document 50 | log.Warning("skipping validation") 51 | output, _ := json.MarshalIndent(doc, "", " ") 52 | fmt.Printf("%s\n", output) 53 | return nil 54 | } 55 | -------------------------------------------------------------------------------- /client/cli/cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | 9 | "github.com/spf13/cobra" 10 | "github.com/spf13/viper" 11 | "go.bryk.io/pkg/errors" 12 | xlog "go.bryk.io/pkg/log" 13 | ) 14 | 15 | var ( 16 | log xlog.Logger 17 | cfgFile = "" 18 | homeDir = "" 19 | ) 20 | 21 | var rootCmd = &cobra.Command{ 22 | Use: "algoid", 23 | Short: "Algorand DID Method: Client", 24 | SilenceErrors: true, 25 | SilenceUsage: true, 26 | Long: `Algorand DID 27 | 28 | Reference client implementation for the "algo" DID method. The platform 29 | allows entities to fully manage Decentralized Identifiers as described 30 | by version v1.0 of the W3C specification. 31 | 32 | For more information: 33 | https://github.com/algorandfoundation/did-algo`, 34 | } 35 | 36 | // Execute will process the CLI invocation. 37 | func Execute() { 38 | // catch any panics 39 | defer func() { 40 | if err := errors.FromRecover(recover()); err != nil { 41 | log.Warning("recovered panic") 42 | fmt.Printf("%+v", err) 43 | os.Exit(1) 44 | } 45 | }() 46 | // execute command 47 | if err := rootCmd.Execute(); err != nil { 48 | if pe := new(errors.Error); errors.Is(err, pe) { 49 | log.WithField("error", err).Error("command failed") 50 | } else { 51 | log.Error(err.Error()) 52 | } 53 | os.Exit(1) 54 | } 55 | } 56 | 57 | func init() { 58 | log = xlog.WithZero(xlog.ZeroOptions{PrettyPrint: true}) 59 | cobra.OnInitialize(initConfig) 60 | rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file ($HOME/.algoid/config.yaml)") 61 | rootCmd.PersistentFlags().StringVar(&homeDir, "home", "", "home directory ($HOME/.algoid)") 62 | if err := viper.BindPFlag("home", rootCmd.PersistentFlags().Lookup("home")); err != nil { 63 | panic(err) 64 | } 65 | } 66 | 67 | func initConfig() { 68 | // Find home directory 69 | home := homeDir 70 | if home == "" { 71 | h, err := os.UserHomeDir() 72 | if err != nil { 73 | fmt.Println(err) 74 | os.Exit(1) 75 | } 76 | home = h 77 | } 78 | 79 | // Set default values 80 | viper.SetDefault("home", filepath.Join(home, ".algoid")) 81 | 82 | // Set configuration file 83 | if cfgFile != "" { 84 | viper.SetConfigFile(cfgFile) 85 | } else { 86 | if cwd, err := os.Getwd(); err == nil { 87 | viper.AddConfigPath(cwd) 88 | } 89 | viper.AddConfigPath(filepath.Join(home, ".algoid")) 90 | viper.AddConfigPath("/etc/algoid") 91 | viper.SetConfigName("config") 92 | } 93 | 94 | // ENV 95 | viper.SetEnvPrefix("algoid") 96 | viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) 97 | viper.AutomaticEnv() 98 | 99 | // Read configuration file 100 | if err := viper.ReadInConfig(); err != nil && viper.ConfigFileUsed() != "" { 101 | fmt.Println("failed to load configuration file:", viper.ConfigFileUsed()) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /client/cli/cmd/service.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import "github.com/spf13/cobra" 4 | 5 | var serviceCmd = &cobra.Command{ 6 | Use: "service", 7 | Short: "Manage services enabled for the identifier", 8 | } 9 | 10 | func init() { 11 | editCmd.AddCommand(serviceCmd) 12 | } 13 | -------------------------------------------------------------------------------- /client/cli/cmd/service_add.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net/url" 7 | "strings" 8 | 9 | "github.com/kennygrant/sanitize" 10 | "github.com/spf13/cobra" 11 | "github.com/spf13/viper" 12 | "go.bryk.io/pkg/cli" 13 | "go.bryk.io/pkg/did" 14 | ) 15 | 16 | var addServiceCmd = &cobra.Command{ 17 | Use: "add", 18 | Short: "Register a new service entry for the DID", 19 | Example: "algoid edit service add [DID] --name my-service --endpoint https://www.agency.com/user_id", 20 | RunE: runAddServiceCmd, 21 | } 22 | 23 | func init() { 24 | params := []cli.Param{ 25 | { 26 | Name: "name", 27 | Usage: "service's reference name", 28 | FlagKey: "service-add.name", 29 | ByDefault: "external-service-#", 30 | Short: "n", 31 | }, 32 | { 33 | Name: "type", 34 | Usage: "type identifier for the service handler", 35 | FlagKey: "service-add.type", 36 | ByDefault: "did.algorand.foundation.ExternalService", 37 | Short: "t", 38 | }, 39 | { 40 | Name: "endpoint", 41 | Usage: "main URL to access the service", 42 | FlagKey: "service-add.endpoint", 43 | ByDefault: "", 44 | Short: "e", 45 | }, 46 | } 47 | if err := cli.SetupCommandParams(addServiceCmd, params, viper.GetViper()); err != nil { 48 | panic(err) 49 | } 50 | serviceCmd.AddCommand(addServiceCmd) 51 | } 52 | 53 | func runAddServiceCmd(_ *cobra.Command, args []string) error { 54 | if len(args) != 1 { 55 | return errors.New("you must specify a DID reference name") 56 | } 57 | if strings.TrimSpace(viper.GetString("service-add.endpoint")) == "" { 58 | return errors.New("service endpoint is required") 59 | } 60 | 61 | // Get store handler 62 | st, err := getClientStore() 63 | if err != nil { 64 | return err 65 | } 66 | 67 | // Get identifier 68 | name := sanitize.Name(args[0]) 69 | log.Info("adding new service") 70 | log.Debugf("retrieving entry with reference name: %s", name) 71 | id, err := st.Get(name) 72 | if err != nil { 73 | return fmt.Errorf("no available record under the provided reference name: %s", name) 74 | } 75 | 76 | // Validate service data 77 | log.Debug("validating parameters") 78 | svc := &did.ServiceEndpoint{ 79 | ID: viper.GetString("service-add.name"), 80 | Type: viper.GetString("service-add.type"), 81 | Endpoint: viper.GetString("service-add.endpoint"), 82 | } 83 | if strings.Count(svc.ID, "#") > 1 { 84 | return errors.New("invalid service name") 85 | } 86 | if strings.Count(svc.ID, "#") == 1 { 87 | svc.ID = strings.Replace(svc.ID, "#", fmt.Sprintf("%d", len(id.Services())+1), 1) 88 | } 89 | svc.ID = sanitize.Name(svc.ID) 90 | if _, err = url.ParseRequestURI(svc.Endpoint); err != nil { 91 | return fmt.Errorf("invalid service endpoint: %s", svc.Endpoint) 92 | } 93 | 94 | // Add service 95 | log.Debugf("registering service with id: %s", svc.ID) 96 | if err = id.AddService(svc); err != nil { 97 | return fmt.Errorf("failed to add new service: %w", err) 98 | } 99 | 100 | // Update record 101 | log.Info("updating local record") 102 | return st.Update(name, id) 103 | } 104 | -------------------------------------------------------------------------------- /client/cli/cmd/service_remove.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/kennygrant/sanitize" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | var removeServiceCmd = &cobra.Command{ 12 | Use: "remove", 13 | Short: "Remove an existing service entry for the DID", 14 | Example: "algoid edit service remove [DID reference name] [service name]", 15 | Aliases: []string{"rm"}, 16 | RunE: runRemoveServiceCmd, 17 | } 18 | 19 | func init() { 20 | serviceCmd.AddCommand(removeServiceCmd) 21 | } 22 | 23 | func runRemoveServiceCmd(_ *cobra.Command, args []string) error { 24 | if len(args) != 2 { 25 | return errors.New("you must specify [DID reference name] [service name]") 26 | } 27 | 28 | // Get store handler 29 | st, err := getClientStore() 30 | if err != nil { 31 | return err 32 | } 33 | 34 | // Get identifier 35 | name := sanitize.Name(args[0]) 36 | log.Info("removing existing service") 37 | log.Debugf("retrieving entry with reference name: %s", name) 38 | id, err := st.Get(name) 39 | if err != nil { 40 | return fmt.Errorf("no available record under the provided reference name: %s", name) 41 | } 42 | 43 | // Remove service 44 | sName := sanitize.Name(args[1]) 45 | log.Debugf("deleting service with name: %s", sName) 46 | if err = id.RemoveService(sName); err != nil { 47 | return fmt.Errorf("failed to remove service: %s", sName) 48 | } 49 | 50 | // Update record 51 | log.Info("updating local record") 52 | return st.Update(name, id) 53 | } 54 | -------------------------------------------------------------------------------- /client/cli/cmd/sync.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | ac "github.com/algorand/go-algorand-sdk/v2/crypto" 8 | "github.com/algorand/go-algorand-sdk/v2/mnemonic" 9 | "github.com/kennygrant/sanitize" 10 | "github.com/spf13/cobra" 11 | "github.com/spf13/viper" 12 | "go.bryk.io/pkg/cli" 13 | ) 14 | 15 | var syncCmd = &cobra.Command{ 16 | Use: "sync", 17 | Short: "Publish a DID instance to the processing network", 18 | Example: "algoid sync [did-name] [wallet-name]", 19 | Aliases: []string{"publish", "update", "upload", "push"}, 20 | RunE: runSyncCmd, 21 | } 22 | 23 | func init() { 24 | params := []cli.Param{ 25 | { 26 | Name: "delete", 27 | Usage: "mark the DID instance as deleted", 28 | FlagKey: "sync.delete", 29 | ByDefault: false, 30 | Short: "d", 31 | }, 32 | } 33 | if err := cli.SetupCommandParams(syncCmd, params, viper.GetViper()); err != nil { 34 | panic(err) 35 | } 36 | rootCmd.AddCommand(syncCmd) 37 | } 38 | 39 | func runSyncCmd(_ *cobra.Command, args []string) error { 40 | if len(args) == 0 { 41 | return errors.New("missing required parameters") 42 | } 43 | 44 | // Get store handler 45 | st, err := getClientStore() 46 | if err != nil { 47 | return err 48 | } 49 | 50 | // Retrieve identifier 51 | didName := sanitize.Name(args[0]) 52 | 53 | walletName := didName 54 | 55 | if len(args) > 1 { 56 | walletName = sanitize.Name(args[1]) 57 | } 58 | 59 | id, err := st.Get(didName) 60 | if err != nil { 61 | return fmt.Errorf("no available record under reference name: %s", didName) 62 | } 63 | 64 | // Get wallet account 65 | wp, err := readSecretValue("enter wallet's passphrase") 66 | if err != nil { 67 | return err 68 | } 69 | 70 | // Decrypt wallet 71 | seed, err := st.OpenWallet(walletName, wp) 72 | if err != nil { 73 | return err 74 | } 75 | 76 | // Restore account handler 77 | key, err := mnemonic.ToPrivateKey(seed) 78 | if err != nil { 79 | return err 80 | } 81 | account, err := ac.AccountFromPrivateKey(key) 82 | if err != nil { 83 | return err 84 | } 85 | 86 | // Get network client 87 | cl, err := getAlgoClient() 88 | if err != nil { 89 | return err 90 | } 91 | 92 | // Submit request 93 | log.Info("submitting request to the network") 94 | if viper.GetBool("sync.delete") { 95 | log.Infof("deleting: %s", didName) 96 | if err = cl.DeleteDID(id, &account); err != nil { 97 | return err 98 | } 99 | log.Info("DID instance deleted") 100 | return nil 101 | } 102 | log.Infof("publishing: %s", didName) 103 | if err := cl.PublishDID(id, &account); err != nil { 104 | return err 105 | } 106 | log.Info("DID instance published") 107 | return nil 108 | } 109 | -------------------------------------------------------------------------------- /client/cli/cmd/ui.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "net/http" 5 | "os" 6 | "os/exec" 7 | "syscall" 8 | 9 | "github.com/algorandfoundation/did-algo/client/ui" 10 | "github.com/spf13/cobra" 11 | "go.bryk.io/pkg/cli" 12 | xhttp "go.bryk.io/pkg/net/http" 13 | ) 14 | 15 | var uiCmdDesc = ` 16 | Graphical client. 17 | 18 | Starts a local graphical user interface that can be used 19 | to create and manage local identifiers and connect your 20 | wallet for more advanced features.` 21 | 22 | var uiCmd = &cobra.Command{ 23 | Use: "ui", 24 | Aliases: []string{"gui"}, 25 | Short: "Start the local graphical client", 26 | RunE: runLocalUI, 27 | Long: uiCmdDesc, 28 | } 29 | 30 | func init() { 31 | rootCmd.AddCommand(uiCmd) 32 | } 33 | 34 | func runLocalUI(_ *cobra.Command, _ []string) error { 35 | // Get store handler 36 | st, err := getClientStore() 37 | if err != nil { 38 | return err 39 | } 40 | 41 | // Get network client 42 | cl, err := getAlgoClient() 43 | if err != nil { 44 | return err 45 | } 46 | 47 | // Local API server 48 | srv, err := ui.LocalAPIServer(st, cl, log) 49 | if err != nil { 50 | return err 51 | } 52 | log.Info("starting local API server") 53 | go func() { 54 | _ = srv.Start() 55 | }() 56 | 57 | log.Info("starting local app server") 58 | mux := http.NewServeMux() 59 | mux.Handle("/", http.FileServer(http.FS(ui.AppContents))) 60 | appSrv, _ := xhttp.NewServer(xhttp.WithHandler(mux), xhttp.WithPort(8080)) 61 | go func() { 62 | _ = appSrv.Start() 63 | }() 64 | if err = exec.Command("open", "http://localhost:8080/").Run(); err != nil { 65 | log.Info("open: http://localhost:8080/") 66 | } 67 | 68 | // Wait for system signals 69 | log.Info("waiting for incoming requests") 70 | <-cli.SignalsHandler([]os.Signal{ 71 | syscall.SIGHUP, 72 | syscall.SIGINT, 73 | syscall.SIGTERM, 74 | syscall.SIGQUIT, 75 | }) 76 | 77 | // Close handler 78 | log.Info("stopping local API server") 79 | _ = appSrv.Stop(true) 80 | return srv.Stop() 81 | } 82 | -------------------------------------------------------------------------------- /client/cli/cmd/utils.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/algorandfoundation/did-algo/client/internal" 7 | "github.com/algorandfoundation/did-algo/client/store" 8 | "github.com/spf13/viper" 9 | ) 10 | 11 | // When reading contents from standard input a maximum of 4MB is expected. 12 | const maxPipeInputSize = 4096 13 | 14 | // Accessor to the local storage handler. 15 | func getClientStore() (*store.LocalStore, error) { 16 | return store.NewLocalStore(viper.GetString("home")) 17 | } 18 | 19 | // Retrieve the application identifier for the active network profile. 20 | func getStorageAppID(network string) (uint, error) { 21 | conf := new(internal.ClientSettings) 22 | if err := viper.UnmarshalKey("network", &conf); err != nil { 23 | return 0, err 24 | } 25 | var profile *internal.NetworkProfile 26 | for _, p := range conf.Profiles { 27 | if p.Name == network { 28 | profile = p 29 | break 30 | } 31 | } 32 | if profile == nil { 33 | return 0, fmt.Errorf("no active profile found") 34 | } 35 | return profile.AppID, nil 36 | } 37 | 38 | // Get network client instance. 39 | func getAlgoClient() (*internal.AlgoDIDClient, error) { 40 | conf := new(internal.ClientSettings) 41 | if err := viper.UnmarshalKey("network", &conf); err != nil { 42 | return nil, err 43 | } 44 | 45 | return internal.NewAlgoClient(conf.Profiles, log) 46 | } 47 | -------------------------------------------------------------------------------- /client/cli/cmd/version.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "runtime" 6 | "time" 7 | 8 | "github.com/algorandfoundation/did-algo/info" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | var versionCmd = &cobra.Command{ 13 | Use: "version", 14 | Short: "Display version information", 15 | RunE: func(_ *cobra.Command, _ []string) error { 16 | var components = map[string]string{ 17 | "Version": info.CoreVersion, 18 | "Build code": info.BuildCode, 19 | "OS/Arch": fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH), 20 | "Go version": runtime.Version(), 21 | "Release": releaseCode(), 22 | } 23 | if info.BuildTimestamp != "" { 24 | rd, err := time.Parse(time.RFC3339, info.BuildTimestamp) 25 | if err == nil { 26 | components["Release Date"] = rd.Format(time.RFC822) 27 | } 28 | } 29 | for k, v := range components { 30 | fmt.Printf("\033[21;37m%-13s:\033[0m %s\n", k, v) 31 | } 32 | return nil 33 | }, 34 | } 35 | 36 | func init() { 37 | rootCmd.AddCommand(versionCmd) 38 | } 39 | 40 | func releaseCode() string { 41 | release := "algoid" 42 | if info.CoreVersion != "" { 43 | release += "@" + info.CoreVersion 44 | } 45 | if info.BuildCode != "" { 46 | release += "+" + info.BuildCode 47 | } 48 | return release 49 | } 50 | -------------------------------------------------------------------------------- /client/cli/cmd/wallet.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | ac "github.com/algorand/go-algorand-sdk/crypto" 7 | "github.com/algorand/go-algorand-sdk/mnemonic" 8 | "github.com/spf13/cobra" 9 | "go.bryk.io/pkg/cli" 10 | ) 11 | 12 | type algoDestination struct { 13 | Address string `json:"address"` 14 | Network string `json:"network"` 15 | Asset string `json:"asset"` 16 | } 17 | 18 | var walletCmd = &cobra.Command{ 19 | Use: "wallet", 20 | Short: "Manage your ALGO wallet(s)", 21 | } 22 | 23 | func init() { 24 | rootCmd.AddCommand(walletCmd) 25 | } 26 | 27 | func readValue(prompt string) (string, error) { 28 | var dest string 29 | fmt.Printf("%s: ", prompt) 30 | _, err := fmt.Scanln(&dest) 31 | return dest, err 32 | } 33 | 34 | func readSecretValue(prompt string) (string, error) { 35 | v, err := cli.ReadSecure(fmt.Sprintf("%s: ", prompt)) 36 | if err != nil { 37 | return "", err 38 | } 39 | return string(v), nil 40 | } 41 | 42 | func getWalletAddress(name, pass string) (string, error) { 43 | // Get local store handler 44 | store, err := getClientStore() 45 | if err != nil { 46 | return "", err 47 | } 48 | 49 | // Decrypt wallet 50 | seed, err := store.OpenWallet(name, pass) 51 | if err != nil { 52 | return "", err 53 | } 54 | 55 | // Restore account handler 56 | key, err := mnemonic.ToPrivateKey(seed) 57 | if err != nil { 58 | return "", err 59 | } 60 | account, err := ac.AccountFromPrivateKey(key) 61 | if err != nil { 62 | return "", err 63 | } 64 | 65 | // Return account address 66 | return account.Address.String(), nil 67 | } 68 | -------------------------------------------------------------------------------- /client/cli/cmd/wallet_connect.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/kennygrant/sanitize" 8 | "github.com/spf13/cobra" 9 | "github.com/spf13/viper" 10 | "go.bryk.io/pkg/cli" 11 | "go.bryk.io/pkg/did" 12 | ) 13 | 14 | const walletConnectCmdDesc = ` 15 | Connect your ALGO wallet to a DID. 16 | 17 | Connecting your ALGO wallet to a DID will allow other users to 18 | discover your ALGO address when resolving your identifier. 19 | 20 | Effectively connecting your ID to a highly secure and efficient 21 | payments channel. Additionally, your counterparties might also 22 | discover or request your credentials when/if required to perform 23 | certain transactions.` 24 | 25 | var walletConnectCmd = &cobra.Command{ 26 | Use: "connect", 27 | Aliases: []string{"link", "ln"}, 28 | RunE: runWalletConnectCmd, 29 | Example: "algoid wallet connect [wallet-name] [did-name]", 30 | Short: "Connect your ALGO wallet to a DID", 31 | Long: walletConnectCmdDesc, 32 | } 33 | 34 | func init() { 35 | params := []cli.Param{ 36 | { 37 | Name: "network", 38 | Usage: "Algorand network to use", 39 | FlagKey: "wallet-connect.network", 40 | ByDefault: "testnet", 41 | Short: "n", 42 | }, 43 | { 44 | Name: "asset", 45 | Usage: "Asset advertised for the connection", 46 | FlagKey: "wallet-connect.asset", 47 | ByDefault: "ALGO", 48 | Short: "a", 49 | }, 50 | } 51 | if err := cli.SetupCommandParams(walletConnectCmd, params, viper.GetViper()); err != nil { 52 | panic(err) 53 | } 54 | // walletCmd.AddCommand(walletConnectCmd) 55 | } 56 | 57 | func runWalletConnectCmd(_ *cobra.Command, args []string) error { 58 | if len(args) != 2 { 59 | return errors.New("missing required parameters") 60 | } 61 | 62 | // Get parameters 63 | name := sanitize.Name(args[0]) 64 | didName := sanitize.Name(args[1]) 65 | wp, err := readSecretValue("enter wallet's passphrase") 66 | if err != nil { 67 | return err 68 | } 69 | 70 | // Get wallet address 71 | address, err := getWalletAddress(name, wp) 72 | if err != nil { 73 | return err 74 | } 75 | 76 | // Get identifier instance 77 | store, err := getClientStore() 78 | if err != nil { 79 | return err 80 | } 81 | id, err := store.Get(didName) 82 | if err != nil { 83 | return fmt.Errorf("no available record under the provided reference name: %s", name) 84 | } 85 | 86 | // Get service entry 87 | svc := id.Service("algo-connect") 88 | if svc == nil { 89 | svc = getServiceEntry() 90 | } 91 | 92 | // Retrieve existing address list and add new one 93 | var addresses []algoDestination 94 | ext := did.Extension{ 95 | ID: "algo-address", 96 | Version: "0.1.0", 97 | } 98 | if err := svc.GetExtension(ext.ID, ext.Version, &addresses); err != nil { 99 | return err 100 | } 101 | for _, entry := range addresses { 102 | if entry.Address == address { 103 | return fmt.Errorf("address already linked to DID: %s", address) 104 | } 105 | } 106 | addresses = append(addresses, algoDestination{ 107 | Address: address, 108 | Network: viper.GetString("wallet-connect.network"), 109 | Asset: viper.GetString("wallet-connect.asset"), 110 | }) 111 | ext.Data = addresses 112 | svc.AddExtension(ext) 113 | 114 | // Update service entry 115 | _ = id.RemoveService("algo-connect") 116 | if err := id.AddService(svc); err != nil { 117 | return err 118 | } 119 | 120 | // Register custom context 121 | id.RegisterContext("https://did.algorand.foundation/v1") 122 | 123 | // Update record 124 | log.Info("updating local DID record") 125 | return store.Update(didName, id) 126 | } 127 | 128 | func getServiceEntry() *did.ServiceEndpoint { 129 | return &did.ServiceEndpoint{ 130 | ID: "algo-connect", 131 | Type: "AlgorandExternalService", 132 | Endpoint: "https://did.algorand.foundation", 133 | Extensions: []did.Extension{ 134 | { 135 | ID: "algo-address", 136 | Version: "0.1.0", 137 | Data: []algoDestination{}, 138 | }, 139 | }, 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /client/cli/cmd/wallet_create.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | ac "github.com/algorand/go-algorand-sdk/v2/crypto" 8 | "github.com/algorand/go-algorand-sdk/v2/mnemonic" 9 | "github.com/kennygrant/sanitize" 10 | "github.com/spf13/cobra" 11 | xlog "go.bryk.io/pkg/log" 12 | ) 13 | 14 | var walletCreateCmd = &cobra.Command{ 15 | Use: "create", 16 | Aliases: []string{"new"}, 17 | Short: "Create a new (standalone) ALGO wallet", 18 | Example: "algoid wallet new [wallet-name]", 19 | RunE: func(_ *cobra.Command, args []string) error { 20 | // Get parameters 21 | if len(args) != 1 { 22 | return errors.New("you must provide a name for your wallet") 23 | } 24 | name := sanitize.Name(args[0]) 25 | wp, err := readSecretValue("enter a secure passphrase for your new wallet") 26 | if err != nil { 27 | return err 28 | } 29 | confirmation, err := readSecretValue("confirm your passphrase") 30 | if err != nil { 31 | return err 32 | } 33 | if wp != confirmation { 34 | return errors.New("passphrase confirmation failed") 35 | } 36 | 37 | // Get local store handler 38 | store, err := getClientStore() 39 | if err != nil { 40 | return err 41 | } 42 | 43 | // Verify this won't replace an existing wallet 44 | if store.WalletExists(name) { 45 | return fmt.Errorf("a wallet named: '%s' already exists", name) 46 | } 47 | 48 | // Create new account and securely store wallet contents 49 | account := ac.GenerateAccount() 50 | seed, err := mnemonic.FromPrivateKey(account.PrivateKey) 51 | if err != nil { 52 | return err 53 | } 54 | if err := store.SaveWallet(name, seed, wp); err != nil { 55 | return err 56 | } 57 | log.WithFields(xlog.Fields{ 58 | "name": name, 59 | "address": account.Address.String(), 60 | }).Info("new wallet created") 61 | return nil 62 | }, 63 | } 64 | 65 | func init() { 66 | walletCmd.AddCommand(walletCreateCmd) 67 | } 68 | -------------------------------------------------------------------------------- /client/cli/cmd/wallet_delete.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/kennygrant/sanitize" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | const walletDeleteCmdDesc = ` 11 | Permanently delete an ALGO wallet. 12 | 13 | This operation cannot be undone and might result in permanent 14 | loss of funds. Use with EXTREME care. 15 | 16 | If you are trying to move a wallet handler to a different system 17 | be sure to first 'export' it to generate a backup file containing 18 | the mnemonic that can be used later to 'restore' the wallet.` 19 | 20 | var walletDeleteCmd = &cobra.Command{ 21 | Use: "delete", 22 | Aliases: []string{"del", "rm"}, 23 | Example: "algoid wallet delete [wallet-name]", 24 | Short: "Permanently delete an ALGO wallet", 25 | Long: walletDeleteCmdDesc, 26 | RunE: func(_ *cobra.Command, args []string) error { 27 | if len(args) != 1 { 28 | return errors.New("you must specify the wallet name") 29 | } 30 | 31 | // Get parameters 32 | name := sanitize.Name(args[0]) 33 | wp, err := readSecretValue("enter wallet's passphrase") 34 | if err != nil { 35 | return err 36 | } 37 | 38 | // Verify wallet parameters 39 | store, err := getClientStore() 40 | if err != nil { 41 | return err 42 | } 43 | _, err = store.OpenWallet(name, wp) 44 | if err != nil { 45 | return err 46 | } 47 | 48 | // Get user confirmation 49 | confirmation, err := readValue("this action cannot be undone, are you sure (y/N)") 50 | if err != nil { 51 | return err 52 | } 53 | if confirmation != "y" { 54 | return errors.New("invalid confirmation value, canceling operation") 55 | } 56 | 57 | // Run delete operation 58 | if err = store.DeleteWallet(name); err != nil { 59 | return err 60 | } 61 | log.Infof("wallet '%s' was permanently deleted", name) 62 | return nil 63 | }, 64 | } 65 | 66 | func init() { 67 | walletCmd.AddCommand(walletDeleteCmd) 68 | } 69 | -------------------------------------------------------------------------------- /client/cli/cmd/wallet_disconnect.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/kennygrant/sanitize" 8 | "github.com/spf13/cobra" 9 | "go.bryk.io/pkg/did" 10 | ) 11 | 12 | var walletDisconnectCmd = &cobra.Command{ 13 | Use: "disconnect", 14 | Aliases: []string{"unlink"}, 15 | Short: "Remove a linked ALGO address from your DID", 16 | Example: "algoid wallet disconnect [did-name] [algo-address]", 17 | RunE: func(_ *cobra.Command, args []string) error { 18 | if len(args) != 2 { 19 | return errors.New("missing required parameters") 20 | } 21 | 22 | // Get parameters 23 | didName := sanitize.Name(args[0]) 24 | addr := args[1] 25 | 26 | // Get identifier instance 27 | store, err := getClientStore() 28 | if err != nil { 29 | return err 30 | } 31 | id, err := store.Get(didName) 32 | if err != nil { 33 | return fmt.Errorf("no available record under the provided reference name: %s", didName) 34 | } 35 | 36 | // Get service entry 37 | svc := id.Service("algo-connect") 38 | if svc == nil { 39 | return fmt.Errorf("no ALGO address linked to the DID") 40 | } 41 | 42 | // Get list of linked address 43 | var oldList []algoDestination 44 | ext := did.Extension{ 45 | ID: "algo-address", 46 | Version: "0.1.0", 47 | } 48 | if err := svc.GetExtension(ext.ID, ext.Version, &oldList); err != nil { 49 | return err 50 | } 51 | 52 | // Filter out provided address 53 | var newList []algoDestination 54 | for _, entry := range oldList { 55 | if entry.Address != addr { 56 | newList = append(newList, entry) 57 | } 58 | } 59 | ext.Data = newList 60 | 61 | // Update service entry 62 | for i, e := range svc.Extensions { 63 | if e.ID == ext.ID { 64 | svc.Extensions = append(svc.Extensions[:i], svc.Extensions[i+1:]...) 65 | break 66 | } 67 | } 68 | svc.AddExtension(ext) 69 | _ = id.RemoveService("algo-connect") 70 | if err = id.AddService(svc); err != nil { 71 | return err 72 | } 73 | 74 | // Update record 75 | log.Info("updating local DID record") 76 | if err = store.Update(didName, id); err != nil { 77 | return err 78 | } 79 | log.Info("address was successfully unlinked: ", addr) 80 | return nil 81 | }, 82 | } 83 | 84 | func init() { 85 | // walletCmd.AddCommand(walletDisconnectCmd) 86 | } 87 | -------------------------------------------------------------------------------- /client/cli/cmd/wallet_export.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/kennygrant/sanitize" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | var walletExportCmd = &cobra.Command{ 13 | Use: "export", 14 | Aliases: []string{"save"}, 15 | Example: "algoid wallet export [wallet-name]", 16 | Short: "Export wallet's master derivation key", 17 | Long: `Export wallet's master derivation key. 18 | 19 | A master derivation key can be exported as a mnemonic in 20 | order to allow a user to securely restore a wallet an its 21 | cryptographic material to prevent loosing access to the 22 | assets protected by it. 23 | 24 | The mnemonic will be automatically saved to a text file 25 | with the name "[wallet-name]-mnemonic.txt". 26 | 27 | Keep in mind that misplacing or sharing the mnemonic can 28 | result in catastrophic security issues and permanent loss 29 | of funds.`, 30 | RunE: func(_ *cobra.Command, args []string) error { 31 | // Get parameters 32 | if len(args) != 1 { 33 | return errors.New("missing required parameters") 34 | } 35 | name := sanitize.Name(args[0]) 36 | wp, err := readSecretValue("enter wallet's passphrase") 37 | if err != nil { 38 | return err 39 | } 40 | 41 | // Get local store handler 42 | store, err := getClientStore() 43 | if err != nil { 44 | return err 45 | } 46 | 47 | // Decrypt wallet 48 | seed, err := store.OpenWallet(name, wp) 49 | if err != nil { 50 | return err 51 | } 52 | 53 | // Save export file 54 | fileName := fmt.Sprintf("%s-mnemonic.txt", name) 55 | err = os.WriteFile(fileName, []byte(seed), 0400) 56 | if err != nil { 57 | return err 58 | } 59 | log.Infof("data stored in file: %s", fileName) 60 | return nil 61 | }, 62 | } 63 | 64 | func init() { 65 | walletCmd.AddCommand(walletExportCmd) 66 | } 67 | -------------------------------------------------------------------------------- /client/cli/cmd/wallet_info.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | ac "github.com/algorand/go-algorand-sdk/v2/crypto" 8 | "github.com/algorand/go-algorand-sdk/v2/mnemonic" 9 | "github.com/kennygrant/sanitize" 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | var walletInfoCmd = &cobra.Command{ 14 | Use: "info", 15 | Short: "Get account information", 16 | Example: "algoid wallet info [wallet-name] [network]", 17 | Aliases: []string{"details", "inspect", "more"}, 18 | RunE: func(_ *cobra.Command, args []string) error { 19 | // Get parameters 20 | if len(args) != 2 { 21 | return errors.New("missing required parameters") 22 | } 23 | name := sanitize.Name(args[0]) 24 | wp, err := readSecretValue("enter wallet's passphrase") 25 | if err != nil { 26 | return err 27 | } 28 | 29 | // Get local store handler 30 | store, err := getClientStore() 31 | if err != nil { 32 | return err 33 | } 34 | 35 | // Decrypt wallet 36 | seed, err := store.OpenWallet(name, wp) 37 | if err != nil { 38 | return err 39 | } 40 | 41 | // Restore account handler 42 | key, err := mnemonic.ToPrivateKey(seed) 43 | if err != nil { 44 | return err 45 | } 46 | account, err := ac.AccountFromPrivateKey(key) 47 | if err != nil { 48 | return err 49 | } 50 | 51 | // Get network client 52 | cl, err := getAlgoClient() 53 | if err != nil { 54 | return err 55 | } 56 | 57 | network := args[1] 58 | // Get account info 59 | info, err := cl.Networks[network].AccountInformation(account.Address.String()) 60 | if err != nil { 61 | return err 62 | } 63 | 64 | // Print results 65 | fmt.Printf("network: %s\n", network) 66 | fmt.Printf("address: %s\n", account.Address.String()) 67 | fmt.Printf("public key: %x\n", account.PublicKey) 68 | fmt.Printf("status: %s\n", info.Status) 69 | fmt.Printf("round: %d\n", info.Round) 70 | fmt.Printf("current balance: %d\n", info.Amount) 71 | fmt.Printf("pending rewards: %d\n", info.PendingRewards) 72 | fmt.Printf("total rewards: %d\n", info.Rewards) 73 | return nil 74 | }, 75 | } 76 | 77 | func init() { 78 | walletCmd.AddCommand(walletInfoCmd) 79 | } 80 | -------------------------------------------------------------------------------- /client/cli/cmd/wallet_list.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | var walletListCmd = &cobra.Command{ 8 | Use: "list", 9 | Aliases: []string{"ls"}, 10 | Short: "List your existing ALGO wallet(s)", 11 | RunE: func(_ *cobra.Command, _ []string) error { 12 | store, err := getClientStore() 13 | if err != nil { 14 | return err 15 | } 16 | list := store.ListWallets() 17 | if len(list) == 0 { 18 | log.Warning("no wallets found") 19 | return nil 20 | } 21 | for _, el := range list { 22 | log.Infof("wallet found: %s", el) 23 | } 24 | return nil 25 | }, 26 | } 27 | 28 | func init() { 29 | walletCmd.AddCommand(walletListCmd) 30 | } 31 | -------------------------------------------------------------------------------- /client/cli/cmd/wallet_pay.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "encoding/base64" 5 | "strconv" 6 | 7 | ac "github.com/algorand/go-algorand-sdk/v2/crypto" 8 | "github.com/algorand/go-algorand-sdk/v2/mnemonic" 9 | "github.com/algorand/go-algorand-sdk/v2/transaction" 10 | "github.com/kennygrant/sanitize" 11 | "github.com/spf13/cobra" 12 | "github.com/spf13/viper" 13 | "go.bryk.io/pkg/cli" 14 | ) 15 | 16 | var walletPayCmd = &cobra.Command{ 17 | Use: "pay", 18 | Short: "Create and submit a new transaction", 19 | Aliases: []string{"txn", "send"}, 20 | Example: "algoid wallet pay [wallet-name] [network]", 21 | RunE: runWalletPayCmd, 22 | } 23 | 24 | func init() { 25 | params := []cli.Param{ 26 | { 27 | Name: "to", 28 | Usage: "Receiver address", 29 | FlagKey: "tx.to", 30 | ByDefault: "", 31 | Short: "r", 32 | }, 33 | { 34 | Name: "amount", 35 | Usage: "Transaction amount", 36 | FlagKey: "tx.amount", 37 | ByDefault: 0, 38 | Short: "a", 39 | }, 40 | { 41 | Name: "submit", 42 | Usage: "Submit transaction to the network (based on your active profile)", 43 | FlagKey: "tx.submit", 44 | ByDefault: false, 45 | Short: "s", 46 | }, 47 | } 48 | if err := cli.SetupCommandParams(walletPayCmd, params, viper.GetViper()); err != nil { 49 | panic(err) 50 | } 51 | walletCmd.AddCommand(walletPayCmd) 52 | } 53 | 54 | func runWalletPayCmd(_ *cobra.Command, args []string) (err error) { 55 | // Get parameters 56 | wallet, receiver, amount, err := getTxParameters(args) 57 | if err != nil { 58 | return err 59 | } 60 | wp, err := readSecretValue("enter wallet's passphrase") 61 | if err != nil { 62 | return err 63 | } 64 | 65 | // Get local store handler 66 | store, err := getClientStore() 67 | if err != nil { 68 | return err 69 | } 70 | 71 | // Decrypt wallet 72 | seed, err := store.OpenWallet(wallet, wp) 73 | if err != nil { 74 | return err 75 | } 76 | 77 | // Restore account handler 78 | key, err := mnemonic.ToPrivateKey(seed) 79 | if err != nil { 80 | return err 81 | } 82 | account, err := ac.AccountFromPrivateKey(key) 83 | if err != nil { 84 | return err 85 | } 86 | 87 | // Get network client 88 | cl, err := getAlgoClient() 89 | if err != nil { 90 | return err 91 | } 92 | 93 | network := args[1] 94 | // Get transaction parameters 95 | params, _ := cl.Networks[network].SuggestedParams() 96 | 97 | // Get sender address 98 | sender := account.Address.String() 99 | 100 | // Build transaction 101 | tx, err := transaction.MakePaymentTxn(sender, receiver, uint64(amount), nil, "", params) 102 | if err != nil { 103 | return err 104 | } 105 | 106 | // Sign transaction 107 | log.Debug("signing transaction") 108 | _, stx, err := ac.SignTransaction(key, tx) 109 | if err != nil { 110 | return err 111 | } 112 | if !viper.GetBool("tx.submit") { 113 | log.Infof("signed transaction: %s", base64.StdEncoding.EncodeToString(stx)) 114 | return nil 115 | } 116 | 117 | // Submit transaction 118 | log.Debug("submitting signed transaction") 119 | txID, err := cl.Networks[network].SubmitTx(stx) 120 | if err != nil { 121 | return err 122 | } 123 | log.Infof("transaction successfully submitted with id: %s", txID) 124 | return nil 125 | } 126 | 127 | func getTxParameters(args []string) (wallet, receiver string, amount int, err error) { 128 | // Wallet name 129 | if len(args) != 1 { 130 | wallet, err = readValue("enter wallet name") 131 | if err != nil { 132 | return "", "", 0, err 133 | } 134 | } else { 135 | wallet = args[0] 136 | } 137 | wallet = sanitize.Name(wallet) 138 | 139 | // Receiver address 140 | receiver = viper.GetString("tx.to") 141 | if receiver == "" { 142 | if receiver, err = readValue("enter receiver address"); err != nil { 143 | return "", "", 0, err 144 | } 145 | } 146 | 147 | // Tx amount 148 | amount = viper.GetInt("tx.amount") 149 | if amount == 0 { 150 | a, err := readValue("enter amount to send") 151 | if err != nil { 152 | return "", "", 0, err 153 | } 154 | amount, err = strconv.Atoi(a) 155 | if err != nil { 156 | return "", "", 0, err 157 | } 158 | } 159 | return wallet, receiver, amount, nil 160 | } 161 | -------------------------------------------------------------------------------- /client/cli/cmd/wallet_rename.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/kennygrant/sanitize" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | var walletRenameCmd = &cobra.Command{ 12 | Use: "rename", 13 | Short: "Rename an existing ALGO wallet", 14 | Example: "algoid wallet rename [current-name] [new-name]", 15 | Aliases: []string{"mv"}, 16 | RunE: func(_ *cobra.Command, args []string) error { 17 | // Get parameters 18 | if len(args) != 2 { 19 | return errors.New("missing required parameters") 20 | } 21 | currentName := sanitize.Name(args[0]) 22 | newName := sanitize.Name(args[1]) 23 | 24 | // Get local store handler 25 | store, err := getClientStore() 26 | if err != nil { 27 | return err 28 | } 29 | 30 | // Verify this won't replace an existing wallet 31 | if store.WalletExists(newName) { 32 | return fmt.Errorf("a wallet named: '%s' already exists", newName) 33 | } 34 | 35 | // Rename wallet 36 | if err := store.RenameWallet(currentName, newName); err != nil { 37 | return err 38 | } 39 | log.Info("wallet renamed successfully") 40 | return nil 41 | }, 42 | } 43 | 44 | func init() { 45 | // walletCmd.AddCommand(walletRenameCmd) 46 | } 47 | -------------------------------------------------------------------------------- /client/cli/cmd/wallet_restore.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | 9 | ac "github.com/algorand/go-algorand-sdk/v2/crypto" 10 | "github.com/algorand/go-algorand-sdk/v2/mnemonic" 11 | "github.com/kennygrant/sanitize" 12 | "github.com/spf13/cobra" 13 | xlog "go.bryk.io/pkg/log" 14 | ) 15 | 16 | var walletRestoreCmd = &cobra.Command{ 17 | Use: "restore", 18 | Short: "Restore a wallet using an existing mnemonic file", 19 | Aliases: []string{"recover"}, 20 | Example: "algoid wallet restore [mnemonic-file]", 21 | RunE: func(_ *cobra.Command, args []string) error { 22 | // Get parameters 23 | if len(args) != 1 { 24 | return errors.New("missing required parameters") 25 | } 26 | 27 | // Verify mnemonic file corresponds to a valid private key 28 | wp, err := os.ReadFile(filepath.Clean(args[0])) 29 | if err != nil { 30 | return fmt.Errorf("failed to read mnemonic file: %w", err) 31 | } 32 | key, err := mnemonic.ToPrivateKey(string(wp)) 33 | if err != nil { 34 | return err 35 | } 36 | account, err := ac.AccountFromPrivateKey(key) 37 | if err != nil { 38 | return err 39 | } 40 | 41 | // Get local store handler 42 | store, err := getClientStore() 43 | if err != nil { 44 | return err 45 | } 46 | 47 | // Ask wallet parameters 48 | name, err := readValue("enter a name for your wallet") 49 | if err != nil { 50 | return err 51 | } 52 | name = sanitize.Name(name) 53 | pass, err := readSecretValue("enter a secure passphrase for your wallet") 54 | if err != nil { 55 | return err 56 | } 57 | 58 | // Verify this won't replace an existing wallet 59 | if store.WalletExists(name) { 60 | return fmt.Errorf("a wallet named: '%s' already exists", name) 61 | } 62 | 63 | // Save wallet file 64 | if err := store.SaveWallet(name, string(wp), pass); err != nil { 65 | return err 66 | } 67 | log.WithFields(xlog.Fields{ 68 | "name": name, 69 | "address": account.Address.String(), 70 | }).Info("new wallet created") 71 | return nil 72 | }, 73 | } 74 | 75 | func init() { 76 | walletCmd.AddCommand(walletRestoreCmd) 77 | } 78 | -------------------------------------------------------------------------------- /client/cli/doc.go: -------------------------------------------------------------------------------- 1 | // Package main provides the client CLI application entrypoint. 2 | package main 3 | -------------------------------------------------------------------------------- /client/cli/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/algorandfoundation/did-algo/client/cli/cmd" 4 | 5 | func main() { 6 | cmd.Execute() 7 | } 8 | -------------------------------------------------------------------------------- /client/internal/contracts.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "embed" 5 | "encoding/json" 6 | "io" 7 | "io/fs" 8 | 9 | "github.com/algorand/go-algorand-sdk/v2/abi" 10 | ) 11 | 12 | // References: 13 | // https://developer.algorand.org/docs/get-details/encoding/ 14 | 15 | // StorageContracts contains the pre-compiled smart contracts to 16 | // support AlgoDID's on-chain storage. 17 | var StorageContracts fs.FS 18 | 19 | //go:embed contracts 20 | var dist embed.FS 21 | 22 | var ( 23 | approvalTeal []byte 24 | clearTeal []byte 25 | ) 26 | 27 | func init() { 28 | StorageContracts, _ = fs.Sub(dist, "contracts") 29 | 30 | // load approval program 31 | approvalFile, err := StorageContracts.Open("AlgoDID.approval.teal") 32 | if err != nil { 33 | panic(err) 34 | } 35 | approvalTeal, err = io.ReadAll(approvalFile) 36 | if err != nil { 37 | panic(err) 38 | } 39 | _ = approvalFile.Close() 40 | 41 | // load clear program 42 | clearFile, err := StorageContracts.Open("AlgoDID.clear.teal") 43 | if err != nil { 44 | panic(err) 45 | } 46 | clearTeal, err = io.ReadAll(clearFile) 47 | if err != nil { 48 | panic(err) 49 | } 50 | _ = clearFile.Close() 51 | } 52 | 53 | // LoadContract loads the AlgoDID smart contract ABI from JSON file. 54 | func LoadContract() *abi.Contract { 55 | abiFile, _ := StorageContracts.Open("AlgoDID.abi.json") 56 | abiContents, _ := io.ReadAll(abiFile) 57 | contract := &abi.Contract{} 58 | _ = json.Unmarshal(abiContents, contract) 59 | _ = abiFile.Close() 60 | return contract 61 | } 62 | -------------------------------------------------------------------------------- /client/internal/contracts/AlgoDID.abi.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "AlgoDID", 3 | "desc": "", 4 | "methods": [ 5 | { 6 | "name": "startUpload", 7 | "args": [ 8 | { 9 | "name": "pubKey", 10 | "type": "address", 11 | "desc": "The pubkey of the DID" 12 | }, 13 | { 14 | "name": "numBoxes", 15 | "type": "uint64", 16 | "desc": "The number of boxes that the data will take up" 17 | }, 18 | { 19 | "name": "endBoxSize", 20 | "type": "uint64", 21 | "desc": "The size of the last box" 22 | }, 23 | { 24 | "name": "mbrPayment", 25 | "type": "pay", 26 | "desc": "Payment from the uploader to cover the box MBR" 27 | } 28 | ], 29 | "desc": "Allocate boxes to begin data upload process", 30 | "returns": { 31 | "type": "void", 32 | "desc": "" 33 | } 34 | }, 35 | { 36 | "name": "upload", 37 | "args": [ 38 | { 39 | "name": "pubKey", 40 | "type": "address", 41 | "desc": "The pubkey of the DID" 42 | }, 43 | { 44 | "name": "boxIndex", 45 | "type": "uint64", 46 | "desc": "The index of the box to upload the given chunk of data to" 47 | }, 48 | { 49 | "name": "offset", 50 | "type": "uint64", 51 | "desc": "The offset within the box to start writing the data" 52 | }, 53 | { 54 | "name": "data", 55 | "type": "byte[]", 56 | "desc": "The data to write" 57 | } 58 | ], 59 | "desc": "Upload data to a specific offset in a box", 60 | "returns": { 61 | "type": "void", 62 | "desc": "" 63 | } 64 | }, 65 | { 66 | "name": "finishUpload", 67 | "args": [ 68 | { 69 | "name": "pubKey", 70 | "type": "address", 71 | "desc": "The address of the DID" 72 | } 73 | ], 74 | "desc": "Mark uploading as false", 75 | "returns": { 76 | "type": "void", 77 | "desc": "" 78 | } 79 | }, 80 | { 81 | "name": "startDelete", 82 | "args": [ 83 | { 84 | "name": "pubKey", 85 | "type": "address", 86 | "desc": "The address of the DID" 87 | } 88 | ], 89 | "desc": "Starts the deletion process for the data associated with a DID", 90 | "returns": { 91 | "type": "void", 92 | "desc": "" 93 | } 94 | }, 95 | { 96 | "name": "deleteData", 97 | "args": [ 98 | { 99 | "name": "pubKey", 100 | "type": "address", 101 | "desc": "The address of the DID" 102 | }, 103 | { 104 | "name": "boxIndex", 105 | "type": "uint64", 106 | "desc": "The index of the box to delete" 107 | } 108 | ], 109 | "desc": "Deletes a box of data", 110 | "returns": { 111 | "type": "void", 112 | "desc": "" 113 | } 114 | }, 115 | { 116 | "name": "updateApplication", 117 | "args": [], 118 | "desc": "Allow the contract to be updated by the creator", 119 | "returns": { 120 | "type": "void", 121 | "desc": "" 122 | } 123 | }, 124 | { 125 | "name": "dummy", 126 | "args": [], 127 | "desc": "Dummy function to add extra box references for deleteData.Boxes are 32k, but a single app call can only include enough references to read/write 8kat a time. Thus when a box is deleted, we need to add additional dummy calls with boxreferences to increase the total read/write budget to 32k.", 128 | "returns": { 129 | "type": "void", 130 | "desc": "" 131 | } 132 | }, 133 | { 134 | "name": "createApplication", 135 | "desc": "", 136 | "returns": { 137 | "type": "void", 138 | "desc": "" 139 | }, 140 | "args": [] 141 | } 142 | ] 143 | } -------------------------------------------------------------------------------- /client/internal/contracts/AlgoDID.clear.teal: -------------------------------------------------------------------------------- 1 | #pragma version 9 2 | int 1 -------------------------------------------------------------------------------- /client/internal/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package internal provides private utilities for the client CLI application. 3 | */ 4 | package internal 5 | -------------------------------------------------------------------------------- /client/internal/driver.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "errors" 5 | 6 | "go.bryk.io/pkg/did" 7 | "go.bryk.io/pkg/did/resolver" 8 | ) 9 | 10 | // Read a DID document from the Algorand network. The method complies 11 | // with the `resolver.Provider` interface. 12 | func (c *AlgoDIDClient) Read(id string) (*did.Document, *did.DocumentMetadata, error) { 13 | if _, err := did.Parse(id); err != nil { 14 | return nil, nil, errors.New(resolver.ErrInvalidDID) 15 | } 16 | doc, err := c.Resolve(id) 17 | if err != nil { 18 | return nil, nil, errors.New(resolver.ErrNotFound) 19 | } 20 | md := new(did.DocumentMetadata) 21 | md.Deactivated = false 22 | return doc, md, nil 23 | } 24 | -------------------------------------------------------------------------------- /client/store/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package store provides a sample storage handler based on local-disk. 3 | */ 4 | package store 5 | -------------------------------------------------------------------------------- /client/ui/api.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/algorandfoundation/did-algo/client/internal" 7 | "github.com/algorandfoundation/did-algo/client/store" 8 | xlog "go.bryk.io/pkg/log" 9 | xhttp "go.bryk.io/pkg/net/http" 10 | mdCors "go.bryk.io/pkg/net/middleware/cors" 11 | mdGzip "go.bryk.io/pkg/net/middleware/gzip" 12 | mdLogging "go.bryk.io/pkg/net/middleware/logging" 13 | mdRecovery "go.bryk.io/pkg/net/middleware/recovery" 14 | ) 15 | 16 | // LocalAPI makes a local provider instance accessible through 17 | // an HTTP server. 18 | type LocalAPI struct { 19 | prv *Provider 20 | log xlog.Logger 21 | srv *xhttp.Server 22 | } 23 | 24 | // LocalAPIServer creates a new instance of the local API server. 25 | func LocalAPIServer(st *store.LocalStore, client *internal.AlgoDIDClient, log xlog.Logger) (*LocalAPI, error) { 26 | // provider instances 27 | p := &Provider{ 28 | st: st, 29 | log: log, 30 | client: client, 31 | } 32 | 33 | // server instance 34 | opts := []xhttp.Option{ 35 | xhttp.WithPort(9090), 36 | xhttp.WithHandler(p.ServerHandler()), 37 | xhttp.WithIdleTimeout(10 * time.Second), 38 | xhttp.WithMiddleware( 39 | mdLogging.Handler(log, nil), 40 | mdCors.Handler(mdCors.Options{ 41 | AllowedOrigins: []string{"*"}, 42 | AllowedMethods: []string{"GET", "POST", "OPTIONS"}, 43 | AllowedHeaders: []string{"content-type"}, 44 | }), 45 | mdGzip.Handler(5), 46 | mdRecovery.Handler(), 47 | ), 48 | } 49 | srv, err := xhttp.NewServer(opts...) 50 | if err != nil { 51 | _ = p.close() 52 | return nil, err 53 | } 54 | return &LocalAPI{ 55 | prv: p, 56 | log: log, 57 | srv: srv, 58 | }, nil 59 | } 60 | 61 | // Start the local API server. 62 | func (el *LocalAPI) Start() error { 63 | return el.srv.Start() 64 | } 65 | 66 | // Stop the local API server. 67 | func (el *LocalAPI) Stop() error { 68 | if err := el.prv.close(); err != nil { 69 | el.log.Warning(err.Error()) 70 | } 71 | return el.srv.Stop(true) 72 | } 73 | -------------------------------------------------------------------------------- /client/ui/app.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "embed" 5 | "io/fs" 6 | "path" 7 | ) 8 | 9 | // AppContents contains a static build of the local graphical 10 | // client. 11 | var AppContents fs.FS 12 | 13 | //go:embed local-app/dist 14 | var dist embed.FS 15 | 16 | func init() { 17 | AppContents, _ = fs.Sub(dist, path.Join("local-app", "dist")) 18 | } 19 | -------------------------------------------------------------------------------- /client/ui/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package ui provides a local GUI for the algoID application. 3 | */ 4 | package ui 5 | -------------------------------------------------------------------------------- /client/ui/local-app/.eslintignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /dist 5 | /.svelte-kit 6 | /package 7 | .env 8 | .env.* 9 | !.env.example 10 | 11 | # Ignore files for PNPM, NPM and YARN 12 | pnpm-lock.yaml 13 | package-lock.json 14 | yarn.lock 15 | -------------------------------------------------------------------------------- /client/ui/local-app/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | root: true, 4 | extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'], 5 | plugins: ['svelte3', '@typescript-eslint'], 6 | overrides: [ 7 | { 8 | files: ['*.svelte'], 9 | processor: 'svelte3/svelte3' 10 | } 11 | ], 12 | parserOptions: { 13 | sourceType: 'module', 14 | ecmaVersion: 2020 15 | }, 16 | settings: { 17 | 'svelte3/typescript': true 18 | }, 19 | env: { 20 | browser: true, 21 | es2017: true, 22 | node: true 23 | }, 24 | rules: { 25 | 'no-unused-vars': 'off', 26 | '@typescript-eslint/no-unused-vars': 'error' 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /client/ui/local-app/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | *.local 12 | 13 | # Editor directories and files 14 | .vscode/* 15 | !.vscode/extensions.json 16 | .idea 17 | .DS_Store 18 | *.suo 19 | *.ntvs* 20 | *.njsproj 21 | *.sln 22 | *.sw? 23 | -------------------------------------------------------------------------------- /client/ui/local-app/.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /client/ui/local-app/.prettierignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /dist 5 | /.svelte-kit 6 | /package 7 | .env 8 | .env.* 9 | !.env.example 10 | 11 | # Ignore files for PNPM, NPM and YARN 12 | pnpm-lock.yaml 13 | package-lock.json 14 | yarn.lock 15 | -------------------------------------------------------------------------------- /client/ui/local-app/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "svelteSortOrder": "options-scripts-markup-styles", 3 | "svelteStrictMode": false, 4 | "svelteAllowShorthand": true, 5 | "svelteIndentScriptAndStyle": true, 6 | "useTabs": false, 7 | "singleQuote": true, 8 | "bracketSameLine": true, 9 | "trailingComma": "none", 10 | "printWidth": 90, 11 | "plugins": [ 12 | "prettier-plugin-svelte", 13 | "prettier-plugin-organize-imports", 14 | "prettier-plugin-multiline-arrays", 15 | "prettier-plugin-tailwindcss" 16 | ], 17 | "pluginSearchDirs": ["."], 18 | "overrides": [ 19 | { 20 | "files": "*.svelte", 21 | "options": { 22 | "parser": "svelte" 23 | } 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /client/ui/local-app/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: * 2 | .DEFAULT_GOAL:=help 3 | 4 | help: 5 | @echo "Commands available" 6 | @sed -n 's/^##//p' ${MAKEFILE_LIST} | column -t -s ':' | sed -e 's/^/ /' | sort 7 | 8 | ## check: Verify code is properly formatted 9 | check: 10 | npm run format 11 | npm run lint 12 | npm run check 13 | 14 | ## build: Build the UI 15 | build: 16 | npm run build 17 | 18 | ## dev: Run the UI in development mode 19 | dev: 20 | npm run dev 21 | 22 | ## deps: Install dependencies (with pnpm) 23 | deps: 24 | pnpm i 25 | 26 | ## updates: List available updates for direct dependencies 27 | updates: 28 | pnpm outdated 29 | -------------------------------------------------------------------------------- /client/ui/local-app/README.md: -------------------------------------------------------------------------------- 1 | # AlgoID Connect UI (beta) 2 | 3 | This application provides a simple GUI for interacting with your local AlgoID 4 | Connect installation. The main features include: 5 | 6 | - Creating and managing identities 7 | - Connecting any WalletConnect-compatible wallet 8 | - Linkining identities to ALGO addresses 9 | 10 | To start the UI locally, run the following command: 11 | 12 | ```bash 13 | algoid ui 14 | ``` 15 | 16 | > Once started, the application will be available at `http://localhost:8080`. 17 | 18 | You can stop it at any time by pressing `Ctrl+C`. 19 | 20 | ![home screen](./home_screen.png) 21 | -------------------------------------------------------------------------------- /client/ui/local-app/dist/algorand.svg: -------------------------------------------------------------------------------- 1 | algorand-algo-logo -------------------------------------------------------------------------------- /client/ui/local-app/dist/at_logo_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algorandfoundation/did-algo/161acb19c5c4e61059dc9e99ee66f708f4419624/client/ui/local-app/dist/at_logo_128x128.png -------------------------------------------------------------------------------- /client/ui/local-app/dist/at_logo_192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algorandfoundation/did-algo/161acb19c5c4e61059dc9e99ee66f708f4419624/client/ui/local-app/dist/at_logo_192x192.png -------------------------------------------------------------------------------- /client/ui/local-app/dist/at_logo_512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algorandfoundation/did-algo/161acb19c5c4e61059dc9e99ee66f708f4419624/client/ui/local-app/dist/at_logo_512x512.png -------------------------------------------------------------------------------- /client/ui/local-app/dist/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algorandfoundation/did-algo/161acb19c5c4e61059dc9e99ee66f708f4419624/client/ui/local-app/dist/favicon-16x16.png -------------------------------------------------------------------------------- /client/ui/local-app/dist/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algorandfoundation/did-algo/161acb19c5c4e61059dc9e99ee66f708f4419624/client/ui/local-app/dist/favicon-32x32.png -------------------------------------------------------------------------------- /client/ui/local-app/dist/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algorandfoundation/did-algo/161acb19c5c4e61059dc9e99ee66f708f4419624/client/ui/local-app/dist/favicon.ico -------------------------------------------------------------------------------- /client/ui/local-app/dist/img/beams.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algorandfoundation/did-algo/161acb19c5c4e61059dc9e99ee66f708f4419624/client/ui/local-app/dist/img/beams.jpg -------------------------------------------------------------------------------- /client/ui/local-app/dist/img/grid.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /client/ui/local-app/dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | AlgoID Connect (beta) 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /client/ui/local-app/home_screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algorandfoundation/did-algo/161acb19c5c4e61059dc9e99ee66f708f4419624/client/ui/local-app/home_screen.png -------------------------------------------------------------------------------- /client/ui/local-app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | AlgoID Connect (beta) 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /client/ui/local-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "algoid-connect", 3 | "private": true, 4 | "version": "0.1.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "preview": "vite preview", 10 | "check": "svelte-check --tsconfig ./tsconfig.json", 11 | "format": "prettier --plugin-search-dir . --write .", 12 | "lint": "prettier --plugin-search-dir . --check . && eslint .", 13 | "test:unit": "vitest", 14 | "test:ui": "vitest --ui" 15 | }, 16 | "dependencies": { 17 | "@walletconnect/client": "^1.8.0", 18 | "algorand-walletconnect-qrcode-modal": "^1.8.0" 19 | }, 20 | "devDependencies": { 21 | "@sveltejs/vite-plugin-svelte": "^2.0.4", 22 | "@tsconfig/svelte": "^4.0.1", 23 | "@types/node": "^18.15.11", 24 | "@typescript-eslint/eslint-plugin": "^5.57.1", 25 | "@typescript-eslint/parser": "^5.57.1", 26 | "@vitest/ui": "^0.30.0", 27 | "@walletconnect/types": "^2.5.2", 28 | "autoprefixer": "^10.4.14", 29 | "eslint": "^8.38.0", 30 | "eslint-config-prettier": "^8.8.0", 31 | "eslint-plugin-svelte3": "^4.0.0", 32 | "jsdom": "^21.1.1", 33 | "postcss": ">=8.4.31", 34 | "prettier": "^2.8.7", 35 | "prettier-plugin-multiline-arrays": "^1.1.3", 36 | "prettier-plugin-organize-imports": "^3.2.2", 37 | "prettier-plugin-svelte": "^2.10.0", 38 | "prettier-plugin-tailwindcss": "^0.2.7", 39 | "prism-themes": "^1.9.0", 40 | "svelte": "^3.58.0", 41 | "svelte-check": "^3.2.0", 42 | "svelte-prism": "^1.1.6", 43 | "tailwindcss": "^3.3.1", 44 | "tough-cookie": ">=4.1.3", 45 | "tslib": "^2.5.0", 46 | "typescript": "^5.0.4", 47 | "vite": ">=4.2.3", 48 | "vite-plugin-node-polyfills": "^0.7.0", 49 | "vitest": "^0.30.0", 50 | "word-wrap": ">=1.2.5" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /client/ui/local-app/postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {} 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /client/ui/local-app/public/algorand.svg: -------------------------------------------------------------------------------- 1 | algorand-algo-logo -------------------------------------------------------------------------------- /client/ui/local-app/public/at_logo_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algorandfoundation/did-algo/161acb19c5c4e61059dc9e99ee66f708f4419624/client/ui/local-app/public/at_logo_128x128.png -------------------------------------------------------------------------------- /client/ui/local-app/public/at_logo_192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algorandfoundation/did-algo/161acb19c5c4e61059dc9e99ee66f708f4419624/client/ui/local-app/public/at_logo_192x192.png -------------------------------------------------------------------------------- /client/ui/local-app/public/at_logo_512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algorandfoundation/did-algo/161acb19c5c4e61059dc9e99ee66f708f4419624/client/ui/local-app/public/at_logo_512x512.png -------------------------------------------------------------------------------- /client/ui/local-app/public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algorandfoundation/did-algo/161acb19c5c4e61059dc9e99ee66f708f4419624/client/ui/local-app/public/favicon-16x16.png -------------------------------------------------------------------------------- /client/ui/local-app/public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algorandfoundation/did-algo/161acb19c5c4e61059dc9e99ee66f708f4419624/client/ui/local-app/public/favicon-32x32.png -------------------------------------------------------------------------------- /client/ui/local-app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algorandfoundation/did-algo/161acb19c5c4e61059dc9e99ee66f708f4419624/client/ui/local-app/public/favicon.ico -------------------------------------------------------------------------------- /client/ui/local-app/public/img/beams.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algorandfoundation/did-algo/161acb19c5c4e61059dc9e99ee66f708f4419624/client/ui/local-app/public/img/beams.jpg -------------------------------------------------------------------------------- /client/ui/local-app/public/img/grid.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /client/ui/local-app/src/App.svelte: -------------------------------------------------------------------------------- 1 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 |
42 | 43 | 48 |
50 | 51 | 52 |
54 |
55 | 56 |
57 |

AlgoID Connect

58 | 59 |
60 | 61 | 62 |

63 | Use this graphical interface to manage your did:algo 65 | Decentralized Identifiers. Connect your wallet and link your 66 | ALGO addresses to a DID to enable account discovery 67 | and facilitate payments and other interactions. 68 |

69 | 70 | 71 | 72 | 73 | 74 | 75 |
76 |
77 | 78 | 79 |
80 |

81 | For more information, or to get the source code for this application, checkout the official repository. 86 |

87 |
88 |
89 | -------------------------------------------------------------------------------- /client/ui/local-app/src/actions/outclick.ts: -------------------------------------------------------------------------------- 1 | import type { ActionReturn } from 'svelte/action'; 2 | 3 | interface Attributes { 4 | // define the custom attribute the will be added on the HTMLElement 5 | // that uses this action. This fixes typescript errors. 6 | 'on:outclick': () => void; 7 | } 8 | 9 | export function outclick(node: HTMLElement): ActionReturn { 10 | const handleClick = (event: MouseEvent) => { 11 | if (!node.contains(event.target as HTMLElement)) { 12 | node.dispatchEvent(new CustomEvent('outclick')); 13 | } 14 | }; 15 | document.addEventListener('click', handleClick, true); 16 | 17 | return { 18 | destroy() { 19 | document.removeEventListener('click', handleClick, true); 20 | } 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /client/ui/local-app/src/app.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | /* custom CSS required to style the connector modal */ 6 | div#walletconnect-qrcode-modal { 7 | @apply bg-gray-700 bg-opacity-75 backdrop-blur-sm; 8 | } 9 | 10 | p#walletconnect-qrcode-text { 11 | @apply font-sans text-base font-normal text-gray-400; 12 | } 13 | 14 | div.walletconnect-modal__base { 15 | @apply max-w-md rounded-md shadow-md; 16 | } 17 | 18 | div.walletconnect-modal__header > img { 19 | @apply hidden; 20 | } 21 | 22 | div.walletconnect-modal__header > p { 23 | @apply hidden; 24 | } 25 | 26 | div.walletconnect-modal__footer a { 27 | @apply inline-flex items-center gap-x-2 rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white hover:bg-indigo-500; 28 | } 29 | -------------------------------------------------------------------------------- /client/ui/local-app/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algorandfoundation/did-algo/161acb19c5c4e61059dc9e99ee66f708f4419624/client/ui/local-app/src/assets/.gitkeep -------------------------------------------------------------------------------- /client/ui/local-app/src/context.ts: -------------------------------------------------------------------------------- 1 | import { getContext, setContext } from 'svelte'; 2 | import type { AlertStatus, ModalOptions } from '~/types'; 3 | 4 | /** 5 | * application context interface 6 | */ 7 | export type AppContext = { 8 | /** 9 | * disply an alert/notification message 10 | */ 11 | showAlert(status: AlertStatus, message: string): void; 12 | 13 | /** 14 | * present a modal window 15 | */ 16 | showModal(options: ModalOptions): void; 17 | 18 | /** 19 | * close the modal window 20 | */ 21 | closeModal(): void; 22 | }; 23 | 24 | /** 25 | * unique key for application context instance 26 | */ 27 | const contextKey = Symbol('app-context-key'); 28 | 29 | /** 30 | * set application context 31 | */ 32 | export const SetContext = function (ctx: AppContext) { 33 | setContext(contextKey, ctx); 34 | }; 35 | 36 | /** 37 | * get main application context instance 38 | */ 39 | export const GetContext = function (): AppContext { 40 | return getContext(contextKey); 41 | }; 42 | -------------------------------------------------------------------------------- /client/ui/local-app/src/lib/Alert.svelte: -------------------------------------------------------------------------------- 1 | 54 | 55 | {#if !hidden} 56 |
59 |
60 |
61 |

{message}

62 |
63 |
64 |
65 | 83 |
84 |
85 |
86 | 87 | 92 | 106 |
107 | {/if} 108 | -------------------------------------------------------------------------------- /client/ui/local-app/src/lib/IdentifierDetails.svelte: -------------------------------------------------------------------------------- 1 | 33 | 34 | 35 |
36 | 37 |
39 |

40 | These are the latest details for the selected identifier. You can use the provided 41 | 42 | 49 | 53 | 54 | 55 | tool to add or remove ALGO addresses associated with this 56 | identifier. 57 |

58 |
59 | 60 |
62 |
63 |

64 | Local reference (name) 65 |

66 |
67 |
68 |

{identifier.name}

69 |
70 |
71 | 72 |
74 |
75 |

Last sync

76 |
77 |
78 |

79 | {formatDate(identifier.last_sync)} 80 |

81 |
82 |
83 | 84 |
86 |
87 |

Linked addresses

88 |
89 |
90 |

91 | {#if !identifier.addresses.length} 92 | / 93 | {:else} 94 | {#each identifier.addresses as entry} 95 | 97 | {formatAddress(entry.address)} 98 | 99 |
100 | {/each} 101 | {/if} 102 |

103 |
104 |
105 | 106 |
108 |
109 |

Current status

110 |
111 |
112 | {#if identifier.active} 113 | 115 | Active 116 | 117 | {:else} 118 | 120 | Deactivated 121 | 122 | {/if} 123 |
124 |
125 | 126 |
128 |
129 | 130 |
131 |
132 |
133 | -------------------------------------------------------------------------------- /client/ui/local-app/src/lib/IdentifierList.svelte: -------------------------------------------------------------------------------- 1 | 50 | 51 |
52 | {#if identifiers.length == 0} 53 | 54 | 72 | {:else} 73 | 74 |
75 | 76 | 77 | 78 | 83 | 88 | 93 | 98 | 103 | 108 | 109 | 110 | 111 | {#each identifiers as item} 112 | 116 | {/each} 117 | 118 |
81 | Reference 82 | 86 | DID 87 | 101 | Status 102 | 106 | Actions 107 |
119 | 120 | 139 |
140 | {/if} 141 |
142 | -------------------------------------------------------------------------------- /client/ui/local-app/src/lib/IdentifierListEntry.svelte: -------------------------------------------------------------------------------- 1 | 33 | 34 | { 36 | dispatch('show_details', { did: identifier }); 37 | }} 38 | class="cursor-pointer odd:bg-white even:bg-slate-50 hover:bg-slate-100"> 39 | 40 | {identifier.name} 41 | 42 | 43 | {formatDID(identifier.did)} 44 | 45 | 46 | {identifier.addresses.length} 47 | 48 | 49 | {formatDate(identifier.last_sync)} 50 | 51 | 52 | {#if identifier.active} 53 | 55 | Active 56 | 57 | {:else} 58 | 60 | Deactivated 61 | 62 | {/if} 63 | 64 | 65 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /client/ui/local-app/src/lib/ManageAddresses.svelte: -------------------------------------------------------------------------------- 1 | 77 | 78 |
79 | 80 |
82 |

83 | Adjust the ALGO addresses associated with this identifier. You can add 84 | as many as you want. The DID document associated will be automatically synced with the 85 | network. 86 |

87 |
88 | 89 |
91 | {#if addresses.length == 0} 92 |

93 | There are no available addresses, connect your wallet and try again! 94 |

95 | {:else} 96 |
97 | {#each addresses as entry} 98 |
99 | { 102 | toggleAddress(entry); 103 | }} /> 104 | {formatAddress(entry.address)} 107 |
108 | {/each} 109 |
110 | {/if} 111 |
112 | 113 |
115 |

Enter the recovery key used to create this identifier.

116 |
117 |

Recovery key

118 |
119 |
120 | 126 |
127 |
128 | 129 |
130 |
131 | 138 | 143 |
144 |
145 |
146 | -------------------------------------------------------------------------------- /client/ui/local-app/src/lib/Modal.svelte: -------------------------------------------------------------------------------- 1 | 82 | 83 | 84 | 85 | {#if !state.hidden} 86 |
{ 89 | dispatch('open'); 90 | }} 91 | on:outroend={() => { 92 | dispatch('close'); 93 | }} 94 | class="absolute bottom-0 left-0 right-0 top-0 z-10 flex flex-col bg-gray-700 bg-opacity-75 backdrop-blur-sm transition-all {styles( 95 | 'wrapper' 96 | )}"> 97 |
102 |
103 | 104 |
105 |
106 |

107 | {formatTitle(state.title)} 108 |

109 | {#if state.subtitle} 110 | {state.subtitle} 111 | {/if} 112 |
113 | 130 |
131 | 132 | 133 |
134 |
135 |
136 | {/if} 137 | -------------------------------------------------------------------------------- /client/ui/local-app/src/lib/NewIdentifier.svelte: -------------------------------------------------------------------------------- 1 | 59 | 60 |
61 |
62 | 63 |
65 |
66 | 67 |
68 |

69 | A decentralized identifier (or DID) is an asset designed to be owned by a controller 71 | entity. A single identifier can be used on any number of services, and you can create 72 | as many identifiers as you want. 73 |

74 |
75 | 76 |
78 |
79 |

80 | Name (local reference) 81 |

82 |
83 |
84 | 89 |
90 |
91 | 92 |
94 |

95 | The recovery key used to create this identifier is not stored locally. If you lose 96 | it ther's no other way to recover it. Please make sure you have a copy of it. 98 |

99 |
100 |

Recovery key

101 |
102 |
103 | 108 |
109 |
110 |

111 | Key confirmation 112 |

113 |
114 |
115 | 120 |
121 |
122 | 123 |
124 |
125 | 132 | 136 |
137 |
138 |
139 |
140 | -------------------------------------------------------------------------------- /client/ui/local-app/src/lib/Switch.svelte: -------------------------------------------------------------------------------- 1 | 48 | 49 | 64 | -------------------------------------------------------------------------------- /client/ui/local-app/src/main.ts: -------------------------------------------------------------------------------- 1 | import '~/app.css'; 2 | import App from '~/App.svelte'; 3 | 4 | const app = new App({ 5 | target: document.getElementById('app') 6 | }); 7 | 8 | export default app; 9 | -------------------------------------------------------------------------------- /client/ui/local-app/src/store.ts: -------------------------------------------------------------------------------- 1 | import { writable } from 'svelte/store'; 2 | import type { AddressEntry, WalletMetadata } from '~/types'; 3 | 4 | // local API client 5 | const apiClient = { 6 | // check if local API server is ready 7 | ready: async () => { 8 | try { 9 | const response = await fetch('http://localhost:9090/ready'); 10 | return response.status === 200; 11 | } catch { 12 | return false; 13 | } 14 | }, 15 | // get list of registered DIDs 16 | list: async () => { 17 | try { 18 | const response = await fetch('http://localhost:9090/list'); 19 | const data = await response.json(); 20 | return data; 21 | } catch { 22 | return []; 23 | } 24 | }, 25 | // register a new DID 26 | register: async (name: string, recovery_key: string) => { 27 | try { 28 | const response = await fetch('http://localhost:9090/register', { 29 | method: 'POST', 30 | headers: { 31 | 'Content-Type': 'application/json' 32 | }, 33 | body: JSON.stringify({ 34 | name, 35 | recovery_key 36 | }) 37 | }); 38 | return response.status === 200; 39 | } catch { 40 | return false; 41 | } 42 | }, 43 | // update DID's addresses 44 | update: async ( 45 | name: string, 46 | did: string, 47 | addresses: AddressEntry[], 48 | passphrase: string 49 | ) => { 50 | try { 51 | const response = await fetch('http://localhost:9090/update', { 52 | method: 'POST', 53 | headers: { 54 | 'Content-Type': 'application/json' 55 | }, 56 | body: JSON.stringify({ 57 | name, 58 | did, 59 | passphrase, 60 | addresses 61 | }) 62 | }); 63 | return response.status === 200; 64 | } catch { 65 | return false; 66 | } 67 | } 68 | }; 69 | 70 | // initialize state's store 71 | const { subscribe, update } = writable({ 72 | identifiers: [], 73 | wallet: { 74 | url: '', 75 | name: '', 76 | icons: [], 77 | addresses: [], 78 | connected: false, 79 | description: '' 80 | } 81 | }); 82 | 83 | // application state handler 84 | export const appState = { 85 | subscribe, 86 | reload: async function () { 87 | const list = await apiClient.list(); 88 | list.sort((a, b) => { 89 | return Date.parse(b.last_sync) - Date.parse(a.last_sync); 90 | }); 91 | update((st) => { 92 | st.identifiers = list; 93 | return st; 94 | }); 95 | }, 96 | setWallet: (md: WalletMetadata) => { 97 | update((st) => { 98 | st.wallet = md; 99 | return st; 100 | }); 101 | }, 102 | removeWallet: () => { 103 | update((st) => { 104 | st.wallet = { 105 | connected: false, 106 | addresses: [], 107 | name: '', 108 | description: '', 109 | url: '', 110 | icons: [] 111 | }; 112 | return st; 113 | }); 114 | }, 115 | createDID: async function (name: string, recovery_key: string): Promise { 116 | const result = await apiClient.register(name, recovery_key); 117 | if (result) { 118 | this.reload(); 119 | } 120 | return result; 121 | }, 122 | updateDID: async function ( 123 | name: string, 124 | did: string, 125 | addresses: AddressEntry[], 126 | passphrase: string 127 | ) { 128 | const result = await apiClient.update(name, did, addresses, passphrase); 129 | if (result) { 130 | this.reload(); 131 | } 132 | return result; 133 | } 134 | }; 135 | -------------------------------------------------------------------------------- /client/ui/local-app/src/types.ts: -------------------------------------------------------------------------------- 1 | import type { SvelteComponent } from 'svelte'; 2 | 3 | /** 4 | * main application state 5 | */ 6 | export interface AppState { 7 | /** 8 | * connected wallet details 9 | */ 10 | wallet: WalletMetadata; 11 | 12 | /** 13 | * DIDs available 14 | */ 15 | identifiers?: IdentifierEntry[]; 16 | } 17 | 18 | /** 19 | * connected wallet details 20 | */ 21 | export interface WalletMetadata { 22 | /** 23 | * whether the wallet is connected or not 24 | */ 25 | connected: boolean; 26 | 27 | /** 28 | * addresses enabled by the user 29 | */ 30 | addresses: string[]; 31 | 32 | /** 33 | * application name 34 | */ 35 | name: string; 36 | 37 | /** 38 | * application description 39 | */ 40 | description: string; 41 | 42 | /** 43 | * application home URL 44 | */ 45 | url: string; 46 | 47 | /** 48 | * custom application icons 49 | */ 50 | icons: string[]; 51 | } 52 | 53 | /** 54 | * existing managed identifiers 55 | */ 56 | export interface IdentifierEntry { 57 | /** 58 | * DID local reference name 59 | */ 60 | name: string; 61 | 62 | /** 63 | * actual DID 64 | */ 65 | did: string; 66 | 67 | /** 68 | * ALGO addresses linked to the identifier 69 | */ 70 | addresses: AddressEntry[]; 71 | 72 | /** 73 | * whether the DID remains active 74 | */ 75 | active: boolean; 76 | 77 | /** 78 | * date when the DID was last sync to the network, 79 | * if at all 80 | */ 81 | last_sync?: string; 82 | 83 | /** 84 | * raw DID document 85 | */ 86 | document: unknown; 87 | } 88 | 89 | /** 90 | * settings available when presenting a modal window 91 | */ 92 | export interface ModalOptions { 93 | /** 94 | * modal main title 95 | */ 96 | title: string; 97 | 98 | /** 99 | * secondary title; optional 100 | */ 101 | subtitle?: string; 102 | 103 | /** 104 | * whether to show the modal as a side panel 105 | */ 106 | asPanel: boolean; 107 | 108 | /** 109 | * child component to render 110 | */ 111 | content: typeof SvelteComponent; 112 | 113 | /** 114 | * properties to pass to the child component 115 | */ 116 | props?: unknown; 117 | } 118 | 119 | /** 120 | * ALGO address available to a DID 121 | */ 122 | export interface AddressEntry { 123 | /** 124 | * ALGO address in text format. 125 | */ 126 | address: string; 127 | 128 | /** 129 | * Algorand network the address belongs to. 130 | */ 131 | network: string; 132 | 133 | /** 134 | * whether the address is enabled/linked on the DID or not 135 | */ 136 | enabled: boolean; 137 | } 138 | 139 | /** 140 | * alert status code 141 | */ 142 | export type AlertStatus = 'success' | 'warning' | 'error'; 143 | -------------------------------------------------------------------------------- /client/ui/local-app/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /client/ui/local-app/svelte.config.js: -------------------------------------------------------------------------------- 1 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; 2 | 3 | export default { 4 | // Consult https://svelte.dev/docs#compile-time-svelte-preprocess 5 | // for more information about preprocessors 6 | preprocess: vitePreprocess() 7 | }; 8 | -------------------------------------------------------------------------------- /client/ui/local-app/tailwind.config.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ['./index.html', './src/**/*.{html,js,svelte,ts}'], 4 | theme: { 5 | extend: {} 6 | }, 7 | plugins: [] 8 | }; 9 | -------------------------------------------------------------------------------- /client/ui/local-app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/svelte/tsconfig.json", 3 | "compilerOptions": { 4 | "ignoreDeprecations": "5.0", 5 | "target": "ESNext", 6 | "useDefineForClassFields": true, 7 | "module": "ESNext", 8 | "resolveJsonModule": true, 9 | /** 10 | * Typecheck JS in `.svelte` and `.js` files by default. 11 | * Disable checkJs if you'd like to use dynamic types in JS. 12 | * Note that setting allowJs false does not prevent the use 13 | * of JS in `.svelte` files. 14 | */ 15 | "allowJs": true, 16 | "checkJs": true, 17 | /** 18 | * To support absolute import paths for components: 19 | * `import Counter from '~/lib/Counter.svelte';` 20 | */ 21 | "paths": { 22 | "~/*": ["./src/*"] 23 | } 24 | }, 25 | "include": [ 26 | "src/**/*.d.ts", 27 | "src/**/*.ts", 28 | "src/**/*.js", 29 | "src/**/*.svelte" 30 | ], 31 | "exclude": [ 32 | "src/**/*.spec.js", 33 | "src/**/*.spec.ts", 34 | "src/**/*.test.js", 35 | "src/**/*.test.ts" 36 | ], 37 | "references": [ 38 | { 39 | "path": "./tsconfig.node.json" 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /client/ui/local-app/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node" 6 | }, 7 | "include": ["vite.config.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /client/ui/local-app/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { svelte } from '@sveltejs/vite-plugin-svelte'; 2 | import * as path from 'path'; 3 | import { nodePolyfills } from 'vite-plugin-node-polyfills'; 4 | import { defineConfig } from 'vitest/config'; 5 | 6 | // https://vitejs.dev/config/ 7 | export default defineConfig({ 8 | plugins: [ 9 | svelte(), 10 | nodePolyfills({ 11 | protocolImports: true 12 | }) 13 | ], 14 | test: { 15 | environment: 'jsdom', 16 | include: ['src/**/*.{test,spec}.{js,ts}'] 17 | }, 18 | resolve: { 19 | alias: { 20 | // To support absolute import paths for components: 21 | // `import Counter from '~/lib/Counter.svelte';` 22 | '~': path.resolve('./src') 23 | } 24 | } 25 | }); 26 | -------------------------------------------------------------------------------- /client/ui/utils.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "go.bryk.io/pkg/did" 5 | ) 6 | 7 | // Return the list of ALGO addresses linked to the provided identifier. 8 | func getAlgoAddress(id *did.Identifier) []addressEntry { 9 | var result = []addressEntry{} 10 | svc := id.Service("algo-connect") 11 | if svc == nil { 12 | return result 13 | } 14 | var addresses []algoDestination 15 | ext := did.Extension{ 16 | ID: "algo-address", 17 | Version: "0.1.0", 18 | } 19 | if err := svc.GetExtension(ext.ID, ext.Version, &addresses); err != nil { 20 | return result 21 | } 22 | for _, entry := range addresses { 23 | result = append(result, addressEntry{ 24 | Address: entry.Address, 25 | Network: entry.Network, 26 | Enabled: true, 27 | }) 28 | } 29 | return result 30 | } 31 | 32 | // Return an empty service entry for `algo-connect`. 33 | func newServiceEntry() *did.ServiceEndpoint { 34 | return &did.ServiceEndpoint{ 35 | ID: "algo-connect", 36 | Type: "AlgorandExternalService", 37 | Endpoint: "https://did.algorand.foundation", 38 | Extensions: []did.Extension{ 39 | { 40 | ID: "algo-address", 41 | Version: "0.1.0", 42 | Data: []algoDestination{}, 43 | }, 44 | }, 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/algorandfoundation/did-algo 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/algorand/go-algorand-sdk v1.24.0 7 | github.com/algorand/go-algorand-sdk/v2 v2.4.0 8 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 9 | github.com/kennygrant/sanitize v1.2.4 10 | github.com/pkg/errors v0.9.1 11 | github.com/spf13/cobra v1.8.0 12 | github.com/spf13/viper v1.18.2 13 | github.com/stretchr/testify v1.8.4 14 | go.bryk.io/pkg v0.0.0-20240216184430-db271b89fadd 15 | golang.org/x/crypto v0.20.0 16 | google.golang.org/genproto/googleapis/api v0.0.0-20240125205218-1f4bbc51befe 17 | google.golang.org/grpc v1.61.0 18 | google.golang.org/protobuf v1.33.0 19 | gopkg.in/yaml.v3 v3.0.1 20 | ) 21 | 22 | require ( 23 | github.com/algorand/avm-abi v0.1.1 // indirect 24 | github.com/algorand/go-codec/codec v1.1.10 // indirect 25 | github.com/awnumar/memcall v0.2.0 // indirect 26 | github.com/awnumar/memguard v0.22.4 // indirect 27 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect 28 | github.com/briandowns/spinner v1.23.0 // indirect 29 | github.com/charmbracelet/lipgloss v0.9.1 // indirect 30 | github.com/charmbracelet/log v0.3.1 // indirect 31 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 32 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect 33 | github.com/fatih/color v1.15.0 // indirect 34 | github.com/felixge/httpsnoop v1.0.4 // indirect 35 | github.com/fsnotify/fsnotify v1.7.0 // indirect 36 | github.com/go-logfmt/logfmt v0.6.0 // indirect 37 | github.com/golang/protobuf v1.5.3 // indirect 38 | github.com/google/go-querystring v1.0.0 // indirect 39 | github.com/google/uuid v1.6.0 // indirect 40 | github.com/gorilla/handlers v1.5.2 // indirect 41 | github.com/hashicorp/hcl v1.0.0 // indirect 42 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 43 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 44 | github.com/magiconair/properties v1.8.7 // indirect 45 | github.com/mattn/go-colorable v0.1.13 // indirect 46 | github.com/mattn/go-isatty v0.0.19 // indirect 47 | github.com/mattn/go-runewidth v0.0.15 // indirect 48 | github.com/mitchellh/mapstructure v1.5.0 // indirect 49 | github.com/mr-tron/base58 v1.2.0 // indirect 50 | github.com/muesli/reflow v0.3.0 // indirect 51 | github.com/muesli/termenv v0.15.2 // indirect 52 | github.com/pelletier/go-toml/v2 v2.1.0 // indirect 53 | github.com/piprate/json-gold v0.5.0 // indirect 54 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 55 | github.com/pquerna/cachecontrol v0.1.0 // indirect 56 | github.com/rivo/uniseg v0.2.0 // indirect 57 | github.com/rogpeppe/go-internal v1.10.0 // indirect 58 | github.com/rs/zerolog v1.32.0 // indirect 59 | github.com/sagikazarmark/locafero v0.4.0 // indirect 60 | github.com/sagikazarmark/slog-shim v0.1.0 // indirect 61 | github.com/sirupsen/logrus v1.9.3 // indirect 62 | github.com/sourcegraph/conc v0.3.0 // indirect 63 | github.com/spf13/afero v1.11.0 // indirect 64 | github.com/spf13/cast v1.6.0 // indirect 65 | github.com/spf13/pflag v1.0.5 // indirect 66 | github.com/subosito/gotenv v1.6.0 // indirect 67 | go.bryk.io/miracl v0.6.1 // indirect 68 | go.uber.org/multierr v1.11.0 // indirect 69 | go.uber.org/zap v1.26.0 // indirect 70 | golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect 71 | golang.org/x/net v0.21.0 // indirect 72 | golang.org/x/sys v0.17.0 // indirect 73 | golang.org/x/term v0.17.0 // indirect 74 | golang.org/x/text v0.14.0 // indirect 75 | google.golang.org/genproto v0.0.0-20240116215550-a9fa1716bcac // indirect 76 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240125205218-1f4bbc51befe // indirect 77 | gopkg.in/ini.v1 v1.67.0 // indirect 78 | ) 79 | -------------------------------------------------------------------------------- /info/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package info provides global parameters for the application. 3 | */ 4 | package info 5 | -------------------------------------------------------------------------------- /info/version.go: -------------------------------------------------------------------------------- 1 | package info 2 | 3 | // CoreVersion defines the semantic version of the build. 4 | var CoreVersion string 5 | 6 | // BuildCode provides the commit identifier used to build the binary. 7 | var BuildCode string 8 | 9 | // BuildTimestamp provides the UTC RFC3339 timestamp of the build. 10 | var BuildTimestamp string 11 | -------------------------------------------------------------------------------- /proto/buf.lock: -------------------------------------------------------------------------------- 1 | # Generated by buf. DO NOT EDIT. 2 | version: v1 3 | deps: 4 | - remote: buf.build 5 | owner: bufbuild 6 | repository: protovalidate 7 | commit: f05a6f4403ce4327bae4f50f281c3ed0 8 | digest: shake256:668a0661b8df44d41839194896329330965fc215f3d2f88057fd60eeb759c2daf6cc6edfdd13b2a653d49fe2896ebedcb1a33c4c5b2dd10919f03ffb7fc52ae6 9 | - remote: buf.build 10 | owner: googleapis 11 | repository: googleapis 12 | commit: 7e6f6e774e29406da95bd61cdcdbc8bc 13 | digest: shake256:fe43dd2265ea0c07d76bd925eeba612667cf4c948d2ce53d6e367e1b4b3cb5fa69a51e6acb1a6a50d32f894f054a35e6c0406f6808a483f2752e10c866ffbf73 14 | - remote: buf.build 15 | owner: grpc-ecosystem 16 | repository: grpc-gateway 17 | commit: 3f42134f4c564983838425bc43c7a65f 18 | digest: shake256:3d11d4c0fe5e05fda0131afefbce233940e27f0c31c5d4e385686aea58ccd30f72053f61af432fa83f1fc11cda57f5f18ca3da26a29064f73c5a0d076bba8d92 19 | -------------------------------------------------------------------------------- /proto/buf.yaml: -------------------------------------------------------------------------------- 1 | version: v1 2 | deps: 3 | - buf.build/googleapis/googleapis 4 | - buf.build/bufbuild/protovalidate 5 | - buf.build/grpc-ecosystem/grpc-gateway 6 | breaking: 7 | use: 8 | - FILE 9 | lint: 10 | service_suffix: API 11 | rpc_allow_google_protobuf_empty_requests: true 12 | use: 13 | - DEFAULT 14 | except: 15 | - RPC_REQUEST_RESPONSE_UNIQUE 16 | - RPC_RESPONSE_STANDARD_NAME 17 | -------------------------------------------------------------------------------- /proto/did/v1/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package didv1 provides the version 1.0 of the protocol buffers definitions for the project. 3 | */ 4 | package didv1 5 | -------------------------------------------------------------------------------- /proto/did/v1/image.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algorandfoundation/did-algo/161acb19c5c4e61059dc9e99ee66f708f4419624/proto/did/v1/image.bin -------------------------------------------------------------------------------- /proto/did/v1/ticket.go: -------------------------------------------------------------------------------- 1 | package didv1 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/binary" 7 | "encoding/hex" 8 | "encoding/json" 9 | "fmt" 10 | "time" 11 | 12 | "github.com/pkg/errors" 13 | "go.bryk.io/pkg/crypto/pow" 14 | "go.bryk.io/pkg/did" 15 | "golang.org/x/crypto/sha3" 16 | ) 17 | 18 | const defaultTicketDifficultyLevel = 24 19 | 20 | // NewTicket returns a properly initialized new ticket instance. 21 | func NewTicket(id *did.Identifier, keyID string) (*Ticket, error) { 22 | // Get safe DID document 23 | contents, err := json.Marshal(id.Document(true)) 24 | if err != nil { 25 | return nil, err 26 | } 27 | 28 | // Get proof 29 | proof, err := id.GetProof(keyID, "did.algorand.foundation") 30 | if err != nil { 31 | return nil, err 32 | } 33 | proofBytes, err := json.Marshal(proof) 34 | if err != nil { 35 | return nil, err 36 | } 37 | 38 | // Create new ticket 39 | t := &Ticket{ 40 | Timestamp: time.Now().UTC().Unix(), 41 | NonceValue: 0, 42 | KeyId: keyID, 43 | Document: contents, 44 | Proof: proofBytes, 45 | Signature: nil, 46 | } 47 | 48 | // Add metadata, if available 49 | if metadata := id.GetMetadata(); metadata != nil { 50 | metadataBytes, err := json.Marshal(metadata) 51 | if err != nil { 52 | return nil, err 53 | } 54 | t.DocumentMetadata = metadataBytes 55 | } 56 | 57 | return t, nil 58 | } 59 | 60 | // GetDID retrieve the DID instance from the ticket contents. 61 | func (t *Ticket) GetDID() (*did.Identifier, error) { 62 | // Restore id instance from document 63 | doc := &did.Document{} 64 | if err := json.Unmarshal(t.Document, doc); err != nil { 65 | return nil, errors.New("invalid ticket contents") 66 | } 67 | id, err := did.FromDocument(doc) 68 | if err != nil { 69 | return nil, err 70 | } 71 | 72 | // Restore metadata, if available 73 | if t.DocumentMetadata != nil { 74 | metadata := &did.DocumentMetadata{} 75 | if err := json.Unmarshal(t.DocumentMetadata, metadata); err != nil { 76 | return nil, errors.New("invalid ticket contents") 77 | } 78 | if err := id.AddMetadata(metadata); err != nil { 79 | return nil, err 80 | } 81 | } 82 | return id, nil 83 | } 84 | 85 | // GetProofLD returns the decoded proof document contained in the ticket. 86 | func (t *Ticket) GetProofLD() (*did.ProofLD, error) { 87 | proof := &did.ProofLD{} 88 | if err := json.Unmarshal(t.Proof, proof); err != nil { 89 | return nil, errors.New("invalid proof contents") 90 | } 91 | return proof, nil 92 | } 93 | 94 | // ResetNonce returns the internal nonce value back to 0. 95 | func (t *Ticket) ResetNonce() { 96 | t.NonceValue = 0 97 | } 98 | 99 | // IncrementNonce will adjust the internal nonce value by 1. 100 | func (t *Ticket) IncrementNonce() { 101 | t.NonceValue++ 102 | } 103 | 104 | // Nonce returns the current value set on the nonce attribute. 105 | func (t *Ticket) Nonce() int64 { 106 | return t.NonceValue 107 | } 108 | 109 | // MarshalBinary returns a deterministic binary encoding for the ticket 110 | // instance using a byte concatenation of the form: 111 | // 'timestamp | nonce | key_id | document | proof | document_metadata' 112 | // where timestamp and nonce are individually encoded using little endian 113 | // byte order. 114 | func (t *Ticket) MarshalBinary() ([]byte, error) { 115 | var tc []byte 116 | nb := bytes.NewBuffer(nil) 117 | tb := bytes.NewBuffer(nil) 118 | kb := make([]byte, hex.EncodedLen(len([]byte(t.KeyId)))) 119 | if err := binary.Write(nb, binary.LittleEndian, t.Nonce()); err != nil { 120 | return nil, fmt.Errorf("failed to encode nonce value: %w", err) 121 | } 122 | if err := binary.Write(tb, binary.LittleEndian, t.GetTimestamp()); err != nil { 123 | return nil, fmt.Errorf("failed to encode nonce value: %w", err) 124 | } 125 | hex.Encode(kb, []byte(t.KeyId)) 126 | tc = append(tc, tb.Bytes()...) 127 | tc = append(tc, nb.Bytes()...) 128 | tc = append(tc, kb...) 129 | tc = append(tc, t.Document...) 130 | tc = append(tc, t.Proof...) 131 | tc = append(tc, t.DocumentMetadata...) 132 | return tc, nil 133 | 134 | // A simpler encoding mechanism using the standard protobuf encoder. 135 | // tc := proto.Clone(t).(*Ticket) 136 | // tc.Signature = nil 137 | // return proto.MarshalOptions{Deterministic: true}.Marshal(tc) 138 | } 139 | 140 | // Solve the ticket challenge using the proof-of-work mechanism. 141 | func (t *Ticket) Solve(ctx context.Context, difficulty uint) string { 142 | if difficulty == 0 { 143 | difficulty = defaultTicketDifficultyLevel 144 | } 145 | return <-pow.Solve(ctx, t, sha3.New256(), difficulty) 146 | } 147 | 148 | // Verify perform all the required validations to ensure the request ticket is 149 | // ready for further processing 150 | // - Challenge is valid 151 | // - Contents are a properly encoded DID instance 152 | // - Contents don’t include any private key, for security reasons no private keys should 153 | // ever be published on the network 154 | // - DID proof is valid 155 | // - Ticket signature is valid. 156 | func (t *Ticket) Verify(difficulty uint) error { 157 | // Challenge is valid 158 | if difficulty == 0 { 159 | difficulty = defaultTicketDifficultyLevel 160 | } 161 | if !pow.Verify(t, sha3.New256(), difficulty) { 162 | return errors.New("invalid ticket challenge") 163 | } 164 | 165 | // Contents are a properly encoded DID instance 166 | id, err := t.GetDID() 167 | if err != nil { 168 | return err 169 | } 170 | 171 | // Verify private keys are not included 172 | for _, k := range id.VerificationMethods() { 173 | if len(k.Private) != 0 { 174 | return errors.New("private keys included on the DID") 175 | } 176 | } 177 | 178 | // Retrieve DID key 179 | key := id.VerificationMethod(t.KeyId) 180 | if key == nil { 181 | return errors.New("the selected key is not available on the DID") 182 | } 183 | 184 | // Verify proof 185 | proof, err := t.GetProofLD() 186 | if err != nil { 187 | return err 188 | } 189 | data, err := id.Document(true).NormalizedLD() 190 | if err != nil { 191 | return err 192 | } 193 | if !key.VerifyProof(data, proof) { 194 | return errors.New("invalid proof") 195 | } 196 | 197 | // Get digest 198 | data, err = t.MarshalBinary() 199 | if err != nil { 200 | return errors.New("failed to re-encode ticket instance") 201 | } 202 | digest := sha3.New256() 203 | if _, err = digest.Write(data); err != nil { 204 | return err 205 | } 206 | 207 | // Verify signature 208 | if !key.Verify(digest.Sum(nil), t.Signature) { 209 | return errors.New("invalid ticket signature") 210 | } 211 | return nil 212 | } 213 | -------------------------------------------------------------------------------- /resolver-config.yaml: -------------------------------------------------------------------------------- 1 | network: 2 | profiles: 3 | - name: mainnet 4 | node: https://mainnet-api.algonode.cloud 5 | node_token: "" 6 | - name: testnet 7 | node: https://testnet-api.algonode.cloud 8 | node_token: "" 9 | profiles: 10 | - name: betanet 11 | node: https://betanet-api.algonode.cloud 12 | node_token: "" 13 | -------------------------------------------------------------------------------- /sample-config.yaml: -------------------------------------------------------------------------------- 1 | network: 2 | profiles: 3 | - name: mainnet 4 | node: https://mainnet-api.algonode.cloud 5 | node_token: "" 6 | - name: testnet 7 | node: https://testnet-api.algonode.cloud 8 | node_token: "" 9 | profiles: 10 | - name: betanet 11 | node: https://betanet-api.algonode.cloud 12 | node_token: "" 13 | -------------------------------------------------------------------------------- /sample-monitor.yml: -------------------------------------------------------------------------------- 1 | global: 2 | scrape_interval: 15s 3 | evaluation_interval: 15s 4 | external_labels: 5 | monitor: 'algoid' 6 | scrape_configs: 7 | - job_name: 'algoid' 8 | scrape_interval: 15s 9 | static_configs: 10 | - targets: ['localhost:9090'] 11 | -------------------------------------------------------------------------------- /scripts/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Utilities 4 | uname_arch() { 5 | arch=$(uname -m) 6 | case $arch in 7 | x86_64) arch="amd64" ;; 8 | x86) arch="386" ;; 9 | i686) arch="386" ;; 10 | i386) arch="386" ;; 11 | aarch64) arch="arm64" ;; 12 | esac 13 | echo ${arch} 14 | } 15 | 16 | uname_os() { 17 | os=$(uname -s | tr '[:upper:]' '[:lower:]') 18 | echo "$os" 19 | } 20 | 21 | # Check requirements 22 | if ! [ -x "$(command -v sudo)" ]; then 23 | echo "this installer requires 'sudo' to be installed" 24 | exit 25 | fi 26 | 27 | if ! [ -x "$(command -v curl)" ]; then 28 | echo "this installer requires 'curl' to be installed" 29 | exit 30 | fi 31 | 32 | # Setup 33 | GITHUB_OWNER="algorandfoundation" 34 | GITHUB_REPO="did-algo" 35 | TAG="latest" 36 | INSTALL_DIR="/usr/local/bin/" 37 | ARCH=$(uname_arch) 38 | OS=$(uname_os) 39 | GITHUB_RELEASES_PAGE=https://github.com/${GITHUB_OWNER}/${GITHUB_REPO}/releases 40 | VERSION=$(curl $GITHUB_RELEASES_PAGE/$TAG -sL -H 'Accept:application/json' | tr -s '\n' ' ' | sed 's/.*"tag_name":"//' | sed 's/".*//' | tr -d v) 41 | NAME=${GITHUB_REPO}_${VERSION}_${OS}_${ARCH} 42 | TARBALL=${NAME}.tar.gz 43 | TARBALL_URL=${GITHUB_RELEASES_PAGE}/download/v${VERSION}/${TARBALL} 44 | 45 | # Install package 46 | echo "Downloading latest $GITHUB_REPO binary from $TARBALL_URL" 47 | tmpfolder=$(mktemp -d) 48 | $(curl $TARBALL_URL -sL -o $tmpfolder/$TARBALL) 49 | 50 | if [ ! -f $tmpfolder/$TARBALL ]; then 51 | echo "Can not download. Exiting..." 52 | exit 14 53 | fi 54 | cd ${tmpfolder} && tar --no-same-owner -xzf "$tmpfolder/$TARBALL" 55 | 56 | if [ ! -f $tmpfolder/$GITHUB_REPO ]; then 57 | echo "Can not find $GITHUB_REPO. Exiting..." 58 | exit 15 59 | fi 60 | 61 | binary=$tmpfolder/$GITHUB_REPO 62 | echo "Installing $GITHUB_REPO to $INSTALL_DIR" 63 | sudo install "$binary" $INSTALL_DIR 64 | echo "Installed $GITHUB_REPO to $INSTALL_DIR" 65 | 66 | # Clean-up 67 | rm -rf "${tmpdir}" 68 | --------------------------------------------------------------------------------