├── .buildkite ├── pipeline.yaml └── secrets.yaml ├── .gitignore ├── .sops.yaml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── DEVELOPING.md ├── LICENSE.md ├── LICENSE_HEADER ├── README.md ├── ci ├── base-image │ └── Dockerfile ├── deploy ├── kubeconfig.yaml ├── node-image │ └── Dockerfile └── run ├── cli ├── Cargo.toml ├── build.rs └── src │ ├── command │ ├── account.rs │ ├── key_pair.rs │ ├── mod.rs │ ├── org.rs │ ├── other.rs │ ├── project.rs │ ├── runtime.rs │ └── user.rs │ ├── key_pair_storage.rs │ ├── lib.rs │ └── main.rs ├── client ├── Cargo.toml ├── examples │ ├── getting_started.rs │ ├── project_registration.rs │ └── user_registration.rs ├── src │ ├── backend │ │ ├── emulator.rs │ │ ├── mod.rs │ │ ├── remote_node.rs │ │ └── remote_node_with_executor.rs │ ├── error.rs │ ├── event.rs │ ├── interface.rs │ ├── lib.rs │ ├── message.rs │ └── transaction.rs └── tests │ └── end_to_end.rs ├── core ├── Cargo.toml ├── README.md └── src │ ├── bytes128.rs │ ├── error.rs │ ├── id.rs │ ├── lib.rs │ ├── message.rs │ ├── project_name.rs │ └── state.rs ├── default.nix ├── local-devnet ├── README.md ├── docker-compose.yaml ├── grafana-dashboards.yaml ├── grafana-dashboards │ └── network.json ├── grafana-datasources.yaml ├── prometheus.yaml └── start-node.sh ├── node ├── Cargo.toml ├── build.rs └── src │ ├── blockchain.rs │ ├── chain_spec.rs │ ├── chain_spec │ └── ffnet.json │ ├── cli.rs │ ├── logger.rs │ ├── main.rs │ ├── metrics.rs │ ├── pow │ ├── blake3_pow.rs │ ├── config.rs │ ├── dummy_pow.rs │ ├── harmonic_mean.rs │ └── mod.rs │ └── service.rs ├── runtime-tests ├── Cargo.toml └── tests │ ├── account.rs │ ├── block_rewards.rs │ ├── id_status.rs │ ├── member_registration.rs │ ├── org_registration.rs │ ├── project_registration.rs │ ├── transfer.rs │ └── user_registration.rs ├── runtime ├── Cargo.toml ├── build.rs └── src │ ├── fees │ ├── mod.rs │ └── payment.rs │ ├── lib.rs │ ├── registry.rs │ ├── registry │ └── inherents.rs │ ├── runtime.rs │ ├── runtime │ └── api.rs │ └── timestamp_in_digest.rs ├── rust-toolchain ├── rustfmt.toml ├── scripts ├── build-client-docs ├── build-release ├── build-runtime-wasm ├── check-license-headers ├── create-release ├── rebuild-runtime-cache ├── run-tests-all-runtimes ├── rustup-setup └── update-substrate └── test-utils ├── Cargo.toml └── src └── lib.rs /.buildkite/pipeline.yaml: -------------------------------------------------------------------------------- 1 | env: 2 | DOCKER_IMAGE: gcr.io/opensourcecoin/radicle-registry/ci-base:98fb1936a3da74a6eeea5d569c0e77a181cb56ae 3 | DOCKER_FILE: ci/base-image/Dockerfile 4 | 5 | .test: &test 6 | label: "Test" 7 | command: "ci/run" 8 | timeout_in_minutes: 60 9 | env: 10 | STEP_DOCKER_FILE: ci/node-image/Dockerfile 11 | STEP_DOCKER_IMAGE: gcr.io/opensourcecoin/radicle-registry/node 12 | SHARED_MASTER_CACHE: true 13 | agents: 14 | platform: "linux" 15 | production: "true" 16 | artifact_paths: 17 | - "artifacts/*" 18 | 19 | steps: 20 | - branches: master 21 | concurrency: 1 22 | concurrency_group: master 23 | <<: *test 24 | - branches: master 25 | concurrency: 1 26 | concurrency_group: master 27 | <<: *test 28 | label: "Test on preview" 29 | agents: 30 | queue: agent-preview 31 | - branches: "!master" 32 | <<: *test 33 | - wait 34 | - label: "Deploy" 35 | branches: master 36 | command: "ci/deploy" 37 | agents: 38 | platform: "linux" 39 | production: "true" 40 | 41 | notify: 42 | - email: "registry-devs@monadic.xyz" 43 | if: | 44 | build.state == "failed" && build.branch == "master" 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | /runtime-cache 4 | .secrets 5 | -------------------------------------------------------------------------------- /.sops.yaml: -------------------------------------------------------------------------------- 1 | creation_rules: 2 | - gcp_kms: projects/radicle-registry-dev/locations/global/keyRings/dev/cryptoKeys/ci-secrets 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Change Log 2 | ========== 3 | 4 | Upcoming 5 | -------- 6 | 7 | ### Breaking changes 8 | 9 | * client: Renamed `MINIMUM_FEE` to `MINIMUM_TX_FEE` 10 | * client: Drop Deposit-related placeholder constants 11 | * node: Blake3PoW requires the timestamp as a digest item 12 | * client: Eliminate `Error::Other` variant and replace it with dedicated 13 | variants for different errors. 14 | * client: Change signature of `Client::block_header` from `Result, 15 | Error>` to `Result, Error>`. 16 | * runtime: Forbid claim of unregistered ids 17 | * client: Eliminate state wrapper types returned from `Client::get_org`, 18 | `Client::get_project`, `Client::get_user`, and `Client::get_checktpoin`. 19 | Instead we return the state objects from `radicle_registry_core::state`. 20 | * client: Replaced the associated `Message::Result` type with `Message::Output`. 21 | This should require no changes on the user’s side since 22 | `TransactionIncluded::result` remains structurally equal. 23 | * runtime: Tx author pays for `UnregisterOrg` 24 | * client: Remove `TransactionIncluded::events` field. 25 | * client: Offline transaction signing now requires users to manually specify the 26 | runtime spec version the transaction is valid for. 27 | * client: Renamed `ClientT::onchain_runtime_version` to 28 | `ClientT::runtime_version`. 29 | * client: `ClientT::get_checkpoint` returns a new `Checkpoint` structure 30 | * runtime: abandon `Checkpoints` storage in favor of `Checkpoints1` 31 | * runtime: abandon `InitialCheckpoints` storage in favor of `InitialCheckpoints1` 32 | * runtime: abandon `Projects` storage in favor of `Projects1` 33 | * runtime: abandon `Users` storage in favor of `Users1` 34 | * runtime: abandon `Orgs` storage in favor of `Orgs1` 35 | * runtime: We introduce the `CheckVersion` transaction validation that requires 36 | authors to include the runtime version in the transaction. 37 | * cli: `project register` now also expects domain type 38 | * client: Move deposit costs into constants for better ergonomics 39 | * client: Use `ProjectDomain::Org(id)` istead of just `id` on project-related references 40 | * Only unregistered a user if not a member of any org 41 | * Tx author needs to have an associated registered user to operate on Orgs 42 | * `Org::members` is now `Vec` 43 | * cli: Binary update required to connect to the on-chain runtime with `spec_version` 6, 44 | no longer including the `CheckVersion` signed extension. 45 | * client: `Client::new_emulator()` now returns a pair of a `Client` and 46 | `EmulatorControl`. 47 | * cli: Move `update-runtime` to `runtime update` 48 | * Rename `TransactionApplied` to `TransactionIncluded` 49 | * cli: Rename the key-pair storage file from 'accounts.json' to 'key-pairs.json' 50 | * cli: Move key-pair related commands under the new `key-pair` command group 51 | * client: `TransactionApplied` result is now `Result<(), TransactionError>` 52 | for all messages. 53 | * cli: Drop `_TX_` from the environment options 54 | * cli: group and clean commands by domain and rename options 55 | * Add `fee` to the Client and CLI APIs 56 | * Normalize core::message and cli commands parameters 57 | * Update `ProjectId`, now alias to `(OrgId, ProjectName)` 58 | * Drop `ProjectDomain` 59 | * Update `Project` to the latest spec containing different fields 60 | * Drop `TransferFromProject` message 61 | * `String32::from_string` now returns `Result` 62 | * `Bytes128::from_vec` now returns `Result` 63 | * Make `ProjectDomain` a wrapper of `String32` and only support the "rad" domain. 64 | * Add `metadata` field to Project, a vector of at most 128 bytes. 65 | * Drop project fields `description` and `img_url` 66 | * Rename `Client::submit` to `Client::sign_and_submit_call`. 67 | * `Client::create` and `Client::create_with_executor` now require a `host` 68 | argument. Use `host = url::Host::parse("127.0.0.1").unwrap()` to have the old 69 | behavior. 70 | * `Client::submit` and `Client::submit_transaction` now return a wrapped 71 | future. This allows consumers to distinguish the accepted and applied states 72 | of a transaction. 73 | * Remove convenience methods for submitting transactions from the client 74 | - `Client::transfer` 75 | - `Client::register_project` 76 | - `Client::create_checkpoint` 77 | - `Client::set_checkpoint` 78 | Calls to these functions can be replaced by calls to `Client::submit`. 79 | * Eliminate `ClientWithExecutor`, use `Client::create_with_executor()` instead. 80 | * Eliminate `MemoryClient`. The memory client is now called the emulator and can 81 | be created with `Client::new_emulator()`. 82 | * All library and binary names have been changed to use dashes instead of 83 | underscores e.g. `radicle_registry_node` becomes `radicle-registry-node`. 84 | * The error type in `get_dispatch_result` has changed from 85 | `Option<&'static str>` to `sp_runtime::DispatchError`. 86 | * The `Client` and `ClientT` methods are now `async`. 87 | * The `--block-author` option was replaced with the `--mine` option. A node only 88 | mines if the option is given. 89 | * Polkadot telemetry was removed 90 | 91 | ### Addition 92 | 93 | * client: Expose `REGISTRATION_FEE` 94 | * client: Add `parse_ss58_address` to parse an `AccountId` from a ss58 formatted string 95 | * client: Add `account_exists` to check whether an account exists on chain 96 | * client: Add `fn get_id_status` to get the status of an id (available, taken or retired) 97 | * Support user project registration 98 | * cli: Add `runtime version` command to check the on-chain runtime version 99 | * cli: Add `update-runtime` command to update the on-chain runtime 100 | * cli: Mutually support local account names where only SS58 address were 101 | supported as params. 102 | * Add `user list` and `user show` CLI commands 103 | * Add `MINIMUM_FEE` to the registry client 104 | * The client emulator now authors blocks when a transaction is submitted. 105 | * Add `TransferFromOrg` message 106 | * Add `Client::get_org` and `Client::list_orgs` 107 | * Add `RegisterOrg` and `UnregisterOrg` messages 108 | * Add Transaction::hash() function 109 | * Offline transaction signing with the following new APIs 110 | * `ClientT::account_nonce()` 111 | * `ClientT::genesis_hash()` 112 | * `Transaction` 113 | * `Transaction::new_signed()` 114 | * Add block header fetching to the client API 115 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Abbreviations 2 | You may use common abbreviations in the codebase as long as they: 3 | - don't obscure their meaning for the sole sake of saving a few characters 4 | - are common programming abbreviations (e.g. `num`, `idx` or `iter`) 5 | 6 | For consistency we always use the following domain-specific abbreviation. You 7 | may not use any other domain-specific abbreviations. 8 | - Tx - Transaction 9 | - PoW - Proof of Work 10 | - Org - Organization 11 | 12 | # Git Flow 13 | 14 | This repository follows the Git rebase flow for the most part. 15 | 16 | ## Branches and pull requests 17 | 18 | 1. Create a separate branch for each issue your are working on 19 | 2. Do your magic 20 | 3. Create a pull request and assign reviewers. 21 | - Assign the registry team as the reviewer when one review from whoever member reviews first is 22 | sufficient. The reviewer who arrives first must overwrite themselves as the reviewer to signal 23 | ownership and avoid racing reviews. 24 | - Assign multiple reviewers individually when you expect a review from everyone listed. 25 | 4. Keep your branch up to date by rebasing it from its base branch 26 | 5. Make sure that all the assigned reviewers approve the changes 27 | 6. Consider squashing the branch's commits when the separate commits don't add value 28 | 7. Merge it via the GitHub UI 29 | 8. Delete the branch after its been merged. GitHub does this automatically. 30 | 31 | If you were asked to review a pull request by being member of a team that was assigned 32 | as a reviewer, always assign yourself and unassign the team before you start the review. 33 | 34 | ## Commits 35 | 36 | 1. Make sure you author your commits with the right username and email 37 | 2. Follow the git commit convention: 38 | - Use the imperative mood in the subject line 39 | - Limit the subject line to 50 chars 40 | - Capitalise the subject line 41 | - Wrap the description at 72 characters 42 | - Have the description preferably explaining what and why instead of how 43 | - Separate the subject from the body with an empty line 44 | 45 | # Documentation 46 | 47 | - Document all the public items of a module especially well given that they constitute its API. 48 | - Document private items when the reader's understanding to fully grasp its usage or implementation 49 | is insufficient, despite the limited context of such items. 50 | - The code must be documented using Rustdoc comments. 51 | - Strive for self-documenting code. 52 | - The reader of the documentation is expected to have general knowledge in the fields of blockchains 53 | and cryptocurrencies. 54 | - Leave additional comments explaining 'why' rather than 'how', strive to have the code clean 55 | and elegant to make this aim viable. 56 | 57 | # Code 58 | 59 | ## Imports 60 | 61 | - Only use import aliases to disambiguate, be it types, modules, or crates (in the case of 62 | having two coexisting versions). 63 | 64 | # Changelog 65 | 66 | We use `./CHANGELOG.md` to record all changes visible to users of the client. 67 | Changes are added to the “Upcoming” section of the change log as part of commit 68 | that makes the change. That is they are included in every pull request. For 69 | breaking changes a migration path must be provided. 70 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "cli", 4 | "client", 5 | "core", 6 | "node", 7 | "runtime", 8 | "runtime-tests", 9 | "test-utils" 10 | ] 11 | -------------------------------------------------------------------------------- /LICENSE_HEADER: -------------------------------------------------------------------------------- 1 | // Radicle Registry 2 | // Copyright (C) 2019 Monadic GmbH 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License version 3 as 6 | // published by the Free Software Foundation. 7 | // 8 | // This program is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU General Public License 14 | // along with this program. If not, see . 15 | 16 | -------------------------------------------------------------------------------- /ci/base-image/Dockerfile: -------------------------------------------------------------------------------- 1 | # Image to run builds of the project 2 | # 3 | # Includes 4 | # 5 | # * Rustup with `nightly-2020-06-10` toolchain 6 | # * Additional rustup components and the wasm32 target 7 | # * sccache v0.2.12 8 | 9 | FROM debian:buster-slim 10 | 11 | SHELL ["/bin/bash", "-c"] 12 | 13 | ENV RUSTUP_HOME=/usr/local/rustup \ 14 | CARGO_HOME=/usr/local/cargo \ 15 | PATH=/usr/local/cargo/bin:$PATH \ 16 | HOME=/build 17 | 18 | RUN apt-get update \ 19 | && apt-get install -y --no-install-recommends \ 20 | ca-certificates \ 21 | clang \ 22 | cmake \ 23 | gcc \ 24 | git \ 25 | libclang-dev \ 26 | libssl-dev \ 27 | llvm-dev \ 28 | make \ 29 | pkg-config \ 30 | curl \ 31 | apt-transport-https \ 32 | ca-certificates \ 33 | gnupg \ 34 | && echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" \ 35 | >> /etc/apt/sources.list.d/google-cloud-sdk.list \ 36 | && curl https://packages.cloud.google.com/apt/doc/apt-key.gpg \ 37 | | apt-key --keyring /usr/share/keyrings/cloud.google.gpg add - \ 38 | && apt-get update \ 39 | && apt-get install -y google-cloud-sdk \ 40 | && apt-get autoremove \ 41 | && rm -rf /var/lib/apt/lists/* 42 | 43 | # Install sccache 44 | RUN set -euxo pipefail; \ 45 | sccache_version=0.2.12; \ 46 | sccache_base=sccache-$sccache_version-x86_64-unknown-linux-musl; \ 47 | curl -sSLO https://github.com/mozilla/sccache/releases/download/$sccache_version/$sccache_base.tar.gz; \ 48 | echo "26fd04c1273952cc2a0f359a71c8a1857137f0ee3634058b3f4a63b69fc8eb7f $sccache_base.tar.gz" | sha256sum -c -; \ 49 | tar -zxf "$sccache_base.tar.gz"; \ 50 | mv "$sccache_base/sccache" /usr/local/bin/sccache; \ 51 | rm -r "$sccache_base.tar.gz" "$sccache_base" 52 | 53 | # Install rustup and default toolchain from RUST_VERSION. This is copied from 54 | # https://github.com/rust-lang/docker-rust/blob/8d0f25416858e2c1f59511a15c2bd0445b402caa/1.39.0/buster/slim/Dockerfile 55 | ENV RUST_VERSION=nightly-2020-06-10 56 | RUN set -eux; \ 57 | dpkgArch="$(dpkg --print-architecture)"; \ 58 | case "${dpkgArch##*-}" in \ 59 | amd64) rustArch='x86_64-unknown-linux-gnu'; rustupSha256='e68f193542c68ce83c449809d2cad262cc2bbb99640eb47c58fc1dc58cc30add' ;; \ 60 | armhf) rustArch='armv7-unknown-linux-gnueabihf'; rustupSha256='7c1c329a676e50c287d8183b88f30cd6afd0be140826a9fbbc0e3d717fab34d7' ;; \ 61 | arm64) rustArch='aarch64-unknown-linux-gnu'; rustupSha256='d861cc86594776414de001b96964be645c4bfa27024052704f0976dc3aed1b18' ;; \ 62 | i386) rustArch='i686-unknown-linux-gnu'; rustupSha256='89f1f797dca2e5c1d75790c3c6b7be0ee473a7f4eca9663e623a41272a358da0' ;; \ 63 | *) echo >&2 "unsupported architecture: ${dpkgArch}"; exit 1 ;; \ 64 | esac; \ 65 | url="https://static.rust-lang.org/rustup/archive/1.20.2/${rustArch}/rustup-init"; \ 66 | curl -sSLO "$url"; \ 67 | echo "${rustupSha256} *rustup-init" | sha256sum -c -; \ 68 | chmod +x rustup-init; \ 69 | ./rustup-init -y --no-modify-path --profile minimal --default-toolchain $RUST_VERSION; \ 70 | rm rustup-init; \ 71 | chmod -R a+w $RUSTUP_HOME $CARGO_HOME; \ 72 | rustup --version; \ 73 | cargo --version; \ 74 | rustc --version; 75 | 76 | # Add components to toolchain 77 | RUN rustup component add rustfmt \ 78 | && rustup component add clippy \ 79 | && rustup target add wasm32-unknown-unknown 80 | -------------------------------------------------------------------------------- /ci/deploy: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Deploy a new node version to the devnet. 4 | 5 | set -euo pipefail 6 | 7 | if [[ ${CI_VERBOSE:-} =~ ^1|true$ ]]; then 8 | set -x 9 | fi 10 | 11 | image="gcr.io/opensourcecoin/radicle-registry/node:$BUILDKITE_COMMIT" 12 | 13 | function kubectl() { 14 | ./kubectl \ 15 | --token "$KUBECTL_TOKEN" \ 16 | --kubeconfig ./ci/kubeconfig.yaml \ 17 | --namespace devnet \ 18 | "$@" 19 | } 20 | 21 | curl -sSLO https://storage.googleapis.com/kubernetes-release/release/v1.17.0/bin/linux/amd64/kubectl 22 | echo "6e0aaaffe5507a44ec6b1b8a0fb585285813b78cc045f8804e70a6aac9d1cb4c *kubectl" | sha256sum -c - 23 | chmod +x kubectl 24 | source .secrets 25 | 26 | kubectl set image statefulset.apps/validator radicle-registry-node="$image" 27 | kubectl set image deployment/miner radicle-registry-node="$image" 28 | kubectl rollout status statefulset.apps/validator --timeout 2m 29 | kubectl rollout status deployment/miner --timeout 2m 30 | -------------------------------------------------------------------------------- /ci/kubeconfig.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Config 3 | current-context: radicle-registry-devnet 4 | clusters: 5 | - name: radicle-registry-devnet 6 | cluster: 7 | certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURDekNDQWZPZ0F3SUJBZ0lRQ0QwOEpSSVJRanZWbzlDQlBrMExrVEFOQmdrcWhraUc5dzBCQVFzRkFEQXYKTVMwd0t3WURWUVFERXlRek5HRmpNVFF5Tmkwd01XRXlMVFEwWVdRdFlUWXdNaTFoWkdNNU56WmlPVEZoTVdZdwpIaGNOTWpBd01qRTRNVEl4TmpJMVdoY05NalV3TWpFMk1UTXhOakkxV2pBdk1TMHdLd1lEVlFRREV5UXpOR0ZqCk1UUXlOaTB3TVdFeUxUUTBZV1F0WVRZd01pMWhaR001TnpaaU9URmhNV1l3Z2dFaU1BMEdDU3FHU0liM0RRRUIKQVFVQUE0SUJEd0F3Z2dFS0FvSUJBUUNUd25xV0ZyZVJrMGZEemE4VzJuZFA1QUN6aUlvaDZRRmVxTVJpcUxuRgpSK0toUGRNTWxnV0dOcVROSUFJVjVhMVdhVyt6NERKNFFDd1pEVTRDZ0gzTlpZeGUxY1ZLdzFwRERVbTVzNlBFClAyVjgzYkQ3RmswSXNpRmNPaDc1cUU1OXNndG1ZbXoySUFiT2MrbzV6c0ZEZDREWTI4c3Fma1NzblJieDVHdEgKd0wwbXhqS0RwUjRTZXhwTWM0YjRETHVjblNBTDhTNDc2c3M3NTlFTzdlQS9YRjZ2a3I3eWlnUVc2dExaNW05LwpYQUpHZUltVy9uQ0VZdDdBZkFONVUvVjNWaGQzcTNRQ1BIQWd4eWdvK0RWclh2OTZBSWZSTldZUy9ta0JCRElzCkFhNmEvWTFIYjdPUTNNK1hQVEVxdGZYSUREd0RCajFNckxXTytxSFR5ZXVMQWdNQkFBR2pJekFoTUE0R0ExVWQKRHdFQi93UUVBd0lDQkRBUEJnTlZIUk1CQWY4RUJUQURBUUgvTUEwR0NTcUdTSWIzRFFFQkN3VUFBNElCQVFCVApIMWI1RC9Zc21HZ0hneXVYbTJJVDNKa3NoQmZLK3F6M3htWlhsV1hIUllkdGY3RGYxK3RHZmJhNnlMeDI1am1QCmhtTEZvS0FZcFhpd1dORGdpM3J6Ni83Z2cxVFBNNmtHVVZBS01ad01OeHJaVkozMmtqbVN6ZnlCYnk2NU0wOUgKcDFEYXBET0gwemNCTndYYUNDaXQ5U0g3eW5hcHlxSWlTd0dnQVcxbnNqeFBCTkovaFVvOUExNzFhZ05UM1U4cAo1MnRDOTNDTXoyelpLR2VKdm1LUmQ0UW8xYXRQZTJ0UE1HeGNudytwZE1MYUErUU9LcnI1MGlnbHEyOUZnaXZVClpISkFab0ViamkrMEI1K2I5M0x0bXVFZHhoRTNZY2hQWkVuZmR3NTlkbVUxeUpTTTlSaE9vZzN6d1QrNTZyVnIKamNtZmNPUnpISU5nQndsQkNmdWUKLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo= 8 | server: https://35.233.92.180 9 | contexts: 10 | - name: radicle-registry-devnet 11 | context: 12 | cluster: radicle-registry-devnet 13 | -------------------------------------------------------------------------------- /ci/node-image/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:buster-slim 2 | 3 | ENTRYPOINT ["radicle-registry-node"] 4 | ADD ./radicle-registry-node /usr/local/bin/ 5 | 6 | -------------------------------------------------------------------------------- /ci/run: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Script run by buildkite that builds and tests the project. 4 | # 5 | # See 6 | # * https://buildkite.com/docs/pipelines/managing-log-output 7 | # * https://buildkite.com/docs/pipelines/writing-build-scripts 8 | 9 | set -euo pipefail 10 | 11 | if [[ ${CI_VERBOSE:-} =~ ^1|true$ ]]; then 12 | set -x 13 | fi 14 | 15 | TIMEFORMAT='elapsed time: %R (user: %U, system: %S)' 16 | 17 | declare -r target_cache="/cache/target" 18 | 19 | export CARGO_HOME=/cache/cargo 20 | 21 | export RUSTC_WRAPPER=sccache 22 | export SCCACHE_DIR=/cache/sccache 23 | # Most of the caching is done through caching ./target 24 | export SCCACHE_CACHE_SIZE="1G" 25 | 26 | echo "--- Prepare cache" 27 | free_cache_space_kb=$(df --output=avail /cache | sed -n 2p) 28 | min_free_cache_kb=$(( 800 * 1024 )) 29 | echo "$(( free_cache_space_kb / 1024 )) MiB free space on /cache" 30 | if [[ $free_cache_space_kb -le $min_free_cache_kb ]]; then 31 | echo "Reseting cache with rm -rf /cache/*" 32 | du -sh /cache/* 33 | rm -rf /cache/* 34 | fi 35 | mkdir -p "$target_cache" 36 | ln -s "$target_cache" ./target 37 | 38 | echo "--- scripts/check-license-headers" 39 | time ./scripts/check-license-headers 40 | 41 | echo "--- cargo fmt" 42 | time cargo fmt --all -- --check 43 | 44 | # parse runtime version 45 | runtime_version="$(awk '$1 == "version" {print $3; exit}' runtime/Cargo.toml | tr -d \")" 46 | spec_version="$(echo "$runtime_version" | awk -F . '{print $2}')" 47 | impl_version="$(echo "$runtime_version" | awk -F . '{print $3}')" 48 | latest_runtime_name="v${spec_version}_${impl_version}.wasm" 49 | latest_spec_latest_impl_name="dev_v${spec_version}_latest.json" 50 | latest_spec_first_impl_name="dev_v${spec_version}_0.json" 51 | 52 | export RUSTFLAGS="-D warnings" 53 | 54 | echo "--- rebuilding runtime cache" 55 | scripts/rebuild-runtime-cache 56 | 57 | echo "--- cargo clippy" 58 | cargo clippy --workspace --all-targets --release -- -D clippy::all 59 | echo "--- cargo clippy (for wasm32 target)" 60 | cargo clippy \ 61 | --manifest-path runtime/Cargo.toml \ 62 | --no-default-features \ 63 | --features no-std \ 64 | --target wasm32-unknown-unknown \ 65 | -- \ 66 | -D clippy::all 67 | 68 | echo "--- cargo doc" 69 | RUSTDOCFLAGS="-D intra-doc-link-resolution-failure" \ 70 | cargo doc --workspace --release --no-deps --document-private-items 71 | 72 | echo "--- scripts/build-release" 73 | ./scripts/build-release 74 | 75 | echo "--- run tests" 76 | # The home directory is not writable on CI 77 | export XDG_DATA_HOME=/tmp/xdg-data-home 78 | export XDG_CONFIG_HOME=/tmp/xdg-config-home 79 | scripts/run-tests-all-runtimes 80 | 81 | echo "--- Copy artifacts" 82 | mkdir artifacts 83 | tar -cpzf artifacts/radicle-registry-cli.tar.gz -C target/release radicle-registry-cli 84 | tar -cpzf artifacts/radicle-registry-node.tar.gz -C target/release radicle-registry-node 85 | cp -a target/release/radicle-registry-node ci/node-image 86 | cp runtime-cache/latest.wasm artifacts/runtime.wasm 87 | 88 | 89 | if [[ -n "${BUILDKITE_TAG:-}" ]] 90 | then 91 | declare -r artifact_scope="${BUILDKITE_TAG}" 92 | elif [[ "${BUILDKITE_BRANCH}" == "master" ]] 93 | then 94 | declare -r artifact_scope="master/${BUILDKITE_COMMIT}" 95 | else 96 | declare -r artifact_scope="$BUILDKITE_JOB_ID" 97 | fi 98 | declare -r artifact_prefix="https://builds.radicle.xyz/radicle-registry/${artifact_scope}" 99 | 100 | { 101 | echo "Artifacts" 102 | echo "* \`gcr.io/opensourcecoin/radicle-registry/node:${BUILDKITE_COMMIT}\`" 103 | for path in artifacts/*; do 104 | url="${artifact_prefix}/${path}" 105 | name=$(cut -d / -f 2- <<< "${path}") 106 | echo "* [\`${name}\`](${url})" 107 | done 108 | } | buildkite-agent annotate --context node-binary --style success 109 | 110 | if [[ "${BUILDKITE_BRANCH}" == "master" ]] 111 | then 112 | echo "--- Upload runtime and specs" 113 | 114 | source .secrets 115 | gcloud auth activate-service-account --key-file=- <<< "$RUNTIME_STORAGE_GCP_CREDENTIALS" 116 | 117 | STORAGE_PREFIX='gs://radicle-registry-runtime/' 118 | 119 | echo "uploading runtime" 120 | # Does not overwrite if exists 121 | gsutil cp -n "runtime-cache/latest.wasm" "$STORAGE_PREFIX$latest_runtime_name" 122 | 123 | echo "uploading latest impl spec" 124 | # Overwrites to account for non-runtime spec changes 125 | gsutil cp "runtime-cache/$latest_spec_latest_impl_name" "$STORAGE_PREFIX$latest_spec_latest_impl_name" 126 | 127 | echo "copying latest impl spec to first impl spec" 128 | # Does not overwrite if exists 129 | gsutil cp -n "$STORAGE_PREFIX$latest_spec_latest_impl_name" "$STORAGE_PREFIX$latest_spec_first_impl_name" 130 | fi 131 | 132 | echo "--- Cleanup cache" 133 | 134 | # Remove all executables that need to be rebuild. 135 | find ./target -maxdepth 2 -executable -type f -exec rm {} \; 136 | 137 | # Remove incremental artifacts for local packages. These artifacts are 138 | # usesless for subsequent builds because incremental builds are based 139 | # on timestamps and we always do a fresh checkout of the repository. 140 | find ./target -name incremental -type d -exec rm -r {} \; 141 | 142 | # Remove artifats from local code 143 | rm -r target/release/deps/radicle* target/release/build/radicle* 144 | find ./target -name 'radicle*' -exec rm -r {} \; 145 | 146 | echo "Size of $target_cache is $(du -sh "$target_cache" | cut -f 1)" 147 | -------------------------------------------------------------------------------- /cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | edition = "2018" 3 | name = "radicle-registry-cli" 4 | description = "CLI tools for interacting with the Radicle Registry" 5 | version = "0.0.0" 6 | authors = ["Monadic GmbH "] 7 | homepage = "https://github.com/radicle-dev/radicle-registry" 8 | documentation = "https://github.com/radicle-dev/radicle-registry" 9 | license = "GPL-3.0-only" 10 | repository = "https://github.com/radicle-dev/radicle-registry" 11 | 12 | [dependencies] 13 | radicle-registry-client = { version = "0.0.0", path = "../client" } 14 | 15 | async-std = { version = "1.4", features = ["attributes"] } 16 | async-trait = "0.1" 17 | derive_more = "0.99" 18 | directories = "2.0.2" 19 | futures = "0.3" 20 | hex = "0.4.0" 21 | itertools = "0.8.2" 22 | lazy_static = "1.4.0" 23 | pretty_env_logger = "0.3.1" 24 | serde = { version = "1.0", features = ["derive"] } 25 | serde_json = "1.0" 26 | structopt = "0.3" 27 | thiserror = "1.0" 28 | url = "1.7" 29 | 30 | 31 | [dependencies.sp-core] 32 | git = "https://github.com/paritytech/substrate" 33 | rev = "v2.0.0-rc4" 34 | 35 | [build-dependencies] 36 | vergen = "3" 37 | -------------------------------------------------------------------------------- /cli/build.rs: -------------------------------------------------------------------------------- 1 | use vergen::{generate_cargo_keys, ConstantsFlags}; 2 | 3 | fn main() { 4 | generate_cargo_keys(ConstantsFlags::all()).unwrap(); 5 | } 6 | -------------------------------------------------------------------------------- /cli/src/command/account.rs: -------------------------------------------------------------------------------- 1 | // Radicle Registry 2 | // Copyright (C) 2019 Monadic GmbH 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License version 3 as 6 | // published by the Free Software Foundation. 7 | // 8 | // This program is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU General Public License 14 | // along with this program. If not, see . 15 | 16 | //! Define the commands supported by the CLI related to Accounts. 17 | 18 | use super::*; 19 | 20 | /// Account related commands 21 | #[derive(StructOpt, Clone)] 22 | pub enum Command { 23 | /// Show account information. 24 | Show(Show), 25 | /// Transfer funds from the author to a recipient account. 26 | Transfer(Transfer), 27 | } 28 | 29 | #[async_trait::async_trait] 30 | impl CommandT for Command { 31 | async fn run(self) -> Result<(), CommandError> { 32 | match self { 33 | Command::Show(cmd) => cmd.run().await, 34 | Command::Transfer(cmd) => cmd.run().await, 35 | } 36 | } 37 | } 38 | 39 | #[derive(StructOpt, Clone)] 40 | pub struct Show { 41 | /// The account's SS58 address or the name of a local key pair. 42 | #[structopt( 43 | value_name = "address_or_name", 44 | parse(try_from_str = parse_account_id), 45 | )] 46 | account_id: AccountId, 47 | 48 | #[structopt(flatten)] 49 | network_options: NetworkOptions, 50 | } 51 | 52 | #[async_trait::async_trait] 53 | impl CommandT for Show { 54 | async fn run(self) -> Result<(), CommandError> { 55 | let client = self.network_options.client().await?; 56 | let balance = client.free_balance(&self.account_id).await?; 57 | println!("ss58 address: {}", self.account_id.to_ss58check()); 58 | println!("balance: {} μRAD", balance); 59 | Ok(()) 60 | } 61 | } 62 | 63 | #[derive(StructOpt, Clone)] 64 | pub struct Transfer { 65 | // The amount to transfer. 66 | amount: Balance, 67 | 68 | /// The recipient account. 69 | /// SS58 address or name of a local key pair. 70 | #[structopt(parse(try_from_str = parse_account_id))] 71 | recipient: AccountId, 72 | 73 | #[structopt(flatten)] 74 | network_options: NetworkOptions, 75 | 76 | #[structopt(flatten)] 77 | tx_options: TxOptions, 78 | } 79 | 80 | #[async_trait::async_trait] 81 | impl CommandT for Transfer { 82 | async fn run(self) -> Result<(), CommandError> { 83 | let client = self.network_options.client().await?; 84 | 85 | let transfer_fut = client 86 | .sign_and_submit_message( 87 | &self.tx_options.author, 88 | message::Transfer { 89 | recipient: self.recipient, 90 | amount: self.amount, 91 | }, 92 | self.tx_options.fee, 93 | ) 94 | .await?; 95 | announce_tx("Transferring funds..."); 96 | 97 | let transfered = transfer_fut.await?; 98 | transfered.result?; 99 | println!( 100 | "✓ Transferred {} μRAD to {} in block {}", 101 | self.amount, self.recipient, transfered.block, 102 | ); 103 | Ok(()) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /cli/src/command/key_pair.rs: -------------------------------------------------------------------------------- 1 | // Radicle Registry 2 | // Copyright (C) 2019 Monadic GmbH 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License version 3 as 6 | // published by the Free Software Foundation. 7 | // 8 | // This program is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU General Public License 14 | // along with this program. If not, see . 15 | 16 | //! Define the commands supported by the CLI related to key-pairs. 17 | 18 | use super::*; 19 | use crate::key_pair_storage; 20 | 21 | /// Key-pair related commands 22 | #[derive(StructOpt, Clone)] 23 | pub enum Command { 24 | /// Generate a random key pair identified by `name` and 25 | /// store it on disk. Fail if there is already a key pair 26 | /// with the given `name`. 27 | Generate(Generate), 28 | /// List all the local key pairs. 29 | List(List), 30 | } 31 | 32 | #[async_trait::async_trait] 33 | impl CommandT for Command { 34 | async fn run(self) -> Result<(), CommandError> { 35 | match self { 36 | Command::Generate(cmd) => cmd.run().await, 37 | Command::List(cmd) => cmd.run().await, 38 | } 39 | } 40 | } 41 | 42 | #[derive(StructOpt, Clone)] 43 | pub struct Generate { 44 | /// The name that uniquely identifies the key pair locally. 45 | name: String, 46 | } 47 | 48 | #[async_trait::async_trait] 49 | impl CommandT for Generate { 50 | async fn run(self) -> Result<(), CommandError> { 51 | let (key_pair, seed) = ed25519::Pair::generate(); 52 | key_pair_storage::add(self.name, key_pair_storage::KeyPairData { seed })?; 53 | println!("✓ Key pair generated successfully"); 54 | println!("ⓘ SS58 address: {}", key_pair.public().to_ss58check()); 55 | Ok(()) 56 | } 57 | } 58 | 59 | #[derive(StructOpt, Clone)] 60 | pub struct List {} 61 | 62 | #[async_trait::async_trait] 63 | impl CommandT for List { 64 | async fn run(self) -> Result<(), CommandError> { 65 | let key_pairs = key_pair_storage::list()?; 66 | println!("Key pairs ({})\n", key_pairs.len()); 67 | for (name, data) in key_pairs { 68 | println!(" '{}'", name); 69 | println!( 70 | " ss58 address: {}\n", 71 | ed25519::Pair::from_seed(&data.seed).public().to_ss58check() 72 | ); 73 | } 74 | Ok(()) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /cli/src/command/mod.rs: -------------------------------------------------------------------------------- 1 | // Radicle Registry 2 | // Copyright (C) 2019 Monadic GmbH 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License version 3 as 6 | // published by the Free Software Foundation. 7 | // 8 | // This program is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU General Public License 14 | // along with this program. If not, see . 15 | 16 | //! Define the commands supported by the CLI. 17 | 18 | use crate::{lookup_key_pair, CommandError, CommandT, NetworkOptions, TxOptions}; 19 | use itertools::Itertools; 20 | use radicle_registry_client::*; 21 | 22 | use sp_core::crypto::Ss58Codec; 23 | use structopt::StructOpt; 24 | 25 | pub mod account; 26 | pub mod key_pair; 27 | pub mod org; 28 | pub mod other; 29 | pub mod project; 30 | pub mod runtime; 31 | pub mod user; 32 | 33 | fn parse_account_id(data: &str) -> Result { 34 | Ss58Codec::from_ss58check(data) 35 | .map_err(|err| format!("{:?}", err)) 36 | .or_else(|address_error| { 37 | lookup_key_pair(data) 38 | .map(|key_pair| key_pair.public()) 39 | .map_err(|key_pair_error| { 40 | format!( 41 | " 42 | ! Could not parse an ss58 address nor find a local key pair with the given name. 43 | ⓘ Error parsing SS58 address: {} 44 | ⓘ Error looking up key pair: {} 45 | ", 46 | address_error, key_pair_error 47 | ) 48 | }) 49 | }) 50 | } 51 | 52 | fn announce_tx(msg: &str) { 53 | println!("{}", msg); 54 | println!("⏳ Transactions might take a while to be processed. Please wait..."); 55 | } 56 | -------------------------------------------------------------------------------- /cli/src/command/other.rs: -------------------------------------------------------------------------------- 1 | // Radicle Registry 2 | // Copyright (C) 2019 Monadic GmbH 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License version 3 as 6 | // published by the Free Software Foundation. 7 | // 8 | // This program is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU General Public License 14 | // along with this program. If not, see . 15 | 16 | //! Define the commands supported by the CLI that 17 | //! are not related to any specific domain. 18 | 19 | use super::*; 20 | 21 | /// Other commands, not related to any specific domain. 22 | #[derive(StructOpt, Clone)] 23 | pub enum Command { 24 | /// Show the genesis hash the node uses 25 | GenesisHash(ShowGenesisHash), 26 | } 27 | 28 | #[async_trait::async_trait] 29 | impl CommandT for Command { 30 | async fn run(self) -> Result<(), CommandError> { 31 | match self { 32 | Command::GenesisHash(cmd) => cmd.run().await, 33 | } 34 | } 35 | } 36 | 37 | #[derive(StructOpt, Clone)] 38 | pub struct ShowGenesisHash { 39 | #[structopt(flatten)] 40 | network_options: NetworkOptions, 41 | } 42 | 43 | #[async_trait::async_trait] 44 | impl CommandT for ShowGenesisHash { 45 | async fn run(self) -> Result<(), CommandError> { 46 | let client = self.network_options.client().await?; 47 | let genesis_hash = client.genesis_hash(); 48 | println!("Genesis block hash: 0x{}", hex::encode(genesis_hash)); 49 | Ok(()) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /cli/src/command/project.rs: -------------------------------------------------------------------------------- 1 | // Radicle Registry 2 | // Copyright (C) 2019 Monadic GmbH 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License version 3 as 6 | // published by the Free Software Foundation. 7 | // 8 | // This program is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU General Public License 14 | // along with this program. If not, see . 15 | 16 | //! Define the commands supported by the CLI related to Projects. 17 | 18 | use super::*; 19 | use structopt::clap::arg_enum; 20 | 21 | /// Project related commands 22 | #[derive(StructOpt, Clone)] 23 | pub enum Command { 24 | /// List all projects in the registry 25 | List(List), 26 | /// Register a project with the given name under the given org. 27 | Register(Register), 28 | } 29 | 30 | #[async_trait::async_trait] 31 | impl CommandT for Command { 32 | async fn run(self) -> Result<(), CommandError> { 33 | match self { 34 | Command::List(cmd) => cmd.run().await, 35 | Command::Register(cmd) => cmd.run().await, 36 | } 37 | } 38 | } 39 | 40 | #[derive(StructOpt, Clone)] 41 | pub struct List { 42 | #[structopt(flatten)] 43 | network_options: NetworkOptions, 44 | } 45 | 46 | #[async_trait::async_trait] 47 | impl CommandT for List { 48 | async fn run(self) -> Result<(), CommandError> { 49 | let client = self.network_options.client().await?; 50 | let project_ids = client.list_projects().await?; 51 | println!("PROJECTS ({})", project_ids.len()); 52 | for (name, org) in project_ids { 53 | println!("{}.{:?}", name, org) 54 | } 55 | Ok(()) 56 | } 57 | } 58 | 59 | #[derive(StructOpt, Clone)] 60 | pub struct Register { 61 | /// Name of the project to register. 62 | project_name: ProjectName, 63 | 64 | /// The type of domain under which to register the project. 65 | #[structopt( 66 | possible_values = &DomainType::variants(), 67 | case_insensitive = true, 68 | )] 69 | domain_type: DomainType, 70 | 71 | /// The id of the domain under which to register the project. 72 | domain_id: Id, 73 | 74 | /// Project state hash. A hex-encoded 32 byte string. Defaults to all zeros. 75 | project_hash: Option, 76 | 77 | #[structopt(flatten)] 78 | network_options: NetworkOptions, 79 | 80 | #[structopt(flatten)] 81 | tx_options: TxOptions, 82 | } 83 | 84 | #[async_trait::async_trait] 85 | impl CommandT for Register { 86 | async fn run(self) -> Result<(), CommandError> { 87 | let client = self.network_options.client().await?; 88 | let project_domain = match self.domain_type { 89 | DomainType::Org => ProjectDomain::Org(self.domain_id), 90 | DomainType::User => ProjectDomain::User(self.domain_id), 91 | }; 92 | let register_project_fut = client 93 | .sign_and_submit_message( 94 | &self.tx_options.author, 95 | message::RegisterProject { 96 | project_name: self.project_name.clone(), 97 | project_domain: project_domain.clone(), 98 | metadata: Bytes128::random(), 99 | }, 100 | self.tx_options.fee, 101 | ) 102 | .await?; 103 | announce_tx("Registering project..."); 104 | 105 | let project_registered = register_project_fut.await?; 106 | project_registered.result?; 107 | println!( 108 | "✓ Project {}.{:?} registered in block {}", 109 | self.project_name, project_domain, project_registered.block, 110 | ); 111 | Ok(()) 112 | } 113 | } 114 | 115 | arg_enum! { 116 | #[derive(Clone, Eq, PartialEq, Debug)] 117 | enum DomainType { 118 | Org, 119 | User, 120 | } 121 | } 122 | 123 | #[cfg(test)] 124 | mod test { 125 | use super::*; 126 | use std::str::FromStr; 127 | 128 | #[test] 129 | fn test_project_domain_from_org() { 130 | for org_input in &["org", "oRg", "ORG"] { 131 | let res = DomainType::from_str(org_input); 132 | assert_eq!(res, Ok(DomainType::Org)); 133 | } 134 | } 135 | 136 | #[test] 137 | fn test_project_domain_from_user() { 138 | for user_input in &["user", "usEr", "USER"] { 139 | let res = DomainType::from_str(user_input); 140 | assert_eq!(res, Ok(DomainType::User)); 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /cli/src/command/runtime.rs: -------------------------------------------------------------------------------- 1 | // Radicle Registry 2 | // Copyright (C) 2019 Monadic GmbH 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License version 3 as 6 | // published by the Free Software Foundation. 7 | // 8 | // This program is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU General Public License 14 | // along with this program. If not, see . 15 | 16 | //! Define the commands supported by the CLI related to the on-chain runtime. 17 | 18 | use super::*; 19 | 20 | /// Runtime related commands 21 | #[derive(StructOpt, Clone)] 22 | pub enum Command { 23 | /// Submit a transaction to update the on-chain runtime. 24 | Update(Update), 25 | 26 | /// Show the version of the on-chain runtime. 27 | Version(ShowVersion), 28 | } 29 | 30 | #[async_trait::async_trait] 31 | impl CommandT for Command { 32 | async fn run(self) -> Result<(), CommandError> { 33 | match self { 34 | Command::Update(cmd) => cmd.run().await, 35 | Command::Version(cmd) => cmd.run().await, 36 | } 37 | } 38 | } 39 | 40 | #[derive(StructOpt, Clone)] 41 | pub struct Update { 42 | /// The path to the (wasm) runtime code to submit 43 | path: std::path::PathBuf, 44 | 45 | #[structopt(flatten)] 46 | network_options: NetworkOptions, 47 | 48 | #[structopt(flatten)] 49 | tx_options: TxOptions, 50 | } 51 | 52 | #[async_trait::async_trait] 53 | impl CommandT for Update { 54 | async fn run(self) -> Result<(), CommandError> { 55 | let client = self.network_options.client().await?; 56 | let new_runtime_code = 57 | std::fs::read(self.path).expect("Invalid path or couldn't read the wasm file"); 58 | 59 | let update_runtime_fut = client 60 | .sign_and_submit_message( 61 | &self.tx_options.author, 62 | message::UpdateRuntime { 63 | code: new_runtime_code, 64 | }, 65 | self.tx_options.fee, 66 | ) 67 | .await?; 68 | announce_tx("Submitting the new on-chain runtime..."); 69 | 70 | update_runtime_fut.await?.result?; 71 | println!("✓ The new on-chain runtime is now published."); 72 | Ok(()) 73 | } 74 | } 75 | 76 | #[derive(StructOpt, Clone)] 77 | pub struct ShowVersion { 78 | #[structopt(flatten)] 79 | network_options: NetworkOptions, 80 | } 81 | 82 | #[async_trait::async_trait] 83 | impl CommandT for ShowVersion { 84 | async fn run(self) -> Result<(), CommandError> { 85 | let client = self.network_options.client().await?; 86 | let v = client.runtime_version().await?; 87 | println!("On-chain runtime version:"); 88 | println!(" spec_version: {}", v.spec_version); 89 | println!(" impl_version: {}", v.impl_version); 90 | Ok(()) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /cli/src/command/user.rs: -------------------------------------------------------------------------------- 1 | // Radicle Registry 2 | // Copyright (C) 2019 Monadic GmbH 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License version 3 as 6 | // published by the Free Software Foundation. 7 | // 8 | // This program is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU General Public License 14 | // along with this program. If not, see . 15 | 16 | //! Define the commands supported by the CLI related to Users. 17 | 18 | use super::*; 19 | 20 | /// User related commands 21 | #[derive(StructOpt, Clone)] 22 | pub enum Command { 23 | /// Register a user. 24 | Register(Register), 25 | /// Unregister a user. 26 | Unregister(Unregister), 27 | /// Show information for a registered user. 28 | Show(Show), 29 | /// List all users in the registry. 30 | List(List), 31 | } 32 | 33 | #[async_trait::async_trait] 34 | impl CommandT for Command { 35 | async fn run(self) -> Result<(), CommandError> { 36 | match self { 37 | user::Command::Register(cmd) => cmd.run().await, 38 | user::Command::Unregister(cmd) => cmd.run().await, 39 | user::Command::Show(cmd) => cmd.run().await, 40 | user::Command::List(cmd) => cmd.run().await, 41 | } 42 | } 43 | } 44 | 45 | #[derive(StructOpt, Clone)] 46 | pub struct Register { 47 | /// Id of the user to register. The valid charset is: 'a-z0-9-' and can't begin or end with 48 | /// a '-', must also not contain more than two '-' in a row. 49 | user_id: Id, 50 | 51 | #[structopt(flatten)] 52 | network_options: NetworkOptions, 53 | 54 | #[structopt(flatten)] 55 | tx_options: TxOptions, 56 | } 57 | 58 | #[async_trait::async_trait] 59 | impl CommandT for Register { 60 | async fn run(self) -> Result<(), CommandError> { 61 | let client = self.network_options.client().await?; 62 | let register_user_fut = client 63 | .sign_and_submit_message( 64 | &self.tx_options.author, 65 | message::RegisterUser { 66 | user_id: self.user_id.clone(), 67 | }, 68 | self.tx_options.fee, 69 | ) 70 | .await?; 71 | announce_tx("Registering user..."); 72 | 73 | register_user_fut.await?.result?; 74 | println!("✓ User {} is now registered.", self.user_id); 75 | Ok(()) 76 | } 77 | } 78 | 79 | #[derive(StructOpt, Clone)] 80 | pub struct Unregister { 81 | /// Id of the org to unregister. 82 | user_id: Id, 83 | 84 | #[structopt(flatten)] 85 | network_options: NetworkOptions, 86 | 87 | #[structopt(flatten)] 88 | tx_options: TxOptions, 89 | } 90 | 91 | #[async_trait::async_trait] 92 | impl CommandT for Unregister { 93 | async fn run(self) -> Result<(), CommandError> { 94 | let client = self.network_options.client().await?; 95 | let unregister_user = client 96 | .sign_and_submit_message( 97 | &self.tx_options.author, 98 | message::UnregisterUser { 99 | user_id: self.user_id.clone(), 100 | }, 101 | self.tx_options.fee, 102 | ) 103 | .await?; 104 | announce_tx("Unregistering user..."); 105 | 106 | unregister_user.await?.result?; 107 | println!("✓ User {} is now unregistered.", self.user_id); 108 | Ok(()) 109 | } 110 | } 111 | 112 | #[derive(StructOpt, Clone)] 113 | pub struct Show { 114 | /// The id of the user 115 | user_id: Id, 116 | 117 | #[structopt(flatten)] 118 | network_options: NetworkOptions, 119 | } 120 | 121 | #[async_trait::async_trait] 122 | impl CommandT for Show { 123 | async fn run(self) -> Result<(), CommandError> { 124 | let client = self.network_options.client().await?; 125 | let user = 126 | client 127 | .get_user(self.user_id.clone()) 128 | .await? 129 | .ok_or(CommandError::UserNotFound { 130 | user_id: self.user_id.clone(), 131 | })?; 132 | let balance = client.free_balance(&user.account_id()).await?; 133 | 134 | println!("id: {}", self.user_id); 135 | println!("account id: {}", user.account_id()); 136 | println!("balance: {} μRAD", balance); 137 | println!("projects: [{}]", user.projects().iter().format(", ")); 138 | Ok(()) 139 | } 140 | } 141 | 142 | #[derive(StructOpt, Clone)] 143 | pub struct List { 144 | #[structopt(flatten)] 145 | network_options: NetworkOptions, 146 | } 147 | 148 | #[async_trait::async_trait] 149 | impl CommandT for List { 150 | async fn run(self) -> Result<(), CommandError> { 151 | let client = self.network_options.client().await?; 152 | let user_ids = client.list_users().await?; 153 | println!("USERS ({})", user_ids.len()); 154 | for user_id in user_ids { 155 | println!("{}", user_id) 156 | } 157 | Ok(()) 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /cli/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Radicle Registry 2 | // Copyright (C) 2019 Monadic GmbH 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License version 3 as 6 | // published by the Free Software Foundation. 7 | // 8 | // This program is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU General Public License 14 | // along with this program. If not, see . 15 | 16 | //! Define the command line parser and interface. 17 | 18 | #![allow(clippy::large_enum_variant)] 19 | 20 | use lazy_static::lazy_static; 21 | use radicle_registry_client::*; 22 | use structopt::StructOpt; 23 | use thiserror::Error as ThisError; 24 | 25 | pub mod key_pair_storage; 26 | 27 | mod command; 28 | use command::{account, key_pair, org, other, project, runtime, user}; 29 | 30 | /// The type that captures the command line. 31 | #[derive(StructOpt, Clone)] 32 | #[structopt( 33 | max_term_width = 80, 34 | version = env!("VERGEN_SEMVER"), 35 | )] 36 | pub struct CommandLine { 37 | #[structopt(subcommand)] 38 | pub command: Command, 39 | } 40 | 41 | impl CommandLine { 42 | pub async fn run(self) -> Result<(), CommandError> { 43 | self.command.run().await 44 | } 45 | } 46 | 47 | /// Network-related command-line options 48 | #[derive(StructOpt, Clone, Debug)] 49 | pub struct NetworkOptions { 50 | /// IP address or domain name that hosts the RPC API 51 | #[structopt( 52 | long, 53 | default_value = "rpc.ff.radicle.network", 54 | env = "RAD_NODE_HOST", 55 | parse(try_from_str = Self::parse_node_host), 56 | )] 57 | pub node_host: url::Host, 58 | } 59 | 60 | impl NetworkOptions { 61 | fn parse_node_host(value: &str) -> Result { 62 | let node_host = match value { 63 | // "localhost" gets translated to its ipv6 version in some 64 | // systems, which causes the client-node rpc connection to 65 | // fail as ipv6 is not yet supported. 66 | "localhost" => "127.0.0.1", 67 | x => x, 68 | }; 69 | url::Host::parse(node_host) 70 | } 71 | 72 | pub async fn client(&self) -> Result { 73 | Client::create_with_executor(self.node_host.clone()).await 74 | } 75 | } 76 | 77 | /// Transaction-related command-line options 78 | #[derive(StructOpt, Clone)] 79 | pub struct TxOptions { 80 | /// The name of the local key-pair to be used to sign transactions. 81 | #[structopt( 82 | long, 83 | env = "RAD_AUTHOR", 84 | value_name = "key_pair_name", 85 | parse(try_from_str = lookup_key_pair) 86 | )] 87 | pub author: ed25519::Pair, 88 | 89 | /// Fee that will be charged to submit transactions. 90 | /// The higher the fee, the higher the priority of a transaction. 91 | #[structopt(long, default_value = &FEE_DEFAULT, env = "RAD_FEE", value_name = "fee")] 92 | pub fee: Balance, 93 | } 94 | 95 | lazy_static! { 96 | static ref FEE_DEFAULT: String = MINIMUM_TX_FEE.to_string(); 97 | } 98 | 99 | fn lookup_key_pair(name: &str) -> Result { 100 | key_pair_storage::get(name) 101 | .map(|data| ed25519::Pair::from_seed(&data.seed)) 102 | .map_err(|e| format!("{}", e)) 103 | } 104 | 105 | /// The supported [CommandLine] commands. 106 | /// The commands are grouped by domain. 107 | #[derive(StructOpt, Clone)] 108 | pub enum Command { 109 | Account(account::Command), 110 | KeyPair(key_pair::Command), 111 | Org(org::Command), 112 | Project(project::Command), 113 | Runtime(runtime::Command), 114 | User(user::Command), 115 | 116 | #[structopt(flatten)] 117 | Other(other::Command), 118 | } 119 | 120 | #[async_trait::async_trait] 121 | impl CommandT for Command { 122 | async fn run(self) -> Result<(), CommandError> { 123 | match self.clone() { 124 | Command::Account(cmd) => cmd.run().await, 125 | Command::KeyPair(cmd) => cmd.run().await, 126 | Command::Org(cmd) => cmd.run().await, 127 | Command::Project(cmd) => cmd.run().await, 128 | Command::User(cmd) => cmd.run().await, 129 | Command::Runtime(cmd) => cmd.run().await, 130 | Command::Other(cmd) => cmd.run().await, 131 | } 132 | } 133 | } 134 | 135 | /// The trait that every command must implement. 136 | #[async_trait::async_trait] 137 | pub trait CommandT { 138 | async fn run(self) -> Result<(), CommandError>; 139 | } 140 | 141 | /// Error returned by [CommandT::run]. 142 | /// 143 | /// Implements [From] for client errors and [key_pair_storage] errors. 144 | #[derive(Debug, ThisError)] 145 | pub enum CommandError { 146 | #[error("client error")] 147 | ClientError(#[from] Error), 148 | 149 | #[error(transparent)] 150 | FailedTransaction(#[from] TransactionError), 151 | 152 | #[error("cannot find org {org_id}")] 153 | OrgNotFound { org_id: Id }, 154 | 155 | #[error("cannot find user {user_id}")] 156 | UserNotFound { user_id: Id }, 157 | 158 | #[error("cannot find project {project_name}.{project_domain:?}")] 159 | ProjectNotFound { 160 | project_name: ProjectName, 161 | project_domain: ProjectDomain, 162 | }, 163 | 164 | #[error(transparent)] 165 | KeyPairStorageError(#[from] key_pair_storage::Error), 166 | } 167 | -------------------------------------------------------------------------------- /cli/src/main.rs: -------------------------------------------------------------------------------- 1 | // Radicle Registry 2 | // Copyright (C) 2019 Monadic GmbH 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License version 3 as 6 | // published by the Free Software Foundation. 7 | // 8 | // This program is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU General Public License 14 | // along with this program. If not, see . 15 | 16 | //! The executable entry point for the Radicle Registry CLI. 17 | 18 | use radicle_registry_cli::CommandLine; 19 | use std::error::Error; 20 | use structopt::StructOpt; 21 | 22 | #[async_std::main] 23 | async fn main() { 24 | pretty_env_logger::init(); 25 | let cmd_line = CommandLine::from_args(); 26 | let result = cmd_line.run().await; 27 | 28 | match result { 29 | Ok(_) => std::process::exit(0), 30 | Err(error) => { 31 | print_error(&error); 32 | std::process::exit(1); 33 | } 34 | } 35 | } 36 | 37 | fn print_error(mut error: &dyn Error) { 38 | eprintln!("Error: {}", error); 39 | while let Some(source) = error.source() { 40 | error = source; 41 | eprintln!(" Caused by: {}", error); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /client/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | edition = "2018" 3 | name = "radicle-registry-client" 4 | description = "Client library to interact with Radicle Registry node" 5 | version = "0.0.0" 6 | authors = ["Monadic GmbH "] 7 | homepage = "https://github.com/radicle-dev/radicle-registry" 8 | documentation = "https://github.com/radicle-dev/radicle-registry" 9 | license = "GPL-3.0-only" 10 | repository = "https://github.com/radicle-dev/radicle-registry" 11 | 12 | [dependencies] 13 | radicle-registry-core = { path = "../core" } 14 | radicle-registry-runtime = { path = "../runtime" } 15 | 16 | async-trait = "0.1" 17 | derive_more = "0.15" 18 | env_logger = "0.7" 19 | failure = "0.1.7" 20 | futures01 = { package = "futures", version = "0.1" } 21 | futures = { version = "0.3", features = ["compat"] } 22 | jsonrpc-core-client = { version = "14.0", features = ["ws"] } 23 | lazy_static = "1.4" 24 | log = "0.4" 25 | parity-scale-codec = "1.0" 26 | serde = "1.0" 27 | thiserror = "1.0.14" 28 | tokio = "0.1" 29 | url = "1.7" 30 | 31 | [dependencies.frame-system] 32 | git = "https://github.com/paritytech/substrate" 33 | rev = "v2.0.0-rc4" 34 | 35 | [dependencies.frame-support] 36 | git = "https://github.com/paritytech/substrate" 37 | rev = "v2.0.0-rc4" 38 | 39 | [dependencies.sc-rpc-api] 40 | git = "https://github.com/paritytech/substrate" 41 | rev = "v2.0.0-rc4" 42 | 43 | [dependencies.sp-core] 44 | git = "https://github.com/paritytech/substrate" 45 | rev = "v2.0.0-rc4" 46 | 47 | [dependencies.sp-inherents] 48 | git = "https://github.com/paritytech/substrate" 49 | rev = "v2.0.0-rc4" 50 | 51 | [dependencies.sp-io] 52 | git = "https://github.com/paritytech/substrate" 53 | rev = "v2.0.0-rc4" 54 | 55 | [dependencies.sp-rpc] 56 | git = "https://github.com/paritytech/substrate" 57 | rev = "v2.0.0-rc4" 58 | 59 | [dependencies.sp-runtime] 60 | git = "https://github.com/paritytech/substrate" 61 | rev = "v2.0.0-rc4" 62 | 63 | [dependencies.sp-state-machine] 64 | git = "https://github.com/paritytech/substrate" 65 | rev = "v2.0.0-rc4" 66 | 67 | [dependencies.sp-transaction-pool] 68 | git = "https://github.com/paritytech/substrate" 69 | rev = "v2.0.0-rc4" 70 | 71 | [dependencies.sp-timestamp] 72 | git = "https://github.com/paritytech/substrate" 73 | rev = "v2.0.0-rc4" 74 | 75 | [dev-dependencies] 76 | async-std = { version = "1.4", features = ["attributes"] } 77 | rand = "0.7.2" 78 | radicle-registry-test-utils = { path = "../test-utils"} 79 | serial_test = "0.3.2" 80 | -------------------------------------------------------------------------------- /client/examples/getting_started.rs: -------------------------------------------------------------------------------- 1 | // Radicle Registry 2 | // Copyright (C) 2019 Monadic GmbH 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License version 3 as 6 | // published by the Free Software Foundation. 7 | // 8 | // This program is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU General Public License 14 | // along with this program. If not, see . 15 | 16 | //! Getting started with the client by transfering funds. 17 | //! 18 | //! We’re transferring some funds from Alice to Bob and will inspect the node state. 19 | //! 20 | //! To run this example you need a running dev node. You can start it with 21 | //! `cargo run -p radicle-registry-node -- --dev`. 22 | 23 | use radicle_registry_client::*; 24 | 25 | #[async_std::main] 26 | async fn main() -> Result<(), Error> { 27 | env_logger::init(); 28 | 29 | // Create a key pair to author transactions from some seed data. This account is initialized 30 | // with funds on the local development chain. 31 | let alice = ed25519::Pair::from_string("//Alice", None).unwrap(); 32 | println!("Sending funds from //Alice ({})", alice.public()); 33 | 34 | // The receiver of the money transfer is Bob. We only need the public key 35 | let bob_public = ed25519::Pair::from_string("//Bob", None).unwrap().public(); 36 | println!("Recipient: //Bob ({})", bob_public); 37 | 38 | // Create and connect to a client on local host 39 | let node_host = url::Host::parse("127.0.0.1").unwrap(); 40 | println!("Connecting to node on {}", node_host); 41 | let client = Client::create_with_executor(node_host).await?; 42 | 43 | // Show balances of Alice’s and Bob’s accounts 44 | let balance_alice = client.free_balance(&alice.public()).await?; 45 | println!("Balance Alice: {}", balance_alice); 46 | let balance_bob = client.free_balance(&bob_public).await?; 47 | println!("Balance Bob: {}", balance_bob); 48 | 49 | // Sign and submit the message. If successful, returns a future that 50 | // resolves when the transaction is included in a block. 51 | print!("Submitting transfer transaction... "); 52 | let transfer_submitted = client 53 | .sign_and_submit_message( 54 | &alice, 55 | message::Transfer { 56 | recipient: bob_public, 57 | amount: 1, 58 | }, 59 | 777, 60 | ) 61 | .await?; 62 | println!("done"); 63 | 64 | print!("Waiting for transaction to be included in block... "); 65 | let transfer_applied = transfer_submitted.await?; 66 | println!("done"); 67 | 68 | // We can use the [TransactionIncluded] struct to get the block. 69 | println!("Transaction included in block {}", transfer_applied.block); 70 | 71 | // We can also use it to get result of applying the transaction in the ledger. This might fail 72 | // for example if the transaction author does not have the necessary funds. 73 | match transfer_applied.result { 74 | Ok(()) => println!("Funds successfully transferred!"), 75 | Err(err) => println!("Failed to transfer funds: {:?}", err), 76 | } 77 | 78 | // Show the new balances 79 | let balance_alice = client.free_balance(&alice.public()).await?; 80 | println!("Balance Alice: {}", balance_alice); 81 | let balance_bob = client.free_balance(&bob_public).await?; 82 | println!("Balance Bob: {}", balance_bob); 83 | 84 | Ok(()) 85 | } 86 | -------------------------------------------------------------------------------- /client/examples/project_registration.rs: -------------------------------------------------------------------------------- 1 | // Radicle Registry 2 | // Copyright (C) 2019 Monadic GmbH 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License version 3 as 6 | // published by the Free Software Foundation. 7 | // 8 | // This program is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU General Public License 14 | // along with this program. If not, see . 15 | 16 | //! Register a project on the ledger 17 | use std::convert::TryFrom; 18 | 19 | use radicle_registry_client::*; 20 | 21 | #[async_std::main] 22 | async fn main() -> Result<(), Error> { 23 | env_logger::init(); 24 | let alice = ed25519::Pair::from_string("//Alice", None).unwrap(); 25 | 26 | let node_host = url::Host::parse("127.0.0.1").unwrap(); 27 | let client = Client::create_with_executor(node_host).await?; 28 | 29 | let project_name = ProjectName::try_from("radicle-registry").unwrap(); 30 | let org_id = Id::try_from("monadic").unwrap(); 31 | 32 | // Register the project 33 | client 34 | .sign_and_submit_message( 35 | &alice, 36 | message::RegisterProject { 37 | project_name: project_name.clone(), 38 | project_domain: ProjectDomain::Org(org_id.clone()), 39 | metadata: Bytes128::random(), 40 | }, 41 | 567, 42 | ) 43 | .await? 44 | .await? 45 | .result 46 | .unwrap(); 47 | 48 | println!( 49 | "Successfully registered project {}.{}", 50 | project_name, org_id 51 | ); 52 | Ok(()) 53 | } 54 | -------------------------------------------------------------------------------- /client/examples/user_registration.rs: -------------------------------------------------------------------------------- 1 | // Radicle Registry 2 | // Copyright (C) 2019 Monadic GmbH 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License version 3 as 6 | // published by the Free Software Foundation. 7 | // 8 | // This program is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU General Public License 14 | // along with this program. If not, see . 15 | 16 | //! Register a user on the ledger. 17 | use sp_core::crypto::Pair; 18 | use std::convert::TryFrom; 19 | 20 | use radicle_registry_client::{ed25519, message, Client, ClientT, Id}; 21 | 22 | #[async_std::main] 23 | async fn main() { 24 | env_logger::init(); 25 | let client = { 26 | let node_host = url::Host::parse("127.0.0.1").unwrap(); 27 | Client::create_with_executor(node_host).await.unwrap() 28 | }; 29 | let alice = ed25519::Pair::from_string("//Alice", None).unwrap(); 30 | let user_id = Id::try_from("cloudhead").unwrap(); 31 | 32 | // Register the user. 33 | client 34 | .sign_and_submit_message( 35 | &alice, 36 | message::RegisterUser { 37 | user_id: user_id.clone(), 38 | }, 39 | 100, 40 | ) 41 | .await 42 | .unwrap() 43 | .await 44 | .unwrap() 45 | .result 46 | .unwrap(); 47 | 48 | println!("Successfully registered user {}", user_id); 49 | } 50 | -------------------------------------------------------------------------------- /client/src/backend/mod.rs: -------------------------------------------------------------------------------- 1 | // Radicle Registry 2 | // Copyright (C) 2019 Monadic GmbH 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License version 3 as 6 | // published by the Free Software Foundation. 7 | // 8 | // This program is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU General Public License 14 | // along with this program. If not, see . 15 | 16 | //! Define trait for client backends and provide emulator and remote node implementation 17 | use futures::future::BoxFuture; 18 | 19 | pub use radicle_registry_runtime::{Hash, Header, RuntimeVersion, UncheckedExtrinsic}; 20 | 21 | use crate::event::Event; 22 | use crate::interface::*; 23 | 24 | mod emulator; 25 | mod remote_node; 26 | mod remote_node_with_executor; 27 | 28 | pub use emulator::{Emulator, EmulatorControl, BLOCK_AUTHOR as EMULATOR_BLOCK_AUTHOR}; 29 | pub use remote_node::RemoteNode; 30 | pub use remote_node_with_executor::RemoteNodeWithExecutor; 31 | 32 | pub type TransactionStatus = sp_transaction_pool::TransactionStatus; 33 | 34 | /// Indicator that a transaction has been included in a block and has run in the runtime. 35 | /// 36 | /// Obtained after a transaction has been submitted and processed. 37 | pub struct TransactionIncluded { 38 | pub tx_hash: TxHash, 39 | /// The hash of the block the transaction is included in. 40 | pub block: Hash, 41 | /// Events emitted by this transaction 42 | pub events: Vec, 43 | } 44 | 45 | /// Backend for talking to the ledger on a block chain. 46 | /// 47 | /// The interface is low-level and mostly agnostic of the runtime code. Transaction extra data and 48 | /// event information from the runtime marks an exception 49 | #[async_trait::async_trait] 50 | pub trait Backend { 51 | /// Submit a signed transaction to the ledger and returns a future that resolves when the 52 | /// transaction has been applied and included in a block. 53 | async fn submit( 54 | &self, 55 | xt: UncheckedExtrinsic, 56 | ) -> Result>, Error>; 57 | 58 | /// Fetch a value from the runtime state storage at the given block. 59 | async fn fetch( 60 | &self, 61 | key: &[u8], 62 | block_hash: Option, 63 | ) -> Result>, Error>; 64 | 65 | /// Fetch all keys with the given prefix from the state storage at the given block. 66 | async fn fetch_keys( 67 | &self, 68 | prefix: &[u8], 69 | block_hash: Option, 70 | ) -> Result>, Error>; 71 | 72 | /// Fetch the header of the given block hash. 73 | /// If the block hash is `None`, fetch the header of the best chain tip. 74 | async fn block_header(&self, block_hash: Option) -> Result, Error>; 75 | 76 | /// Get the genesis hash of the blockchain. This must be obtained on backend creation. 77 | fn get_genesis_hash(&self) -> Hash; 78 | 79 | /// Get the runtime version at the latest block 80 | async fn runtime_version(&self) -> Result; 81 | } 82 | -------------------------------------------------------------------------------- /client/src/backend/remote_node_with_executor.rs: -------------------------------------------------------------------------------- 1 | // Radicle Registry 2 | // Copyright (C) 2019 Monadic GmbH 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License version 3 as 6 | // published by the Free Software Foundation. 7 | // 8 | // This program is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU General Public License 14 | // along with this program. If not, see . 15 | 16 | //! Provides [RemoteNodeWithExecutor] backend 17 | use futures::compat::Executor01CompatExt; 18 | use futures::future::BoxFuture; 19 | use futures::task::SpawnExt; 20 | use std::sync::Arc; 21 | 22 | use crate::backend; 23 | use crate::interface::*; 24 | 25 | /// Client backend that wraps [crate::backend::RemoteNode] but spawns all futures in 26 | /// its own executor using [tokio::runtime::Runtime]. 27 | #[derive(Clone)] 28 | pub struct RemoteNodeWithExecutor { 29 | backend: backend::RemoteNode, 30 | runtime: Arc, 31 | } 32 | 33 | impl RemoteNodeWithExecutor { 34 | pub async fn create(host: url::Host) -> Result { 35 | let runtime = tokio::runtime::Runtime::new().unwrap(); 36 | let backend = Executor01CompatExt::compat(runtime.executor()) 37 | .spawn_with_handle(backend::RemoteNode::create(host)) 38 | .unwrap() 39 | .await?; 40 | Ok(RemoteNodeWithExecutor { 41 | backend, 42 | runtime: Arc::new(runtime), 43 | }) 44 | } 45 | } 46 | 47 | #[async_trait::async_trait] 48 | impl backend::Backend for RemoteNodeWithExecutor { 49 | async fn submit( 50 | &self, 51 | xt: backend::UncheckedExtrinsic, 52 | ) -> Result>, Error> { 53 | let exec = Executor01CompatExt::compat(self.runtime.executor()); 54 | let backend = self.backend.clone(); 55 | let handle = exec 56 | .spawn_with_handle(async move { backend.submit(xt).await }) 57 | .unwrap(); 58 | let fut = handle.await?; 59 | Ok(Box::pin(exec.spawn_with_handle(fut).unwrap())) 60 | } 61 | 62 | async fn fetch( 63 | &self, 64 | key: &[u8], 65 | block_hash: Option, 66 | ) -> Result>, Error> { 67 | let backend = self.backend.clone(); 68 | let key = Vec::from(key); 69 | let handle = Executor01CompatExt::compat(self.runtime.executor()) 70 | .spawn_with_handle(async move { backend.fetch(&key, block_hash).await }) 71 | .unwrap(); 72 | handle.await 73 | } 74 | 75 | async fn fetch_keys( 76 | &self, 77 | prefix: &[u8], 78 | block_hash: Option, 79 | ) -> Result>, Error> { 80 | let backend = self.backend.clone(); 81 | let prefix = Vec::from(prefix); 82 | let handle = Executor01CompatExt::compat(self.runtime.executor()) 83 | .spawn_with_handle(async move { backend.fetch_keys(&prefix, block_hash).await }) 84 | .unwrap(); 85 | handle.await 86 | } 87 | 88 | async fn block_header( 89 | &self, 90 | block_hash: Option, 91 | ) -> Result, Error> { 92 | let backend = self.backend.clone(); 93 | let handle = Executor01CompatExt::compat(self.runtime.executor()) 94 | .spawn_with_handle(async move { backend.block_header(block_hash).await }) 95 | .unwrap(); 96 | handle.await 97 | } 98 | 99 | fn get_genesis_hash(&self) -> Hash { 100 | self.backend.get_genesis_hash() 101 | } 102 | 103 | async fn runtime_version(&self) -> Result { 104 | self.backend.runtime_version().await 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /client/src/error.rs: -------------------------------------------------------------------------------- 1 | // Radicle Registry 2 | // Copyright (C) 2019 Monadic GmbH 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License version 3 as 6 | // published by the Free Software Foundation. 7 | // 8 | // This program is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU General Public License 14 | // along with this program. If not, see . 15 | 16 | use failure::{Compat, Fail}; 17 | use jsonrpc_core_client::RpcError; 18 | use parity_scale_codec::Error as CodecError; 19 | 20 | use crate::event::EventExtractionError; 21 | 22 | /// Error that may be returned by any of the [crate::ClientT] methods 23 | #[derive(Debug, thiserror::Error)] 24 | pub enum Error { 25 | /// Failed to decode state value 26 | #[error("Failed to decode state value")] 27 | StateDecoding { 28 | #[source] 29 | error: CodecError, 30 | /// Key for the value we tried to decode 31 | key: Vec, 32 | }, 33 | 34 | /// Error from the underlying RPC connection 35 | #[error("Error from the underlying RPC connection")] 36 | Rpc(#[source] Compat), 37 | 38 | /// Invalid transaction 39 | #[error("Invalid transaction")] 40 | InvalidTransaction, 41 | 42 | /// Chain is running an incompatible runtime specification version 43 | #[error("Chain is running an incompatible runtime specification version {0}")] 44 | IncompatibleRuntimeVersion(u32), 45 | 46 | /// Failed to extract required events for a transaction 47 | #[error("Failed to extract required events for transaction {tx_hash}")] 48 | EventExtraction { 49 | error: EventExtractionError, 50 | tx_hash: crate::TxHash, 51 | }, 52 | 53 | /// Events for a transaction are missing from a block. 54 | /// 55 | /// This indicates an internal error or a node error since we only look for events in the block 56 | /// that includes the transaction. 57 | #[error("Events for transaction {tx_hash} missing in block {block_hash}")] 58 | EventsMissing { 59 | block_hash: crate::BlockHash, 60 | tx_hash: crate::TxHash, 61 | }, 62 | 63 | #[error("Could not obtain header of tip of best chain")] 64 | BestChainTipHeaderMissing, 65 | 66 | /// Block could not be found. 67 | #[error("Block {block_hash} could not be found")] 68 | BlockMissing { block_hash: crate::BlockHash }, 69 | 70 | /// Invalid response from the node for the `chain.block_hash` method. 71 | /// 72 | /// The node is violating the application protocol. 73 | #[error("Invalid response from the node for the chain.block_hash method")] 74 | InvalidBlockHashResponse { 75 | response: sp_rpc::list::ListOrValue>, 76 | }, 77 | 78 | /// RPC subscription author.watch_extrinsic terminated prematurely. 79 | /// 80 | /// The node is violating the application protocol. 81 | #[error("RPC subscription author.watch_extrinsic terminated prematurely")] 82 | WatchExtrinsicStreamTerminated, 83 | 84 | /// Invalid [crate::backend::TransactionStatus] received in `author.watch_extrinsic` RPC 85 | /// subsription. 86 | /// 87 | /// The node is violating the application protocol. 88 | #[error("Invalid transaction status {tx_status:?} for transaction {tx_hash}")] 89 | InvalidTransactionStatus { 90 | tx_hash: crate::TxHash, 91 | tx_status: crate::backend::TransactionStatus, 92 | }, 93 | } 94 | 95 | impl From for Error { 96 | fn from(error: RpcError) -> Self { 97 | Error::Rpc(error.compat()) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /client/src/event.rs: -------------------------------------------------------------------------------- 1 | // Radicle Registry 2 | // Copyright (C) 2019 Monadic GmbH 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License version 3 as 6 | // published by the Free Software Foundation. 7 | // 8 | // This program is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU General Public License 14 | // along with this program. If not, see . 15 | 16 | //! Access to runtime events and helpers to extract events for transactions. 17 | use radicle_registry_core::TransactionError; 18 | use radicle_registry_runtime::{event, DispatchError}; 19 | 20 | pub use radicle_registry_runtime::event::{transaction_index, Event, Record, *}; 21 | 22 | #[derive(thiserror::Error, Debug)] 23 | pub enum EventExtractionError { 24 | #[error("ExtrinsicSuccess or ExtrinsicFailed event not found")] 25 | ExstrinsicStatusMissing, 26 | #[error("Required event is missing")] 27 | EventMissing, 28 | } 29 | 30 | /// Looks for `ExtrinsicSuccess` and `ExtrinsicFailed` in the events and constructs the inner 31 | /// result accordingly. Returns an [EventExtractionError::ExstrinsicStatusMissing] error if none of 32 | /// these events is found. 33 | pub fn get_dispatch_result( 34 | events: &[Event], 35 | ) -> Result, EventExtractionError> { 36 | events 37 | .iter() 38 | .find_map(|event| extrinsic_result(event).map(|e| e.map_err(TransactionError::from))) 39 | .ok_or_else(|| EventExtractionError::ExstrinsicStatusMissing) 40 | } 41 | 42 | /// Extracts the extrinsic result from the event. 43 | /// 44 | /// If the event is either `ExtrinsicSuccess` or `ExtrinsicFailed` it returns `Ok` or the 45 | /// `Err`, respectively. If the event is neither of those it returns `None`. 46 | fn extrinsic_result(event: &Event) -> Option> { 47 | match event { 48 | Event::system(event) => match event { 49 | event::System::ExtrinsicSuccess(_) => Some(Ok(())), 50 | event::System::ExtrinsicFailed(dispatch_error, _) => Some(Err(*dispatch_error)), 51 | _ => None, 52 | }, 53 | _ => None, 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /client/src/interface.rs: -------------------------------------------------------------------------------- 1 | // Radicle Registry 2 | // Copyright (C) 2019 Monadic GmbH 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License version 3 as 6 | // published by the Free Software Foundation. 7 | // 8 | // This program is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU General Public License 14 | // along with this program. If not, see . 15 | 16 | //! Provide an abstract trait for the registry client and the necessary types. 17 | //! 18 | //! The [ClientT] trait defines one method for each transaction of the registry ledger as well as 19 | //! methods to get the ledger state. 20 | use futures::future::BoxFuture; 21 | 22 | pub use radicle_registry_core::*; 23 | 24 | pub use radicle_registry_runtime::{ 25 | state, Balance, BlockNumber, Event, Hash, Header, RuntimeVersion, 26 | }; 27 | pub use sp_core::crypto::{ 28 | Pair as CryptoPair, Public as CryptoPublic, SecretStringError as CryptoError, 29 | }; 30 | pub use sp_core::{ed25519, H256}; 31 | 32 | pub use crate::error::Error; 33 | pub use crate::message::Message; 34 | pub use crate::transaction::{Transaction, TransactionExtra}; 35 | 36 | /// The hash of a block. Uniquely identifies a block. 37 | #[doc(inline)] 38 | pub type BlockHash = Hash; 39 | 40 | /// The hash of a transaction. Uniquely identifies a transaction. 41 | #[doc(inline)] 42 | pub type TxHash = Hash; 43 | 44 | /// The header of a block 45 | #[doc(inline)] 46 | pub type BlockHeader = Header; 47 | 48 | /// Result of a transaction being included in a block. 49 | /// 50 | /// Returned after submitting an transaction to the blockchain. 51 | #[derive(Clone, Debug)] 52 | pub struct TransactionIncluded { 53 | pub tx_hash: TxHash, 54 | /// The hash of the block the transaction is included in. 55 | pub block: Hash, 56 | /// The result of the runtime message. 57 | /// 58 | /// See [Message::result_from_events]. 59 | pub result: Result<(), TransactionError>, 60 | } 61 | 62 | /// Return type for all [ClientT] methods. 63 | pub type Response = BoxFuture<'static, Result>; 64 | 65 | /// The availability status of an org or user Id 66 | #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] 67 | #[serde(rename_all = "lowercase")] 68 | pub enum IdStatus { 69 | /// The id is available and can be claimed 70 | Available, 71 | 72 | /// The id is curently taken by a user or by an org 73 | Taken, 74 | 75 | /// The id has been unregistered and is now retired 76 | Retired, 77 | } 78 | 79 | /// Trait for ledger clients sending transactions and looking up state. 80 | #[async_trait::async_trait] 81 | pub trait ClientT { 82 | /// Submit a signed transaction. 83 | /// 84 | /// ```no_run 85 | /// # use radicle_registry_client::*; 86 | /// # async fn example(client: Client, tx: Transaction) -> Result<(), Error> { 87 | /// 88 | /// // Submit the transaction to the node. 89 | /// // 90 | /// // If this is successful the transaction has been accepted by the node. The node will then 91 | /// // dissemniate the transaction to the network. 92 | /// // 93 | /// // This call fails if the transaction is invalid or if the RPC communication with the node 94 | /// // failed. 95 | /// let tx_included_fut = client.submit_transaction(tx).await?; 96 | /// 97 | /// // We can now wait for the transaction to be included in a block. 98 | /// // 99 | /// // This will error if the transaction becomes invalid (for example due to the nonce) or if 100 | /// // we fail to retrieve the transaction state from the node. 101 | /// // 102 | /// // This will not error if the transaction errored while applying. See 103 | /// // TransactionIncluded::result for that. 104 | /// let tx_included = tx_included_fut.await?; 105 | /// 106 | /// Ok(()) 107 | /// # } 108 | /// ``` 109 | /// 110 | /// See the `getting_started` example for more details. 111 | async fn submit_transaction( 112 | &self, 113 | transaction: Transaction, 114 | ) -> Result, Error>; 115 | 116 | /// Sign and submit a ledger message as a transaction to the blockchain. 117 | /// 118 | /// Same as [ClientT::submit_transaction] but takes care of signing the message. 119 | async fn sign_and_submit_message( 120 | &self, 121 | author: &ed25519::Pair, 122 | message: Message_, 123 | fee: Balance, 124 | ) -> Result, Error>; 125 | 126 | /// Check whether a given account exists on chain. 127 | async fn account_exists(&self, account_id: &AccountId) -> Result; 128 | 129 | /// Fetch the nonce for the given account from the chain state 130 | async fn account_nonce( 131 | &self, 132 | account_id: &AccountId, 133 | ) -> Result; 134 | 135 | /// Fetch the header of the given block hash 136 | async fn block_header(&self, block_hash: BlockHash) -> Result, Error>; 137 | 138 | /// Fetch the header of the best chain tip 139 | async fn block_header_best_chain(&self) -> Result; 140 | 141 | /// Return the genesis hash of the chain we are communicating with. 142 | fn genesis_hash(&self) -> Hash; 143 | 144 | /// Get the runtime version at the latest block 145 | async fn runtime_version(&self) -> Result; 146 | 147 | async fn free_balance(&self, account_id: &AccountId) -> Result; 148 | 149 | async fn get_id_status(&self, id: &Id) -> Result; 150 | 151 | async fn get_org(&self, org_id: Id) -> Result, Error>; 152 | 153 | async fn list_orgs(&self) -> Result, Error>; 154 | 155 | async fn get_user(&self, user_id: Id) -> Result, Error>; 156 | 157 | async fn list_users(&self) -> Result, Error>; 158 | 159 | async fn get_project( 160 | &self, 161 | project_name: ProjectName, 162 | project_domain: ProjectDomain, 163 | ) -> Result, Error>; 164 | 165 | async fn list_projects(&self) -> Result, Error>; 166 | } 167 | -------------------------------------------------------------------------------- /core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | edition = "2018" 3 | name = "radicle-registry-core" 4 | description = "Primitive types and functions used in the Radicle Registry" 5 | authors = ["Monadic GmbH "] 6 | version = "0.0.0" 7 | homepage = "https://github.com/radicle-dev/radicle-registry" 8 | documentation = "https://github.com/radicle-dev/radicle-registry" 9 | license = "GPL-3.0-only" 10 | repository = "https://github.com/radicle-dev/radicle-registry" 11 | 12 | [features] 13 | default = ["std"] 14 | std = [ 15 | "parity-scale-codec/std", 16 | "rand", 17 | "serde", 18 | "sp-core/std", 19 | "sp-runtime/std", 20 | "thiserror", 21 | ] 22 | 23 | [dependencies] 24 | derive-try-from-primitive = "1.0.0" 25 | rand = { version = "0.7.2", optional = true } 26 | serde = { version = "1.0", features = ["derive"], optional = true } 27 | thiserror = { version = "1.0", optional = true } 28 | 29 | [dependencies.parity-scale-codec] 30 | default-features = false 31 | features = ["derive", "full"] 32 | version = "1.0.0" 33 | 34 | [dependencies.sp-core] 35 | git = "https://github.com/paritytech/substrate" 36 | rev = "v2.0.0-rc4" 37 | default-features = false 38 | 39 | [dependencies.sp-runtime] 40 | git = "https://github.com/paritytech/substrate" 41 | rev = "v2.0.0-rc4" 42 | default-features = false 43 | -------------------------------------------------------------------------------- /core/README.md: -------------------------------------------------------------------------------- 1 | # radicle-registry-core 2 | 3 | This package provides the types that constitute the registry ledger and provides 4 | exhaustive documentation how these types behave in the ledger. 5 | 6 | These types are the entities that are stored in the state, the different 7 | transaction message types, and their constituent types. 8 | 9 | ## Transaction Messages 10 | 11 | Transaction messages effect a change in the ledger state. They are submitted to 12 | the ledger as part of a transaction. All possible transaction messages are 13 | defined in the `message` module. 14 | 15 | For each message we document how it changes the state and what preconditions 16 | must be satisfied. The documentation must be comprehensive and exhaustive and 17 | cover all edge cases. The documentation for a message has the following sections 18 | 19 |
20 |
State changes
21 |
Describes which entities are added, removed, or updated in the ledger 22 | state.
23 |
State-dependent validations
24 |
Describes the validations that are required for the message to be applied 25 | successfully and that depend on the current ledger state state.
26 | 27 | 28 | ## State 29 | 30 | All entities that are stored in the ledger state are defined in the `state` 31 | module. 32 | 33 | For each entity version the documentation has the following sections 34 | 35 |
36 |
Storage
37 |
Describes how the entity is stored in the state and how the state storage 38 | key is calculated.
39 |
Invariants
40 |
Describes the invariants of the data in the state entity that always hold.
41 |
Relevant messages
42 |
Links to message types that effect or use the entity.
43 | 44 | 45 | ### Versioning 46 | 47 | To make the runtime state backwards compatible, every state entity that is added 48 | must be versioned using the following schema. 49 | Please follow the naming convention introduced in the examples as closely as possible. 50 | 51 | The storage defines the structure of the data. 52 | If it's altered, e.g. a key type is changed or it's converting from a map to a list, 53 | a new version must be added. 54 | The storage must include its version in its name: 55 | 56 | ```rust 57 | // registry.rs 58 | 59 | pub mod store { 60 | ... 61 | decl_storage! { 62 | ... 63 | pub Users1: map hasher(blake2_128_concat) Id => Option; 64 | pub Users2: map hasher(blake2_128_concat) NewId => Option; 65 | ``` 66 | 67 | The stored data must be an enum capable of containing different data versions. 68 | It must be versioned consistently and independently of the storage version: 69 | 70 | ```rust 71 | // state.rs 72 | 73 | pub enum Users1Data { 74 | V1(UserV1) 75 | V2(UserV2) 76 | } 77 | 78 | pub enum Users2Data { 79 | V3(UserId) 80 | V4(UserV4) 81 | } 82 | ``` 83 | 84 | If the stored data is a specialized data structure, 85 | it must be versioned same as the stored data, which contains it: 86 | 87 | ```rust 88 | // state.rs 89 | 90 | pub struct UserV1 { 91 | ... 92 | } 93 | 94 | pub enum UserV2 { 95 | ... 96 | } 97 | 98 | // UserId is general purpose 99 | 100 | pub struct UserV4 { 101 | ... 102 | } 103 | ``` 104 | 105 | Existing version variants may never be altered. Only new variants may be added. 106 | 107 | ## Errors 108 | 109 | To maintain compatibility between different runtime versions we follow a simple 110 | policy for changes to `RegistryError`. 111 | 112 | Error variants are not removed immediately if they become unused in the runtime. 113 | Instead we annotate them with the `#[deprecated]` attribute and remove them once 114 | we intend to not support them any more. Whenever a variant is removed its 115 | discriminant value may not be used anymore. New variants are assigned a 116 | discriminant that has never been taken by an old variant. For this reason all 117 | discriminants are explicitly set 118 | -------------------------------------------------------------------------------- /core/src/bytes128.rs: -------------------------------------------------------------------------------- 1 | // Radicle Registry 2 | // Copyright (C) 2019 Monadic GmbH 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License version 3 as 6 | // published by the Free Software Foundation. 7 | // 8 | // This program is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU General Public License 14 | // along with this program. If not, see . 15 | 16 | //! `Bytes128` type, and its validation tests. 17 | 18 | use alloc::vec::Vec; 19 | use core::convert::TryFrom; 20 | use parity_scale_codec::{Decode, Encode, Error as CodecError, Input}; 21 | 22 | /// Byte vector that is limited to 128 bytes. 23 | #[derive(Encode, Clone, Debug, Eq, PartialEq)] 24 | pub struct Bytes128(Vec); 25 | 26 | impl Bytes128 { 27 | const MAXIMUM_SUPPORTED_LENGTH: usize = 128; 28 | 29 | /// Smart constructor that attempts to build a Bytes128 30 | /// from a Vec with an arbitrary size. It fails if the 31 | /// input vector is larger than Bytes128::MAXIMUM_SUPPORTED_LENGTH. 32 | pub fn from_vec(vector: Vec) -> Result { 33 | if vector.len() > Self::MAXIMUM_SUPPORTED_LENGTH { 34 | Err(InordinateVectorError()) 35 | } else { 36 | Ok(Bytes128(vector)) 37 | } 38 | } 39 | } 40 | 41 | impl TryFrom> for Bytes128 { 42 | type Error = InordinateVectorError; 43 | 44 | fn try_from(value: Vec) -> Result { 45 | Bytes128::from_vec(value) 46 | } 47 | } 48 | 49 | impl From for Vec { 50 | fn from(value: Bytes128) -> Self { 51 | value.0 52 | } 53 | } 54 | 55 | /// Bytes128 random functions useful for unit testing. 56 | /// 57 | /// Note that since these fuctions make use of rand, we need to guard 58 | /// with the std feature to be able to compile it for wasm. 59 | #[cfg(feature = "std")] 60 | impl Bytes128 { 61 | /// Generate a random Bytes128 vector with as many bytes as its limit. 62 | pub fn random() -> Self { 63 | Self::from_vec(Self::random_vector(Self::MAXIMUM_SUPPORTED_LENGTH)).unwrap() 64 | } 65 | 66 | /// Generate a random Bytes128 vector with as many bytes as specified with 'size'. 67 | pub fn random_with_size(size: usize) -> Result { 68 | Bytes128::from_vec(Self::random_vector(size)) 69 | } 70 | 71 | /// Generate a random Vec with as many bytes as specified with 'size'. 72 | fn random_vector(size: usize) -> Vec { 73 | (0..size).map(|_| rand::random::()).collect() 74 | } 75 | } 76 | 77 | impl Decode for Bytes128 { 78 | fn decode(input: &mut I) -> Result { 79 | let decoded: Vec = Vec::decode(input)?; 80 | Bytes128::from_vec(decoded).map_err(|_| CodecError::from("Bytes128 input too long")) 81 | } 82 | } 83 | 84 | /// Error type for a failed attempt to build a Bytes128 value from an inordinate Vec. 85 | #[derive(Encode, Clone, Debug, Eq, PartialEq)] 86 | pub struct InordinateVectorError(); 87 | 88 | #[cfg(feature = "std")] 89 | impl core::fmt::Display for InordinateVectorError { 90 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 91 | write!( 92 | f, 93 | "The provided vectors's length exceeds the Bytes128 limit of {} bytes", 94 | Bytes128::MAXIMUM_SUPPORTED_LENGTH, 95 | ) 96 | } 97 | } 98 | 99 | #[cfg(test)] 100 | mod test { 101 | use super::*; 102 | 103 | #[test] 104 | fn test_from_valid_sized_vectors() { 105 | for size in 0..=Bytes128::MAXIMUM_SUPPORTED_LENGTH { 106 | let random_vector = random_vector(size); 107 | assert_eq!( 108 | Bytes128::from_vec(random_vector.clone()).unwrap(), 109 | Bytes128(random_vector) 110 | ); 111 | } 112 | } 113 | 114 | #[test] 115 | fn test_from_inordinate_vectors() { 116 | for size in Bytes128::MAXIMUM_SUPPORTED_LENGTH + 1..Bytes128::MAXIMUM_SUPPORTED_LENGTH + 10 117 | { 118 | let random_vector = random_vector(size); 119 | assert_eq!( 120 | Bytes128::from_vec(random_vector), 121 | Err(InordinateVectorError()) 122 | ); 123 | } 124 | } 125 | 126 | #[test] 127 | fn decode_after_encode_is_identity() { 128 | let bytes128 = Bytes128::random(); 129 | let encoded = bytes128.encode(); 130 | let decoded = ::decode(&mut &encoded[..]).unwrap(); 131 | 132 | assert_eq!(bytes128, decoded) 133 | } 134 | 135 | #[test] 136 | fn decode_inordinate_vector_fails() { 137 | // Encode a malformed bytes128 and verify that it fails to decode. 138 | // Note that we use Bytes128(vec) instead of Bytes128::from_vec(). 139 | let inordinate_bytes128 = Bytes128(random_vector(129)); 140 | let encoded = inordinate_bytes128.encode(); 141 | let decoding_result = ::decode(&mut &encoded[..]); 142 | 143 | assert!(decoding_result.is_err()) 144 | } 145 | 146 | fn random_vector(size: usize) -> Vec { 147 | (0..size).map(|_| rand::random::()).collect() 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /core/src/error.rs: -------------------------------------------------------------------------------- 1 | // Radicle Registry 2 | // Copyright (C) 2019 Monadic GmbH 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License version 3 as 6 | // published by the Free Software Foundation. 7 | // 8 | // This program is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU General Public License 14 | // along with this program. If not, see . 15 | 16 | use crate::DispatchError; 17 | 18 | use core::convert::{TryFrom, TryInto}; 19 | use derive_try_from_primitive::TryFromPrimitive; 20 | 21 | /// Error that may be the result of executing a transaction. 22 | /// 23 | /// The error is either a [RegistryError] if it originated from our registry code or a 24 | /// [DispatchError] from other substrate modules. 25 | #[derive(Clone, Copy, Debug, Eq, PartialEq)] 26 | #[cfg_attr(feature = "std", derive(thiserror::Error))] 27 | pub enum TransactionError { 28 | #[cfg_attr(feature = "std", error(transparent))] 29 | RegistryError(#[cfg_attr(feature = "std", from)] RegistryError), 30 | 31 | #[cfg_attr(feature = "std", error("{0:?}"))] 32 | OtherDispatchError(DispatchError), 33 | } 34 | 35 | impl From for TransactionError { 36 | fn from(dispatch_error: DispatchError) -> Self { 37 | dispatch_error 38 | .try_into() 39 | .map(TransactionError::RegistryError) 40 | .unwrap_or(TransactionError::OtherDispatchError(dispatch_error)) 41 | } 42 | } 43 | 44 | /// Errors describing failed Registry transactions. 45 | #[derive(Clone, Copy, Debug, Eq, PartialEq, TryFromPrimitive)] 46 | #[cfg_attr(feature = "std", derive(thiserror::Error))] 47 | #[repr(u8)] 48 | pub enum RegistryError { 49 | #[cfg_attr(feature = "std", error("the provided checkpoint does not exist"))] 50 | InexistentCheckpointId = 0, 51 | 52 | #[cfg_attr( 53 | feature = "std", 54 | error("a registered project must have an initial checkpoint") 55 | )] 56 | InexistentInitialProjectCheckpoint = 1, 57 | 58 | #[cfg_attr(feature = "std", error("the provided org does not exist"))] 59 | InexistentOrg = 2, 60 | 61 | #[cfg_attr(feature = "std", error("the provided project does not exist"))] 62 | InexistentProjectId = 3, 63 | 64 | #[cfg_attr(feature = "std", error("the provided user does not exist"))] 65 | InexistentUser = 4, 66 | 67 | #[deprecated(note = "Superseded by IdAlreadyTaken")] 68 | #[cfg_attr(feature = "std", error("an org with the same ID already exists"))] 69 | DuplicateOrgId = 5, 70 | 71 | #[cfg_attr(feature = "std", error("a project with the same ID already exists"))] 72 | DuplicateProjectId = 6, 73 | 74 | #[deprecated(note = "Superseded by IdAlreadyTaken")] 75 | #[cfg_attr(feature = "std", error("a user with the same ID already exists."))] 76 | DuplicateUserId = 7, 77 | 78 | #[cfg_attr(feature = "std", error("the user is already a member of the org"))] 79 | AlreadyAMember = 8, 80 | 81 | #[cfg_attr(feature = "std", error("the provided fee is insufficient"))] 82 | InsufficientFee = 9, 83 | 84 | #[cfg_attr(feature = "std", error("the sender is not a project member"))] 85 | InsufficientSenderPermissions = 10, 86 | 87 | #[cfg_attr( 88 | feature = "std", 89 | error("the provided checkpoint is not a descendant of the project's initial checkpoint") 90 | )] 91 | InvalidCheckpointAncestry = 11, 92 | 93 | #[cfg_attr( 94 | feature = "std", 95 | error("the provided user is not eligible for unregistration") 96 | )] 97 | UnregisterableUser = 12, 98 | 99 | #[cfg_attr( 100 | feature = "std", 101 | error("the provided org is not elibile for unregistration") 102 | )] 103 | UnregisterableOrg = 13, 104 | 105 | #[cfg_attr( 106 | feature = "std", 107 | error("the account is already associated with a user") 108 | )] 109 | UserAccountAssociated = 14, 110 | 111 | #[cfg_attr( 112 | feature = "std", 113 | error("the tx author needs to have an associated user") 114 | )] 115 | AuthorHasNoAssociatedUser = 15, 116 | 117 | #[cfg_attr( 118 | feature = "std", 119 | error( 120 | "failed to update the chain runtime, ensure that the author is the chain's sudo key, \ 121 | the 'spec_name' matches and the WASM 'spec_version' is greater" 122 | ) 123 | )] 124 | FailedChainRuntimeUpdate = 16, 125 | 126 | #[cfg_attr( 127 | feature = "std", 128 | error("an org or a user with the same ID already exists") 129 | )] 130 | IdAlreadyTaken = 17, 131 | 132 | #[cfg_attr( 133 | feature = "std", 134 | error("the ID has been unregistered and can't be claimed again") 135 | )] 136 | IdRetired = 18, 137 | 138 | #[cfg_attr( 139 | feature = "std", 140 | error("the author has insufficient funds to cover the registration fee") 141 | )] 142 | FailedRegistrationFeePayment = 19, 143 | } 144 | 145 | // The index with which the registry runtime module is declared 146 | // in the Radicle Registry runtime - see the `construct_runtime` 147 | // declaration in the `runtime` crate. 148 | const REGISTRY_ERROR_INDEX: u8 = 7; 149 | 150 | impl From for DispatchError { 151 | fn from(error: RegistryError) -> Self { 152 | DispatchError::Module { 153 | index: REGISTRY_ERROR_INDEX, 154 | error: error as u8, 155 | message: None, 156 | } 157 | } 158 | } 159 | 160 | impl TryFrom for RegistryError { 161 | type Error = &'static str; 162 | 163 | fn try_from(dispatch_error: DispatchError) -> Result { 164 | if let DispatchError::Module { 165 | index, 166 | error, 167 | message: _, 168 | } = dispatch_error 169 | { 170 | if index == REGISTRY_ERROR_INDEX { 171 | return error.try_into().map_err(|_| { 172 | "Failed to build the RegistryError variant specified in the DispatchError" 173 | }); 174 | } 175 | } 176 | 177 | Err("The given DispatchError does not wrap a RegistryError.") 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /core/src/id.rs: -------------------------------------------------------------------------------- 1 | // Radicle Registry 2 | // Copyright (C) 2019 Monadic GmbH 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License version 3 as 6 | // published by the Free Software Foundation. 7 | // 8 | // This program is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU General Public License 14 | // along with this program. If not, see . 15 | 16 | //! `Id` is the unique identifier for Orgs and Users. 17 | //! 18 | //! [orgs spec](https://github.com/radicle-dev/registry-spec/blob/0b7699ac597bd935b13facc9152789d111e138ca/body.tex#L110-L119) 19 | //! [user spec](https://github.com/radicle-dev/registry-spec/blob/1b7699ac597bd935b13facc9152789d111e138ca/body.tex#L452-L459) 20 | 21 | use alloc::string::{String, ToString}; 22 | use core::convert::{From, Into, TryFrom}; 23 | use parity_scale_codec as codec; 24 | 25 | #[derive(codec::Encode, Clone, Debug, Eq, PartialEq)] 26 | #[cfg_attr(feature = "std", serde(try_from = "String"))] 27 | #[cfg_attr(feature = "std", derive(serde::Deserialize, serde::Serialize))] 28 | pub struct Id(String); 29 | 30 | impl Id { 31 | fn from_string(input: String) -> Result { 32 | // Must be at least 1 character. 33 | if input.is_empty() { 34 | return Err(InvalidIdError("must be at least 1 character")); 35 | } 36 | // Must be no longer than 32. 37 | if input.len() > 32 { 38 | return Err(InvalidIdError("must not exceed 32 characters")); 39 | } 40 | // Must only contain a-z, 0-9 and '-' characters. 41 | if !input 42 | .chars() 43 | .all(|c| c.is_ascii_digit() || c.is_ascii_lowercase() || c == '-') 44 | { 45 | return Err(InvalidIdError("must only include a-z, 0-9 and '-'")); 46 | } 47 | 48 | // Must not start with a '-'. 49 | if input.starts_with('-') { 50 | return Err(InvalidIdError("must not start with a '-'")); 51 | } 52 | // Must not end with a '-'. 53 | if input.ends_with('-') { 54 | return Err(InvalidIdError("must not end with a '-'")); 55 | } 56 | // Must not contain sequences of more than one '-'. 57 | if input.contains("--") { 58 | return Err(InvalidIdError( 59 | "must not have more than one consecutive '-'", 60 | )); 61 | } 62 | 63 | let id = Self(input); 64 | 65 | Ok(id) 66 | } 67 | } 68 | 69 | impl codec::Decode for Id { 70 | fn decode(input: &mut I) -> Result { 71 | let decoded: String = String::decode(input)?; 72 | 73 | match Id::try_from(decoded) { 74 | Ok(id) => Ok(id), 75 | Err(err) => Err(codec::Error::from(err.what())), 76 | } 77 | } 78 | } 79 | 80 | impl Into for Id { 81 | fn into(self) -> String { 82 | self.0 83 | } 84 | } 85 | 86 | impl TryFrom for Id { 87 | type Error = InvalidIdError; 88 | 89 | fn try_from(input: String) -> Result { 90 | Self::from_string(input) 91 | } 92 | } 93 | 94 | impl TryFrom<&str> for Id { 95 | type Error = InvalidIdError; 96 | 97 | fn try_from(input: &str) -> Result { 98 | Self::from_string(input.into()) 99 | } 100 | } 101 | 102 | impl core::str::FromStr for Id { 103 | type Err = InvalidIdError; 104 | 105 | fn from_str(s: &str) -> Result { 106 | Self::from_string(s.to_string()) 107 | } 108 | } 109 | 110 | #[cfg(feature = "std")] 111 | impl core::fmt::Display for Id { 112 | fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { 113 | write!(f, "{}", self.0) 114 | } 115 | } 116 | 117 | /// Error type when conversion from an input failed. 118 | #[derive(codec::Encode, Clone, Debug, Eq, PartialEq)] 119 | pub struct InvalidIdError(&'static str); 120 | 121 | impl InvalidIdError { 122 | /// Error description 123 | /// 124 | /// This function returns an actual error str. 125 | pub fn what(&self) -> &'static str { 126 | self.0 127 | } 128 | } 129 | 130 | #[cfg(feature = "std")] 131 | impl std::fmt::Display for InvalidIdError { 132 | fn fmt(&self, f: &mut core::fmt::Formatter) -> std::fmt::Result { 133 | write!(f, "InvalidIdError({})", self.0) 134 | } 135 | } 136 | 137 | #[cfg(feature = "std")] 138 | impl std::error::Error for InvalidIdError { 139 | fn description(&self) -> &str { 140 | self.0 141 | } 142 | } 143 | 144 | impl From<&'static str> for InvalidIdError { 145 | fn from(s: &'static str) -> Self { 146 | Self(s) 147 | } 148 | } 149 | 150 | #[cfg(test)] 151 | mod test { 152 | use super::Id; 153 | use parity_scale_codec::{Decode, Encode}; 154 | 155 | #[test] 156 | fn id_too_short() { 157 | assert!(Id::from_string("".into()).is_err()); 158 | } 159 | 160 | #[test] 161 | fn id_too_long() { 162 | let input = std::iter::repeat("X").take(33).collect::(); 163 | let too_long = Id::from_string(input); 164 | assert!(too_long.is_err()); 165 | } 166 | 167 | #[test] 168 | fn id_invalid_characters() { 169 | let invalid_characters = Id::from_string("AZ+*".into()); 170 | assert!(invalid_characters.is_err()); 171 | } 172 | 173 | #[test] 174 | fn id_invalid_prefix() { 175 | let invalid_prefix = Id::from_string("-radicle".into()); 176 | assert!(invalid_prefix.is_err()); 177 | } 178 | 179 | #[test] 180 | fn id_invalid_suffix() { 181 | let invalid_suffix = Id::from_string("radicle-".into()); 182 | assert!(invalid_suffix.is_err()); 183 | } 184 | 185 | #[test] 186 | fn id_double_dash() { 187 | let double_dash = Id::from_string("radicle--registry".into()); 188 | assert!(double_dash.is_err()); 189 | } 190 | 191 | #[test] 192 | fn id_valid() { 193 | let valid = Id::from_string("radicle-registry001".into()); 194 | assert!(valid.is_ok()); 195 | } 196 | 197 | #[test] 198 | fn encode_then_decode() { 199 | let id = Id::from_string("monadic".into()).unwrap(); 200 | let encoded = id.encode(); 201 | let decoded = Id::decode(&mut &encoded[..]).unwrap(); 202 | 203 | assert_eq!(id, decoded) 204 | } 205 | 206 | #[test] 207 | fn encoded_then_decode_invalid() { 208 | let invalid = Encode::encode("-Invalid-"); 209 | let decoded = Id::decode(&mut &invalid[..]); 210 | 211 | assert!(decoded.is_err()); 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /core/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Radicle Registry 2 | // Copyright (C) 2019 Monadic GmbH 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License version 3 as 6 | // published by the Free Software Foundation. 7 | // 8 | // This program is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU General Public License 14 | // along with this program. If not, see . 15 | 16 | //! Basic types used in the Radicle Registry. 17 | 18 | #![cfg_attr(not(feature = "std"), no_std)] 19 | 20 | extern crate alloc; 21 | 22 | use parity_scale_codec::{Decode, Encode}; 23 | use sp_core::ed25519; 24 | use sp_runtime::traits::BlakeTwo256; 25 | 26 | pub use sp_runtime::DispatchError; 27 | 28 | pub mod message; 29 | pub mod state; 30 | 31 | pub mod bytes128; 32 | pub use bytes128::Bytes128; 33 | 34 | mod id; 35 | pub use id::{Id, InvalidIdError}; 36 | 37 | mod project_name; 38 | pub use project_name::{InvalidProjectNameError, ProjectName}; 39 | 40 | mod error; 41 | pub use error::{RegistryError, TransactionError}; 42 | 43 | /// The hashing algorithm to use 44 | pub type Hashing = BlakeTwo256; 45 | 46 | /// Identifier for accounts, an Ed25519 public key. 47 | /// 48 | /// Each account has an associated [state::AccountBalance] and [state::AccountTransactionIndex]. 49 | pub type AccountId = ed25519::Public; 50 | 51 | /// Amout of currency denominated in μRAD. 52 | /// 53 | /// The non-negative balance of anything storing the amount of currency. 54 | /// It can be used to represent the value of anything describing an amount, 55 | /// e.g. an account balance, the value of a fee, etc. 56 | pub type Balance = u128; 57 | 58 | /// Convert amount of RAD into balance denominated in μRAD. 59 | pub const fn rad_to_balance(rad: u64) -> Balance { 60 | rad as u128 * 1_000_000 61 | } 62 | 63 | /// The id of a project. Used as storage key. 64 | pub type ProjectId = (ProjectName, ProjectDomain); 65 | 66 | /// The domain under which a [crate::state::Projects1Data] lives. 67 | #[derive(Decode, Encode, Clone, Debug, Eq, PartialEq)] 68 | #[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))] 69 | pub enum ProjectDomain { 70 | Org(Id), 71 | User(Id), 72 | } 73 | 74 | impl ProjectDomain { 75 | pub fn id(&self) -> Id { 76 | match self { 77 | Self::Org(id) | Self::User(id) => id.clone(), 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /core/src/project_name.rs: -------------------------------------------------------------------------------- 1 | // Radicle Registry 2 | // Copyright (C) 2019 Monadic GmbH 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License version 3 as 6 | // published by the Free Software Foundation. 7 | // 8 | // This program is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU General Public License 14 | // along with this program. If not, see . 15 | 16 | //! The name associated to a project. 17 | //! 18 | //! https://github.com/radicle-dev/registry-spec/blob/master/body.tex#L306 19 | 20 | use alloc::string::{String, ToString}; 21 | use core::convert::{From, Into, TryFrom}; 22 | use parity_scale_codec as codec; 23 | 24 | #[derive(codec::Encode, Clone, Debug, Eq, PartialEq)] 25 | #[cfg_attr(feature = "std", serde(try_from = "String"))] 26 | #[cfg_attr(feature = "std", derive(serde::Deserialize, serde::Serialize))] 27 | pub struct ProjectName(String); 28 | 29 | impl ProjectName { 30 | fn from_string(input: String) -> Result { 31 | // Must be at least 1 character. 32 | if input.is_empty() { 33 | return Err(InvalidProjectNameError("must be at least 1 character")); 34 | } 35 | // Must be no longer than 32. 36 | if input.len() > 32 { 37 | return Err(InvalidProjectNameError("must not exceed 32 characters")); 38 | } 39 | 40 | // Must only contain a-z, 0-9, '-', '_' and '.' characters. 41 | { 42 | let check_charset = |c: char| { 43 | c.is_ascii_digit() || c.is_ascii_lowercase() || c == '-' || c == '_' || c == '.' 44 | }; 45 | 46 | if !input.chars().all(check_charset) { 47 | return Err(InvalidProjectNameError( 48 | "must only include a-z, 0-9, '-', '_' and '.'", 49 | )); 50 | } 51 | } 52 | 53 | // Must not equal '.' or '..'. 54 | if input == "." || input == ".." { 55 | return Err(InvalidProjectNameError("must not be equal to '.' or '..'")); 56 | } 57 | 58 | let id = Self(input); 59 | 60 | Ok(id) 61 | } 62 | } 63 | 64 | impl codec::Decode for ProjectName { 65 | fn decode(input: &mut I) -> Result { 66 | let decoded: String = String::decode(input)?; 67 | 68 | match Self::try_from(decoded) { 69 | Ok(id) => Ok(id), 70 | Err(err) => Err(codec::Error::from(err.what())), 71 | } 72 | } 73 | } 74 | 75 | impl Into for ProjectName { 76 | fn into(self) -> String { 77 | self.0 78 | } 79 | } 80 | 81 | impl TryFrom for ProjectName { 82 | type Error = InvalidProjectNameError; 83 | 84 | fn try_from(input: String) -> Result { 85 | Self::from_string(input) 86 | } 87 | } 88 | 89 | impl TryFrom<&str> for ProjectName { 90 | type Error = InvalidProjectNameError; 91 | 92 | fn try_from(input: &str) -> Result { 93 | Self::from_string(input.to_string()) 94 | } 95 | } 96 | 97 | impl core::str::FromStr for ProjectName { 98 | type Err = InvalidProjectNameError; 99 | 100 | fn from_str(s: &str) -> Result { 101 | Self::from_string(s.to_string()) 102 | } 103 | } 104 | 105 | #[cfg(feature = "std")] 106 | impl core::fmt::Display for ProjectName { 107 | fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { 108 | write!(f, "{}", self.0) 109 | } 110 | } 111 | 112 | /// Error type when conversion from an inordinate input failed. 113 | #[derive(codec::Encode, Clone, Debug, Eq, PartialEq)] 114 | pub struct InvalidProjectNameError(&'static str); 115 | 116 | impl InvalidProjectNameError { 117 | /// Error description 118 | /// 119 | /// This function returns an actual error str when running in `std` 120 | /// environment, but `""` on `no_std`. 121 | #[cfg(feature = "std")] 122 | pub fn what(&self) -> &'static str { 123 | self.0 124 | } 125 | 126 | /// Error description 127 | /// 128 | /// This function returns an actual error str when running in `std` 129 | /// environment, but `""` on `no_std`. 130 | #[cfg(not(feature = "std"))] 131 | pub fn what(&self) -> &'static str { 132 | "" 133 | } 134 | } 135 | 136 | #[cfg(feature = "std")] 137 | impl std::fmt::Display for InvalidProjectNameError { 138 | fn fmt(&self, f: &mut core::fmt::Formatter) -> std::fmt::Result { 139 | write!(f, "InvalidProjectNameError({})", self.0) 140 | } 141 | } 142 | 143 | #[cfg(feature = "std")] 144 | impl std::error::Error for InvalidProjectNameError { 145 | fn description(&self) -> &str { 146 | self.0 147 | } 148 | } 149 | 150 | impl From<&'static str> for InvalidProjectNameError { 151 | #[cfg(feature = "std")] 152 | fn from(s: &'static str) -> Self { 153 | Self(s) 154 | } 155 | 156 | #[cfg(not(feature = "std"))] 157 | fn from(s: &'static str) -> Self { 158 | InvalidProjectNameError(s) 159 | } 160 | } 161 | 162 | #[cfg(test)] 163 | mod test { 164 | use super::ProjectName; 165 | use parity_scale_codec::{Decode, Encode}; 166 | 167 | #[test] 168 | fn name_too_short() { 169 | assert!(ProjectName::from_string("".into()).is_err()); 170 | } 171 | 172 | #[test] 173 | fn name_too_long() { 174 | let input = std::iter::repeat("X").take(33).collect::(); 175 | let too_long = ProjectName::from_string(input); 176 | assert!(too_long.is_err()); 177 | } 178 | 179 | #[test] 180 | fn name_invalid_characters() { 181 | let invalid_characters = ProjectName::from_string("AZ+*".into()); 182 | assert!(invalid_characters.is_err()); 183 | } 184 | 185 | #[test] 186 | fn name_is_dot() { 187 | let dot = ProjectName::from_string(".".into()); 188 | assert!(dot.is_err()); 189 | } 190 | 191 | #[test] 192 | fn name_is_double_dot() { 193 | let dot = ProjectName::from_string("..".into()); 194 | assert!(dot.is_err()); 195 | } 196 | 197 | #[test] 198 | fn name_valid() { 199 | let valid = ProjectName::from_string("--radicle_registry001".into()); 200 | assert!(valid.is_ok()); 201 | } 202 | 203 | #[test] 204 | fn encode_then_decode() { 205 | let id = ProjectName::from_string("monadic".into()).unwrap(); 206 | let encoded = id.encode(); 207 | let decoded = ::decode(&mut &encoded[..]).unwrap(); 208 | 209 | assert_eq!(id, decoded) 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | # copied expressions from https://nixos.wiki/wiki/Rust 2 | # and Mozilla's nix overlay README 3 | # https://www.scala-native.org/en/latest/user/setup.html 4 | let 5 | moz_overlay = import (builtins.fetchTarball https://github.com/mozilla/nixpkgs-mozilla/archive/master.tar.gz); 6 | pkgs = import { overlays = [ moz_overlay ]; }; 7 | in 8 | with pkgs; 9 | stdenv.mkDerivation { 10 | name = "clang-env-with-nightly-rust"; 11 | buildInputs = [ 12 | (pkgs.rustChannelOf { rustToolchain = ./rust-toolchain; }).rust 13 | clang 14 | llvmPackages.libclang 15 | olm 16 | openssl 17 | pkgconfig 18 | # Needed for libp2p to run its build.rs setup. 19 | protobuf 20 | zlib 21 | ]; 22 | # why do we need to set the library path manually? 23 | shellHook = '' 24 | export LIBCLANG_PATH="${pkgs.llvmPackages.libclang}/lib"; 25 | # Needed for libp2p to run its build.rs setup. 26 | export PROTOC="${pkgs.protobuf}/bin/protoc"; 27 | ''; 28 | } 29 | -------------------------------------------------------------------------------- /local-devnet/README.md: -------------------------------------------------------------------------------- 1 | Local DevNet 2 | ============ 3 | 4 | This directory provides tools to run and experiment with a local devnet. 5 | 6 | To run the devnet you need to build the registry node with 7 | ```bash 8 | cargo build -p radicle-registry-node 9 | ``` 10 | Then you can start the network with `docker-compose up` 11 | 12 | Overview 13 | -------- 14 | 15 | * Devnet consists of three miner nodes. 16 | * Chain data of nodes is persisted in Docker volumes. 17 | * We expose the RPC API of the first node on the standard RPC API port. 18 | * See `local_devnet()` for chain spec in `../node/src/chain_spec.rs`. 19 | * You need to build the node using a target that is compatible with Ubuntu 19.10 20 | (Eoan). 21 | 22 | Monitoring 23 | ---------- 24 | 25 | The local DevNet provides Grafana dashboards for the nodes at 26 | `http://localhost:9004`. The login credentials are `admin:admin`. 27 | 28 | Monitoring can be configured with `./prometheus.yaml`, `./grafana-datasources.yaml`, `./grafana-dashboards.yaml`, and `./grafana-dashboards`. 29 | -------------------------------------------------------------------------------- /local-devnet/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | 3 | # Configuration shared between node services 4 | x-node: &node 5 | image: ubuntu:eoan-20191101 6 | volumes: 7 | - ../target/release/radicle-registry-node:/usr/local/bin/radicle-registry-node:ro 8 | - ./start-node.sh:/usr/local/bin/start-node.sh:ro 9 | # Used by the --base-path option 10 | - /data 11 | command: 12 | - /usr/local/bin/start-node.sh 13 | 14 | services: 15 | alice: 16 | <<: *node 17 | ports: 18 | - "9944:9944" 19 | environment: 20 | NODE_NAME: alice 21 | bob: 22 | <<: *node 23 | environment: 24 | NODE_NAME: bob 25 | charlie: 26 | <<: *node 27 | environment: 28 | NODE_NAME: charlie 29 | 30 | prometheus: 31 | image: prom/prometheus 32 | ports: 33 | - "9090:9090" 34 | volumes: 35 | - ./prometheus.yaml:/etc/prometheus/prometheus.yml 36 | - prometheus-data:/prometheus 37 | 38 | grafana: 39 | image: grafana/grafana 40 | ports: 41 | - "9004:3000" 42 | volumes: 43 | - grafana-data:/var/lib/grafana 44 | - ./grafana-datasources.yaml:/etc/grafana/provisioning/datasources/datasources.yaml 45 | - ./grafana-dashboards.yaml:/etc/grafana/provisioning/dashboards/dashboards.yaml 46 | - ./grafana-dashboards:/var/lib/grafana/dashboards 47 | 48 | volumes: 49 | prometheus-data: 50 | grafana-data: 51 | -------------------------------------------------------------------------------- /local-devnet/grafana-dashboards.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | providers: 3 | - name: dashboards 4 | folder: '' 5 | type: file 6 | editable: true 7 | allowUiUpdates: true 8 | options: 9 | path: /var/lib/grafana/dashboards 10 | -------------------------------------------------------------------------------- /local-devnet/grafana-dashboards/network.json: -------------------------------------------------------------------------------- 1 | { 2 | "annotations": { 3 | "list": [ 4 | { 5 | "builtIn": 1, 6 | "datasource": "-- Grafana --", 7 | "enable": true, 8 | "hide": true, 9 | "iconColor": "rgba(0, 211, 255, 1)", 10 | "name": "Annotations & Alerts", 11 | "type": "dashboard" 12 | } 13 | ] 14 | }, 15 | "editable": true, 16 | "gnetId": null, 17 | "graphTooltip": 0, 18 | "id": 1, 19 | "links": [], 20 | "panels": [ 21 | { 22 | "aliasColors": {}, 23 | "bars": false, 24 | "dashLength": 10, 25 | "dashes": false, 26 | "datasource": null, 27 | "fill": 1, 28 | "fillGradient": 0, 29 | "gridPos": { 30 | "h": 8, 31 | "w": 12, 32 | "x": 0, 33 | "y": 0 34 | }, 35 | "hiddenSeries": false, 36 | "id": 4, 37 | "legend": { 38 | "avg": false, 39 | "current": false, 40 | "max": false, 41 | "min": false, 42 | "show": true, 43 | "total": false, 44 | "values": false 45 | }, 46 | "lines": true, 47 | "linewidth": 1, 48 | "nullPointMode": "null", 49 | "options": { 50 | "dataLinks": [] 51 | }, 52 | "percentage": false, 53 | "pointradius": 2, 54 | "points": false, 55 | "renderer": "flot", 56 | "seriesOverrides": [], 57 | "spaceLength": 10, 58 | "stack": false, 59 | "steppedLine": false, 60 | "targets": [ 61 | { 62 | "expr": "rate(substrate_block_height_number{status=\"sync_target\"}[10m]) * 60", 63 | "legendFormat": "{{instance}}", 64 | "refId": "A" 65 | }, 66 | { 67 | "expr": "avg(rate(substrate_block_height_number{status=\"sync_target\"}[10m]) * 60)", 68 | "intervalFactor": 1, 69 | "legendFormat": "average", 70 | "refId": "B" 71 | } 72 | ], 73 | "thresholds": [], 74 | "timeFrom": null, 75 | "timeRegions": [], 76 | "timeShift": null, 77 | "title": "Block production rate over 10 minutes by node", 78 | "tooltip": { 79 | "shared": true, 80 | "sort": 0, 81 | "value_type": "individual" 82 | }, 83 | "type": "graph", 84 | "xaxis": { 85 | "buckets": null, 86 | "mode": "time", 87 | "name": null, 88 | "show": true, 89 | "values": [] 90 | }, 91 | "yaxes": [ 92 | { 93 | "format": "none", 94 | "label": "blocks/minute", 95 | "logBase": 1, 96 | "max": null, 97 | "min": null, 98 | "show": true 99 | }, 100 | { 101 | "format": "short", 102 | "label": null, 103 | "logBase": 1, 104 | "max": null, 105 | "min": null, 106 | "show": true 107 | } 108 | ], 109 | "yaxis": { 110 | "align": false, 111 | "alignLevel": null 112 | } 113 | }, 114 | { 115 | "aliasColors": {}, 116 | "bars": false, 117 | "dashLength": 10, 118 | "dashes": false, 119 | "datasource": null, 120 | "fill": 0, 121 | "fillGradient": 0, 122 | "gridPos": { 123 | "h": 9, 124 | "w": 12, 125 | "x": 0, 126 | "y": 8 127 | }, 128 | "hiddenSeries": false, 129 | "id": 2, 130 | "legend": { 131 | "avg": false, 132 | "current": false, 133 | "max": false, 134 | "min": false, 135 | "show": true, 136 | "total": false, 137 | "values": false 138 | }, 139 | "lines": true, 140 | "linewidth": 1, 141 | "nullPointMode": "null", 142 | "options": { 143 | "dataLinks": [] 144 | }, 145 | "percentage": false, 146 | "pointradius": 2, 147 | "points": false, 148 | "renderer": "flot", 149 | "seriesOverrides": [], 150 | "spaceLength": 10, 151 | "stack": false, 152 | "steppedLine": true, 153 | "targets": [ 154 | { 155 | "expr": "substrate_block_height_number{status=\"best\"}", 156 | "instant": false, 157 | "intervalFactor": 1, 158 | "legendFormat": "{{instance}}", 159 | "refId": "A" 160 | } 161 | ], 162 | "thresholds": [], 163 | "timeFrom": null, 164 | "timeRegions": [], 165 | "timeShift": null, 166 | "title": "Block height by node", 167 | "tooltip": { 168 | "shared": true, 169 | "sort": 0, 170 | "value_type": "individual" 171 | }, 172 | "type": "graph", 173 | "xaxis": { 174 | "buckets": null, 175 | "mode": "time", 176 | "name": null, 177 | "show": true, 178 | "values": [] 179 | }, 180 | "yaxes": [ 181 | { 182 | "format": "short", 183 | "label": "block height", 184 | "logBase": 1, 185 | "max": null, 186 | "min": null, 187 | "show": true 188 | }, 189 | { 190 | "format": "short", 191 | "label": null, 192 | "logBase": 1, 193 | "max": null, 194 | "min": null, 195 | "show": true 196 | } 197 | ], 198 | "yaxis": { 199 | "align": false, 200 | "alignLevel": null 201 | } 202 | } 203 | ], 204 | "schemaVersion": 21, 205 | "style": "dark", 206 | "tags": [], 207 | "templating": { 208 | "list": [] 209 | }, 210 | "time": { 211 | "from": "now-15m", 212 | "to": "now" 213 | }, 214 | "timepicker": { 215 | "refresh_intervals": [ 216 | "5s", 217 | "10s", 218 | "30s", 219 | "1m", 220 | "5m", 221 | "15m", 222 | "30m", 223 | "1h", 224 | "2h", 225 | "1d" 226 | ] 227 | }, 228 | "timezone": "", 229 | "title": "Network", 230 | "uid": "nQSOAF9Zz", 231 | "version": 5 232 | } 233 | -------------------------------------------------------------------------------- /local-devnet/grafana-datasources.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | datasources: 3 | - name: Prometheus 4 | type: prometheus 5 | isDefault: true 6 | access: proxy 7 | url: "http://prometheus:9090" 8 | jsonData: 9 | timeInterval: 5s 10 | -------------------------------------------------------------------------------- /local-devnet/prometheus.yaml: -------------------------------------------------------------------------------- 1 | scrape_configs: 2 | - job_name: registry-nodes 3 | scrape_interval: 5s 4 | static_configs: 5 | - targets: 6 | - alice:9615 7 | - bob:9615 8 | - charlie:9615 9 | relabel_configs: 10 | - source_labels: ["__address__"] 11 | regex: "([^:]*):.*" 12 | target_label: "instance" 13 | -------------------------------------------------------------------------------- /local-devnet/start-node.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/bash 2 | 3 | set -euo pipefail 4 | 5 | declare -a extra_args 6 | if [[ "$NODE_NAME" = "alice" ]]; then 7 | extra_args=( 8 | # Boot node id: QmRpheLN4JWdAnY7HGJfWFNbfkQCb6tFf4vvA6hgjMZKrR 9 | --node-key 0000000000000000000000000000000000000000000000000000000000000001 10 | ) 11 | fi 12 | 13 | # Adress for the seed string //Mine 14 | block_author=5HYpUCg4KKiwpih63PUHmGeNrK2XeTxKR83yNKbZeTsvSKNq 15 | 16 | exec /usr/local/bin/radicle-registry-node \ 17 | --data-path /data \ 18 | --chain local-devnet \ 19 | --mine "$block_author" \ 20 | --unsafe-rpc-external \ 21 | --prometheus-external \ 22 | --bootnodes /dns4/alice/tcp/30333/p2p/QmRpheLN4JWdAnY7HGJfWFNbfkQCb6tFf4vvA6hgjMZKrR \ 23 | "${extra_args[@]}" 24 | -------------------------------------------------------------------------------- /node/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | edition = "2018" 3 | name = "radicle-registry-node" 4 | description = "Full node for the Radicle Registry network" 5 | authors = ["Monadic GmbH "] 6 | version = "0.0.0" 7 | license = "GPL-3.0-only" 8 | homepage = "https://github.com/radicle-dev/radicle-registry" 9 | documentation = "https://github.com/radicle-dev/radicle-registry" 10 | repository = "https://github.com/radicle-dev/radicle-registry" 11 | 12 | [[bin]] 13 | name = "radicle-registry-node" 14 | 15 | [dependencies] 16 | radicle-registry-runtime = { path = "../runtime" } 17 | 18 | blake3 = "0.2.1" 19 | env_logger = "0.7" 20 | futures = "0.3.1" 21 | lazy_static = "1.4.0" 22 | log = "0.4.8" 23 | num-bigint = "0.2.6" 24 | num-traits = "0.2.11" 25 | rand = "0.7.3" 26 | serde = "1.0.104" 27 | serde_json = "1.0.48" 28 | structopt = "0.3" 29 | time = "0.2" 30 | 31 | [dependencies.sc-basic-authorship] 32 | git = "https://github.com/paritytech/substrate" 33 | rev = "v2.0.0-rc4" 34 | 35 | [dependencies.sc-chain-spec] 36 | git = "https://github.com/paritytech/substrate" 37 | rev = "v2.0.0-rc4" 38 | 39 | [dependencies.sc-cli] 40 | git = "https://github.com/paritytech/substrate" 41 | rev = "v2.0.0-rc4" 42 | 43 | [dependencies.sc-client-api] 44 | git = "https://github.com/paritytech/substrate" 45 | rev = "v2.0.0-rc4" 46 | 47 | [dependencies.sc-consensus] 48 | git = "https://github.com/paritytech/substrate" 49 | rev = "v2.0.0-rc4" 50 | 51 | [dependencies.sc-consensus-pow] 52 | git = "https://github.com/paritytech/substrate" 53 | rev = "v2.0.0-rc4" 54 | 55 | [dependencies.sc-executor] 56 | git = "https://github.com/paritytech/substrate" 57 | rev = "v2.0.0-rc4" 58 | 59 | [dependencies.sc-network] 60 | git = "https://github.com/paritytech/substrate" 61 | rev = "v2.0.0-rc4" 62 | 63 | [dependencies.sc-service] 64 | git = "https://github.com/paritytech/substrate" 65 | rev = "v2.0.0-rc4" 66 | 67 | [dependencies.sc-telemetry] 68 | git = "https://github.com/paritytech/substrate" 69 | rev = "v2.0.0-rc4" 70 | 71 | [dependencies.sc-transaction-pool] 72 | git = "https://github.com/paritytech/substrate" 73 | rev = "v2.0.0-rc4" 74 | 75 | [dependencies.sp-api] 76 | git = "https://github.com/paritytech/substrate" 77 | rev = "v2.0.0-rc4" 78 | 79 | [dependencies.sp-blockchain] 80 | git = "https://github.com/paritytech/substrate" 81 | rev = "v2.0.0-rc4" 82 | 83 | [dependencies.sp-consensus] 84 | git = "https://github.com/paritytech/substrate" 85 | rev = "v2.0.0-rc4" 86 | 87 | [dependencies.sp-consensus-pow] 88 | git = "https://github.com/paritytech/substrate" 89 | rev = "v2.0.0-rc4" 90 | 91 | [dependencies.sp-core] 92 | git = "https://github.com/paritytech/substrate" 93 | rev = "v2.0.0-rc4" 94 | 95 | [dependencies.sp-database] 96 | git = "https://github.com/paritytech/substrate" 97 | rev = "v2.0.0-rc4" 98 | 99 | [dependencies.sp-inherents] 100 | git = "https://github.com/paritytech/substrate" 101 | rev = "v2.0.0-rc4" 102 | 103 | [dependencies.sp-io] 104 | git = "https://github.com/paritytech/substrate" 105 | rev = "v2.0.0-rc4" 106 | 107 | [dependencies.sp-runtime] 108 | git = "https://github.com/paritytech/substrate" 109 | rev = "v2.0.0-rc4" 110 | 111 | [dependencies.sp-transaction-pool] 112 | git = "https://github.com/paritytech/substrate" 113 | rev = "v2.0.0-rc4" 114 | 115 | [dependencies.substrate-prometheus-endpoint] 116 | git = "https://github.com/paritytech/substrate" 117 | rev = "v2.0.0-rc4" 118 | 119 | [build-dependencies] 120 | vergen = "3" 121 | -------------------------------------------------------------------------------- /node/build.rs: -------------------------------------------------------------------------------- 1 | use vergen::{generate_cargo_keys, ConstantsFlags}; 2 | 3 | fn main() { 4 | generate_cargo_keys(ConstantsFlags::all()).unwrap(); 5 | } 6 | -------------------------------------------------------------------------------- /node/src/blockchain.rs: -------------------------------------------------------------------------------- 1 | // Radicle Registry 2 | // Copyright (C) 2019 Monadic GmbH 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License version 3 as 6 | // published by the Free Software Foundation. 7 | // 8 | // This program is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU General Public License 14 | // along with this program. If not, see . 15 | 16 | //! Provides blockchain data type. 17 | 18 | pub use radicle_registry_runtime::{Hash, Header}; 19 | use sp_runtime::OpaqueExtrinsic; 20 | 21 | /// Block with an opaque blob of bytes as extrinsics. 22 | /// 23 | /// In contrast to [radicle_registry_runtime::Block] serialization is stable for this type even if 24 | /// the extrinsic type changes. This is because the extrinsic type here is [OpaqueExtrinsic], which 25 | /// can contain all past and future versions of [radicle_registry_runtime::UncheckedExtrinsic] as 26 | /// their serialization. 27 | /// 28 | /// It is safe to deserialize [Block] from a serialized [radicle_registry_runtime::Block]. 29 | pub type Block = sp_runtime::generic::Block; 30 | -------------------------------------------------------------------------------- /node/src/chain_spec.rs: -------------------------------------------------------------------------------- 1 | // Radicle Registry 2 | // Copyright (C) 2019 Monadic GmbH 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License version 3 as 6 | // published by the Free Software Foundation. 7 | // 8 | // This program is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU General Public License 14 | // along with this program. If not, see . 15 | 16 | //! Provides constructor functions to create [ChainSpec]s. 17 | use crate::pow::config::Config as PowAlgConfig; 18 | use radicle_registry_runtime::{genesis, AccountId, Balance}; 19 | use sc_service::{config::MultiaddrWithPeerId, ChainType, GenericChainSpec}; 20 | use sp_core::{crypto::CryptoType, Pair}; 21 | use std::convert::TryFrom; 22 | use std::path::PathBuf; 23 | 24 | /// Specialized `ChainSpec`. This is a specialization of the general Substrate ChainSpec type. 25 | pub type ChainSpec = GenericChainSpec; 26 | 27 | const FFNET_CHAIN_SPEC: &[u8] = include_bytes!("./chain_spec/ffnet.json"); 28 | const LATEST_RUNTIME_WASM: &[u8] = include_bytes!("../../runtime-cache/latest.wasm"); 29 | 30 | /// Chain for local development with a single node. 31 | /// 32 | /// If `runtime` is given, it is used as the genesis runtime. Uses dummy PoW that does not eat up 33 | /// your CPU. 34 | pub fn dev() -> ChainSpec { 35 | ChainParams { 36 | id: String::from("dev"), 37 | chain_type: ChainType::Development, 38 | boot_nodes: vec![], 39 | pow_alg: PowAlgConfig::Dummy, 40 | runtime: LATEST_RUNTIME_WASM.to_owned(), 41 | balances: dev_balances(), 42 | sudo_key: account_id("Alice"), 43 | } 44 | .into_chain_spec() 45 | } 46 | 47 | /// Chain that is running on the cloud and is frequently updated and reset. 48 | pub fn devnet() -> ChainSpec { 49 | ChainParams { 50 | id: String::from("devnet"), 51 | chain_type: ChainType::Development, 52 | boot_nodes: vec![ 53 | // The peer ID is the public key generated from the secret key 0x000...001. 54 | "/ip4/35.233.120.254/tcp/30333/p2p/QmRpheLN4JWdAnY7HGJfWFNbfkQCb6tFf4vvA6hgjMZKrR" 55 | .parse() 56 | .expect("Parsing a genesis peer address failed"), 57 | ], 58 | pow_alg: PowAlgConfig::Blake3, 59 | runtime: LATEST_RUNTIME_WASM.to_owned(), 60 | balances: dev_balances(), 61 | sudo_key: account_id("Alice"), 62 | } 63 | .into_chain_spec() 64 | } 65 | 66 | /// Chain for running a cluster of nodes locally. 67 | /// 68 | /// If `runtime` is given, it is used as the genesis runtime. Similar to [dev] but uses proper PoW 69 | /// consensus. 70 | pub fn local_devnet() -> ChainSpec { 71 | ChainParams { 72 | id: String::from("local-devnet"), 73 | chain_type: ChainType::Development, 74 | boot_nodes: vec![], 75 | pow_alg: PowAlgConfig::Blake3, 76 | runtime: LATEST_RUNTIME_WASM.to_owned(), 77 | balances: dev_balances(), 78 | sudo_key: account_id("Alice"), 79 | } 80 | .into_chain_spec() 81 | } 82 | 83 | /// First public test net. 84 | pub fn ffnet() -> ChainSpec { 85 | ChainSpec::from_json_bytes(FFNET_CHAIN_SPEC).expect("Unable to parse ffnet chain spec") 86 | } 87 | 88 | /// Chain spec loaded dynamically from a file 89 | pub fn from_spec_file(path: PathBuf) -> Result { 90 | ChainSpec::from_json_file(path) 91 | } 92 | 93 | /// Parameters to construct a [ChainSpec] with [ChainParams::into_chain_spec]. 94 | #[derive(Debug, Clone)] 95 | struct ChainParams { 96 | id: String, 97 | chain_type: ChainType, 98 | boot_nodes: Vec, 99 | pow_alg: PowAlgConfig, 100 | runtime: Vec, 101 | balances: Vec<(AccountId, Balance)>, 102 | sudo_key: AccountId, 103 | } 104 | 105 | impl ChainParams { 106 | fn into_chain_spec(self) -> ChainSpec { 107 | let ChainParams { 108 | id, 109 | chain_type, 110 | boot_nodes, 111 | pow_alg, 112 | runtime, 113 | balances, 114 | sudo_key, 115 | } = self; 116 | let make_genesis_config = move || genesis::GenesisConfig { 117 | system: Some(genesis::SystemConfig { 118 | code: runtime.clone(), 119 | changes_trie_config: Default::default(), 120 | }), 121 | pallet_balances: Some(genesis::BalancesConfig { 122 | balances: balances.clone(), 123 | }), 124 | pallet_sudo: Some(genesis::SudoConfig { key: sudo_key }), 125 | }; 126 | GenericChainSpec::from_genesis( 127 | &id, 128 | &id, 129 | chain_type, 130 | make_genesis_config, 131 | boot_nodes, 132 | None, // telemetry endpoints 133 | Some(&id), 134 | Some(sc_service::Properties::try_from(pow_alg).unwrap()), 135 | None, // no extensions 136 | ) 137 | } 138 | } 139 | 140 | fn dev_balances() -> Vec<(AccountId, Balance)> { 141 | let init_balance = 1u128 << 60; 142 | vec![ 143 | (account_id("Alice"), init_balance), 144 | (account_id("Bob"), init_balance), 145 | (account_id("Alice//stash"), init_balance), 146 | (account_id("Bob//stash"), init_balance), 147 | ] 148 | } 149 | 150 | /// Helper function to generate an account ID from a seed 151 | fn account_id(seed: &str) -> AccountId { 152 | ::Pair::from_string(&format!("//{}", seed), None) 153 | .expect("Parsing the account key pair seed failed") 154 | .public() 155 | } 156 | -------------------------------------------------------------------------------- /node/src/logger.rs: -------------------------------------------------------------------------------- 1 | // Radicle Registry 2 | // Copyright (C) 2019 Monadic GmbH 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License version 3 as 6 | // published by the Free Software Foundation. 7 | // 8 | // This program is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU General Public License 14 | // along with this program. If not, see . 15 | 16 | //! Provides [init] to initialize our custom logger. 17 | use env_logger::fmt::Color; 18 | use std::io::Write as _; 19 | 20 | /// Initializes [env_logger] using the `RUST_LOG` environment variables and our custom formatter. 21 | pub fn init() { 22 | let env = env_logger::Env::new().default_filter_or("info"); 23 | env_logger::Builder::from_env(env) 24 | .format(format_record) 25 | .target(env_logger::Target::Stdout) 26 | .init(); 27 | } 28 | 29 | fn format_record( 30 | formatter: &mut env_logger::fmt::Formatter, 31 | record: &log::Record, 32 | ) -> std::io::Result<()> { 33 | let time = time::OffsetDateTime::now_local(); 34 | 35 | let context = format!( 36 | "{time}.{ms:03} {level:<5} {target}", 37 | time = time.format("%H:%M:%S"), 38 | ms = time.millisecond(), 39 | target = record.target(), 40 | level = record.level(), 41 | ); 42 | 43 | writeln!( 44 | formatter, 45 | "{context} {msg}", 46 | context = formatter 47 | .style() 48 | // Using black with `set_intense(true)` results in grey output. 49 | .set_color(Color::Black) 50 | .set_intense(true) 51 | .value(context), 52 | msg = record.args() 53 | ) 54 | } 55 | -------------------------------------------------------------------------------- /node/src/main.rs: -------------------------------------------------------------------------------- 1 | // Radicle Registry 2 | // Copyright (C) 2019 Monadic GmbH 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License version 3 as 6 | // published by the Free Software Foundation. 7 | // 8 | // This program is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU General Public License 14 | // along with this program. If not, see . 15 | 16 | //! Substrate Node Template CLI library. 17 | 18 | #![warn(missing_docs)] 19 | 20 | mod blockchain; 21 | mod chain_spec; 22 | mod cli; 23 | mod logger; 24 | mod metrics; 25 | mod pow; 26 | mod service; 27 | 28 | use crate::cli::Cli; 29 | use sc_cli::SubstrateCli; 30 | 31 | fn main() { 32 | match Cli::from_args().run() { 33 | Ok(_) => (), 34 | Err(error) => { 35 | log::error!("{}", error); 36 | std::process::exit(1); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /node/src/metrics.rs: -------------------------------------------------------------------------------- 1 | // Radicle Registry 2 | // Copyright (C) 2019 Monadic GmbH 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License version 3 as 6 | // published by the Free Software Foundation. 7 | // 8 | // This program is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU General Public License 14 | // along with this program. If not, see . 15 | 16 | use futures::StreamExt; 17 | use std::convert::TryFrom; 18 | use std::future::Future; 19 | 20 | use sc_client_api::{AuxStore, BlockBackend, BlockImportNotification, BlockchainEvents}; 21 | use sc_service::{AbstractService, Error}; 22 | use sp_runtime::{generic::BlockId, traits::Block as _}; 23 | use substrate_prometheus_endpoint::prometheus::core::Atomic; 24 | use substrate_prometheus_endpoint::{Gauge, Registry, U64}; 25 | 26 | use crate::pow::Difficulty; 27 | 28 | pub fn register_metrics(service: &S) -> Result<(), Error> 29 | where 30 | S: AbstractService, 31 | S::Client: BlockchainEvents + BlockBackend + AuxStore, 32 | { 33 | let registry = match service.prometheus_registry() { 34 | Some(registry) => registry, 35 | None => { 36 | log::warn!("Prometheus is disabled, some metrics won't be collected"); 37 | return Ok(()); 38 | } 39 | }; 40 | register_best_block_metrics(service, ®istry)?; 41 | Ok(()) 42 | } 43 | 44 | fn register_best_block_metrics(service: &S, registry: &Registry) -> Result<(), Error> 45 | where 46 | S: AbstractService, 47 | S::Client: BlockchainEvents + BlockBackend + AuxStore, 48 | { 49 | let update_difficulty_gauge = create_difficulty_gauge_updater(service, registry)?; 50 | let update_block_size_gauges = create_block_size_gauges_updater(service, registry)?; 51 | let update_reorganization_gauges = create_reorganization_gauges_updater(registry)?; 52 | let task = service 53 | .client() 54 | .import_notification_stream() 55 | .for_each(move |info| { 56 | if info.is_new_best { 57 | update_difficulty_gauge(&info); 58 | update_block_size_gauges(&info); 59 | update_reorganization_gauges(&info); 60 | } 61 | futures::future::ready(()) 62 | }); 63 | spawn_metric_task(service, "best_block", task); 64 | Ok(()) 65 | } 66 | 67 | fn create_difficulty_gauge_updater( 68 | service: &S, 69 | registry: &Registry, 70 | ) -> Result), Error> 71 | where 72 | S: AbstractService, 73 | S::Client: AuxStore, 74 | { 75 | let difficulty_gauge = register_gauge::( 76 | ®istry, 77 | "best_block_difficulty", 78 | "The difficulty of the best block in the chain", 79 | )?; 80 | let client = service.client(); 81 | let updater = move |info: &BlockImportNotification| { 82 | let difficulty_res = 83 | sc_consensus_pow::PowAux::::read::<_, S::Block>(&*client, &info.hash); 84 | let difficulty = match difficulty_res { 85 | Ok(difficulty) => u64::try_from(difficulty.difficulty).unwrap_or(u64::MAX), 86 | Err(_) => return, 87 | }; 88 | difficulty_gauge.set(difficulty); 89 | }; 90 | Ok(updater) 91 | } 92 | 93 | fn create_block_size_gauges_updater( 94 | service: &S, 95 | registry: &Registry, 96 | ) -> Result), Error> 97 | where 98 | S: AbstractService, 99 | S::Client: BlockBackend, 100 | { 101 | let transactions_gauge = register_gauge::( 102 | ®istry, 103 | "best_block_transactions", 104 | "Number of transactions in the best block in the chain", 105 | )?; 106 | let length_gauge = register_gauge::( 107 | ®istry, 108 | "best_block_length", 109 | "Length in bytes of the best block in the chain", 110 | )?; 111 | let client = service.client(); 112 | let updater = move |info: &BlockImportNotification| { 113 | let body = match client.block_body(&BlockId::hash(info.hash)) { 114 | Ok(Some(body)) => body, 115 | _ => return, 116 | }; 117 | transactions_gauge.set(body.len() as u64); 118 | let encoded_block = S::Block::encode_from(&info.header, &body); 119 | length_gauge.set(encoded_block.len() as u64); 120 | }; 121 | Ok(updater) 122 | } 123 | 124 | fn create_reorganization_gauges_updater( 125 | registry: &Registry, 126 | ) -> Result), Error> { 127 | let reorg_length_gauge = register_gauge::( 128 | ®istry, 129 | "best_block_reorganization_length", 130 | "Number of blocks rolled back to establish the best block in the chain", 131 | )?; 132 | let reorg_count_gauge = register_gauge::( 133 | ®istry, 134 | "best_block_reorganization_count", 135 | "Number of best block reorganizations, which occurred in the chain", 136 | )?; 137 | let updater = move |info: &BlockImportNotification| { 138 | if let Some(tree_route) = &info.tree_route { 139 | let retracted_count = tree_route.retracted().len(); 140 | reorg_length_gauge.set(retracted_count as u64); 141 | if retracted_count != 0 { 142 | reorg_count_gauge.inc(); 143 | } 144 | } 145 | }; 146 | Ok(updater) 147 | } 148 | 149 | fn register_gauge( 150 | registry: &Registry, 151 | gauge_name: &str, 152 | gauge_help: &str, 153 | ) -> Result, Error> { 154 | let gauge = Gauge::new(gauge_name, gauge_help) 155 | .map_err(|e| format!("failed to create metric gauge '{}': {}", gauge_name, e))?; 156 | substrate_prometheus_endpoint::register(gauge, ®istry) 157 | .map_err(|e| format!("failed to register metric gauge '{}': {}", gauge_name, e).into()) 158 | } 159 | 160 | fn spawn_metric_task( 161 | service: &impl AbstractService, 162 | name: &str, 163 | task: impl Future + Send + 'static, 164 | ) { 165 | let task_name = Box::leak(format!("{}_metric_notifier", name).into_boxed_str()); 166 | service.spawn_task_handle().spawn(&*task_name, task); 167 | } 168 | -------------------------------------------------------------------------------- /node/src/pow/config.rs: -------------------------------------------------------------------------------- 1 | // Radicle Registry 2 | // Copyright (C) 2019 Monadic GmbH 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License version 3 as 6 | // published by the Free Software Foundation. 7 | // 8 | // This program is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU General Public License 14 | // along with this program. If not, see . 15 | 16 | use sc_service::{config::Configuration, Properties}; 17 | use std::convert::{TryFrom, TryInto}; 18 | 19 | /// Configuration of PoW algorithm, can be stored as chain spec property 20 | #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] 21 | pub enum Config { 22 | Dummy, 23 | Blake3, 24 | } 25 | 26 | impl Config { 27 | const PROPERTY_KEY: &'static str = "pow_alg"; 28 | } 29 | 30 | impl<'a> TryFrom<&'a Configuration> for Config { 31 | type Error = &'static str; 32 | 33 | fn try_from(config: &'a Configuration) -> Result { 34 | config.chain_spec.as_ref().properties().try_into() 35 | } 36 | } 37 | 38 | impl TryFrom for Config { 39 | type Error = &'static str; 40 | 41 | fn try_from(mut properties: Properties) -> Result { 42 | let pow_alg_str = properties 43 | .remove(Config::PROPERTY_KEY) 44 | .ok_or("properies do not contain PoW algorithm")?; 45 | serde_json::from_value(pow_alg_str).map_err(|_| "PoW algorithm property malformed") 46 | } 47 | } 48 | 49 | impl TryFrom for Properties { 50 | type Error = &'static str; 51 | 52 | fn try_from(config: Config) -> Result { 53 | let key = Config::PROPERTY_KEY.to_string(); 54 | let value = serde_json::to_value(config) 55 | .map_err(|_| "failed to serialize PoW algorithm into a property")?; 56 | let mut map = Properties::with_capacity(1); 57 | map.insert(key, value); 58 | Ok(map) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /node/src/pow/dummy_pow.rs: -------------------------------------------------------------------------------- 1 | // Radicle Registry 2 | // Copyright (C) 2019 Monadic GmbH 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License version 3 as 6 | // published by the Free Software Foundation. 7 | // 8 | // This program is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU General Public License 14 | // along with this program. If not, see . 15 | 16 | use crate::blockchain::{Block, Hash}; 17 | use crate::pow::Difficulty; 18 | use sc_consensus_pow::{Error, PowAlgorithm}; 19 | use sp_consensus_pow::Seal; 20 | use sp_runtime::generic::BlockId; 21 | use sp_runtime::traits::Block as BlockT; 22 | 23 | /// This is a dummy implementation of a PoW algorithm that doesn't do anything and 24 | /// **provides no security**. Do not use it outside of a tightly controlled devnet! 25 | /// 26 | /// It produces no seals. The mining process consists of 27 | /// a random length sleep and successful returning of a 0-byte nonce. 28 | /// 29 | /// It accepts all seals. Verification is always successful. 30 | /// 31 | /// The mining behavior is controlled by [MINE_DURATION] and [MINE_SUCCESS_PROBABILITY]. The 32 | /// average block time is 33 | /// 34 | /// (MINE_DURATION / n) * (1 / MINE_SUCCESS_PROBABILITY) 35 | /// 36 | /// where `n` is the number of miners. 37 | #[derive(Clone)] 38 | pub struct DummyPow; 39 | 40 | const MINE_DURATION: std::time::Duration = std::time::Duration::from_millis(10); 41 | const MINE_SUCCESS_PROBABILITY: f32 = 0.005; 42 | 43 | impl PowAlgorithm for DummyPow { 44 | type Difficulty = Difficulty; 45 | 46 | fn difficulty( 47 | &self, 48 | _parent: ::Hash, 49 | ) -> Result> { 50 | Ok(1.into()) 51 | } 52 | 53 | fn verify( 54 | &self, 55 | _parent: &BlockId, 56 | _pre_hash: &Hash, 57 | _seal: &Seal, 58 | _difficulty: Self::Difficulty, 59 | ) -> Result> { 60 | Ok(true) 61 | } 62 | 63 | fn mine( 64 | &self, 65 | _parent: &BlockId, 66 | _pre_hash: &Hash, 67 | _difficulty: Self::Difficulty, 68 | _round: u32, 69 | ) -> Result, Error> { 70 | std::thread::sleep(MINE_DURATION); 71 | if rand::random::() < MINE_SUCCESS_PROBABILITY { 72 | Ok(Some(vec![])) 73 | } else { 74 | Ok(None) 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /node/src/pow/mod.rs: -------------------------------------------------------------------------------- 1 | // Radicle Registry 2 | // Copyright (C) 2019 Monadic GmbH 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License version 3 as 6 | // published by the Free Software Foundation. 7 | // 8 | // This program is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU General Public License 14 | // along with this program. If not, see . 15 | 16 | pub mod blake3_pow; 17 | pub mod config; 18 | pub mod dummy_pow; 19 | mod harmonic_mean; 20 | 21 | pub type Difficulty = sp_core::U256; 22 | -------------------------------------------------------------------------------- /runtime-tests/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | edition = "2018" 3 | name = "radicle-registry-runtime-tests" 4 | description = "Tests for the runtime using the client" 5 | authors = ["Monadic GmbH "] 6 | version = "0.0.0" 7 | homepage = "https://github.com/radicle-dev/radicle-registry" 8 | documentation = "https://github.com/radicle-dev/radicle-registry" 9 | license = "GPL-3.0-only" 10 | repository = "https://github.com/radicle-dev/radicle-registry" 11 | 12 | [dependencies] 13 | async-std = { version = "1.4", features = ["attributes"] } 14 | rand = "0.7.2" 15 | 16 | radicle-registry-client = { path = "../client" } 17 | radicle-registry-runtime = { path = "../runtime" } 18 | radicle-registry-test-utils = { path = "../test-utils"} 19 | 20 | [dependencies.sp-state-machine] 21 | git = "https://github.com/paritytech/substrate" 22 | rev = "v2.0.0-rc4" 23 | 24 | [dependencies.sp-runtime] 25 | git = "https://github.com/paritytech/substrate" 26 | rev = "v2.0.0-rc4" 27 | -------------------------------------------------------------------------------- /runtime-tests/tests/account.rs: -------------------------------------------------------------------------------- 1 | // Radicle Registry 2 | // Copyright (C) 2019 Monadic GmbH 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License version 3 as 6 | // published by the Free Software Foundation. 7 | // 8 | // This program is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU General Public License 14 | // along with this program. If not, see . 15 | 16 | use radicle_registry_client::*; 17 | use radicle_registry_test_utils::*; 18 | 19 | /// Assert that a known account is recognized as existent on chain 20 | #[async_std::test] 21 | async fn account_exists() { 22 | let (client, _) = Client::new_emulator(); 23 | let account_on_chain = key_pair_with_associated_user(&client).await.0.public(); 24 | 25 | assert!( 26 | client.account_exists(&account_on_chain).await.unwrap(), 27 | "Random account shouldn't exist on chain" 28 | ); 29 | } 30 | 31 | /// Assert that a random account id does not exist on chain 32 | #[async_std::test] 33 | async fn random_account_does_not_exist() { 34 | let (client, _) = Client::new_emulator(); 35 | let random_account = ed25519::Pair::generate().0.public(); 36 | 37 | assert!( 38 | !client.account_exists(&random_account).await.unwrap(), 39 | "Account was expected to be on chain" 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /runtime-tests/tests/block_rewards.rs: -------------------------------------------------------------------------------- 1 | // Radicle Registry 2 | // Copyright (C) 2019 Monadic GmbH 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License version 3 as 6 | // published by the Free Software Foundation. 7 | // 8 | // This program is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU General Public License 14 | // along with this program. If not, see . 15 | 16 | use radicle_registry_client::*; 17 | use radicle_registry_runtime::registry::BLOCK_REWARD; 18 | use radicle_registry_test_utils::*; 19 | use sp_runtime::Permill; 20 | 21 | /// Assert that block rewards and transaction fees are credited to the block author. 22 | #[async_std::test] 23 | async fn block_rewards_credited() { 24 | let (client, _) = Client::new_emulator(); 25 | 26 | let alice = key_pair_with_funds(&client).await; 27 | let bob = ed25519::Pair::generate().0.public(); 28 | let author_balance = client.free_balance(&EMULATOR_BLOCK_AUTHOR).await.unwrap(); 29 | 30 | let fee = 3000; 31 | submit_ok_with_fee( 32 | &client, 33 | &alice, 34 | message::Transfer { 35 | recipient: bob, 36 | amount: 1000, 37 | }, 38 | fee, 39 | ) 40 | .await; 41 | 42 | let rewards = client.free_balance(&EMULATOR_BLOCK_AUTHOR).await.unwrap() - author_balance; 43 | let fee_reward = Permill::from_percent(99) * fee; 44 | assert_eq!(rewards, fee_reward + BLOCK_REWARD); 45 | } 46 | -------------------------------------------------------------------------------- /runtime-tests/tests/id_status.rs: -------------------------------------------------------------------------------- 1 | // Radicle Registry 2 | // Copyright (C) 2019 Monadic GmbH 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License version 3 as 6 | // published by the Free Software Foundation. 7 | // 8 | // This program is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU General Public License 14 | // along with this program. If not, see . 15 | 16 | /// Runtime tests implemented with [MemoryClient]. 17 | /// 18 | /// High-level runtime tests that only use [MemoryClient] and treat the runtime as a black box. 19 | /// 20 | /// The tests in this module concern orgs registration. 21 | use radicle_registry_client::*; 22 | use radicle_registry_test_utils::*; 23 | 24 | #[async_std::test] 25 | async fn test_available() { 26 | let (client, _) = Client::new_emulator(); 27 | let status = client.get_id_status(&random_id()).await.unwrap(); 28 | 29 | assert_eq!(status, IdStatus::Available); 30 | } 31 | 32 | /// Test that an Id is Taken by an org 33 | #[async_std::test] 34 | async fn test_taken_by_org() { 35 | let (client, _) = Client::new_emulator(); 36 | let (author, _) = key_pair_with_associated_user(&client).await; 37 | let (org_id, _) = register_random_org(&client, &author).await; 38 | 39 | let status = client.get_id_status(&org_id).await.unwrap(); 40 | assert_eq!(status, IdStatus::Taken); 41 | } 42 | 43 | /// Test that an Id is Taken by a user 44 | #[async_std::test] 45 | async fn test_taken_by_user() { 46 | let (client, _) = Client::new_emulator(); 47 | let (_, user_id) = key_pair_with_associated_user(&client).await; 48 | 49 | let status = client.get_id_status(&user_id).await.unwrap(); 50 | assert_eq!(status, IdStatus::Taken); 51 | } 52 | 53 | /// Test that an Id is Retired once unregistered by an org 54 | #[async_std::test] 55 | async fn test_retired_by_org() { 56 | let (client, _) = Client::new_emulator(); 57 | let (author, _) = key_pair_with_associated_user(&client).await; 58 | 59 | // Register org 60 | let (org_id, _) = register_random_org(&client, &author).await; 61 | 62 | // Unregister org 63 | let unregister_org_message = message::UnregisterOrg { 64 | org_id: org_id.clone(), 65 | }; 66 | let tx_unregister_applied = submit_ok(&client, &author, unregister_org_message.clone()).await; 67 | assert_eq!(tx_unregister_applied.result, Ok(())); 68 | 69 | let status = client.get_id_status(&org_id).await.unwrap(); 70 | assert_eq!(status, IdStatus::Retired); 71 | } 72 | 73 | /// Test that an Id is Retired once unregistered by a user 74 | #[async_std::test] 75 | async fn test_retired_by_user() { 76 | let (client, _) = Client::new_emulator(); 77 | // Register user 78 | let (author, user_id) = key_pair_with_associated_user(&client).await; 79 | 80 | // Unregister user 81 | let unregister_user_message = message::UnregisterUser { 82 | user_id: user_id.clone(), 83 | }; 84 | let tx_unregister_applied = submit_ok(&client, &author, unregister_user_message.clone()).await; 85 | assert_eq!(tx_unregister_applied.result, Ok(())); 86 | 87 | let status = client.get_id_status(&user_id).await.unwrap(); 88 | assert_eq!(status, IdStatus::Retired); 89 | } 90 | -------------------------------------------------------------------------------- /runtime-tests/tests/transfer.rs: -------------------------------------------------------------------------------- 1 | // Radicle Registry 2 | // Copyright (C) 2019 Monadic GmbH 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License version 3 as 6 | // published by the Free Software Foundation. 7 | // 8 | // This program is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU General Public License 14 | // along with this program. If not, see . 15 | 16 | /// Runtime tests implemented with [MemoryClient]. 17 | /// 18 | /// High-level runtime tests that only use [MemoryClient] and treat the runtime as a black box. 19 | /// 20 | /// The tests in this module concern transferring funds. 21 | use radicle_registry_client::*; 22 | use radicle_registry_test_utils::*; 23 | 24 | #[async_std::test] 25 | async fn transfer_fail() { 26 | let (client, _) = Client::new_emulator(); 27 | let alice = key_pair_with_funds(&client).await; 28 | let bob = key_pair_with_funds(&client).await.public(); 29 | 30 | let balance_alice = client.free_balance(&alice.public()).await.unwrap(); 31 | let tx_included = submit_ok( 32 | &client, 33 | &alice, 34 | message::Transfer { 35 | recipient: bob, 36 | amount: balance_alice + 1, 37 | }, 38 | ) 39 | .await; 40 | assert!(tx_included.result.is_err()); 41 | } 42 | 43 | // Test that we can transfer any amount within a reasonable range. 44 | // Affected by the [crate::ExistentialDeposit] parameter. 45 | #[async_std::test] 46 | async fn transfer_any_amount() { 47 | let (client, _) = Client::new_emulator(); 48 | let donator = key_pair_with_funds(&client).await; 49 | let receipient = ed25519::Pair::generate().0.public(); 50 | 51 | for amount in (1..10000).step_by(500) { 52 | let tx_included = submit_ok( 53 | &client, 54 | &donator, 55 | message::Transfer { 56 | recipient: receipient, 57 | amount, 58 | }, 59 | ) 60 | .await; 61 | assert_eq!( 62 | tx_included.result, 63 | Ok(()), 64 | "Failed to transfer {} μRAD", 65 | amount 66 | ); 67 | } 68 | } 69 | 70 | /// Test that we can transfer money to an org account and that the 71 | /// org owner can transfer money from an org to another account. 72 | #[async_std::test] 73 | async fn org_account_transfer() { 74 | let (client, _) = Client::new_emulator(); 75 | let (author, _) = key_pair_with_associated_user(&client).await; 76 | 77 | let bob = ed25519::Pair::generate().0.public(); 78 | let (org_id, org) = register_random_org(&client, &author).await; 79 | 80 | let org_inigial_balance = client.free_balance(&org.account_id()).await.unwrap(); 81 | let alice_initial_balance = client.free_balance(&author.public()).await.unwrap(); 82 | let random_fee = random_balance(); 83 | let transfer_amount = 2000; 84 | submit_ok_with_fee( 85 | &client, 86 | &author, 87 | message::Transfer { 88 | recipient: org.account_id(), 89 | amount: transfer_amount, 90 | }, 91 | random_fee, 92 | ) 93 | .await; 94 | let org_balance_after_transfer = client.free_balance(&org.account_id()).await.unwrap(); 95 | assert_eq!( 96 | org_balance_after_transfer, 97 | org_inigial_balance + transfer_amount 98 | ); 99 | assert_eq!( 100 | client.free_balance(&author.public()).await.unwrap(), 101 | alice_initial_balance - transfer_amount - random_fee, 102 | "The tx fee was not charged properly." 103 | ); 104 | 105 | assert_eq!(client.free_balance(&bob).await.unwrap(), 0); 106 | let initial_balance_org = client.free_balance(&org.account_id()).await.unwrap(); 107 | let org_transfer_fee = random_balance(); 108 | let org_transfer_amount = 1000; 109 | submit_ok_with_fee( 110 | &client, 111 | &author, 112 | message::TransferFromOrg { 113 | org_id, 114 | recipient: bob, 115 | amount: org_transfer_amount, 116 | }, 117 | org_transfer_fee, 118 | ) 119 | .await; 120 | assert_eq!(client.free_balance(&bob).await.unwrap(), 1000); 121 | assert_eq!( 122 | client.free_balance(&org.account_id()).await.unwrap(), 123 | initial_balance_org - org_transfer_amount - org_transfer_fee 124 | ); 125 | } 126 | 127 | #[async_std::test] 128 | /// Test that a transfer from an org account fails if the sender is not an org member. 129 | async fn org_account_transfer_non_member() { 130 | let (client, _) = Client::new_emulator(); 131 | let (author, _) = key_pair_with_associated_user(&client).await; 132 | let (org_id, org) = register_random_org(&client, &author).await; 133 | 134 | let initial_balance = client.free_balance(&org.account_id()).await.unwrap(); 135 | 136 | let bad_actor = ed25519::Pair::generate().0; 137 | // The bad actor needs funds to submit transactions. 138 | transfer(&client, &author, bad_actor.public(), 1000).await; 139 | 140 | let random_fee = random_balance(); 141 | submit_ok_with_fee( 142 | &client, 143 | &bad_actor, 144 | message::TransferFromOrg { 145 | org_id, 146 | recipient: bad_actor.public(), 147 | amount: 1000, 148 | }, 149 | random_fee, 150 | ) 151 | .await; 152 | 153 | assert_eq!( 154 | client.free_balance(&org.account_id()).await.unwrap(), 155 | initial_balance, 156 | ); 157 | assert_eq!( 158 | client.free_balance(&bad_actor.public()).await.unwrap(), 159 | initial_balance - random_fee, 160 | "The tx fee was not charged properly." 161 | ); 162 | } 163 | -------------------------------------------------------------------------------- /runtime/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | edition = "2018" 3 | name = "radicle-registry-runtime" 4 | description = "Substrate chain runtime for the Radicle Registry" 5 | authors = ["Monadic GmbH "] 6 | version = "0.19.0" 7 | homepage = "https://github.com/radicle-dev/radicle-registry" 8 | documentation = "https://github.com/radicle-dev/radicle-registry" 9 | license = "GPL-3.0-only" 10 | repository = "https://github.com/radicle-dev/radicle-registry" 11 | 12 | [features] 13 | default = ["std"] 14 | no-std = [] 15 | std = [ 16 | "frame-executive/std", 17 | "frame-support/std", 18 | "frame-system/std", 19 | "pallet-balances/std", 20 | "pallet-sudo/std", 21 | "pallet-timestamp/std", 22 | "parity-scale-codec/std", 23 | "radicle-registry-core/std", 24 | "serde", 25 | "sp-api/std", 26 | "sp-block-builder/std", 27 | "sp-consensus-pow/std", 28 | "sp-core/std", 29 | "sp-io/std", 30 | "sp-offchain/std", 31 | "sp-runtime/std", 32 | "sp-session/std", 33 | "sp-std/std", 34 | "sp-transaction-pool/std", 35 | "sp-version/std" 36 | ] 37 | 38 | [dependencies.radicle-registry-core] 39 | path = "../core" 40 | version = "0.0.0" 41 | default-features = false 42 | 43 | [dependencies.parity-scale-codec] 44 | default-features = false 45 | features = ["derive", "full"] 46 | version = "1.0.0" 47 | 48 | [dependencies.serde] 49 | features = ["derive"] 50 | optional = true 51 | version = "1.0.101" 52 | 53 | [dependencies.pallet-balances] 54 | git = "https://github.com/paritytech/substrate" 55 | rev = "v2.0.0-rc4" 56 | default_features = false 57 | 58 | [dependencies.sp-api] 59 | git = "https://github.com/paritytech/substrate" 60 | rev = "v2.0.0-rc4" 61 | default_features = false 62 | 63 | [dependencies.sp-timestamp] 64 | git = "https://github.com/paritytech/substrate" 65 | rev = "v2.0.0-rc4" 66 | default_features = false 67 | 68 | [dependencies.frame-executive] 69 | git = "https://github.com/paritytech/substrate" 70 | rev = "v2.0.0-rc4" 71 | default_features = false 72 | 73 | [dependencies.sp-offchain] 74 | git = "https://github.com/paritytech/substrate" 75 | rev = "v2.0.0-rc4" 76 | default-features = false 77 | 78 | [dependencies.sp-core] 79 | git = "https://github.com/paritytech/substrate" 80 | rev = "v2.0.0-rc4" 81 | default_features = false 82 | 83 | [dependencies.sp-std] 84 | git = "https://github.com/paritytech/substrate" 85 | rev = "v2.0.0-rc4" 86 | default_features = false 87 | 88 | [dependencies.sp-io] 89 | git = "https://github.com/paritytech/substrate" 90 | rev = "v2.0.0-rc4" 91 | default_features = false 92 | 93 | [dependencies.sp-runtime] 94 | git = "https://github.com/paritytech/substrate" 95 | rev = "v2.0.0-rc4" 96 | default_features = false 97 | 98 | [dependencies.pallet-randomness-collective-flip] 99 | git = "https://github.com/paritytech/substrate" 100 | rev = "v2.0.0-rc4" 101 | default_features = false 102 | 103 | [dependencies.sp-consensus-pow] 104 | git = "https://github.com/paritytech/substrate" 105 | rev = "v2.0.0-rc4" 106 | default-features = false 107 | 108 | [dependencies.sp-session] 109 | git = "https://github.com/paritytech/substrate" 110 | rev = "v2.0.0-rc4" 111 | default-features = false 112 | 113 | [dependencies.sp-block-builder] 114 | git = "https://github.com/paritytech/substrate" 115 | rev = "v2.0.0-rc4" 116 | default-features = false 117 | 118 | [dependencies.sp-transaction-pool] 119 | git = "https://github.com/paritytech/substrate" 120 | rev = "v2.0.0-rc4" 121 | default-features = false 122 | 123 | [dependencies.sp-inherents] 124 | git = "https://github.com/paritytech/substrate" 125 | rev = "v2.0.0-rc4" 126 | default-features = false 127 | 128 | [dependencies.pallet-sudo] 129 | git = "https://github.com/paritytech/substrate" 130 | rev = "v2.0.0-rc4" 131 | default_features = false 132 | 133 | [dependencies.frame-support] 134 | git = "https://github.com/paritytech/substrate" 135 | rev = "v2.0.0-rc4" 136 | default_features = false 137 | 138 | [dependencies.frame-system] 139 | git = "https://github.com/paritytech/substrate" 140 | rev = "v2.0.0-rc4" 141 | default_features = false 142 | 143 | [dependencies.pallet-timestamp] 144 | git = "https://github.com/paritytech/substrate" 145 | rev = "v2.0.0-rc4" 146 | default_features = false 147 | 148 | [dependencies.sp-version] 149 | git = "https://github.com/paritytech/substrate" 150 | rev = "v2.0.0-rc4" 151 | default_features = false 152 | 153 | [build-dependencies] 154 | substrate-wasm-builder-runner = "1.0.6" 155 | -------------------------------------------------------------------------------- /runtime/build.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::ffi::OsStr; 3 | use substrate_wasm_builder_runner::WasmBuilder; 4 | 5 | const BUILD_WASM_BINARY_OUR_DIR_ENV: &str = "BUILD_WASM_BINARY_OUT_DIR"; 6 | 7 | fn main() { 8 | println!( 9 | "cargo:rerun-if-env-changed={}", 10 | BUILD_WASM_BINARY_OUR_DIR_ENV 11 | ); 12 | if let Some(wasm_binary_dir) = env::var_os(BUILD_WASM_BINARY_OUR_DIR_ENV) { 13 | build_wasm_runtime_in_dir(&wasm_binary_dir); 14 | } 15 | } 16 | 17 | fn build_wasm_runtime_in_dir(out_dir: impl AsRef) { 18 | env::set_var("WASM_TARGET_DIRECTORY", out_dir); 19 | env::set_var("WASM_BUILD_TYPE", "release"); 20 | WasmBuilder::new() 21 | .with_current_project() 22 | .with_wasm_builder_from_crates("1.0.9") 23 | .export_heap_base() 24 | .import_memory() 25 | .build(); 26 | } 27 | -------------------------------------------------------------------------------- /runtime/src/fees/mod.rs: -------------------------------------------------------------------------------- 1 | // Radicle Registry 2 | // Copyright (C) 2019 Monadic GmbH 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License version 3 as 6 | // published by the Free Software Foundation. 7 | // 8 | // This program is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU General Public License 14 | // along with this program. If not, see . 15 | 16 | //! Fee charging logic as [SignedExtension] for [PayTxFee]. 17 | 18 | use crate::{AccountId, Balance, Call}; 19 | 20 | use frame_support::dispatch::DispatchInfo; 21 | use parity_scale_codec::{Decode, Encode}; 22 | use sp_runtime::traits::SignedExtension; 23 | use sp_runtime::transaction_validity::{ 24 | InvalidTransaction, TransactionValidity, TransactionValidityError, ValidTransaction, 25 | }; 26 | 27 | mod payment; 28 | 29 | pub use payment::{pay_registration_fee, pay_tx_fee}; 30 | 31 | /// The minimum acceptable tx fee 32 | pub const MINIMUM_TX_FEE: Balance = 1; 33 | 34 | /// The registration fee 35 | pub const REGISTRATION_FEE: Balance = 10; 36 | 37 | /// Pay the transaction fee indicated by the author. 38 | /// The fee should be higher or equal to [MINIMUM_TX_FEE]. 39 | /// The higher the fee, the higher the priority of a transaction. 40 | #[derive(Debug, Encode, Decode, Clone, Eq, PartialEq)] 41 | pub struct PayTxFee { 42 | pub fee: Balance, 43 | } 44 | 45 | impl SignedExtension for PayTxFee { 46 | const IDENTIFIER: &'static str = "PayTxFee"; 47 | 48 | type AccountId = AccountId; 49 | type Call = Call; 50 | type AdditionalSigned = (); 51 | type Pre = (); 52 | 53 | fn additional_signed(&self) -> sp_std::result::Result<(), TransactionValidityError> { 54 | Ok(()) 55 | } 56 | 57 | fn validate( 58 | &self, 59 | author: &Self::AccountId, 60 | call: &Self::Call, 61 | _info: &DispatchInfo, 62 | _len: usize, 63 | ) -> TransactionValidity { 64 | let error = TransactionValidityError::Invalid(InvalidTransaction::Payment); 65 | if self.fee < MINIMUM_TX_FEE { 66 | return Err(error); 67 | } 68 | pay_tx_fee(author, self.fee, call).map_err(|_| error)?; 69 | 70 | let mut valid_tx = ValidTransaction::default(); 71 | valid_tx.priority = self.fee as u64; 72 | Ok(valid_tx) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /runtime/src/fees/payment.rs: -------------------------------------------------------------------------------- 1 | // Radicle Registry 2 | // Copyright (C) 2019 Monadic GmbH 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License version 3 as 6 | // published by the Free Software Foundation. 7 | // 8 | // This program is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU General Public License 14 | // along with this program. If not, see . 15 | 16 | use crate::registry::{org_has_member_with_account, store}; 17 | use crate::{call, AccountId, Call, DispatchError}; 18 | use radicle_registry_core::*; 19 | 20 | use frame_support::storage::{StorageMap as _, StorageValue as _}; 21 | use frame_support::traits::{ 22 | Currency, ExistenceRequirement, Imbalance, WithdrawReason, WithdrawReasons, 23 | }; 24 | use sp_runtime::Permill; 25 | 26 | type NegativeImbalance = >::NegativeImbalance; 27 | 28 | /// Share of a transaction fee that is burned rather than credited to the block author. 29 | const BURN_SHARE: Permill = Permill::from_percent(1); 30 | 31 | pub fn pay_tx_fee(author: &AccountId, fee: Balance, call: &Call) -> Result<(), DispatchError> { 32 | let payer = payer_account(*author, call); 33 | let withdrawn_fee = withdraw( 34 | fee, 35 | &payer, 36 | WithdrawReason::TransactionPayment | WithdrawReason::Tip, 37 | )?; 38 | let (burn, reward) = withdrawn_fee.split(BURN_SHARE * fee); 39 | drop(burn); 40 | 41 | // The block author is only available when this function is run as part of the block execution. 42 | // If this function is run as part of transaction validation the block author is not set. In 43 | // that case we don’t need to credit the block author. 44 | if let Some(block_author) = store::BlockAuthor::get() { 45 | crate::runtime::Balances::resolve_creating(&block_author, reward); 46 | } 47 | 48 | Ok(()) 49 | } 50 | 51 | pub fn pay_registration_fee(author: &AccountId) -> Result<(), RegistryError> { 52 | let _burnt = withdraw(super::REGISTRATION_FEE, author, WithdrawReason::Fee.into()) 53 | .map_err(|_| RegistryError::FailedRegistrationFeePayment)?; 54 | Ok(()) 55 | } 56 | 57 | fn withdraw( 58 | fee: Balance, 59 | payer: &AccountId, 60 | withhdraw_reasons: WithdrawReasons, 61 | ) -> Result { 62 | >::withdraw( 63 | payer, 64 | fee, 65 | withhdraw_reasons, 66 | ExistenceRequirement::KeepAlive, 67 | ) 68 | } 69 | 70 | /// Find which account should pay for a given runtime call. 71 | /// Authorize calls that involve another paying entity than the tx author. 72 | /// The tx author pays for all unauthorized calls. 73 | fn payer_account(author: AccountId, call: &Call) -> AccountId { 74 | match call { 75 | Call::Registry(registry_call) => match registry_call { 76 | // Transactions payed by the org 77 | call::Registry::register_project(m) => match &m.project_domain { 78 | ProjectDomain::Org(org_id) => org_payer_account(author, org_id), 79 | ProjectDomain::User(_user_id) => author, 80 | }, 81 | call::Registry::transfer_from_org(m) => org_payer_account(author, &m.org_id), 82 | call::Registry::register_member(m) => org_payer_account(author, &m.org_id), 83 | 84 | // Transactions paid by the author 85 | call::Registry::register_org(_) 86 | | call::Registry::unregister_org(_) 87 | | call::Registry::transfer(_) 88 | | call::Registry::register_user(_) 89 | | call::Registry::unregister_user(_) => author, 90 | 91 | // Inherents 92 | call::Registry::set_block_author(_) => { 93 | panic!("Inherent calls are not allowed for signed extrinsics") 94 | } 95 | 96 | crate::registry::Call::__PhantomItem(_, _) => { 97 | unreachable!("__PhantomItem should never be used.") 98 | } 99 | }, 100 | _ => author, 101 | } 102 | } 103 | 104 | /// Find which account should pay for an org-related call. 105 | /// When the User associated with `author` is a member of the org 106 | /// identified by `org_id`, return that org's account, otherwise the author's. 107 | fn org_payer_account(author: AccountId, org_id: &Id) -> AccountId { 108 | match store::Orgs1::get(org_id) { 109 | Some(org) => { 110 | if org_has_member_with_account(&org, author) { 111 | org.account_id() 112 | } else { 113 | author 114 | } 115 | } 116 | None => author, 117 | } 118 | } 119 | 120 | #[cfg(test)] 121 | mod test { 122 | use super::*; 123 | use crate::{genesis::GenesisConfig, runtime::Balances}; 124 | 125 | use core::convert::TryFrom; 126 | use frame_support::traits::Currency; 127 | use sp_core::{crypto::Pair, ed25519}; 128 | use sp_runtime::BuildStorage; 129 | 130 | #[test] 131 | fn test_pay_tx_fee() { 132 | let genesis_config = GenesisConfig { 133 | pallet_balances: None, 134 | pallet_sudo: None, 135 | system: None, 136 | }; 137 | 138 | let mut test_ext = sp_io::TestExternalities::new(genesis_config.build_storage().unwrap()); 139 | 140 | test_ext.execute_with(move || { 141 | let block_author = ed25519::Pair::from_string("//Bob", None).unwrap().public(); 142 | store::BlockAuthor::put(block_author); 143 | 144 | let tx_author = ed25519::Pair::from_string("//Alice", None) 145 | .unwrap() 146 | .public(); 147 | let _imbalance = Balances::deposit_creating(&tx_author, 3000); 148 | 149 | let call = call::Registry::register_user(message::RegisterUser { 150 | user_id: Id::try_from("alice").unwrap(), 151 | }) 152 | .into(); 153 | let fee = 1000; 154 | pay_tx_fee(&tx_author, fee, &call).unwrap(); 155 | 156 | let block_author_balance = Balances::free_balance(&block_author); 157 | assert_eq!(block_author_balance, 990); 158 | 159 | let tx_author_balance = Balances::free_balance(&tx_author); 160 | assert_eq!(tx_author_balance, 2000) 161 | }); 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /runtime/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Radicle Registry 2 | // Copyright (C) 2019 Monadic GmbH 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License version 3 as 6 | // published by the Free Software Foundation. 7 | // 8 | // This program is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU General Public License 14 | // along with this program. If not, see . 15 | 16 | //! The Substrate Node Template runtime. This can be compiled with `#[no_std]`, ready for Wasm. 17 | 18 | // We allow two clippy lints because the `impl_runtime_apis` and `construct_runtime` macros produce 19 | // code that would fail otherwise. 20 | #![allow( 21 | clippy::not_unsafe_ptr_arg_deref, 22 | clippy::string_lit_as_bytes, 23 | clippy::unnecessary_mut_passed 24 | )] 25 | #![cfg_attr(not(feature = "std"), no_std)] 26 | // `construct_runtime!` does a lot of recursion and requires us to increase the limit to 256. 27 | #![recursion_limit = "256"] 28 | 29 | #[cfg(all(feature = "std", feature = "no-std"))] 30 | std::compile_error!("Features \"std\" and \"no-std\" cannot be enabled simultaneously. Maybe a dependency implicitly enabled the \"std\" feature."); 31 | 32 | extern crate alloc; 33 | 34 | use sp_core::ed25519; 35 | use sp_runtime::traits::BlakeTwo256; 36 | use sp_runtime::{create_runtime_str, generic}; 37 | pub use sp_version::RuntimeVersion; 38 | 39 | pub use radicle_registry_core::*; 40 | pub use runtime::api as runtime_api; 41 | pub use runtime::api::{api, RuntimeApi}; 42 | pub use runtime::{Call, Event, Origin, Runtime}; 43 | 44 | pub mod fees; 45 | pub mod registry; 46 | mod runtime; 47 | pub mod timestamp_in_digest; 48 | 49 | pub use registry::DecodeKey; 50 | 51 | /// An index to a block. 52 | pub type BlockNumber = u32; 53 | 54 | /// Alias to 512-bit hash when used in the context of a transaction signature on the chain. 55 | pub type Signature = ed25519::Signature; 56 | 57 | /// A hash of some data used by the chain. 58 | /// 59 | /// Same as [sp_runtime::traits::Hash::Output] for [Hashing]. 60 | pub type Hash = sp_core::H256; 61 | 62 | /// Block header type as expected by this runtime. 63 | pub type Header = generic::Header; 64 | 65 | /// Block type as expected by this runtime. 66 | pub type Block = generic::Block; 67 | 68 | /// The SignedExtension to the basic transaction logic. 69 | pub type SignedExtra = ( 70 | frame_system::CheckTxVersion, 71 | frame_system::CheckGenesis, 72 | frame_system::CheckEra, 73 | frame_system::CheckNonce, 74 | frame_system::CheckWeight, 75 | crate::fees::PayTxFee, 76 | ); 77 | 78 | /// Unchecked extrinsic type as expected by this runtime. 79 | pub type UncheckedExtrinsic = generic::UncheckedExtrinsic; 80 | 81 | /// A timestamp: milliseconds since the unix epoch. 82 | type Moment = u64; 83 | 84 | pub const SPEC_VERSION: u32 = 19; 85 | 86 | /// This runtime version. 87 | pub const VERSION: RuntimeVersion = RuntimeVersion { 88 | spec_name: create_runtime_str!("radicle-registry"), 89 | impl_name: create_runtime_str!("radicle-registry"), 90 | spec_version: SPEC_VERSION, 91 | transaction_version: SPEC_VERSION, 92 | impl_version: 0, 93 | apis: runtime::api::VERSIONS, 94 | // Ignored by us. Only `spec_version` and `impl_version` are relevant. 95 | authoring_version: 3, 96 | }; 97 | 98 | #[test] 99 | fn crate_versions() { 100 | assert_eq!( 101 | env!("CARGO_PKG_VERSION_MINOR"), 102 | format!("{}", SPEC_VERSION), 103 | "Runtime spec_version does not match crate minor version" 104 | ); 105 | assert_eq!( 106 | env!("CARGO_PKG_VERSION_PATCH"), 107 | format!("{}", VERSION.impl_version), 108 | "Runtime impl_version does not match crate patch version" 109 | ); 110 | } 111 | 112 | /// The version infromation used to identify this runtime when compiled natively. 113 | #[cfg(feature = "std")] 114 | pub fn native_version() -> sp_version::NativeVersion { 115 | sp_version::NativeVersion { 116 | runtime_version: VERSION, 117 | can_author_with: Default::default(), 118 | } 119 | } 120 | 121 | pub mod store { 122 | pub use crate::registry::store::*; 123 | pub type Account = frame_system::Account; 124 | #[doc(inline)] 125 | pub use crate::registry::DecodeKey; 126 | } 127 | 128 | pub mod event { 129 | pub use crate::runtime::Event; 130 | pub type Record = frame_system::EventRecord; 131 | pub type System = frame_system::Event; 132 | 133 | /// Return the index of the transaction in the block that dispatched the event. 134 | /// 135 | /// Returns `None` if the event was not dispatched as part of a transaction. 136 | #[cfg(feature = "std")] 137 | pub fn transaction_index(record: &Record) -> Option { 138 | match record.phase { 139 | frame_system::Phase::ApplyExtrinsic(i) => Some(i), 140 | _ => None, 141 | } 142 | } 143 | } 144 | 145 | pub mod call { 146 | pub type Registry = crate::registry::Call; 147 | pub type System = frame_system::Call; 148 | pub type Sudo = pallet_sudo::Call; 149 | } 150 | 151 | #[cfg(feature = "std")] 152 | pub mod genesis { 153 | pub use crate::runtime::{BalancesConfig, GenesisConfig, SudoConfig, SystemConfig}; 154 | } 155 | -------------------------------------------------------------------------------- /runtime/src/registry/inherents.rs: -------------------------------------------------------------------------------- 1 | // Radicle Registry 2 | // Copyright (C) 2019 Monadic GmbH 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License version 3 as 6 | // published by the Free Software Foundation. 7 | // 8 | // This program is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU General Public License 14 | // along with this program. If not, see . 15 | 16 | //! Defines [AuthoringInherentData] for the registry module and implement [ProvideInherent]. 17 | 18 | use frame_support::traits::GetCallName as _; 19 | use parity_scale_codec::{Decode, Encode}; 20 | use sp_inherents::{InherentIdentifier, IsFatalError, ProvideInherent}; 21 | use sp_runtime::RuntimeString; 22 | 23 | use radicle_registry_core::AccountId; 24 | 25 | use super::{Call, Module, Trait}; 26 | use crate::Hash; 27 | 28 | const INHERENT_IDENTIFIER: InherentIdentifier = *b"registry"; 29 | 30 | /// Structured inherent data for authoring blocks 31 | #[derive(Encode, Decode)] 32 | pub struct AuthoringInherentData { 33 | pub block_author: AccountId, 34 | } 35 | 36 | #[cfg(feature = "std")] 37 | impl sp_inherents::ProvideInherentData for AuthoringInherentData { 38 | fn inherent_identifier(&self) -> &'static InherentIdentifier { 39 | &INHERENT_IDENTIFIER 40 | } 41 | 42 | fn provide_inherent_data( 43 | &self, 44 | inherent_data: &mut sp_inherents::InherentData, 45 | ) -> Result<(), sp_inherents::Error> { 46 | inherent_data.put_data(INHERENT_IDENTIFIER, &self) 47 | } 48 | 49 | fn error_to_string(&self, mut error: &[u8]) -> Option { 50 | let inherent_error = CheckInherentError::decode(&mut error).ok()?; 51 | Some(format!("{}", inherent_error)) 52 | } 53 | } 54 | 55 | /// Error returned for the [ProvideInherent] implementation of [Module]. 56 | #[derive(Encode)] 57 | #[cfg_attr(feature = "std", derive(Decode))] 58 | pub enum CheckInherentError { 59 | /// The call is forbidden for an inherent. `name` is the name of the call as returned by 60 | /// [frame_support::traits::GetCallName::get_call_name]. 61 | ForbiddenCall { name: RuntimeString }, 62 | } 63 | 64 | impl IsFatalError for CheckInherentError { 65 | fn is_fatal_error(&self) -> bool { 66 | true 67 | } 68 | } 69 | 70 | #[cfg(feature = "std")] 71 | impl std::fmt::Display for CheckInherentError { 72 | fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { 73 | match self { 74 | CheckInherentError::ForbiddenCall { name } => { 75 | write!(f, "Call {} is forbidden for inherents", name) 76 | } 77 | } 78 | } 79 | } 80 | 81 | impl ProvideInherent for Module 82 | where 83 | T: frame_system::Trait< 84 | AccountId = AccountId, 85 | BaseCallFilter = (), 86 | Origin = crate::Origin, 87 | Call = crate::Call, 88 | Hash = Hash, 89 | OnNewAccount = (), 90 | >, 91 | ::Event: From>, 92 | ::OnKilledAccount: 93 | frame_support::traits::OnKilledAccount, 94 | { 95 | type Call = Call; 96 | type Error = CheckInherentError; 97 | const INHERENT_IDENTIFIER: InherentIdentifier = INHERENT_IDENTIFIER; 98 | 99 | fn create_inherent(raw_data: &sp_inherents::InherentData) -> Option { 100 | let data = raw_data 101 | .get_data::(&INHERENT_IDENTIFIER) 102 | .expect("Failed to decode registry AuhoringInherentData") 103 | .expect("AuhoringInherentData for registry is missing"); 104 | 105 | Some(Call::set_block_author(data.block_author)) 106 | } 107 | 108 | fn check_inherent( 109 | call: &Self::Call, 110 | _data: &sp_inherents::InherentData, 111 | ) -> Result<(), Self::Error> { 112 | match call { 113 | Call::set_block_author(_) => Ok(()), 114 | _ => Err(CheckInherentError::ForbiddenCall { 115 | name: RuntimeString::from(call.get_call_name()), 116 | }), 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /runtime/src/runtime.rs: -------------------------------------------------------------------------------- 1 | // Radicle Registry 2 | // Copyright (C) 2019 Monadic GmbH 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License version 3 as 6 | // published by the Free Software Foundation. 7 | // 8 | // This program is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU General Public License 14 | // along with this program. If not, see . 15 | 16 | use alloc::{boxed::Box, vec::Vec}; 17 | use frame_support::{construct_runtime, parameter_types, weights::Weight}; 18 | use frame_system as system; 19 | use radicle_registry_core::{state::AccountTransactionIndex, Balance}; 20 | use sp_runtime::{traits::Block as BlockT, Perbill}; 21 | use sp_timestamp::OnTimestampSet; 22 | use sp_version::RuntimeVersion; 23 | 24 | use crate::{ 25 | registry, timestamp_in_digest, AccountId, Block, BlockNumber, Hash, Hashing, Header, Moment, 26 | UncheckedExtrinsic, VERSION, 27 | }; 28 | 29 | pub mod api; 30 | 31 | parameter_types! { 32 | pub const BlockHashCount: BlockNumber = 250; 33 | pub const MaximumBlockWeight: Weight = 1_000_000_000_000; 34 | pub const AvailableBlockRatio: Perbill = Perbill::from_percent(75); 35 | pub const MaximumBlockLength: u32 = 5 * 1024 * 1024; 36 | pub const Version: RuntimeVersion = VERSION; 37 | } 38 | 39 | impl frame_system::Trait for Runtime { 40 | /// The basic call filter to use in Origin. All origins are built with this filter as base, 41 | /// except Root. 42 | type BaseCallFilter = (); 43 | 44 | /// The identifier used to distinguish between accounts. 45 | type AccountId = AccountId; 46 | 47 | /// The aggregated dispatch type that is available for extrinsics. 48 | type Call = Call; 49 | 50 | /// The lookup mechanism to get account ID from whatever is passed in dispatchers. 51 | type Lookup = sp_runtime::traits::IdentityLookup; 52 | 53 | /// The index type for storing how many extrinsics an account has signed. 54 | type Index = AccountTransactionIndex; 55 | 56 | /// The index type for blocks. 57 | type BlockNumber = BlockNumber; 58 | 59 | /// The type for hashing blocks and tries. 60 | type Hash = Hash; 61 | 62 | /// The hashing algorithm used. 63 | type Hashing = Hashing; 64 | 65 | /// The header type. 66 | type Header = Header; 67 | 68 | /// The ubiquitous event type. 69 | type Event = Event; 70 | 71 | /// The ubiquitous origin type. 72 | type Origin = Origin; 73 | 74 | /// Maximum number of block number to block hash mappings to keep (oldest pruned first). 75 | type BlockHashCount = BlockHashCount; 76 | 77 | /// Maximum weight of each block. With a default weight system of 1byte == 1weight, 4mb is ok. 78 | type MaximumBlockWeight = MaximumBlockWeight; 79 | 80 | /// Maximum size of all encoded transactions (in bytes) that are allowed in one block. 81 | type MaximumBlockLength = MaximumBlockLength; 82 | 83 | /// Portion of the block weight that is available to all normal transactions. 84 | type AvailableBlockRatio = AvailableBlockRatio; 85 | 86 | /// The weight of database operations that the runtime can invoke. 87 | type DbWeight = (); 88 | 89 | /// The base weight of executing a block, independent of the transactions in the block. 90 | type BlockExecutionWeight = (); 91 | 92 | /// The base weight of an Extrinsic in the block, independent of the of extrinsic being executed. 93 | type ExtrinsicBaseWeight = (); 94 | 95 | /// The maximal weight of a single Extrinsic. This should be set to at most 96 | /// `MaximumBlockWeight - AverageOnInitializeWeight`. The limit only applies to extrinsics 97 | /// containing `Normal` dispatch class calls. 98 | type MaximumExtrinsicWeight = (); 99 | 100 | /// Version of the runtime. 101 | type Version = Version; 102 | /// Converts a module to the index of the module in `construct_runtime!`. 103 | /// 104 | /// This type is being generated by `construct_runtime!`. 105 | type ModuleToIndex = ModuleToIndex; 106 | /// What to do if a new account is created. 107 | type OnNewAccount = (); 108 | /// What to do if an account is fully reaped from the system. 109 | type OnKilledAccount = Balances; 110 | /// The data to be stored in an account. 111 | type AccountData = pallet_balances::AccountData; 112 | } 113 | 114 | parameter_types! { 115 | /// Minimum time between blocks in milliseconds 116 | pub const MinimumPeriod: Moment = 300; 117 | } 118 | 119 | impl pallet_timestamp::Trait for Runtime { 120 | /// A timestamp: milliseconds since the unix epoch. 121 | type Moment = Moment; 122 | type OnTimestampSet = StoreTimestampInDigest; 123 | type MinimumPeriod = MinimumPeriod; 124 | } 125 | 126 | pub struct StoreTimestampInDigest; 127 | 128 | impl OnTimestampSet for StoreTimestampInDigest { 129 | fn on_timestamp_set(timestamp: Moment) { 130 | let item = timestamp_in_digest::digest_item(timestamp); 131 | frame_system::Module::::deposit_log(item); 132 | } 133 | } 134 | 135 | parameter_types! { 136 | /// The minimum amount required to keep an account open. 137 | /// Transfers leaving the recipient with less than this 138 | /// value fail. 139 | pub const ExistentialDeposit: u128 = 1; 140 | pub const TransferFee: u128 = 0; 141 | pub const CreationFee: u128 = 0; 142 | pub const TransactionBaseFee: u128 = 0; 143 | pub const TransactionByteFee: u128 = 0; 144 | } 145 | 146 | impl pallet_balances::Trait for Runtime { 147 | type Balance = Balance; 148 | type Event = Event; 149 | type DustRemoval = (); 150 | type ExistentialDeposit = ExistentialDeposit; 151 | type AccountStore = System; 152 | } 153 | 154 | impl pallet_sudo::Trait for Runtime { 155 | type Event = Event; 156 | type Call = Call; 157 | } 158 | 159 | impl registry::Trait for Runtime {} 160 | 161 | construct_runtime!( 162 | pub enum Runtime where 163 | Block = Block, 164 | NodeBlock = Block, 165 | UncheckedExtrinsic = UncheckedExtrinsic 166 | { 167 | System: system::{Module, Call, Storage, Config, Event}, 168 | Timestamp: pallet_timestamp::{Module, Call, Storage, Inherent}, 169 | RandomnessCollectiveFlip: pallet_randomness_collective_flip::{Module, Call, Storage}, 170 | Balances: pallet_balances::{Module, Call, Storage, Config, Event}, 171 | Sudo: pallet_sudo::{Module, Call, Config, Storage, Event}, 172 | Registry: registry::{Module, Call, Storage, Inherent}, 173 | } 174 | ); 175 | -------------------------------------------------------------------------------- /runtime/src/runtime/api.rs: -------------------------------------------------------------------------------- 1 | // Radicle Registry 2 | // Copyright (C) 2019 Monadic GmbH 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License version 3 as 6 | // published by the Free Software Foundation. 7 | // 8 | // This program is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU General Public License 14 | // along with this program. If not, see . 15 | 16 | //! Implements Substrate runtime APIs and provide a function based interface for the runtime APIs. 17 | use alloc::vec::Vec; 18 | use frame_support::{ensure, fail, traits::Randomness}; 19 | use sp_core::OpaqueMetadata; 20 | use sp_runtime::traits::Block as BlockT; 21 | use sp_runtime::{ 22 | transaction_validity::{InvalidTransaction, TransactionSource, TransactionValidity}, 23 | ApplyExtrinsicResult, 24 | }; 25 | use sp_version::RuntimeVersion; 26 | 27 | use super::{ 28 | registry, AllModules, Block, Call, Header, InherentDataExt, RandomnessCollectiveFlip, Runtime, 29 | UncheckedExtrinsic, VERSION, 30 | }; 31 | 32 | type Executive = frame_executive::Executive< 33 | Runtime, 34 | Block, 35 | frame_system::ChainContext, 36 | Runtime, 37 | AllModules, 38 | >; 39 | 40 | pub const VERSIONS: sp_version::ApisVec = RUNTIME_API_VERSIONS; 41 | 42 | /// See [sp_api::Core::initialize_block] 43 | pub fn initialize_block(header: &Header) { 44 | Executive::initialize_block(header) 45 | } 46 | 47 | /// See [sp_block_builder::BlockBuilder::inherent_extrinsics]. 48 | pub fn inherent_extrinsics(data: sp_inherents::InherentData) -> Vec { 49 | data.create_extrinsics() 50 | } 51 | 52 | /// See [sp_block_builder::BlockBuilder::apply_extrinsic]. 53 | pub fn apply_extrinsic(extrinsic: UncheckedExtrinsic) -> ApplyExtrinsicResult { 54 | validate_extrinsic_call(&extrinsic)?; 55 | Executive::apply_extrinsic(extrinsic) 56 | } 57 | 58 | /// See [sp_block_builder::BlockBuilder::finalize_block]. 59 | pub fn finalize_block() -> Header { 60 | Executive::finalize_block() 61 | } 62 | 63 | const SIGNED_INHERENT_CALL_ERROR: InvalidTransaction = InvalidTransaction::Custom(1); 64 | const FOBIDDEN_CALL_ERROR: InvalidTransaction = InvalidTransaction::Custom(2); 65 | const UNSGINED_CALL_ERROR: InvalidTransaction = InvalidTransaction::Custom(3); 66 | 67 | /// Validate that the call of the extrinsic is allowed. 68 | /// 69 | /// * We forbid calls reserved for inherents when the extrinsic is not signed. 70 | /// * We forbid any calls to the [super::Balances] or [super::System] module. 71 | /// * We ensure that the extrinsic is signed for non-inherent calls. 72 | /// 73 | fn validate_extrinsic_call(xt: &UncheckedExtrinsic) -> Result<(), InvalidTransaction> { 74 | match xt.function { 75 | // Inherents are only allowed if they are unsigned. 76 | Call::Timestamp(_) | Call::Registry(registry::Call::set_block_author(_)) => { 77 | ensure!(xt.signature.is_none(), SIGNED_INHERENT_CALL_ERROR) 78 | } 79 | 80 | // Forbidden internals. 81 | Call::Balances(_) | Call::System(_) => fail!(FOBIDDEN_CALL_ERROR), 82 | 83 | // Impossible cases that cannot be constructed. 84 | Call::RandomnessCollectiveFlip(_) => fail!(FOBIDDEN_CALL_ERROR), 85 | 86 | // Allowed calls for signed extrinsics. 87 | Call::Registry(_) | Call::Sudo(_) => ensure!(xt.signature.is_some(), UNSGINED_CALL_ERROR), 88 | } 89 | 90 | Ok(()) 91 | } 92 | 93 | sp_api::impl_runtime_apis! { 94 | impl sp_api::Core for Runtime { 95 | fn version() -> RuntimeVersion { 96 | VERSION 97 | } 98 | 99 | fn execute_block(block: Block) { 100 | Executive::execute_block(block) 101 | } 102 | 103 | fn initialize_block(header: &::Header) { 104 | initialize_block(header); 105 | } 106 | } 107 | 108 | impl sp_api::Metadata for Runtime { 109 | fn metadata() -> OpaqueMetadata { 110 | Runtime::metadata().into() 111 | } 112 | } 113 | 114 | impl sp_block_builder::BlockBuilder for Runtime { 115 | fn apply_extrinsic(extrinsic: ::Extrinsic) -> ApplyExtrinsicResult { 116 | apply_extrinsic(extrinsic) 117 | } 118 | 119 | fn finalize_block() -> ::Header { 120 | finalize_block() 121 | } 122 | 123 | fn inherent_extrinsics(data: sp_inherents::InherentData) -> Vec<::Extrinsic> { 124 | inherent_extrinsics(data) 125 | } 126 | 127 | fn check_inherents( 128 | block: Block, 129 | data: sp_inherents::InherentData, 130 | ) -> sp_inherents::CheckInherentsResult { 131 | data.check_extrinsics(&block) 132 | } 133 | 134 | fn random_seed() -> ::Hash { 135 | RandomnessCollectiveFlip::random_seed() 136 | } 137 | } 138 | 139 | impl sp_transaction_pool::runtime_api::TaggedTransactionQueue for Runtime { 140 | fn validate_transaction(source: TransactionSource, tx: ::Extrinsic) -> TransactionValidity { 141 | validate_extrinsic_call(&tx)?; 142 | Executive::validate_transaction(source, tx) 143 | } 144 | } 145 | 146 | impl sp_offchain::OffchainWorkerApi for Runtime { 147 | fn offchain_worker(header: &::Header) { 148 | Executive::offchain_worker(header) 149 | } 150 | } 151 | 152 | // An implementation for the `SessionKeys` runtime API is required by the types 153 | // of [sc_service::ServiceBuilder]. However, the implementation is otherwise unused 154 | // and has no effect on the behavior of the runtime. Hence we implement a dummy 155 | // version. 156 | impl sp_session::SessionKeys for Runtime { 157 | fn generate_session_keys(_seed: Option>) -> Vec { 158 | Default::default() 159 | } 160 | 161 | fn decode_session_keys( 162 | _encoded: Vec, 163 | ) -> Option, sp_core::crypto::KeyTypeId)>> { 164 | None 165 | } 166 | } 167 | 168 | impl sp_consensus_pow::TimestampApi for Runtime { 169 | fn timestamp() -> u64 { 170 | pallet_timestamp::Module::::get() 171 | } 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /runtime/src/timestamp_in_digest.rs: -------------------------------------------------------------------------------- 1 | // Radicle Registry 2 | // Copyright (C) 2019 Monadic GmbH 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License version 3 as 6 | // published by the Free Software Foundation. 7 | // 8 | // This program is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU General Public License 14 | // along with this program. If not, see . 15 | 16 | use crate::{Hash, Moment}; 17 | use parity_scale_codec::Encode; 18 | #[cfg(feature = "std")] 19 | use parity_scale_codec::{DecodeAll, Error}; 20 | #[cfg(feature = "std")] 21 | use sp_runtime::Digest; 22 | use sp_runtime::{ConsensusEngineId, DigestItem}; 23 | 24 | const CONSENSUS_ID: ConsensusEngineId = *b"time"; 25 | 26 | #[cfg(feature = "std")] 27 | pub fn load(digest: &Digest) -> Option> { 28 | digest 29 | .log(|item| match item { 30 | DigestItem::Consensus(CONSENSUS_ID, encoded) => Some(encoded), 31 | _ => None, 32 | }) 33 | .map(|encoded| DecodeAll::decode_all(encoded)) 34 | } 35 | 36 | pub fn digest_item(timestamp: Moment) -> DigestItem { 37 | DigestItem::Consensus(CONSENSUS_ID, timestamp.encode()) 38 | } 39 | -------------------------------------------------------------------------------- /rust-toolchain: -------------------------------------------------------------------------------- 1 | nightly-2020-06-10 2 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | edition = "2018" 2 | -------------------------------------------------------------------------------- /scripts/build-client-docs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Build documentation for client library 4 | 5 | set -euo pipefail 6 | 7 | cargo doc \ 8 | --no-deps \ 9 | -p radicle-registry-client \ 10 | -p radicle-registry-core \ 11 | -p sp-core \ 12 | -p sp-runtime \ 13 | -p frame-system \ 14 | -p frame-support \ 15 | "$@" 16 | -------------------------------------------------------------------------------- /scripts/build-release: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Build the CLI and node binaries for release. The resulting binaries can be found at: 4 | # * `./target/release/radicle-registry-cli` 5 | # * `./target/release/radicle-registry-node` 6 | 7 | DEFAULT_CHAIN=ffnet cargo build --workspace --all-targets --release 8 | -------------------------------------------------------------------------------- /scripts/build-runtime-wasm: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | if [ $# -ne 1 ] || [ "$1" = --help ] ; then 6 | echo "Usage: $0 " 7 | exit 1 8 | fi 9 | 10 | export BUILD_WASM_BINARY_OUT_DIR="$(mktemp -d)" 11 | cargo build -p radicle-registry-runtime --release 12 | mv "$BUILD_WASM_BINARY_OUT_DIR/radicle_registry_runtime.wasm" "$1" 13 | rm -r "$BUILD_WASM_BINARY_OUT_DIR" 14 | -------------------------------------------------------------------------------- /scripts/check-license-headers: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Iterate over all rust source files and make sure the 4 | # files include the GPL3 license header. 5 | # 6 | # If run with the `--fix` option license headers are prepended if they are 7 | # missing. 8 | 9 | set -euo pipefail 10 | IFS=$'\n' 11 | 12 | license_header=$(cat ./LICENSE_HEADER) 13 | license_header_size=$(echo "$license_header" | wc --bytes) 14 | 15 | shopt -s extglob 16 | 17 | for file in !(target)/**/*.rs; do 18 | file_start=$(head --bytes $license_header_size "$file") 19 | 20 | if [[ "$file_start" != "$license_header" ]]; then 21 | if [[ "${1:-}" = "--fix" ]]; then 22 | sed -i -e '1rLICENSE_HEADER' -e '1{h;d}' -e '2{x;G}' "$file" 23 | echo "Adding license header to $file" 24 | else 25 | echo "License header missing for $file. Rerun with --fix to add license header." 26 | exit 1 27 | fi 28 | fi 29 | done 30 | -------------------------------------------------------------------------------- /scripts/create-release: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Create a Github draft release from the current master branch 4 | 5 | set -euo pipefail 6 | 7 | if ! ( command -v hub 1>/dev/null ); then 8 | echo "hub was not found. Please install it from https://github.com/github/hub" 9 | exit 1 10 | fi 11 | 12 | echo "Running git --fetch tags" 13 | git fetch --tags 14 | commit=$(git rev-parse origin/master) 15 | 16 | echo "Download build artifacts" 17 | base_url="https://builds.radicle.xyz/radicle-registry/master/$commit/artifacts" 18 | artifacts_dir=$(mktemp -d) 19 | ( 20 | cd "$artifacts_dir" 21 | curl -sfSLO "$base_url/radicle-registry-cli.tar.gz" 22 | curl -sfSLO "$base_url/radicle-registry-node.tar.gz" 23 | curl -sfSLO "$base_url/runtime.wasm" 24 | ) 25 | 26 | release_name="$(date +%Y.%m.%d)" 27 | declare -i release_counter=0 28 | 29 | while true; do 30 | if [[ $(git tag -l "$release_name" | wc -l) -eq 0 ]]; then 31 | break 32 | fi 33 | echo "Tag $release_name already exists" 34 | release_counter+=1 35 | release_name="$(date +%Y.%m.%d)-$release_counter" 36 | done 37 | 38 | echo "Creating and pushing tag" 39 | git tag --sign --message "$release_name" "$release_name" "$commit" 40 | git push origin "refs/tags/$release_name" 41 | 42 | echo "Creating Github draft release \"$release_name\"" 43 | hub release create \ 44 | --draft \ 45 | --prerelease \ 46 | --attach "$artifacts_dir/radicle-registry-cli.tar.gz" \ 47 | --attach "$artifacts_dir/radicle-registry-node.tar.gz" \ 48 | "$release_name" 49 | rm -rf "$artifacts_dir" 50 | echo "Created draft release. Publish it on Github." 51 | -------------------------------------------------------------------------------- /scripts/rebuild-runtime-cache: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | if [ $# -ne 0 ] || [ "${1-}" = --help ] ; then 6 | echo "Rebuild runtime-cache directory." 7 | return 0 8 | fi 9 | 10 | runtime_cache="runtime-cache/" 11 | mkdir -p "$runtime_cache" 12 | latest_runtime_local_name="latest.wasm" 13 | 14 | # parse runtime version 15 | runtime_version="$(awk '$1 == "version" {print $3; exit}' runtime/Cargo.toml | tr -d \")" 16 | spec_version="$(echo "$runtime_version" | awk -F . '{print $2}')" 17 | impl_version="$(echo "$runtime_version" | awk -F . '{print $3}')" 18 | prev_spec_version=$(($spec_version - 1)) 19 | latest_runtime_name="v${spec_version}_${impl_version}.wasm" 20 | latest_spec_latest_impl_name="dev_v${spec_version}_latest.json" 21 | latest_spec_first_impl_name="dev_v${spec_version}_0.json" 22 | prev_spec_last_impl_name="dev_v${prev_spec_version}_latest.json" 23 | 24 | # params: [] 25 | download_cache_file() 26 | { 27 | curl -o "$runtime_cache${2-$1}" -fSs "https://storage.googleapis.com/radicle-registry-runtime/$1" \ 28 | || ( echo "could not download $1 from the storage" && false ) 29 | } 30 | 31 | # fetch or build the latest runtime 32 | new_runtime=0 33 | download_cache_file "$latest_runtime_name" "$latest_runtime_local_name" || new_runtime=1 34 | if [ "$new_runtime" == "1" ] 35 | then 36 | echo "building runtime for spec $spec_version impl $impl_version" 37 | ./scripts/build-runtime-wasm "$runtime_cache$latest_runtime_local_name" 38 | fi 39 | 40 | echo "building spec file for spec $spec_version impl $impl_version" 41 | # uses --release to reuse as many artifacts as possible on the CI 42 | RUST_LOG=off cargo run -p radicle-registry-node --release -- build-spec --chain dev > "$runtime_cache$latest_spec_latest_impl_name" 43 | 44 | if [ "$impl_version" != "0" ] 45 | then 46 | echo "fetching spec file for spec $spec_version impl 0" 47 | download_cache_file "$latest_spec_first_impl_name" 48 | fi 49 | 50 | if [ "$spec_version" == "19" ] 51 | then 52 | echo "spec version $spec_version is designed for no backward compatiblity, \ 53 | skipping fetching spec file for spec $prev_spec_version" 54 | exit 0 55 | fi 56 | 57 | echo "fetching spec file for spec $prev_spec_version last impl" 58 | download_cache_file "$prev_spec_last_impl_name" 59 | -------------------------------------------------------------------------------- /scripts/run-tests-all-runtimes: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | if [ $# -ne 0 ] 6 | then 7 | echo "Run tests for all runtimes in runtime-cache." 8 | echo "Run only after running scripts/build-release and scripts/rebuild-runtime-cache!" 9 | return 0 10 | fi 11 | 12 | # params: 13 | run_tests_with_spec() 14 | { 15 | echo "--- cargo test for spec $1" 16 | echo "Starting radicle-registry-node" 17 | 18 | RUST_LOG=error ./target/release/radicle-registry-node --dev --spec "$1" & 19 | registry_node_pid=$! 20 | 21 | # Give the node time to start up 22 | sleep 4 23 | 24 | echo "Testing radicle-registry" 25 | # We build tests in release mode so that we can reuse the artifacts 26 | # from 'cargo build' 27 | RUST_BACKTRACE=1 RUST_LOG=info \ 28 | cargo test --workspace --release --color=always 29 | kill "$registry_node_pid" 30 | } 31 | 32 | echo "Make sure you've run 'scripts/build-release' and 'scripts/rebuild-runtime-cache'!" 33 | for spec_file in runtime-cache/*.json; do 34 | run_tests_with_spec "$spec_file" 35 | done 36 | -------------------------------------------------------------------------------- /scripts/rustup-setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | rustup component add rustfmt 6 | rustup component add clippy 7 | rustup target add wasm32-unknown-unknown 8 | -------------------------------------------------------------------------------- /scripts/update-substrate: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | new_rev="${1:-}" 6 | 7 | if [[ -z "$new_rev" ]]; then 8 | echo "Revision argument is missing" 9 | echo "Usage: $0 " 10 | exit 1 11 | fi 12 | 13 | # Reads `Cargo.lock` and extracts the first revision of the `substrate` 14 | # repo that is used. 15 | function get_substrate_rev () { 16 | while read -r LINE; do 17 | if [[ "$LINE" =~ ^\source\ .*git\+https://github\.com/paritytech/substrate.*\?rev=([^#]+) ]]; then 18 | local rev=${BASH_REMATCH[1]} 19 | if [[ "$rev" != "$new_rev" ]]; then 20 | echo "${BASH_REMATCH[1]}" 21 | return 22 | fi 23 | fi 24 | done < Cargo.lock 25 | echo "Cannot find reference to repository \"https://github.com/paritytech/substrate\" in Cargo.lock" 1>&2 26 | return 1 27 | } 28 | 29 | substrate_rev=$(get_substrate_rev) 30 | 31 | sed -i "s/${substrate_rev}/$1/g" -- */Cargo.toml 32 | cargo update 33 | -------------------------------------------------------------------------------- /test-utils/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | edition = "2018" 3 | name = "radicle-registry-test-utils" 4 | description = "Testing utilities used in Radicle Registry development" 5 | version = "0.0.0" 6 | authors = ["Monadic GmbH "] 7 | homepage = "https://github.com/radicle-dev/radicle-registry" 8 | documentation = "https://github.com/radicle-dev/radicle-registry" 9 | license = "GPL-3.0-only" 10 | repository = "https://github.com/radicle-dev/radicle-registry" 11 | 12 | [dependencies] 13 | rand = "0.7.2" 14 | radicle-registry-client = { path = "../client" } 15 | radicle-registry-core = { path = "../core" } 16 | 17 | [dependencies.sp-core] 18 | git = "https://github.com/paritytech/substrate" 19 | rev = "v2.0.0-rc4" 20 | --------------------------------------------------------------------------------