├── .github └── workflows │ └── ci.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── bin ├── cli │ ├── .gitignore │ ├── Cargo.toml │ ├── gen-chain-spec.sh │ └── src │ │ ├── command.rs │ │ └── main.rs ├── client │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── node │ ├── Cargo.toml │ ├── build.rs │ └── src │ │ ├── lib.rs │ │ └── main.rs └── runtime │ ├── Cargo.toml │ ├── build.rs │ └── src │ └── lib.rs ├── chain ├── client │ ├── Cargo.toml │ └── src │ │ ├── error.rs │ │ ├── lib.rs │ │ └── subxt.rs └── pallet │ ├── Cargo.toml │ └── src │ ├── lib.rs │ ├── mock.rs │ └── tests.rs ├── faucet ├── cli │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── client │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── ffi │ ├── Cargo.toml │ ├── src │ │ ├── ffi.rs │ │ └── lib.rs │ └── tests │ │ └── impl_ffi_macro.rs └── pallet │ ├── Cargo.toml │ └── src │ ├── lib.rs │ ├── mock.rs │ └── tests.rs └── identity ├── cli ├── Cargo.toml └── src │ ├── account.rs │ ├── device.rs │ ├── id.rs │ ├── key.rs │ ├── lib.rs │ └── wallet.rs ├── client ├── Cargo.toml ├── github-proof-instructions.md ├── github-proof-template.md └── src │ ├── claim.rs │ ├── client.rs │ ├── error.rs │ ├── github.rs │ ├── lib.rs │ ├── service.rs │ ├── subxt.rs │ └── utils.rs ├── ffi ├── Cargo.toml ├── src │ ├── ffi.rs │ └── lib.rs └── tests │ └── impl_ffi_macro.rs ├── pallet ├── Cargo.toml └── src │ ├── lib.rs │ ├── mock.rs │ └── tests.rs └── utils ├── Cargo.toml └── src └── lib.rs /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: 2 | pull_request: 3 | branches: 4 | - master 5 | push: 6 | branches: 7 | - master 8 | 9 | name: sunshine-identity 10 | 11 | jobs: 12 | ci: 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | toolchain: 17 | #- rust: stable 18 | - rust: nightly 19 | platform: 20 | - target: x86_64-unknown-linux-gnu 21 | host: ubuntu-latest 22 | cross: false 23 | 24 | # - target: x86_64-apple-darwin 25 | # host: macos-latest 26 | # cross: false 27 | # - target: x86_64-pc-windows-msvc 28 | # host: windows-latest 29 | # cross: false 30 | # - target: armv7-linux-androideabi 31 | # host: ubuntu-latest 32 | # cross: true 33 | # - target: aarch64-linux-android 34 | # host: ubuntu-latest 35 | # cross: true 36 | # - target: aarch64-apple-ios 37 | # host: macos-latest 38 | # cross: true 39 | # - target: wasm32-unknown-unknown 40 | # host: ubuntu-latest 41 | # cross: true 42 | env: 43 | RUST_BACKTRACE: 1 44 | CARGO_INCREMENTAL: 0 45 | LLVM_CONFIG_PATH: /usr/local/opt/llvm/bin/llvm-config 46 | NDK_HOME: /usr/local/lib/android/sdk/ndk-bundle 47 | 48 | runs-on: ${{ matrix.platform.host }} 49 | steps: 50 | - name: install packages on ubuntu 51 | if: matrix.platform.host == 'ubuntu-latest' 52 | run: sudo apt-get install libclang-dev 53 | - name: install packages on macos 54 | if: matrix.platform.host == 'macos-latest' 55 | run: brew install llvm 56 | - name: install packages on windows 57 | if: matrix.platform.host == 'windows-latest' 58 | run: choco install llvm 59 | 60 | - name: Checkout sources 61 | uses: actions/checkout@v2 62 | 63 | - name: Cache cargo folder 64 | uses: actions/cache@v1 65 | with: 66 | path: ~/.cargo 67 | key: ${{ matrix.platform.target }}-cargo-${{ matrix.toolchain.rust }} 68 | 69 | - name: Install rust toolchain 70 | uses: hecrj/setup-rust-action@v1 71 | with: 72 | rust-version: ${{ matrix.toolchain.rust }} 73 | targets: ${{ matrix.platform.target }}, wasm32-unknown-unknown 74 | 75 | - name: Install cargo-ndk 76 | if: contains(matrix.platform.target, 'android') 77 | run: cargo install cargo-ndk 78 | 79 | - name: Build 80 | if: contains(matrix.platform.target, 'android') == false 81 | run: cargo build --all --target ${{ matrix.platform.target }} 82 | 83 | - name: Build android 84 | if: contains(matrix.platform.target, 'android') 85 | run: cargo ndk --android-platform 29 --target ${{ matrix.platform.target }} build --all 86 | 87 | - name: Rust tests 88 | if: matrix.platform.cross == false 89 | run: cargo test --all 90 | 91 | lint-rust: 92 | runs-on: ubuntu-latest 93 | steps: 94 | - name: Checkout sources 95 | uses: actions/checkout@v2 96 | 97 | - name: Cache cargo folder 98 | uses: actions/cache@v1 99 | with: 100 | path: ~/.cargo 101 | key: lint-cargo 102 | 103 | - name: Install rust toolchain 104 | uses: hecrj/setup-rust-action@v1 105 | with: 106 | rust-version: nightly 107 | components: clippy, rustfmt 108 | targets: wasm32-unknown-unknown 109 | 110 | - name: cargo fmt 111 | run: cargo fmt --all -- --check 112 | 113 | - name: cargo clippy 114 | run: cargo clippy --workspace --examples --tests -- -D warnings 115 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .vscode 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "bin/cli", 4 | "bin/client", 5 | "bin/node", 6 | "bin/runtime", 7 | "chain/client", 8 | "chain/pallet", 9 | "faucet/cli", 10 | "faucet/client", 11 | "faucet/ffi", 12 | "faucet/pallet", 13 | "identity/cli", 14 | "identity/client", 15 | "identity/ffi", 16 | "identity/pallet", 17 | "identity/utils", 18 | ] 19 | 20 | [patch.crates-io] 21 | frame-executive = { git = "https://github.com/dvc94ch/substrate", branch = "dvc-bitswap" } 22 | frame-metadata = { git = "https://github.com/dvc94ch/substrate", branch = "dvc-bitswap" } 23 | frame-support = { git = "https://github.com/dvc94ch/substrate", branch = "dvc-bitswap" } 24 | frame-system = { git = "https://github.com/dvc94ch/substrate", branch = "dvc-bitswap" } 25 | 26 | pallet-aura = { git = "https://github.com/dvc94ch/substrate", branch = "dvc-bitswap" } 27 | pallet-balances = { git = "https://github.com/dvc94ch/substrate", branch = "dvc-bitswap" } 28 | pallet-grandpa = { git = "https://github.com/dvc94ch/substrate", branch = "dvc-bitswap" } 29 | pallet-im-online = { git = "https://github.com/dvc94ch/substrate", branch = "dvc-bitswap" } 30 | pallet-indices = { git = "https://github.com/dvc94ch/substrate", branch = "dvc-bitswap" } 31 | pallet-randomness-collective-flip = { git = "https://github.com/dvc94ch/substrate", branch = "dvc-bitswap" } 32 | pallet-staking = { git = "https://github.com/dvc94ch/substrate", branch = "dvc-bitswap" } 33 | pallet-timestamp = { git = "https://github.com/dvc94ch/substrate", branch = "dvc-bitswap" } 34 | pallet-transaction-payment = { git = "https://github.com/dvc94ch/substrate", branch = "dvc-bitswap" } 35 | 36 | sc-basic-authorship = { git = "https://github.com/dvc94ch/substrate", branch = "dvc-bitswap" } 37 | sc-cli = { git = "https://github.com/dvc94ch/substrate", branch = "dvc-bitswap" } 38 | sc-client-api = { git = "https://github.com/dvc94ch/substrate", branch = "dvc-bitswap" } 39 | sc-client-db = { git = "https://github.com/dvc94ch/substrate", branch = "dvc-bitswap" } 40 | sc-consensus = { git = "https://github.com/dvc94ch/substrate", branch = "dvc-bitswap" } 41 | sc-consensus-aura = { git = "https://github.com/dvc94ch/substrate", branch = "dvc-bitswap" } 42 | sc-executor = { git = "https://github.com/dvc94ch/substrate", branch = "dvc-bitswap" } 43 | sc-finality-grandpa = { git = "https://github.com/dvc94ch/substrate", branch = "dvc-bitswap" } 44 | sc-informant = { git = "https://github.com/dvc94ch/substrate", branch = "dvc-bitswap" } 45 | sc-network = { git = "https://github.com/dvc94ch/substrate", branch = "dvc-bitswap" } 46 | sc-rpc-api = { git = "https://github.com/dvc94ch/substrate", branch = "dvc-bitswap" } 47 | sc-service = { git = "https://github.com/dvc94ch/substrate", branch = "dvc-bitswap" } 48 | sc-transaction-pool = { git = "https://github.com/dvc94ch/substrate", branch = "dvc-bitswap" } 49 | 50 | sp-api = { git = "https://github.com/dvc94ch/substrate", branch = "dvc-bitswap" } 51 | sp-application-crypto = { git = "https://github.com/dvc94ch/substrate", branch = "dvc-bitswap" } 52 | sp-authority-discovery = { git = "https://github.com/dvc94ch/substrate", branch = "dvc-bitswap" } 53 | sp-block-builder = { git = "https://github.com/dvc94ch/substrate", branch = "dvc-bitswap" } 54 | sp-consensus = { git = "https://github.com/dvc94ch/substrate", branch = "dvc-bitswap" } 55 | sp-consensus-aura = { git = "https://github.com/dvc94ch/substrate", branch = "dvc-bitswap" } 56 | sp-consensus-babe = { git = "https://github.com/dvc94ch/substrate", branch = "dvc-bitswap" } 57 | sp-core = { git = "https://github.com/dvc94ch/substrate", branch = "dvc-bitswap" } 58 | sp-database = { git = "https://github.com/dvc94ch/substrate", branch = "dvc-bitswap" } 59 | sp-finality-grandpa = { git = "https://github.com/dvc94ch/substrate", branch = "dvc-bitswap" } 60 | sp-inherents = { git = "https://github.com/dvc94ch/substrate", branch = "dvc-bitswap" } 61 | sp-io = { git = "https://github.com/dvc94ch/substrate", branch = "dvc-bitswap" } 62 | sp-keyring = { git = "https://github.com/dvc94ch/substrate", branch = "dvc-bitswap" } 63 | sp-offchain = { git = "https://github.com/dvc94ch/substrate", branch = "dvc-bitswap" } 64 | sp-rpc = { git = "https://github.com/dvc94ch/substrate", branch = "dvc-bitswap" } 65 | sp-runtime = { git = "https://github.com/dvc94ch/substrate", branch = "dvc-bitswap" } 66 | sp-session = { git = "https://github.com/dvc94ch/substrate", branch = "dvc-bitswap" } 67 | sp-std = { git = "https://github.com/dvc94ch/substrate", branch = "dvc-bitswap" } 68 | sp-transaction-pool = { git = "https://github.com/dvc94ch/substrate", branch = "dvc-bitswap" } 69 | sp-trie = { git = "https://github.com/dvc94ch/substrate", branch = "dvc-bitswap" } 70 | sp-version = { git = "https://github.com/dvc94ch/substrate", branch = "dvc-bitswap" } 71 | 72 | substrate-subxt = { git = "https://github.com/paritytech/substrate-subxt" } 73 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sunshine Identity Module 2 | 3 | 4 | implementation of [Keybase Local Key Security](https://book.keybase.io/docs/crypto/local-key-security) on substrate, using [ipfs-rust/ipfs-embed](https://github.com/ipfs-rust/ipfs-embed) 5 | 6 | ## Build 7 | 8 | Build Wasm and native code: 9 | 10 | ```bash 11 | cargo build --release 12 | ``` 13 | 14 | ## Start the node 15 | 16 | Purge any existing developer chain state: 17 | 18 | ```bash 19 | ./target/release/node-identity purge-chain --dev 20 | ``` 21 | 22 | Start a development chain with: 23 | 24 | ```bash 25 | ./target/release/node-identity --dev 26 | ``` 27 | 28 | ## Setup your account 29 | 30 | Set your device key to `//Alice`: 31 | 32 | ```bash 33 | cli-identity key set --suri //Alice 34 | Please enter a new password (8+ characters): 35 | 36 | Your device id is 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY 37 | Your user id is 0 38 | ``` 39 | 40 | Add a paper backup for your account: 41 | 42 | ```bash 43 | cli-identity device paperkey 44 | Generating a new paper key. 45 | Here is your secret paper key phrase: 46 | 47 | mandate robust earth scan shrimp second pipe pitch eternal snap glare tooth 48 | bean crucial river bar crash nice sorry laundry oppose filter aunt swear 49 | 50 | Write it down and keep somewhere safe. 51 | ``` 52 | 53 | and list your device keys: 54 | 55 | ```bash 56 | cli-identity device list 57 | 5Fe8Da8o2TQY6heaopRA9Zs2dpiJ2GFtvWThnd89uxYEXK1q 58 | 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY 59 | ``` 60 | 61 | ## Prove your online identity 62 | 63 | Add a new online identity: 64 | 65 | ```bash 66 | cli-identity id prove dvc94ch@github 67 | Claiming dvc94ch@github... 68 | Please *publicly* post the following Gist, and name it 'substrate-identity-proof.md'. 69 | 70 | ### substrate identity proof 71 | 72 | I hereby claim: 73 | 74 | * I am dvc94ch on github. 75 | * I am 0 on the substrate chain with genesis hash mzyTJZVm7IXDUBeZwyWk6rG1YGIt8BQnNzrshKJCalYI. 76 | * I have a public key 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY valid at block with hash mfBFseDYNX31Poqei8A/9teYmxJIj4PFROoKLKEPaStE. 77 | 78 | To claim this, I am signing this object: 79 | 80 | {"block":[124,17,108,120,54,13,95,125,79,162,167,162,240,15,253,181,230,38,196,146,35,224,241,81,58,130,139,40,67,218,74,209],"body":{"Ownership":[{"Github":["dvc94ch"]}]},"ctime":1591448931056,"expire_in":18446744073709551615,"genesis":[207,36,201,101,89,187,33,112,212,5,230,112,201,105,58,172,109,88,24,139,124,5,9,205,206,187,33,40,144,154,149,130],"prev":null,"public":"5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY","seqno":1,"uid":0} 81 | 82 | with the key 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY, yielding the signature: 83 | 84 | mAU6Gon1dqctnS/zPKHd9gWFvEJBqADvgYQy0OFuamA5CwVQk7papR0xBq8DijRqSXVGpJtNFmy7aYJk5cGLxv4c 85 | 86 | And finally, I am proving ownership of the github account by posting this as a gist. 87 | ``` 88 | 89 | and list your identities: 90 | 91 | ```bash 92 | cli-identity id list 93 | Your user id is 0 94 | dvc94ch@github https://gist.github.com/da8bbf9c69976a3d750e2c433126852b 95 | ``` 96 | 97 | ## Receive payments to your public identity 98 | 99 | Transfer a balance from `//Bob` to `dvc94ch@github`: 100 | ```bash 101 | cli-identity --path /tmp/bob key set --suri //Bob 102 | Please enter a new password (8+ characters): 103 | 104 | Your device id is 5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty 105 | Your user id is 1 106 | ``` 107 | 108 | make sure ipfs is running so that `//Bob` can fetch `//Alice`'s identity: 109 | 110 | ```bash 111 | cli-identity run 112 | ``` 113 | 114 | finally make the transfer: 115 | 116 | ```bash 117 | cli-identity --path /tmp/bob wallet transfer dvc94ch@github 10000 118 | transfered 10000 to 5Fe8Da8o2TQY6heaopRA9Zs2dpiJ2GFtvWThnd89uxYEXK1q 119 | ``` 120 | -------------------------------------------------------------------------------- /bin/cli/.gitignore: -------------------------------------------------------------------------------- 1 | chain-spec.json 2 | -------------------------------------------------------------------------------- /bin/cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "test-cli" 3 | version = "0.2.0" 4 | authors = ["David Craven "] 5 | edition = "2018" 6 | publish = false 7 | 8 | [dependencies] 9 | async-std = { version = "1.6.4", features = ["attributes"] } 10 | async-trait = "0.1.40" 11 | clap = "3.0.0-beta.2" 12 | dirs = "3.0.1" 13 | env_logger = "0.7.1" 14 | sunshine-cli-utils = { git = "https://github.com/sunshine-protocol/sunshine-core" } 15 | sunshine-faucet-cli = { path = "../../faucet/cli" } 16 | sunshine-identity-cli = { path = "../../identity/cli" } 17 | test-client = { path = "../client" } 18 | -------------------------------------------------------------------------------- /bin/cli/gen-chain-spec.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | NODE=../../target/debug/test-node 3 | $NODE build-spec --dev > chain-spec.json 4 | -------------------------------------------------------------------------------- /bin/cli/src/command.rs: -------------------------------------------------------------------------------- 1 | use clap::Clap; 2 | use std::path::PathBuf; 3 | use sunshine_faucet_cli::MintCommand; 4 | use sunshine_identity_cli::{account, device, id, key, wallet}; 5 | 6 | #[derive(Clone, Debug, Clap)] 7 | pub struct Opts { 8 | #[clap(subcommand)] 9 | pub cmd: SubCommand, 10 | #[clap(short = 'p', long = "path")] 11 | pub path: Option, 12 | #[clap(short = 'c', long = "chain-spec")] 13 | pub chain_spec: Option, 14 | } 15 | 16 | #[derive(Clone, Debug, Clap)] 17 | pub enum SubCommand { 18 | Key(KeyCommand), 19 | Account(AccountCommand), 20 | Device(DeviceCommand), 21 | Id(IdCommand), 22 | Wallet(WalletCommand), 23 | Run, 24 | } 25 | 26 | #[derive(Clone, Debug, Clap)] 27 | pub struct KeyCommand { 28 | #[clap(subcommand)] 29 | pub cmd: KeySubCommand, 30 | } 31 | 32 | #[derive(Clone, Debug, Clap)] 33 | pub enum KeySubCommand { 34 | Set(key::KeySetCommand), 35 | Unlock(key::KeyUnlockCommand), 36 | Lock(key::KeyLockCommand), 37 | } 38 | 39 | #[derive(Clone, Debug, Clap)] 40 | pub struct AccountCommand { 41 | #[clap(subcommand)] 42 | pub cmd: AccountSubCommand, 43 | } 44 | 45 | #[derive(Clone, Debug, Clap)] 46 | pub enum AccountSubCommand { 47 | Create(account::AccountCreateCommand), 48 | Password(account::AccountPasswordCommand), 49 | Mint(MintCommand), 50 | } 51 | 52 | #[derive(Clone, Debug, Clap)] 53 | pub struct DeviceCommand { 54 | #[clap(subcommand)] 55 | pub cmd: DeviceSubCommand, 56 | } 57 | 58 | #[derive(Clone, Debug, Clap)] 59 | pub enum DeviceSubCommand { 60 | Add(device::DeviceAddCommand), 61 | Remove(device::DeviceRemoveCommand), 62 | List(device::DeviceListCommand), 63 | Paperkey(device::DevicePaperkeyCommand), 64 | } 65 | 66 | #[derive(Clone, Debug, Clap)] 67 | pub struct IdCommand { 68 | #[clap(subcommand)] 69 | pub cmd: IdSubCommand, 70 | } 71 | 72 | #[derive(Clone, Debug, Clap)] 73 | pub enum IdSubCommand { 74 | List(id::IdListCommand), 75 | Prove(id::IdProveCommand), 76 | Revoke(id::IdRevokeCommand), 77 | } 78 | 79 | #[derive(Clone, Debug, Clap)] 80 | pub struct WalletCommand { 81 | #[clap(subcommand)] 82 | pub cmd: WalletSubCommand, 83 | } 84 | 85 | #[derive(Clone, Debug, Clap)] 86 | pub enum WalletSubCommand { 87 | Balance(wallet::WalletBalanceCommand), 88 | Transfer(wallet::WalletTransferCommand), 89 | } 90 | -------------------------------------------------------------------------------- /bin/cli/src/main.rs: -------------------------------------------------------------------------------- 1 | use crate::command::*; 2 | use async_std::task; 3 | use clap::Clap; 4 | use std::time::Duration; 5 | use sunshine_cli_utils::{set_key, Client as _, ConfigDirNotFound, Result}; 6 | use sunshine_faucet_cli::MintCommand; 7 | use sunshine_identity_cli::key::KeySetCommand; 8 | use test_client::{identity::IdentityClient, Client}; 9 | 10 | mod command; 11 | 12 | #[async_std::main] 13 | async fn main() -> Result<()> { 14 | env_logger::init(); 15 | let opts: Opts = Opts::parse(); 16 | let root = if let Some(root) = opts.path { 17 | root 18 | } else { 19 | dirs::config_dir() 20 | .ok_or(ConfigDirNotFound)? 21 | .join("sunshine-identity") 22 | }; 23 | let chain_spec = if let Some(chain_spec) = opts.chain_spec { 24 | chain_spec 25 | } else { 26 | unimplemented!(); 27 | }; 28 | 29 | let mut client = Client::new(&root, &chain_spec).await?; 30 | 31 | let mut password_changes = if client.chain_signer().is_ok() { 32 | let sub = client.subscribe_password_changes().await?; 33 | client.update_password().await?; 34 | Some(sub) 35 | } else { 36 | None 37 | }; 38 | 39 | match opts.cmd { 40 | SubCommand::Key(KeyCommand { cmd }) => match cmd { 41 | KeySubCommand::Set(KeySetCommand { 42 | paperkey, 43 | suri, 44 | force, 45 | }) => { 46 | let account_id = set_key(&mut client, paperkey, suri.as_deref(), force).await?; 47 | println!("your device key is {}", account_id.to_string()); 48 | MintCommand.exec(&client).await?; 49 | let uid = client.fetch_uid(&account_id).await?.unwrap(); 50 | println!("your user id is {}", uid); 51 | Ok(()) 52 | } 53 | KeySubCommand::Unlock(cmd) => cmd.exec(&mut client).await, 54 | KeySubCommand::Lock(cmd) => cmd.exec(&mut client).await, 55 | }, 56 | SubCommand::Account(AccountCommand { cmd }) => match cmd { 57 | AccountSubCommand::Create(cmd) => cmd.exec(&client).await, 58 | AccountSubCommand::Password(cmd) => cmd.exec(&client).await, 59 | AccountSubCommand::Mint(cmd) => cmd.exec(&client).await, 60 | }, 61 | SubCommand::Device(DeviceCommand { cmd }) => match cmd { 62 | DeviceSubCommand::Add(cmd) => cmd.exec(&client).await, 63 | DeviceSubCommand::Remove(cmd) => cmd.exec(&client).await, 64 | DeviceSubCommand::List(cmd) => cmd.exec(&client).await, 65 | DeviceSubCommand::Paperkey(cmd) => cmd.exec(&client).await, 66 | }, 67 | SubCommand::Id(IdCommand { cmd }) => match cmd { 68 | IdSubCommand::List(cmd) => cmd.exec(&client).await, 69 | IdSubCommand::Prove(cmd) => cmd.exec(&client).await, 70 | IdSubCommand::Revoke(cmd) => cmd.exec(&client).await, 71 | }, 72 | SubCommand::Wallet(WalletCommand { cmd }) => match cmd { 73 | WalletSubCommand::Balance(cmd) => cmd.exec(&client).await, 74 | WalletSubCommand::Transfer(cmd) => cmd.exec(&client).await, 75 | }, 76 | SubCommand::Run => loop { 77 | if let Some(sub) = password_changes.as_mut() { 78 | if sub.next().await.is_some() { 79 | client.update_password().await?; 80 | } 81 | } else { 82 | task::sleep(Duration::from_millis(100)).await 83 | } 84 | }, 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /bin/client/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "test-client" 3 | version = "0.2.0" 4 | authors = ["David Craven "] 5 | edition = "2018" 6 | publish = false 7 | 8 | [features] 9 | mock = [ 10 | "sunshine-client-utils/mock", 11 | ] 12 | 13 | [dependencies] 14 | async-trait = "0.1.40" 15 | libipld = { version = "0.6.1", default-features = false } 16 | substrate-subxt = "0.12.0" 17 | sunshine-chain-client = { path = "../../chain/client" } 18 | sunshine-client-utils = { git = "https://github.com/sunshine-protocol/sunshine-core" } 19 | sunshine-faucet-client = { path = "../../faucet/client" } 20 | sunshine-identity-client = { path = "../../identity/client" } 21 | test-node = { path = "../node" } 22 | -------------------------------------------------------------------------------- /bin/client/src/lib.rs: -------------------------------------------------------------------------------- 1 | use libipld::cache::IpldCache; 2 | use libipld::cbor::DagCborCodec; 3 | use libipld::derive_cache; 4 | use libipld::store::Store; 5 | use sc_service::{Configuration, RpcHandlers, TaskManager}; 6 | use std::ops::Deref; 7 | use substrate_subxt::balances::{AccountData, Balances}; 8 | use substrate_subxt::sp_runtime::traits::{IdentifyAccount, Verify}; 9 | use substrate_subxt::system::System; 10 | use substrate_subxt::{extrinsic, sp_core, sp_runtime}; 11 | use sunshine_chain_client::Chain; 12 | use sunshine_client_utils::codec::hasher::{TreeHashBlake2b256, TreeHasherBlake2b256, BLAKE2B_256}; 13 | use sunshine_client_utils::codec::Cid; 14 | use sunshine_client_utils::crypto::keychain::KeyType; 15 | use sunshine_client_utils::crypto::sr25519; 16 | use sunshine_client_utils::{ 17 | sc_service, ChainSpecError, GenericClient, Network, Node as NodeT, 18 | OffchainClient as OffchainClientT, OffchainStore, 19 | }; 20 | use sunshine_faucet_client::Faucet; 21 | use sunshine_identity_client::{Claim, Identity}; 22 | 23 | pub use sunshine_chain_client as chain; 24 | pub use sunshine_client_utils as client; 25 | pub use sunshine_faucet_client as faucet; 26 | pub use sunshine_identity_client as identity; 27 | 28 | pub type AccountId = <::Signer as IdentifyAccount>::AccountId; 29 | pub type Uid = u32; 30 | 31 | #[derive(Clone, Debug, Eq, PartialEq)] 32 | pub struct Runtime; 33 | 34 | impl System for Runtime { 35 | type Index = u32; 36 | type BlockNumber = u32; 37 | type Hash = sp_core::H256; 38 | type Hashing = sp_runtime::traits::BlakeTwo256; 39 | type AccountId = AccountId; 40 | type Address = AccountId; 41 | type Header = sp_runtime::generic::Header; 42 | type Extrinsic = sp_runtime::OpaqueExtrinsic; 43 | type AccountData = (); 44 | } 45 | 46 | impl Balances for Runtime { 47 | type Balance = u128; 48 | } 49 | 50 | impl Chain for Runtime { 51 | type ChainId = u64; 52 | type Number = u64; 53 | type TrieHasher = TreeHasherBlake2b256; 54 | type TrieHash = TreeHashBlake2b256; 55 | } 56 | 57 | impl Faucet for Runtime {} 58 | 59 | impl Identity for Runtime { 60 | type Uid = Uid; 61 | type Cid = Cid; 62 | type Mask = [u8; 32]; 63 | type Gen = u16; 64 | type IdAccountData = AccountData<::Balance>; 65 | } 66 | 67 | impl substrate_subxt::Runtime for Runtime { 68 | type Signature = sp_runtime::MultiSignature; 69 | type Extra = extrinsic::DefaultExtra; 70 | } 71 | 72 | pub struct OffchainClient { 73 | store: S, 74 | claims: IpldCache, 75 | } 76 | 77 | impl Deref for OffchainClient { 78 | type Target = S; 79 | 80 | fn deref(&self) -> &Self::Target { 81 | &self.store 82 | } 83 | } 84 | 85 | derive_cache!(OffchainClient, claims, DagCborCodec, Claim); 86 | 87 | impl OffchainClient { 88 | pub fn new(store: S) -> Self { 89 | Self { 90 | claims: IpldCache::new(store.clone(), DagCborCodec, BLAKE2B_256, 64), 91 | store, 92 | } 93 | } 94 | } 95 | 96 | impl From for OffchainClient { 97 | fn from(store: S) -> Self { 98 | Self::new(store) 99 | } 100 | } 101 | 102 | impl OffchainClientT for OffchainClient {} 103 | 104 | #[derive(Clone, Copy)] 105 | pub struct Node; 106 | 107 | impl NodeT for Node { 108 | type ChainSpec = test_node::ChainSpec; 109 | type Block = test_node::OpaqueBlock; 110 | type Runtime = Runtime; 111 | 112 | fn impl_name() -> &'static str { 113 | test_node::IMPL_NAME 114 | } 115 | 116 | fn impl_version() -> &'static str { 117 | test_node::IMPL_VERSION 118 | } 119 | 120 | fn author() -> &'static str { 121 | test_node::AUTHOR 122 | } 123 | 124 | fn copyright_start_year() -> i32 { 125 | test_node::COPYRIGHT_START_YEAR 126 | } 127 | 128 | fn chain_spec_dev() -> Self::ChainSpec { 129 | test_node::development_config() 130 | } 131 | 132 | fn chain_spec_from_json_bytes(json: Vec) -> Result { 133 | Self::ChainSpec::from_json_bytes(json).map_err(ChainSpecError) 134 | } 135 | 136 | fn new_light( 137 | config: Configuration, 138 | ) -> Result<(TaskManager, RpcHandlers, Network), sc_service::Error> { 139 | Ok(test_node::new_light(config)?) 140 | } 141 | 142 | fn new_full( 143 | config: Configuration, 144 | ) -> Result<(TaskManager, RpcHandlers, Network), sc_service::Error> { 145 | Ok(test_node::new_full(config)?) 146 | } 147 | } 148 | 149 | pub struct UserDevice; 150 | 151 | impl KeyType for UserDevice { 152 | const KEY_TYPE: u8 = 0; 153 | type Pair = sr25519::Pair; 154 | } 155 | 156 | pub type Client = GenericClient>>; 157 | -------------------------------------------------------------------------------- /bin/node/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "test-node" 3 | version = "0.2.0" 4 | authors = ["David Craven "] 5 | edition = "2018" 6 | publish = false 7 | 8 | [dependencies] 9 | sc-executor = "0.8.0" 10 | sp-core = "2.0.0" 11 | sp-runtime = "2.0.0" 12 | sunshine-codec = { git = "https://github.com/sunshine-protocol/sunshine-core" } 13 | sunshine-node-utils = { git = "https://github.com/sunshine-protocol/sunshine-core" } 14 | test-runtime = { path = "../runtime" } 15 | # cli deps 16 | sc-cli = "0.8.0" 17 | sc-service = { version = "0.8.0", default-features = false } 18 | structopt = "0.3.18" 19 | 20 | [build-dependencies] 21 | substrate-build-script-utils = "2.0.0" 22 | -------------------------------------------------------------------------------- /bin/node/build.rs: -------------------------------------------------------------------------------- 1 | use substrate_build_script_utils::{generate_cargo_keys, rerun_if_git_head_changed}; 2 | 3 | fn main() { 4 | generate_cargo_keys(); 5 | 6 | rerun_if_git_head_changed(); 7 | } 8 | -------------------------------------------------------------------------------- /bin/node/src/lib.rs: -------------------------------------------------------------------------------- 1 | use sc_executor::native_executor_instance; 2 | use sc_service::ChainType; 3 | use sp_core::{sr25519, Pair, Public}; 4 | use sp_runtime::traits::{IdentifyAccount, Verify}; 5 | use sunshine_node_utils::node_service; 6 | pub use test_runtime::opaque::Block as OpaqueBlock; 7 | use test_runtime::{ 8 | AccountId, AuraConfig, BalancesConfig, GenesisConfig, GrandpaConfig, Signature, SystemConfig, 9 | WASM_BINARY, 10 | }; 11 | 12 | pub const IMPL_NAME: &str = "Sunshine Identity Test Node"; 13 | pub const IMPL_VERSION: &str = env!("CARGO_PKG_VERSION"); 14 | pub const DESCRIPTION: &str = env!("CARGO_PKG_DESCRIPTION"); 15 | pub const AUTHOR: &str = env!("CARGO_PKG_AUTHORS"); 16 | pub const SUPPORT_URL: &str = env!("CARGO_PKG_HOMEPAGE"); 17 | pub const COPYRIGHT_START_YEAR: i32 = 2020; 18 | pub const EXECUTABLE_NAME: &str = env!("CARGO_PKG_NAME"); 19 | 20 | native_executor_instance!( 21 | pub Executor, 22 | test_runtime::api::dispatch, 23 | test_runtime::native_version, 24 | ); 25 | 26 | node_service!( 27 | test_runtime::opaque::Block, 28 | test_runtime::RuntimeApi, 29 | Executor 30 | ); 31 | 32 | /// Specialized `ChainSpec`. 33 | pub type ChainSpec = sc_service::GenericChainSpec; 34 | 35 | /// Helper function to generate a crypto pair from seed 36 | pub fn get_from_seed(seed: &str) -> ::Public { 37 | TPublic::Pair::from_string(&format!("//{}", seed), None) 38 | .expect("static values are valid; qed") 39 | .public() 40 | } 41 | 42 | type AccountPublic = ::Signer; 43 | 44 | /// Helper function to generate an account ID from seed 45 | pub fn get_account_id_from_seed(seed: &str) -> AccountId 46 | where 47 | AccountPublic: From<::Public>, 48 | { 49 | AccountPublic::from(get_from_seed::(seed)).into_account() 50 | } 51 | 52 | /// Helper function to generate an authority key for Aura 53 | pub fn get_authority_keys_from_seed(s: &str) -> (AuraId, GrandpaId) { 54 | (get_from_seed::(s), get_from_seed::(s)) 55 | } 56 | 57 | pub fn development_config() -> ChainSpec { 58 | ChainSpec::from_genesis( 59 | "Development", 60 | "dev", 61 | ChainType::Development, 62 | || { 63 | testnet_genesis( 64 | // initial authorities 65 | vec![get_authority_keys_from_seed("Alice")], 66 | // endowed accounts 67 | vec![ 68 | get_account_id_from_seed::("Alice"), 69 | get_account_id_from_seed::("Bob"), 70 | get_account_id_from_seed::("Alice//stash"), 71 | get_account_id_from_seed::("Bob//stash"), 72 | ], 73 | ) 74 | }, 75 | vec![], 76 | None, 77 | None, 78 | None, 79 | None, 80 | ) 81 | } 82 | 83 | pub fn local_testnet_config() -> ChainSpec { 84 | ChainSpec::from_genesis( 85 | "Local Testnet", 86 | "local_testnet", 87 | ChainType::Local, 88 | || { 89 | testnet_genesis( 90 | // initial authorities 91 | vec![ 92 | get_authority_keys_from_seed("Alice"), 93 | get_authority_keys_from_seed("Bob"), 94 | ], 95 | // endowed accounts 96 | vec![ 97 | get_account_id_from_seed::("Alice"), 98 | get_account_id_from_seed::("Bob"), 99 | get_account_id_from_seed::("Charlie"), 100 | get_account_id_from_seed::("Dave"), 101 | get_account_id_from_seed::("Eve"), 102 | get_account_id_from_seed::("Ferdie"), 103 | get_account_id_from_seed::("Alice//stash"), 104 | get_account_id_from_seed::("Bob//stash"), 105 | get_account_id_from_seed::("Charlie//stash"), 106 | get_account_id_from_seed::("Dave//stash"), 107 | get_account_id_from_seed::("Eve//stash"), 108 | get_account_id_from_seed::("Ferdie//stash"), 109 | ], 110 | ) 111 | }, 112 | vec![], 113 | None, 114 | None, 115 | None, 116 | None, 117 | ) 118 | } 119 | 120 | fn testnet_genesis( 121 | initial_authorities: Vec<(AuraId, GrandpaId)>, 122 | endowed_accounts: Vec, 123 | ) -> GenesisConfig { 124 | GenesisConfig { 125 | frame_system: Some(SystemConfig { 126 | code: WASM_BINARY.to_vec(), 127 | changes_trie_config: Default::default(), 128 | }), 129 | pallet_balances: Some(BalancesConfig { 130 | balances: endowed_accounts 131 | .iter() 132 | .cloned() 133 | .map(|k| (k, 1 << 60)) 134 | .collect(), 135 | }), 136 | pallet_aura: Some(AuraConfig { 137 | authorities: initial_authorities.iter().map(|x| (x.0.clone())).collect(), 138 | }), 139 | pallet_grandpa: Some(GrandpaConfig { 140 | authorities: initial_authorities 141 | .iter() 142 | .map(|x| (x.1.clone(), 1)) 143 | .collect(), 144 | }), 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /bin/node/src/main.rs: -------------------------------------------------------------------------------- 1 | use sc_cli::{RunCmd, Runner, RuntimeVersion, SubstrateCli}; 2 | use sc_service::{ChainSpec, DatabaseConfig, Role}; 3 | use sunshine_codec::Multihash; 4 | use structopt::StructOpt; 5 | 6 | #[derive(Debug, StructOpt)] 7 | pub struct Cli { 8 | #[structopt(subcommand)] 9 | pub subcommand: Option, 10 | 11 | #[structopt(flatten)] 12 | pub run: RunCmd, 13 | } 14 | 15 | #[derive(Debug, StructOpt)] 16 | pub enum Subcommand { 17 | PurgeChain(sc_cli::PurgeChainCmd), 18 | } 19 | 20 | impl SubstrateCli for Cli { 21 | fn impl_name() -> String { 22 | test_node::IMPL_NAME.into() 23 | } 24 | 25 | fn impl_version() -> String { 26 | test_node::IMPL_VERSION.into() 27 | } 28 | 29 | fn description() -> String { 30 | test_node::DESCRIPTION.into() 31 | } 32 | 33 | fn author() -> String { 34 | test_node::AUTHOR.into() 35 | } 36 | 37 | fn support_url() -> String { 38 | test_node::SUPPORT_URL.into() 39 | } 40 | 41 | fn copyright_start_year() -> i32 { 42 | test_node::COPYRIGHT_START_YEAR 43 | } 44 | 45 | fn executable_name() -> String { 46 | test_node::EXECUTABLE_NAME.into() 47 | } 48 | 49 | fn load_spec(&self, id: &str) -> Result, String> { 50 | Ok(match id { 51 | "dev" => Box::new(test_node::development_config()), 52 | "" | "local" => Box::new(test_node::local_testnet_config()), 53 | path => Box::new(test_node::ChainSpec::from_json_file(path.into())?), 54 | }) 55 | } 56 | 57 | fn native_runtime_version(_: &Box) -> &'static RuntimeVersion { 58 | &test_runtime::VERSION 59 | } 60 | } 61 | 62 | fn main() -> sc_cli::Result<()> { 63 | let cli = ::from_args(); 64 | match &cli.subcommand { 65 | Some(Subcommand::PurgeChain(cmd)) => { 66 | let mut runner = cli.create_runner(cmd)?; 67 | force_parity_db(&mut runner); 68 | runner.sync_run(|config| cmd.run(config.database)) 69 | } 70 | None => { 71 | let mut runner = cli.create_runner(&cli.run)?; 72 | force_parity_db(&mut runner); 73 | runner.run_node_until_exit(|config| { 74 | match config.role { 75 | Role::Light => test_node::new_light::(config), 76 | _ => test_node::new_full::(config), 77 | } 78 | .map(|service| service.0) 79 | }) 80 | } 81 | } 82 | } 83 | 84 | fn force_parity_db(runner: &mut Runner) { 85 | let config = runner.config_mut(); 86 | let path = config.database.path().unwrap().to_path_buf(); 87 | config.database = DatabaseConfig::ParityDb { path }; 88 | } 89 | -------------------------------------------------------------------------------- /bin/runtime/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "test-runtime" 3 | version = "0.2.0" 4 | authors = ["David Craven "] 5 | edition = "2018" 6 | publish = false 7 | 8 | [dependencies] 9 | frame-executive = { version = "2.0.0", default-features = false } 10 | frame-support = { version = "2.0.0", default-features = false } 11 | frame-system = { version = "2.0.0", default-features = false } 12 | pallet-aura = { version = "2.0.0", default-features = false } 13 | pallet-balances = { version = "2.0.0", default-features = false } 14 | pallet-grandpa = { version = "2.0.0", default-features = false } 15 | pallet-randomness-collective-flip = { version = "2.0.0", default-features = false } 16 | pallet-timestamp = { version = "2.0.0", default-features = false } 17 | pallet-transaction-payment = { version = "2.0.0", default-features = false } 18 | parity-scale-codec = { version = "1.3.5", default-features = false, features = ["derive"] } 19 | serde = { version = "1.0.116", optional = true, features = ["derive"] } 20 | sp-api = { version = "2.0.0", default-features = false } 21 | sp-block-builder = { version = "2.0.0", default-features = false } 22 | sp-consensus-aura = { version = "0.8.0", default-features = false } 23 | sp-core = { version = "2.0.0", default-features = false } 24 | sp-inherents = { default-features = false, version = "2.0.0" } 25 | sp-io = { version = "2.0.0", default-features = false } 26 | sp-offchain = { version = "2.0.0", default-features = false } 27 | sp-runtime = { version = "2.0.0", default-features = false } 28 | sp-session = { version = "2.0.0", default-features = false } 29 | sp-std = { version = "2.0.0", default-features = false } 30 | sp-transaction-pool = { version = "2.0.0", default-features = false } 31 | sp-version = { version = "2.0.0", default-features = false } 32 | 33 | sunshine-chain-pallet = { default-features = false, path = "../../chain/pallet" } 34 | sunshine-codec = { default-features = false, git = "https://github.com/sunshine-protocol/sunshine-core" } 35 | sunshine-faucet-pallet = { default-features = false, path = "../../faucet/pallet" } 36 | sunshine-identity-pallet = { default-features = false, path = "../../identity/pallet" } 37 | 38 | [build-dependencies] 39 | substrate-wasm-builder-runner = "1.0.6" 40 | 41 | [features] 42 | default = ["std"] 43 | std = [ 44 | "frame-executive/std", 45 | "frame-support/std", 46 | "frame-system/std", 47 | "pallet-aura/std", 48 | "pallet-balances/std", 49 | "pallet-grandpa/std", 50 | "pallet-randomness-collective-flip/std", 51 | "pallet-timestamp/std", 52 | "pallet-transaction-payment/std", 53 | "parity-scale-codec/std", 54 | "serde", 55 | "sp-api/std", 56 | "sp-block-builder/std", 57 | "sp-consensus-aura/std", 58 | "sp-core/std", 59 | "sp-inherents/std", 60 | "sp-io/std", 61 | "sp-offchain/std", 62 | "sp-runtime/std", 63 | "sp-session/std", 64 | "sp-std/std", 65 | "sp-transaction-pool/std", 66 | "sp-version/std", 67 | "sunshine-chain-pallet/std", 68 | "sunshine-codec/std", 69 | "sunshine-faucet-pallet/std", 70 | "sunshine-identity-pallet/std", 71 | ] 72 | -------------------------------------------------------------------------------- /bin/runtime/build.rs: -------------------------------------------------------------------------------- 1 | use substrate_wasm_builder_runner::WasmBuilder; 2 | 3 | fn main() { 4 | WasmBuilder::new() 5 | .with_current_project() 6 | .with_wasm_builder_from_crates("1.0.9") 7 | .export_heap_base() 8 | .import_memory() 9 | .build() 10 | } 11 | -------------------------------------------------------------------------------- /bin/runtime/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! The Substrate Node Template runtime. This can be compiled with `#[no_std]`, ready for Wasm. 2 | #![allow(clippy::large_enum_variant)] 3 | #![allow(clippy::unnecessary_mut_passed)] 4 | #![cfg_attr(not(feature = "std"), no_std)] 5 | // `construct_runtime!` does a lot of recursion and requires us to increase the limit to 256. 6 | #![recursion_limit = "256"] 7 | 8 | // Make the WASM binary available. 9 | #[cfg(feature = "std")] 10 | include!(concat!(env!("OUT_DIR"), "/wasm_binary.rs")); 11 | 12 | use pallet_grandpa::{ 13 | fg_primitives, AuthorityId as GrandpaId, AuthorityList as GrandpaAuthorityList, 14 | }; 15 | use sp_api::impl_runtime_apis; 16 | use sp_consensus_aura::sr25519::AuthorityId as AuraId; 17 | use sp_core::{crypto::KeyTypeId, OpaqueMetadata}; 18 | use sp_runtime::{ 19 | create_runtime_str, generic, impl_opaque_keys, 20 | traits::{ 21 | BlakeTwo256, Block as BlockT, IdentifyAccount, IdentityLookup, NumberFor, Saturating, 22 | Verify, 23 | }, 24 | transaction_validity::{TransactionSource, TransactionValidity}, 25 | ApplyExtrinsicResult, MultiSignature, 26 | }; 27 | use sp_std::prelude::*; 28 | #[cfg(feature = "std")] 29 | use sp_version::NativeVersion; 30 | use sp_version::RuntimeVersion; 31 | 32 | // A few exports that help ease life for downstream crates. 33 | pub use frame_support::{ 34 | construct_runtime, parameter_types, 35 | traits::{KeyOwnerProofSystem, Randomness}, 36 | weights::{ 37 | constants::{BlockExecutionWeight, ExtrinsicBaseWeight, RocksDbWeight, WEIGHT_PER_SECOND}, 38 | IdentityFee, Weight, 39 | }, 40 | StorageValue, 41 | }; 42 | pub use pallet_balances::Call as BalancesCall; 43 | pub use pallet_timestamp::Call as TimestampCall; 44 | #[cfg(any(feature = "std", test))] 45 | pub use sp_runtime::BuildStorage; 46 | pub use sp_runtime::{Perbill, Permill}; 47 | 48 | /// An index to a block. 49 | pub type BlockNumber = u32; 50 | 51 | /// Alias to 512-bit hash when used in the context of a transaction signature on the chain. 52 | pub type Signature = MultiSignature; 53 | 54 | /// Some way of identifying an account on the chain. We intentionally make it equivalent 55 | /// to the public key of our transaction signing scheme. 56 | pub type AccountId = <::Signer as IdentifyAccount>::AccountId; 57 | 58 | /// The type for looking up accounts. We don't expect more than 4 billion of them, but you 59 | /// never know... 60 | pub type AccountIndex = u32; 61 | 62 | /// Balance of an account. 63 | pub type Balance = u128; 64 | 65 | /// Index of a transaction in the chain. 66 | pub type Index = u32; 67 | 68 | /// A hash of some data used by the chain. 69 | pub type Hash = sp_core::H256; 70 | 71 | /// Digest item type. 72 | pub type DigestItem = generic::DigestItem; 73 | 74 | /// Opaque types. These are used by the CLI to instantiate machinery that don't need to know 75 | /// the specifics of the runtime. They can then be made to be agnostic over specific formats 76 | /// of data like extrinsics, allowing for them to continue syncing the network through upgrades 77 | /// to even the core data structures. 78 | pub mod opaque { 79 | use super::*; 80 | 81 | pub use sp_runtime::OpaqueExtrinsic as UncheckedExtrinsic; 82 | 83 | /// Opaque block header type. 84 | pub type Header = generic::Header; 85 | /// Opaque block type. 86 | pub type Block = generic::Block; 87 | /// Opaque block identifier type. 88 | pub type BlockId = generic::BlockId; 89 | 90 | impl_opaque_keys! { 91 | pub struct SessionKeys { 92 | pub aura: Aura, 93 | pub grandpa: Grandpa, 94 | } 95 | } 96 | } 97 | 98 | /// This runtime version. 99 | pub const VERSION: RuntimeVersion = RuntimeVersion { 100 | spec_name: create_runtime_str!("node-template"), 101 | impl_name: create_runtime_str!("node-template"), 102 | authoring_version: 1, 103 | spec_version: 1, 104 | impl_version: 1, 105 | apis: RUNTIME_API_VERSIONS, 106 | transaction_version: 1, 107 | }; 108 | 109 | pub const MILLISECS_PER_BLOCK: u64 = 6000; 110 | 111 | pub const SLOT_DURATION: u64 = MILLISECS_PER_BLOCK; 112 | 113 | // These time units are defined in number of blocks. 114 | pub const MINUTES: BlockNumber = 60_000 / (MILLISECS_PER_BLOCK as BlockNumber); 115 | pub const HOURS: BlockNumber = MINUTES * 60; 116 | pub const DAYS: BlockNumber = HOURS * 24; 117 | 118 | /// The version information used to identify this runtime when compiled natively. 119 | #[cfg(feature = "std")] 120 | pub fn native_version() -> NativeVersion { 121 | NativeVersion { 122 | runtime_version: VERSION, 123 | can_author_with: Default::default(), 124 | } 125 | } 126 | 127 | parameter_types! { 128 | pub const BlockHashCount: BlockNumber = 2400; 129 | /// We allow for 2 seconds of compute with a 6 second average block time. 130 | pub const MaximumBlockWeight: Weight = 2 * WEIGHT_PER_SECOND; 131 | pub const AvailableBlockRatio: Perbill = Perbill::from_percent(75); 132 | /// Assume 10% of weight for average on_initialize calls. 133 | pub MaximumExtrinsicWeight: Weight = AvailableBlockRatio::get() 134 | .saturating_sub(Perbill::from_percent(10)) * MaximumBlockWeight::get(); 135 | pub const MaximumBlockLength: u32 = 5 * 1024 * 1024; 136 | pub const Version: RuntimeVersion = VERSION; 137 | } 138 | 139 | impl frame_system::Trait for Runtime { 140 | /// The basic call filter to use in dispatchable. 141 | type BaseCallFilter = (); 142 | /// The identifier used to distinguish between accounts. 143 | type AccountId = AccountId; 144 | /// The aggregated dispatch type that is available for extrinsics. 145 | type Call = Call; 146 | /// The lookup mechanism to get account ID from whatever is passed in dispatchers. 147 | type Lookup = IdentityLookup; 148 | /// The index type for storing how many extrinsics an account has signed. 149 | type Index = Index; 150 | /// The index type for blocks. 151 | type BlockNumber = BlockNumber; 152 | /// The type for hashing blocks and tries. 153 | type Hash = Hash; 154 | /// The hashing algorithm used. 155 | type Hashing = BlakeTwo256; 156 | /// The header type. 157 | type Header = generic::Header; 158 | /// The ubiquitous event type. 159 | type Event = Event; 160 | /// The ubiquitous origin type. 161 | type Origin = Origin; 162 | /// Maximum number of block number to block hash mappings to keep (oldest pruned first). 163 | type BlockHashCount = BlockHashCount; 164 | /// Maximum weight of each block. 165 | type MaximumBlockWeight = MaximumBlockWeight; 166 | /// The weight of database operations that the runtime can invoke. 167 | type DbWeight = RocksDbWeight; 168 | /// The weight of the overhead invoked on the block import process, independent of the 169 | /// extrinsics included in that block. 170 | type BlockExecutionWeight = BlockExecutionWeight; 171 | /// The base weight of any extrinsic processed by the runtime, independent of the 172 | /// logic of that extrinsic. (Signature verification, nonce increment, fee, etc...) 173 | type ExtrinsicBaseWeight = ExtrinsicBaseWeight; 174 | /// The maximum weight that a single extrinsic of `Normal` dispatch class can have, 175 | /// idependent of the logic of that extrinsics. (Roughly max block weight - average on 176 | /// initialize cost). 177 | type MaximumExtrinsicWeight = MaximumExtrinsicWeight; 178 | /// Maximum size of all encoded transactions (in bytes) that are allowed in one block. 179 | type MaximumBlockLength = MaximumBlockLength; 180 | /// Portion of the block weight that is available to all normal transactions. 181 | type AvailableBlockRatio = AvailableBlockRatio; 182 | /// Version of the runtime. 183 | type Version = Version; 184 | /// Converts a module to the index of the module in `construct_runtime!`. 185 | /// 186 | /// This type is being generated by `construct_runtime!`. 187 | type PalletInfo = PalletInfo; 188 | /// What to do if a new account is created. 189 | type OnNewAccount = (); 190 | /// What to do if an account is fully reaped from the system. 191 | type OnKilledAccount = (); 192 | /// The data to be stored in an account. 193 | type AccountData = (); 194 | /// Weight information for the extrinsics of this pallet. 195 | type SystemWeightInfo = (); 196 | } 197 | 198 | impl pallet_aura::Trait for Runtime { 199 | type AuthorityId = AuraId; 200 | } 201 | 202 | impl pallet_grandpa::Trait for Runtime { 203 | type Event = Event; 204 | type Call = Call; 205 | 206 | type KeyOwnerProofSystem = (); 207 | 208 | type KeyOwnerProof = 209 | >::Proof; 210 | 211 | type KeyOwnerIdentification = >::IdentificationTuple; 215 | 216 | type HandleEquivocation = (); 217 | 218 | type WeightInfo = (); 219 | } 220 | 221 | parameter_types! { 222 | pub const MinimumPeriod: u64 = SLOT_DURATION / 2; 223 | } 224 | 225 | impl pallet_timestamp::Trait for Runtime { 226 | /// A timestamp: milliseconds since the unix epoch. 227 | type Moment = u64; 228 | type OnTimestampSet = Aura; 229 | type MinimumPeriod = MinimumPeriod; 230 | type WeightInfo = (); 231 | } 232 | 233 | parameter_types! { 234 | pub const ExistentialDeposit: u128 = 500; 235 | pub const MaxLocks: u32 = 50; 236 | } 237 | 238 | impl pallet_balances::Trait for Runtime { 239 | type Balance = Balance; 240 | type Event = Event; 241 | type DustRemoval = (); 242 | type ExistentialDeposit = ExistentialDeposit; 243 | type MaxLocks = MaxLocks; 244 | type AccountStore = Identity; 245 | type WeightInfo = (); 246 | } 247 | 248 | parameter_types! { 249 | pub const TransactionByteFee: Balance = 1; 250 | } 251 | 252 | impl pallet_transaction_payment::Trait for Runtime { 253 | type Currency = pallet_balances::Module; 254 | type OnTransactionPayment = (); 255 | type TransactionByteFee = TransactionByteFee; 256 | type WeightToFee = IdentityFee; 257 | type FeeMultiplierUpdate = (); 258 | } 259 | 260 | impl sunshine_chain_pallet::Trait for Runtime { 261 | type ChainId = u64; 262 | type Number = u64; 263 | type TrieHasher = sunshine_codec::hasher::TreeHasherBlake2b256; 264 | type TrieHash = sunshine_codec::hasher::TreeHashBlake2b256; 265 | type Event = Event; 266 | } 267 | 268 | impl sunshine_faucet_pallet::Trait for Runtime { 269 | const MINT_UNIT: Self::Balance = 1_000_000_000; 270 | type Event = Event; 271 | } 272 | 273 | impl sunshine_identity_pallet::Trait for Runtime { 274 | type Uid = u32; 275 | type Cid = sunshine_codec::Cid; 276 | type Mask = [u8; 32]; 277 | type Gen = u16; 278 | type AccountData = pallet_balances::AccountData; 279 | type Event = Event; 280 | } 281 | 282 | construct_runtime!( 283 | pub enum Runtime where 284 | Block = Block, 285 | NodeBlock = opaque::Block, 286 | UncheckedExtrinsic = UncheckedExtrinsic 287 | { 288 | System: frame_system::{Module, Call, Config, Storage, Event}, 289 | RandomnessCollectiveFlip: pallet_randomness_collective_flip::{Module, Call, Storage}, 290 | Timestamp: pallet_timestamp::{Module, Call, Storage, Inherent}, 291 | Aura: pallet_aura::{Module, Config, Inherent}, 292 | Grandpa: pallet_grandpa::{Module, Call, Storage, Config, Event}, 293 | Balances: pallet_balances::{Module, Call, Storage, Config, Event}, 294 | TransactionPayment: pallet_transaction_payment::{Module, Storage}, 295 | 296 | Chain: sunshine_chain_pallet::{Module, Call, Event, Storage}, 297 | Faucet: sunshine_faucet_pallet::{Module, Call, Event, ValidateUnsigned}, 298 | Identity: sunshine_identity_pallet::{Module, Call, Storage, Event}, 299 | } 300 | ); 301 | 302 | /// The address format for describing accounts. 303 | pub type Address = AccountId; 304 | /// Block header type as expected by this runtime. 305 | pub type Header = generic::Header; 306 | /// Block type as expected by this runtime. 307 | pub type Block = generic::Block; 308 | /// A Block signed with a Justification 309 | pub type SignedBlock = generic::SignedBlock; 310 | /// BlockId type as expected by this runtime. 311 | pub type BlockId = generic::BlockId; 312 | /// The SignedExtension to the basic transaction logic. 313 | pub type SignedExtra = ( 314 | frame_system::CheckSpecVersion, 315 | frame_system::CheckTxVersion, 316 | frame_system::CheckGenesis, 317 | frame_system::CheckEra, 318 | frame_system::CheckNonce, 319 | frame_system::CheckWeight, 320 | pallet_transaction_payment::ChargeTransactionPayment, 321 | ); 322 | /// Unchecked extrinsic type as expected by this runtime. 323 | pub type UncheckedExtrinsic = generic::UncheckedExtrinsic; 324 | /// Extrinsic type that has already been checked. 325 | pub type CheckedExtrinsic = generic::CheckedExtrinsic; 326 | /// Executive: handles dispatch to the various modules. 327 | pub type Executive = frame_executive::Executive< 328 | Runtime, 329 | Block, 330 | frame_system::ChainContext, 331 | Runtime, 332 | AllModules, 333 | >; 334 | 335 | impl_runtime_apis! { 336 | impl sp_api::Core for Runtime { 337 | fn version() -> RuntimeVersion { 338 | VERSION 339 | } 340 | 341 | fn execute_block(block: Block) { 342 | Executive::execute_block(block) 343 | } 344 | 345 | fn initialize_block(header: &::Header) { 346 | Executive::initialize_block(header) 347 | } 348 | } 349 | 350 | impl sp_api::Metadata for Runtime { 351 | fn metadata() -> OpaqueMetadata { 352 | Runtime::metadata().into() 353 | } 354 | } 355 | 356 | impl sp_block_builder::BlockBuilder for Runtime { 357 | fn apply_extrinsic(extrinsic: ::Extrinsic) -> ApplyExtrinsicResult { 358 | Executive::apply_extrinsic(extrinsic) 359 | } 360 | 361 | fn finalize_block() -> ::Header { 362 | Executive::finalize_block() 363 | } 364 | 365 | fn inherent_extrinsics(data: sp_inherents::InherentData) -> Vec<::Extrinsic> { 366 | data.create_extrinsics() 367 | } 368 | 369 | fn check_inherents( 370 | block: Block, 371 | data: sp_inherents::InherentData, 372 | ) -> sp_inherents::CheckInherentsResult { 373 | data.check_extrinsics(&block) 374 | } 375 | 376 | fn random_seed() -> ::Hash { 377 | RandomnessCollectiveFlip::random_seed() 378 | } 379 | } 380 | 381 | impl sp_transaction_pool::runtime_api::TaggedTransactionQueue for Runtime { 382 | fn validate_transaction( 383 | source: TransactionSource, 384 | tx: ::Extrinsic, 385 | ) -> TransactionValidity { 386 | Executive::validate_transaction(source, tx) 387 | } 388 | } 389 | 390 | impl sp_offchain::OffchainWorkerApi for Runtime { 391 | fn offchain_worker(header: &::Header) { 392 | Executive::offchain_worker(header) 393 | } 394 | } 395 | 396 | impl sp_consensus_aura::AuraApi for Runtime { 397 | fn slot_duration() -> u64 { 398 | Aura::slot_duration() 399 | } 400 | 401 | fn authorities() -> Vec { 402 | Aura::authorities() 403 | } 404 | } 405 | 406 | impl sp_session::SessionKeys for Runtime { 407 | fn generate_session_keys(seed: Option>) -> Vec { 408 | opaque::SessionKeys::generate(seed) 409 | } 410 | 411 | fn decode_session_keys( 412 | encoded: Vec, 413 | ) -> Option, KeyTypeId)>> { 414 | opaque::SessionKeys::decode_into_raw_public_keys(&encoded) 415 | } 416 | } 417 | 418 | impl fg_primitives::GrandpaApi for Runtime { 419 | fn grandpa_authorities() -> GrandpaAuthorityList { 420 | Grandpa::grandpa_authorities() 421 | } 422 | 423 | fn submit_report_equivocation_unsigned_extrinsic( 424 | _equivocation_proof: fg_primitives::EquivocationProof< 425 | ::Hash, 426 | NumberFor, 427 | >, 428 | _key_owner_proof: fg_primitives::OpaqueKeyOwnershipProof, 429 | ) -> Option<()> { 430 | None 431 | } 432 | 433 | fn generate_key_ownership_proof( 434 | _set_id: fg_primitives::SetId, 435 | _authority_id: GrandpaId, 436 | ) -> Option { 437 | // NOTE: this is the only implementation possible since we've 438 | // defined our key owner proof type as a bottom type (i.e. a type 439 | // with no values). 440 | None 441 | } 442 | } 443 | } 444 | -------------------------------------------------------------------------------- /chain/client/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sunshine-chain-client" 3 | version = "0.1.0" 4 | authors = ["David Craven "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | frame-support = "2.0.0" 9 | libipld = { version = "0.6.1", default-features = false } 10 | log = "0.4.11" 11 | parity-scale-codec = "1.3.5" 12 | substrate-subxt = "0.12.0" 13 | sunshine-client-utils = { git = "https://github.com/sunshine-protocol/sunshine-core" } 14 | thiserror = "1.0.20" 15 | 16 | [dev-dependencies] 17 | async-std = { version = "1.6.4", features = ["attributes"] } 18 | env_logger = "0.7.1" 19 | test-client = { path = "../../bin/client", features = ["mock"] } 20 | -------------------------------------------------------------------------------- /chain/client/src/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | #[derive(Debug, Error)] 4 | #[error("Couldn't create chain.")] 5 | pub struct CreateChain; 6 | 7 | #[derive(Debug, Error)] 8 | #[error("Couldn't author block.")] 9 | pub struct AuthorBlock; 10 | 11 | #[derive(Debug, Error)] 12 | #[error("Couldn't add authority.")] 13 | pub struct AddAuthority; 14 | 15 | #[derive(Debug, Error)] 16 | #[error("Couldn't remove authority.")] 17 | pub struct RemoveAuthority; 18 | -------------------------------------------------------------------------------- /chain/client/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod error; 2 | mod subxt; 3 | 4 | pub use subxt::*; 5 | 6 | use crate::error::{AddAuthority, AuthorBlock, CreateChain, RemoveAuthority}; 7 | use core::marker::PhantomData; 8 | use libipld::alias; 9 | use libipld::block::Block; 10 | use libipld::cid::Cid; 11 | use libipld::store::{dyn_alias, Store, StoreParams}; 12 | use parity_scale_codec::{Decode, Encode}; 13 | use sp_runtime::traits::CheckedSub; 14 | use std::ops::Deref; 15 | use substrate_subxt::{ 16 | sp_runtime, system::System, Event, EventSubscription, EventsDecoder, Runtime, SignedExtension, 17 | SignedExtra, 18 | }; 19 | use sunshine_client_utils::codec::codec::TreeCodec; 20 | use sunshine_client_utils::codec::hasher::BLAKE2B_256_TREE; 21 | use sunshine_client_utils::codec::trie::{OffchainBlock, TreeDecode, TreeEncode}; 22 | use sunshine_client_utils::GenericBlock; 23 | use sunshine_client_utils::{async_trait, Client, Node, OffchainStore, Result}; 24 | 25 | fn chain_alias(chain_id: R::ChainId) -> String { 26 | dyn_alias(alias!(chain), chain_id.into()) 27 | } 28 | 29 | struct ChainEventSubscription> { 30 | _marker: PhantomData, 31 | subscription: EventSubscription, 32 | } 33 | 34 | impl> ChainEventSubscription { 35 | async fn subscribe(client: &substrate_subxt::Client) -> Result { 36 | let sub = client.subscribe_events().await?; 37 | let mut decoder = EventsDecoder::::new(client.metadata().clone()); 38 | decoder.with_chain(); 39 | let mut subscription = EventSubscription::::new(sub, decoder); 40 | subscription.filter_event::(); 41 | Ok(Self { 42 | _marker: PhantomData, 43 | subscription, 44 | }) 45 | } 46 | 47 | async fn next(&mut self) -> Option> { 48 | match self.subscription.next().await { 49 | Some(Ok(raw)) => Some(E::decode(&mut &raw.data[..]).map_err(Into::into)), 50 | Some(Err(err)) => Some(Err(err.into())), 51 | None => None, 52 | } 53 | } 54 | } 55 | 56 | struct NewBlockSubscription { 57 | sub: ChainEventSubscription>, 58 | chain_id: R::ChainId, 59 | next: R::Number, 60 | } 61 | 62 | impl NewBlockSubscription { 63 | async fn subscribe( 64 | client: &substrate_subxt::Client, 65 | chain_id: R::ChainId, 66 | start: R::Number, 67 | ) -> Result { 68 | let sub = ChainEventSubscription::subscribe(client).await?; 69 | Ok(Self { 70 | sub, 71 | chain_id, 72 | next: start, 73 | }) 74 | } 75 | 76 | fn set_next(&mut self, next: R::Number) { 77 | self.next = next; 78 | } 79 | 80 | async fn next(&mut self) -> Option>> { 81 | while let Some(res) = self.sub.next().await { 82 | if res.is_err() { 83 | return Some(res); 84 | } 85 | let event = res.unwrap(); 86 | if event.chain_id != self.chain_id { 87 | continue; 88 | } 89 | if event.number < self.next { 90 | continue; 91 | } 92 | if event.number > self.next { 93 | unreachable!(); 94 | } 95 | self.next = self.next + 1u8.into(); 96 | return Some(Ok(event)); 97 | } 98 | None 99 | } 100 | } 101 | 102 | pub struct BlockSubscription { 103 | _marker: PhantomData, 104 | store: S, 105 | sub: NewBlockSubscription, 106 | sync_buf: Vec>, 107 | alias: String, 108 | } 109 | 110 | impl BlockSubscription 111 | where 112 | ::Codecs: Into, 113 | { 114 | async fn subscribe( 115 | client: &substrate_subxt::Client, 116 | store: &S, 117 | chain_id: R::ChainId, 118 | start: R::Number, 119 | ) -> Result { 120 | let alias = chain_alias::(chain_id); 121 | let mut sub = NewBlockSubscription::subscribe(client, chain_id, start).await?; 122 | let height = client.chain_height(chain_id, None).await?; 123 | let mut sync_buf = 124 | Vec::with_capacity(height.checked_sub(&start).unwrap_or_default().into() as usize); 125 | if height > start { 126 | let root: Cid = client.chain_root(chain_id, None).await?.unwrap().into(); 127 | let mut next = root; 128 | loop { 129 | let block = Self::fetch_block(&store, &next).await?; 130 | let ancestor = block.ancestor; 131 | let number = block.number; 132 | sync_buf.push(block); 133 | if number <= start { 134 | break; 135 | } 136 | next = ancestor.unwrap().into(); 137 | } 138 | store.alias(&alias, Some(&root)).await?; 139 | } 140 | sub.set_next(height); 141 | Ok(Self { 142 | _marker: PhantomData, 143 | store: store.clone(), 144 | sub, 145 | sync_buf, 146 | alias, 147 | }) 148 | } 149 | 150 | async fn fetch_block( 151 | store: &S, 152 | cid: &Cid, 153 | ) -> Result> { 154 | let block: OffchainBlock = store.get(cid).await?.decode::()?; 155 | GenericBlock::decode(&block) 156 | } 157 | 158 | pub async fn next(&mut self) -> Option>> { 159 | if let Some(next) = self.sync_buf.pop() { 160 | return Some(Ok(next)); 161 | } 162 | if let Some(res) = self.sub.next().await { 163 | Some( 164 | async move { 165 | let cid = res?.root.into(); 166 | let block = Self::fetch_block(&self.store, &cid).await?; 167 | self.store.alias(&self.alias, Some(&cid)).await?; 168 | Ok(block) 169 | } 170 | .await, 171 | ) 172 | } else { 173 | None 174 | } 175 | } 176 | } 177 | 178 | #[async_trait] 179 | pub trait ChainClient: Client + Sized 180 | where 181 | N::Runtime: Chain, 182 | { 183 | async fn create_chain(&self) -> Result<::ChainId>; 184 | async fn author_block( 185 | &self, 186 | chain_id: ::ChainId, 187 | block: &B, 188 | ) -> Result<::Number>; 189 | async fn subscribe( 190 | &self, 191 | chain_id: ::ChainId, 192 | number: ::Number, 193 | ) -> Result, B>>; 194 | async fn authorities( 195 | &self, 196 | chain_id: ::ChainId, 197 | ) -> Result::AccountId>>; 198 | async fn add_authority( 199 | &self, 200 | chain_id: ::ChainId, 201 | authority: &::AccountId, 202 | ) -> Result<::Number>; 203 | async fn remove_authority( 204 | &self, 205 | chain_id: ::ChainId, 206 | authority: &::AccountId, 207 | ) -> Result<::Number>; 208 | } 209 | 210 | #[async_trait] 211 | impl ChainClient for C 212 | where 213 | N: Node, 214 | N::Runtime: Chain, 215 | <<::Extra as SignedExtra>::Extra as SignedExtension>::AdditionalSigned: Send + Sync, 216 | C: Client, 217 | { 218 | async fn create_chain(&self) -> Result<::ChainId> { 219 | Ok(self 220 | .chain_client() 221 | .create_chain_and_watch(&self.chain_signer()?) 222 | .await? 223 | .new_chain()? 224 | .ok_or(CreateChain)? 225 | .chain_id) 226 | } 227 | 228 | async fn author_block( 229 | &self, 230 | chain_id: ::ChainId, 231 | block: &B, 232 | ) -> Result<::Number> { 233 | let signer = self.chain_signer()?; 234 | let mut number = self.chain_client().chain_height(chain_id, None).await?; 235 | loop { 236 | let ancestor = self.chain_client().chain_root(chain_id, None).await?; 237 | let full_block = GenericBlock::<_, ::Number, ::TrieHasher> { 238 | number, 239 | ancestor, 240 | payload: block, 241 | }; 242 | let sealed = full_block.seal()?; 243 | let block = Block::encode(TreeCodec, BLAKE2B_256_TREE, &sealed.offchain)?; 244 | log::info!( 245 | "created block {:?} {:?} with ancestor {:?}", 246 | number, 247 | block.cid(), 248 | ancestor 249 | ); 250 | self.offchain_client().insert(&block).await?; 251 | let result = self 252 | .chain_client() 253 | .author_block_and_watch(&signer, chain_id, *sealed.offchain.root(), &sealed.proof) 254 | .await; 255 | if let Err(err) = &result { 256 | let height = self.chain_client().chain_height(chain_id, None).await?; 257 | if height > number { 258 | number = height; 259 | log::info!("chain height changed {:?}, retrying.\n{:?}", height, err); 260 | continue; 261 | } 262 | } 263 | result?.new_block()?.ok_or(AuthorBlock)?; 264 | return Ok(number); 265 | } 266 | } 267 | 268 | async fn subscribe( 269 | &self, 270 | chain_id: ::ChainId, 271 | number: ::Number, 272 | ) -> Result, B>> { 273 | BlockSubscription::subscribe( 274 | self.chain_client(), 275 | self.offchain_client().deref(), 276 | chain_id, 277 | number, 278 | ) 279 | .await 280 | } 281 | 282 | async fn authorities(&self, chain_id: ::ChainId) -> Result::AccountId>> { 283 | Ok(self.chain_client().authorities(chain_id, None).await?) 284 | } 285 | 286 | async fn add_authority( 287 | &self, 288 | chain_id: ::ChainId, 289 | authority: &::AccountId, 290 | ) -> Result<::Number> { 291 | Ok(self 292 | .chain_client() 293 | .add_authority_and_watch(&self.chain_signer()?, chain_id, authority) 294 | .await? 295 | .authority_added()? 296 | .ok_or(AddAuthority)? 297 | .number) 298 | } 299 | 300 | async fn remove_authority( 301 | &self, 302 | chain_id: ::ChainId, 303 | authority: &::AccountId, 304 | ) -> Result<::Number> { 305 | Ok(self 306 | .chain_client() 307 | .remove_authority_and_watch(&self.chain_signer()?, chain_id, authority) 308 | .await? 309 | .authority_removed()? 310 | .ok_or(RemoveAuthority)? 311 | .number) 312 | } 313 | } 314 | 315 | #[cfg(test)] 316 | mod tests { 317 | use async_std::prelude::*; 318 | use parity_scale_codec::{Decode, Encode}; 319 | use test_client::chain::{Chain, ChainClient, ChainRootStoreExt}; 320 | use test_client::client::{AccountKeyring, Client as _, Node as _}; 321 | use test_client::{Client, Node, Runtime}; 322 | 323 | #[derive(Clone, Debug, Eq, PartialEq, Decode, Encode)] 324 | struct Block { 325 | description: String, 326 | } 327 | 328 | async fn assert_chain_pinned(client: &Client, chain_id: ::Number) { 329 | let root = client 330 | .chain_client() 331 | .chain_root(chain_id, None) 332 | .await 333 | .unwrap() 334 | .unwrap() 335 | .into(); 336 | assert_eq!( 337 | client.offchain_client().pinned(&root).await.unwrap(), 338 | Some(true) 339 | ); 340 | } 341 | 342 | #[async_std::test] 343 | async fn test_chain() { 344 | env_logger::try_init().ok(); 345 | let node = Node::new_mock(); 346 | let (client, _tmp) = Client::mock(&node, AccountKeyring::Alice).await; 347 | 348 | let chain_id = client.create_chain().await.unwrap(); 349 | assert_eq!(chain_id, 0); 350 | 351 | assert_eq!(client.authorities(chain_id).await.unwrap().len(), 1); 352 | 353 | let number = client 354 | .add_authority(chain_id, &AccountKeyring::Eve.to_account_id()) 355 | .await 356 | .unwrap(); 357 | assert_eq!(number, 0); 358 | assert_eq!(client.authorities(chain_id).await.unwrap().len(), 2); 359 | 360 | let number = client 361 | .remove_authority(chain_id, &AccountKeyring::Eve.to_account_id()) 362 | .await 363 | .unwrap(); 364 | assert_eq!(number, 0); 365 | assert_eq!(client.authorities(chain_id).await.unwrap().len(), 1); 366 | 367 | let mut sub = client.subscribe(chain_id, 0).await.unwrap(); 368 | 369 | let mut block = Block { 370 | description: "the genesis block".into(), 371 | }; 372 | let number = client.author_block(chain_id, &block).await.unwrap(); 373 | assert_eq!(number, 0); 374 | 375 | let block2 = sub.next().await.unwrap().unwrap(); 376 | assert_eq!(block2.number, number); 377 | assert!(block2.ancestor.is_none()); 378 | assert_eq!(block, block2.payload); 379 | 380 | block.description = "first block".into(); 381 | let number = client.author_block(chain_id, &block).await.unwrap(); 382 | assert_eq!(number, 1); 383 | 384 | let block2 = sub.next().await.unwrap().unwrap(); 385 | assert_eq!(block2.number, number); 386 | assert!(block2.ancestor.is_some()); 387 | assert_eq!(block, block2.payload); 388 | 389 | assert_chain_pinned(&client, chain_id).await; 390 | } 391 | 392 | #[async_std::test] 393 | async fn test_sync() { 394 | env_logger::try_init().ok(); 395 | let node = Node::new_mock(); 396 | let (client, _tmp) = Client::mock(&node, AccountKeyring::Alice).await; 397 | 398 | let chain_id = client.create_chain().await.unwrap(); 399 | assert_eq!(chain_id, 0); 400 | 401 | client.author_block(chain_id, &0u64).await.unwrap(); 402 | client.author_block(chain_id, &1u64).await.unwrap(); 403 | client.author_block(chain_id, &2u64).await.unwrap(); 404 | 405 | let mut sub = client.subscribe::(chain_id, 1).await.unwrap(); 406 | let b1 = sub.next().await.unwrap().unwrap(); 407 | assert_eq!(b1.payload, 1); 408 | let b2 = sub.next().await.unwrap().unwrap(); 409 | assert_eq!(b2.payload, 2); 410 | 411 | assert_chain_pinned(&client, chain_id).await; 412 | } 413 | 414 | #[async_std::test] 415 | async fn test_concurrent() { 416 | env_logger::try_init().ok(); 417 | let node = Node::new_mock(); 418 | let (client1, _tmp) = Client::mock(&node, AccountKeyring::Alice).await; 419 | let (client2, _tmp) = Client::mock(&node, AccountKeyring::Bob).await; 420 | 421 | let chain_id = client1.create_chain().await.unwrap(); 422 | assert_eq!(chain_id, 0); 423 | client1 424 | .add_authority(chain_id, &AccountKeyring::Bob.to_account_id()) 425 | .await 426 | .unwrap(); 427 | 428 | let a = client1.author_block(chain_id, &0u64); 429 | let b = client2.author_block(chain_id, &1u64); 430 | 431 | let (ra, rb) = a.join(b).await; 432 | ra.unwrap(); 433 | rb.unwrap(); 434 | } 435 | } 436 | -------------------------------------------------------------------------------- /chain/client/src/subxt.rs: -------------------------------------------------------------------------------- 1 | use frame_support::Parameter; 2 | use parity_scale_codec::{Decode, Encode}; 3 | use sp_core::Hasher; 4 | use sp_runtime::traits::{CheckedAdd, CheckedSub, Member}; 5 | use std::marker::PhantomData; 6 | use substrate_subxt::system::{System, SystemEventsDecoder}; 7 | use substrate_subxt::{module, Call, Event, Store}; 8 | use substrate_subxt::{sp_core, sp_runtime}; 9 | use sunshine_client_utils::codec::Cid; 10 | 11 | #[module] 12 | pub trait Chain: System { 13 | /// Chain ID type. 14 | type ChainId: Parameter + Member + Copy + Default + CheckedAdd + From + Into; 15 | 16 | /// Trie hasher. 17 | #[module(ignore)] 18 | type TrieHasher: Hasher; 19 | 20 | /// Trie hash. 21 | type TrieHash: Parameter 22 | + Member 23 | + AsRef<[u8]> 24 | + AsMut<[u8]> 25 | + Eq 26 | + Default 27 | + Copy 28 | + core::hash::Hash 29 | + Into; 30 | 31 | /// Block number type. 32 | type Number: Parameter 33 | + Member 34 | + Copy 35 | + Default 36 | + CheckedAdd 37 | + CheckedSub 38 | + From 39 | + Encode 40 | + Ord 41 | + Into; 42 | } 43 | 44 | #[derive(Clone, Debug, Eq, Encode, PartialEq, Store)] 45 | pub struct AuthoritiesStore { 46 | #[store(returns = Vec<::AccountId>)] 47 | pub chain_id: T::ChainId, 48 | } 49 | 50 | #[derive(Clone, Debug, Eq, Encode, PartialEq, Store)] 51 | pub struct ChainRootStore { 52 | #[store(returns = Option)] 53 | pub chain_id: T::ChainId, 54 | } 55 | 56 | #[derive(Clone, Debug, Eq, Encode, PartialEq, Store)] 57 | pub struct ChainHeightStore { 58 | #[store(returns = T::Number)] 59 | pub chain_id: T::ChainId, 60 | } 61 | 62 | #[derive(Call, Clone, Debug, Eq, Encode, PartialEq)] 63 | pub struct CreateChainCall { 64 | pub _runtime: PhantomData, 65 | } 66 | 67 | #[derive(Call, Clone, Debug, Eq, Encode, PartialEq)] 68 | pub struct AddAuthorityCall<'a, T: Chain> { 69 | pub chain_id: T::ChainId, 70 | pub authority: &'a ::AccountId, 71 | } 72 | 73 | #[derive(Call, Clone, Debug, Eq, Encode, PartialEq)] 74 | pub struct RemoveAuthorityCall<'a, T: Chain> { 75 | pub chain_id: T::ChainId, 76 | pub authority: &'a ::AccountId, 77 | } 78 | 79 | #[derive(Call, Clone, Debug, Eq, Encode, PartialEq)] 80 | pub struct AuthorBlockCall<'a, T: Chain> { 81 | pub chain_id: T::ChainId, 82 | pub root: T::TrieHash, 83 | pub proof: &'a [Vec], 84 | } 85 | 86 | #[derive(Clone, Debug, Decode, Eq, Event, PartialEq)] 87 | pub struct NewChainEvent { 88 | pub chain_id: T::ChainId, 89 | } 90 | 91 | #[derive(Clone, Debug, Decode, Eq, Event, PartialEq)] 92 | pub struct NewBlockEvent { 93 | pub chain_id: T::ChainId, 94 | pub number: T::Number, 95 | pub who: ::AccountId, 96 | pub root: T::TrieHash, 97 | } 98 | 99 | #[derive(Clone, Debug, Decode, Eq, Event, PartialEq)] 100 | pub struct AuthorityAddedEvent { 101 | pub chain_id: T::ChainId, 102 | pub number: T::Number, 103 | pub who: ::AccountId, 104 | pub authority: ::AccountId, 105 | } 106 | 107 | #[derive(Clone, Debug, Decode, Eq, Event, PartialEq)] 108 | pub struct AuthorityRemovedEvent { 109 | pub chain_id: T::ChainId, 110 | pub number: T::Number, 111 | pub who: ::AccountId, 112 | pub authority: ::AccountId, 113 | } 114 | -------------------------------------------------------------------------------- /chain/pallet/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sunshine-chain-pallet" 3 | version = "0.1.0" 4 | authors = ["David Craven "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | frame-support = { version = "2.0.0", default-features = false } 9 | frame-system = { version = "2.0.0", default-features = false } 10 | orml-utilities = { version = "0.2.0", default-features = false } 11 | parity-scale-codec = { version = "1.3.5", default-features = false } 12 | sp-core = { version = "2.0.0", default-features = false } 13 | sp-runtime = { version = "2.0.0", default-features = false } 14 | sp-std = { version = "2.0.0", default-features = false } 15 | sp-trie = { version = "2.0.0", default-features = false } 16 | 17 | [dev-dependencies] 18 | sp-io = { version = "2.0.0", default-features = false } 19 | sunshine-client-utils = { version = "0.1.0", git = "https://github.com/sunshine-protocol/sunshine-core" } 20 | 21 | [features] 22 | default = ["std"] 23 | std = [ 24 | "frame-support/std", 25 | "frame-system/std", 26 | "orml-utilities/std", 27 | "parity-scale-codec/std", 28 | "sp-core/std", 29 | "sp-runtime/std", 30 | "sp-std/std", 31 | "sp-trie/std", 32 | ] 33 | -------------------------------------------------------------------------------- /chain/pallet/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Chain module. 2 | #![cfg_attr(not(feature = "std"), no_std)] 3 | 4 | #[cfg(test)] 5 | mod mock; 6 | 7 | #[cfg(test)] 8 | mod tests; 9 | 10 | use frame_support::dispatch::DispatchResult; 11 | use frame_support::{decl_error, decl_event, decl_module, decl_storage, Parameter}; 12 | use frame_system::{ensure_signed, Trait as System}; 13 | use orml_utilities::OrderedSet; 14 | use parity_scale_codec::Encode; 15 | use sp_core::Hasher; 16 | use sp_runtime::traits::{CheckedAdd, Member}; 17 | use sp_std::prelude::*; 18 | use sp_trie::Layout; 19 | 20 | /// The pallet's configuration trait. 21 | pub trait Trait: System { 22 | /// Chain ID type. 23 | type ChainId: Parameter + Member + Copy + Default + CheckedAdd + From; 24 | 25 | /// Block number type. 26 | type Number: Parameter + Member + Copy + Default + CheckedAdd + From + Encode + Ord; 27 | 28 | /// Trie hasher. 29 | type TrieHasher: Hasher; 30 | 31 | /// Trie hash. 32 | type TrieHash: Parameter 33 | + Member 34 | + AsRef<[u8]> 35 | + AsMut<[u8]> 36 | + Eq 37 | + Default 38 | + Copy 39 | + core::hash::Hash; 40 | 41 | /// The overarching event type. 42 | type Event: From> + Into<::Event>; 43 | } 44 | 45 | decl_storage! { 46 | trait Store for Module as ChainModule { 47 | ChainIdCounter: T::ChainId; 48 | 49 | pub Authorities get(fn authorities): map 50 | hasher(blake2_128_concat) T::ChainId 51 | => OrderedSet<::AccountId>; 52 | 53 | pub ChainRoot get(fn chain_head): map 54 | hasher(blake2_128_concat) T::ChainId 55 | => Option; 56 | 57 | pub ChainHeight get(fn block_number): map 58 | hasher(blake2_128_concat) T::ChainId 59 | => ::Number; 60 | } 61 | } 62 | 63 | decl_event! { 64 | pub enum Event 65 | where 66 | AccountId = ::AccountId, 67 | Number = ::Number, 68 | ChainId = ::ChainId, 69 | TrieHash = ::TrieHash, 70 | { 71 | NewChain(ChainId), 72 | NewBlock(ChainId, Number, AccountId, TrieHash), 73 | AuthorityAdded(ChainId, Number, AccountId, AccountId), 74 | AuthorityRemoved(ChainId, Number, AccountId, AccountId), 75 | } 76 | } 77 | 78 | decl_error! { 79 | pub enum Error for Module { 80 | /// Unauthorized to create a block for the chain. 81 | Unauthorized, 82 | /// Invalid proof occurs when the block number or 83 | /// the previous block don't match what's on chain. 84 | /// This can occur due to a race condition, and the 85 | /// user needs to resubmit with updated fields. 86 | InvalidProof, 87 | /// ChainId overflow. 88 | ChainIdOverflow, 89 | /// Block number overflow. 90 | BlockNumberOverflow, 91 | } 92 | } 93 | 94 | decl_module! { 95 | pub struct Module for enum Call where origin: T::Origin { 96 | type Error = Error; 97 | 98 | fn deposit_event() = default; 99 | 100 | /// Create a new chain. 101 | #[weight = 0] 102 | pub fn create_chain(origin) -> DispatchResult { 103 | let who = ensure_signed(origin)?; 104 | let chain_id = >::get(); 105 | let next_chain_id = chain_id 106 | .checked_add(&1u8.into()) 107 | .ok_or(Error::::ChainIdOverflow)?; 108 | >::put(next_chain_id); 109 | Self::deposit_event(RawEvent::NewChain(chain_id)); 110 | Self::add_authority_to_chain(chain_id, who.clone(), who); 111 | Ok(()) 112 | } 113 | 114 | /// Add an authority. 115 | #[weight = 0] 116 | pub fn add_authority( 117 | origin, 118 | chain_id: T::ChainId, 119 | authority: ::AccountId, 120 | ) -> DispatchResult { 121 | let who = ensure_signed(origin)?; 122 | Self::ensure_authorized(chain_id, &who)?; 123 | Self::add_authority_to_chain(chain_id, who, authority); 124 | Ok(()) 125 | } 126 | 127 | /// Remove an authority. 128 | #[weight = 0] 129 | pub fn remove_authority(origin, chain_id: T::ChainId, authority: ::AccountId) -> DispatchResult { 130 | let who = ensure_signed(origin)?; 131 | Self::ensure_authorized(chain_id, &who)?; 132 | Self::remove_authority_from_chain(chain_id, who, authority); 133 | Ok(()) 134 | } 135 | 136 | /// Author block. 137 | #[weight = 0] 138 | pub fn author_block( 139 | origin, 140 | chain_id: T::ChainId, 141 | root: T::TrieHash, 142 | proof: Vec>, 143 | ) -> DispatchResult { 144 | let who = ensure_signed(origin)?; 145 | Self::ensure_authorized(chain_id, &who)?; 146 | let ancestor = >::get(chain_id); 147 | let number = >::get(chain_id); 148 | let height = number.checked_add(&1u8.into()) 149 | .ok_or(Error::::BlockNumberOverflow)?; 150 | sp_trie::verify_trie_proof::, _, _, _>( 151 | &root, 152 | &proof, 153 | &[ 154 | (&b"number"[..], Some(number.encode())), 155 | (&b"ancestor"[..], Some(ancestor.encode())), 156 | ], 157 | ).map_err(|_| Error::::InvalidProof)?; 158 | >::insert(chain_id, root); 159 | >::insert(chain_id, height); 160 | Self::deposit_event(RawEvent::NewBlock(chain_id, number, who, root)); 161 | Ok(()) 162 | } 163 | } 164 | } 165 | 166 | impl Module { 167 | fn height(chain_id: T::ChainId) -> T::Number { 168 | >::get(chain_id) 169 | } 170 | 171 | fn is_authority(chain_id: T::ChainId, who: &::AccountId) -> bool { 172 | >::get(chain_id).contains(who) 173 | } 174 | 175 | fn ensure_authorized( 176 | chain_id: T::ChainId, 177 | who: &::AccountId, 178 | ) -> Result<(), Error> { 179 | if Self::is_authority(chain_id, who) { 180 | Ok(()) 181 | } else { 182 | Err(Error::::Unauthorized) 183 | } 184 | } 185 | 186 | fn add_authority_to_chain( 187 | chain_id: T::ChainId, 188 | who: ::AccountId, 189 | authority: ::AccountId, 190 | ) { 191 | if !Self::is_authority(chain_id, &authority) { 192 | >::mutate(chain_id, |authorities| { 193 | authorities.insert(authority.clone()) 194 | }); 195 | let number = Self::height(chain_id); 196 | Self::deposit_event(RawEvent::AuthorityAdded(chain_id, number, who, authority)); 197 | } 198 | } 199 | 200 | fn remove_authority_from_chain( 201 | chain_id: T::ChainId, 202 | who: ::AccountId, 203 | authority: ::AccountId, 204 | ) { 205 | if Self::is_authority(chain_id, &authority) { 206 | >::mutate(chain_id, |authorities| authorities.remove(&authority)); 207 | let number = Self::height(chain_id); 208 | Self::deposit_event(RawEvent::AuthorityRemoved(chain_id, number, who, authority)); 209 | } 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /chain/pallet/src/mock.rs: -------------------------------------------------------------------------------- 1 | use crate::{Module, Trait}; 2 | use frame_support::{impl_outer_origin, parameter_types, weights::Weight}; 3 | use frame_system as system; 4 | use sp_core::H256; 5 | use sp_runtime::{ 6 | testing::Header, 7 | traits::{BlakeTwo256, IdentityLookup}, 8 | Perbill, 9 | }; 10 | 11 | impl_outer_origin! { 12 | pub enum Origin for Test {} 13 | } 14 | 15 | #[derive(Clone, Eq, PartialEq)] 16 | pub struct Test; 17 | 18 | parameter_types! { 19 | pub const BlockHashCount: u64 = 250; 20 | pub const MaximumBlockWeight: Weight = 1024; 21 | pub const MaximumBlockLength: u32 = 2 * 1024; 22 | pub const AvailableBlockRatio: Perbill = Perbill::from_percent(75); 23 | } 24 | impl system::Trait for Test { 25 | type BaseCallFilter = (); 26 | type Origin = Origin; 27 | type Call = (); 28 | type Index = u64; 29 | type BlockNumber = u64; 30 | type Hash = H256; 31 | type Hashing = BlakeTwo256; 32 | type AccountId = u64; 33 | type Lookup = IdentityLookup; 34 | type Header = Header; 35 | type Event = (); 36 | type BlockHashCount = BlockHashCount; 37 | type MaximumBlockWeight = MaximumBlockWeight; 38 | type DbWeight = (); 39 | type BlockExecutionWeight = (); 40 | type ExtrinsicBaseWeight = (); 41 | type MaximumExtrinsicWeight = MaximumBlockWeight; 42 | type MaximumBlockLength = MaximumBlockLength; 43 | type AvailableBlockRatio = AvailableBlockRatio; 44 | type Version = (); 45 | type PalletInfo = (); 46 | type AccountData = (); 47 | type OnNewAccount = (); 48 | type OnKilledAccount = (); 49 | type SystemWeightInfo = (); 50 | } 51 | impl Trait for Test { 52 | type ChainId = u64; 53 | type Number = u64; 54 | type TrieHasher = sunshine_client_utils::codec::hasher::TreeHasherBlake2b256; 55 | type TrieHash = sunshine_client_utils::codec::hasher::TreeHashBlake2b256; 56 | type Event = (); 57 | } 58 | pub type ChainModule = Module; 59 | 60 | pub fn new_test_ext() -> sp_io::TestExternalities { 61 | system::GenesisConfig::default() 62 | .build_storage::() 63 | .unwrap() 64 | .into() 65 | } 66 | -------------------------------------------------------------------------------- /chain/pallet/src/tests.rs: -------------------------------------------------------------------------------- 1 | use crate::mock::*; 2 | use frame_support::assert_ok; 3 | use sunshine_client_utils::codec::trie::TreeEncode; 4 | use sunshine_client_utils::GenericBlock; 5 | 6 | type Block = GenericBlock<(), u64, sunshine_client_utils::codec::hasher::TreeHasherBlake2b256>; 7 | 8 | #[test] 9 | fn test_block_authoring() { 10 | new_test_ext().execute_with(|| { 11 | let key = Origin::signed(1); 12 | assert_ok!(ChainModule::create_chain(key.clone())); 13 | let chain_id = 0; 14 | 15 | let block = Block { 16 | number: 0, 17 | ancestor: None, 18 | payload: (), 19 | } 20 | .seal() 21 | .unwrap(); 22 | assert_ok!(ChainModule::author_block( 23 | key.clone(), 24 | chain_id, 25 | *block.offchain.root(), 26 | block.proof 27 | )); 28 | 29 | let block = Block { 30 | number: 1, 31 | ancestor: Some(*block.offchain.root()), 32 | payload: (), 33 | } 34 | .seal() 35 | .unwrap(); 36 | assert_ok!(ChainModule::author_block( 37 | key.clone(), 38 | chain_id, 39 | *block.offchain.root(), 40 | block.proof.clone(), 41 | )); 42 | 43 | assert!( 44 | ChainModule::author_block(key, chain_id, *block.offchain.root(), block.proof).is_err() 45 | ); 46 | }); 47 | } 48 | -------------------------------------------------------------------------------- /faucet/cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sunshine-faucet-cli" 3 | version = "0.2.1" 4 | authors = ["David Craven "] 5 | edition = "2018" 6 | description = "Cli for the faucet module." 7 | license = "ISC" 8 | documentation = "https://docs.rs/sunshine-faucet-cli" 9 | repository = "https://github.com/sunshine-protocol/sunshine-identity" 10 | 11 | [dependencies] 12 | clap = "3.0.0-beta.2" 13 | substrate-subxt = "0.12.0" 14 | sunshine-client-utils = { git = "https://github.com/sunshine-protocol/sunshine-core" } 15 | sunshine-faucet-client = { version = "^0.2.0", path = "../client" } 16 | -------------------------------------------------------------------------------- /faucet/cli/src/lib.rs: -------------------------------------------------------------------------------- 1 | use clap::Clap; 2 | use substrate_subxt::balances::Balances; 3 | use sunshine_client_utils::{Node, Result}; 4 | use sunshine_faucet_client::{Faucet, FaucetClient}; 5 | 6 | #[derive(Clone, Debug, Clap)] 7 | pub struct MintCommand; 8 | 9 | impl MintCommand { 10 | pub async fn exec>(&self, client: &C) -> Result<()> 11 | where 12 | N::Runtime: Faucet, 13 | ::Balance: std::fmt::Display, 14 | { 15 | let amount = client.mint().await?.unwrap().amount; 16 | println!("minted {} tokens into your account", amount); 17 | Ok(()) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /faucet/client/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sunshine-faucet-client" 3 | version = "0.2.0" 4 | authors = ["David Craven "] 5 | edition = "2018" 6 | description = "Client for the faucet module." 7 | license = "ISC" 8 | documentation = "https://docs.rs/sunshine-faucet-client" 9 | repository = "https://github.com/sunshine-protocol/sunshine-identity" 10 | 11 | [dependencies] 12 | parity-scale-codec = "1.3.5" 13 | substrate-subxt = "0.12.0" 14 | sunshine-client-utils = { git = "https://github.com/sunshine-protocol/sunshine-core" } 15 | # until subxt supports better event decoding we need to include it as a dependency 16 | sunshine-identity-client = { version = "0.2.0", path = "../../identity/client" } 17 | 18 | [dev-dependencies] 19 | async-std = { version = "1.6.4", features = ["attributes"] } 20 | test-client = { path = "../../bin/client", features = ["mock"] } 21 | -------------------------------------------------------------------------------- /faucet/client/src/lib.rs: -------------------------------------------------------------------------------- 1 | use parity_scale_codec::{Decode, Encode}; 2 | use substrate_subxt::balances::{Balances, BalancesEventsDecoder}; 3 | use substrate_subxt::system::{System, SystemEventsDecoder}; 4 | use substrate_subxt::{module, Call, Event, Runtime, SignedExtension, SignedExtra}; 5 | use sunshine_client_utils::{async_trait, Client, Node, Result}; 6 | use sunshine_identity_client::{Identity, IdentityEventsDecoder}; 7 | 8 | #[module] 9 | pub trait Faucet: Identity + Balances + System {} 10 | 11 | #[derive(Call, Clone, Debug, Eq, Encode, PartialEq)] 12 | pub struct MintCall<'a, T: Faucet> { 13 | pub account: &'a ::AccountId, 14 | } 15 | 16 | #[derive(Clone, Debug, Decode, Eq, Event, PartialEq)] 17 | pub struct MintedEvent { 18 | pub account: ::AccountId, 19 | pub amount: ::Balance, 20 | } 21 | 22 | #[async_trait] 23 | pub trait FaucetClient: Client 24 | where 25 | N::Runtime: Faucet, 26 | { 27 | async fn mint(&self) -> Result>>; 28 | } 29 | 30 | #[async_trait] 31 | impl FaucetClient for C 32 | where 33 | N: Node, 34 | N::Runtime: Faucet, 35 | <<::Extra as SignedExtra>::Extra as SignedExtension>::AdditionalSigned: Send + Sync, 36 | C: Client, 37 | { 38 | async fn mint(&self) -> Result>> { 39 | let account = self.signer()?.account_id(); 40 | let call = MintCall { account }; 41 | let unsigned = self.chain_client().create_unsigned(call)?; 42 | let decoder = self.chain_client().events_decoder::>(); 43 | let event = self 44 | .chain_client() 45 | .submit_and_watch_extrinsic(unsigned, decoder) 46 | .await? 47 | .minted()?; 48 | Ok(event) 49 | } 50 | } 51 | 52 | #[cfg(test)] 53 | mod tests { 54 | use test_client::client::{AccountKeyring, Node as _}; 55 | use test_client::faucet::FaucetClient; 56 | use test_client::{Client, Node}; 57 | 58 | #[async_std::test] 59 | async fn test_mint() { 60 | let node = Node::new_mock(); 61 | let (client, _tmp) = Client::mock(&node, AccountKeyring::Eve).await; 62 | client.mint().await.unwrap(); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /faucet/ffi/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sunshine-faucet-ffi" 3 | version = "0.1.0" 4 | authors = ["David Craven ", "Shady Khalifa "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | sunshine-ffi-utils = { git = "https://github.com/sunshine-protocol/sunshine-core" } 9 | sunshine-client-utils = { git = "https://github.com/sunshine-protocol/sunshine-core" } 10 | substrate-subxt = "0.12.0" 11 | sunshine-faucet-client = { version = "^0.2", path = "../../faucet/client" } 12 | thiserror = "1.0.20" 13 | 14 | [dev-dependencies] 15 | test-client = { path = "../../bin/client" } 16 | -------------------------------------------------------------------------------- /faucet/ffi/src/ffi.rs: -------------------------------------------------------------------------------- 1 | use crate::utils::async_std::sync::RwLock; 2 | use std::marker::PhantomData; 3 | use substrate_subxt::balances::Balances; 4 | use sunshine_client_utils::{Node, Result}; 5 | use sunshine_faucet_client::{Faucet as SunshineFaucet, FaucetClient}; 6 | use thiserror::Error; 7 | 8 | #[derive(Clone, Debug)] 9 | pub struct Faucet<'a, C, N> 10 | where 11 | N: Node, 12 | N::Runtime: SunshineFaucet, 13 | C: FaucetClient + Send + Sync, 14 | { 15 | client: &'a RwLock, 16 | _runtime: PhantomData, 17 | } 18 | 19 | impl<'a, C, N> Faucet<'a, C, N> 20 | where 21 | N: Node, 22 | N::Runtime: SunshineFaucet, 23 | C: FaucetClient + Send + Sync, 24 | { 25 | pub fn new(client: &'a RwLock) -> Self { 26 | Self { 27 | client, 28 | _runtime: PhantomData, 29 | } 30 | } 31 | 32 | pub async fn mint(&self) -> Result<::Balance> { 33 | let event = self.client.read().await.mint().await?; 34 | if let Some(minted) = event { 35 | Ok(minted.amount) 36 | } else { 37 | Err(Error::FailedToMint.into()) 38 | } 39 | } 40 | } 41 | 42 | #[derive(Debug, Error)] 43 | pub enum Error { 44 | #[error("failed to mint")] 45 | FailedToMint, 46 | } 47 | -------------------------------------------------------------------------------- /faucet/ffi/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub use sunshine_ffi_utils as utils; 2 | #[doc(hidden)] 3 | pub mod ffi; 4 | 5 | /// Generate the FFI for the provided runtime 6 | /// 7 | /// ### Example 8 | /// ``` 9 | /// use test_client::Client; 10 | /// use sunshine_faucet_ffi::impl_ffi; 11 | /// 12 | /// impl_ffi!(client: Client); 13 | /// ``` 14 | #[macro_export] 15 | macro_rules! impl_ffi { 16 | () => { 17 | use $crate::ffi::Faucet; 18 | gen_ffi! { 19 | /// Try to mint the current account, this only enabled in testnet and behind a feature flag 20 | /// returned the minted amount or null if there is any errors 21 | Faucet::mint => fn client_faucet_mint() -> String; 22 | } 23 | }; 24 | (client: $client: ty) => { 25 | #[allow(unused)] 26 | use $crate::utils::*; 27 | gen_ffi!(client = $client); 28 | $crate::impl_ffi!(); 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /faucet/ffi/tests/impl_ffi_macro.rs: -------------------------------------------------------------------------------- 1 | use sunshine_faucet_ffi::impl_ffi; 2 | use test_client::Client; 3 | 4 | // Test how the macro expands 5 | // cargo expand --package sunshine-faucet-ffi --test impl_ffi_macro -- test_impl_ffi_macro 6 | #[test] 7 | fn test_impl_ffi_macro() { 8 | impl_ffi!(client: Client); 9 | } 10 | -------------------------------------------------------------------------------- /faucet/pallet/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sunshine-faucet-pallet" 3 | version = "0.2.0" 4 | authors = ["David Craven "] 5 | edition = "2018" 6 | description = "Faucet runtime module." 7 | license = "ISC" 8 | documentation = "https://docs.rs/sunshine-faucet-client" 9 | repository = "https://github.com/sunshine-protocol/sunshine-identity" 10 | 11 | [dependencies] 12 | codec = { package = "parity-scale-codec", version = "1.3.0", default-features = false, features = ["derive"] } 13 | frame-support = { version = "2.0.0", default-features = false } 14 | frame-system = { version = "2.0.0", default-features = false } 15 | pallet-balances = { version = "2.0.0", default-features = false } 16 | sp-core = { version = "2.0.0", default-features = false } 17 | sp-io = { version = "2.0.0", default-features = false } 18 | sp-runtime = { version = "2.0.0", default-features = false } 19 | 20 | [features] 21 | default = ["std"] 22 | std = [ 23 | "codec/std", 24 | "frame-support/std", 25 | "frame-system/std", 26 | "pallet-balances/std", 27 | ] 28 | -------------------------------------------------------------------------------- /faucet/pallet/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Faucet module. 2 | #![cfg_attr(not(feature = "std"), no_std)] 3 | 4 | use frame_support::dispatch::DispatchResult; 5 | use frame_support::traits::Currency; 6 | use frame_support::unsigned::{TransactionSource, TransactionValidity, ValidateUnsigned}; 7 | use frame_support::{decl_event, decl_module}; 8 | use frame_system::{self as system, ensure_none, Trait as System}; 9 | use pallet_balances::{self as balances, Trait as Balances}; 10 | use sp_runtime::transaction_validity::ValidTransaction; 11 | 12 | #[cfg(test)] 13 | mod mock; 14 | 15 | #[cfg(test)] 16 | mod tests; 17 | 18 | /// The pallet's configuration trait. 19 | pub trait Trait: Balances { 20 | const MINT_UNIT: Self::Balance; 21 | type Event: From> + Into<::Event>; 22 | } 23 | 24 | decl_module! { 25 | /// The module declaration. 26 | pub struct Module for enum Call where origin: T::Origin { 27 | // Initialize events. 28 | fn deposit_event() = default; 29 | 30 | /// Mint balance into an account. 31 | #[weight = 0] 32 | pub fn mint(origin, key: ::AccountId) -> DispatchResult { 33 | let _ = ensure_none(origin)?; 34 | let imbalance = as Currency<::AccountId>>::deposit_creating(&key, T::MINT_UNIT); 35 | drop(imbalance); 36 | Self::deposit_event(RawEvent::Minted(key, T::MINT_UNIT)); 37 | Ok(()) 38 | } 39 | } 40 | } 41 | 42 | decl_event!( 43 | pub enum Event 44 | where 45 | AccountId = ::AccountId, 46 | Balance = ::Balance, 47 | { 48 | Minted(AccountId, Balance), 49 | } 50 | ); 51 | 52 | impl ValidateUnsigned for Module { 53 | type Call = Call; 54 | 55 | fn validate_unsigned(_source: TransactionSource, _call: &Self::Call) -> TransactionValidity { 56 | let current_block = >::block_number(); 57 | ValidTransaction::with_tag_prefix("Faucet") 58 | .and_provides(current_block) 59 | .build() 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /faucet/pallet/src/mock.rs: -------------------------------------------------------------------------------- 1 | use crate::{Module, Trait}; 2 | use frame_support::{impl_outer_origin, parameter_types, weights::Weight}; 3 | use frame_system as system; 4 | use pallet_balances as balances; 5 | use sp_core::H256; 6 | use sp_runtime::{ 7 | testing::Header, 8 | traits::{BlakeTwo256, IdentityLookup}, 9 | Perbill, 10 | }; 11 | 12 | impl_outer_origin! { 13 | pub enum Origin for Test {} 14 | } 15 | 16 | #[derive(Clone, Eq, PartialEq)] 17 | pub struct Test; 18 | 19 | parameter_types! { 20 | pub const BlockHashCount: u64 = 250; 21 | pub const MaximumBlockWeight: Weight = 1024; 22 | pub const MaximumBlockLength: u32 = 2 * 1024; 23 | pub const AvailableBlockRatio: Perbill = Perbill::from_percent(75); 24 | } 25 | impl system::Trait for Test { 26 | type BaseCallFilter = (); 27 | type Origin = Origin; 28 | type Call = (); 29 | type Index = u64; 30 | type BlockNumber = u64; 31 | type Hash = H256; 32 | type Hashing = BlakeTwo256; 33 | type AccountId = u64; 34 | type Lookup = IdentityLookup; 35 | type Header = Header; 36 | type Event = (); 37 | type BlockHashCount = BlockHashCount; 38 | type MaximumBlockWeight = MaximumBlockWeight; 39 | type DbWeight = (); 40 | type BlockExecutionWeight = (); 41 | type ExtrinsicBaseWeight = (); 42 | type MaximumExtrinsicWeight = MaximumBlockWeight; 43 | type MaximumBlockLength = MaximumBlockLength; 44 | type AvailableBlockRatio = AvailableBlockRatio; 45 | type Version = (); 46 | type PalletInfo = (); 47 | type AccountData = balances::AccountData; 48 | type OnNewAccount = (); 49 | type OnKilledAccount = (); 50 | type SystemWeightInfo = (); 51 | } 52 | parameter_types! { 53 | pub const ExistentialDeposit: u128 = 500; 54 | pub const MaxLocks: u32 = 50; 55 | } 56 | impl balances::Trait for Test { 57 | type Balance = u128; 58 | type Event = (); 59 | type ExistentialDeposit = ExistentialDeposit; 60 | type MaxLocks = MaxLocks; 61 | type DustRemoval = (); 62 | type AccountStore = system::Module; 63 | type WeightInfo = (); 64 | } 65 | pub const MINT_UNIT: u128 = 10_000_000; 66 | pub type AccountStore = ::AccountStore; 67 | impl Trait for Test { 68 | const MINT_UNIT: Self::Balance = MINT_UNIT; 69 | type Event = (); 70 | } 71 | pub type FaucetModule = Module; 72 | pub type BalancesModule = balances::Module; 73 | 74 | pub fn new_test_ext() -> sp_io::TestExternalities { 75 | system::GenesisConfig::default() 76 | .build_storage::() 77 | .unwrap() 78 | .into() 79 | } 80 | -------------------------------------------------------------------------------- /faucet/pallet/src/tests.rs: -------------------------------------------------------------------------------- 1 | use crate::mock::*; 2 | use frame_support::assert_ok; 3 | use frame_support::traits::StoredMap; 4 | 5 | #[test] 6 | fn mint() { 7 | new_test_ext().execute_with(|| { 8 | let total_issuance = BalancesModule::total_issuance(); 9 | let free_balance = AccountStore::get(&1).free; 10 | assert_ok!(FaucetModule::mint(Origin::from(None), 1)); 11 | assert_eq!(BalancesModule::total_issuance(), total_issuance + MINT_UNIT); 12 | assert_eq!(AccountStore::get(&1).free, free_balance + MINT_UNIT); 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /identity/cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sunshine-identity-cli" 3 | version = "0.2.3" 4 | authors = ["David Craven "] 5 | edition = "2018" 6 | description = "Cli interface for the identity module." 7 | license = "ISC" 8 | documentation = "https://docs.rs/sunshine-identity-cli" 9 | repository = "https://github.com/sunshine-protocol/sunshine-identity" 10 | 11 | [dependencies] 12 | clap = "3.0.0-beta.2" 13 | codec = { version = "1.3.0", package = "parity-scale-codec" } 14 | qr2term = "0.2.1" 15 | substrate-subxt = "0.12.0" 16 | sunshine-cli-utils = { git = "https://github.com/sunshine-protocol/sunshine-core" } 17 | sunshine-identity-client = { version = "0.2.1", path = "../client" } 18 | textwrap = { version = "0.12.1", features = ["terminal_size"] } 19 | -------------------------------------------------------------------------------- /identity/cli/src/account.rs: -------------------------------------------------------------------------------- 1 | use clap::Clap; 2 | use substrate_subxt::sp_core::crypto::Ss58Codec; 3 | use substrate_subxt::system::System; 4 | use sunshine_cli_utils::client::crypto::ss58::Ss58; 5 | use sunshine_cli_utils::{ask_for_new_password, Node, Result}; 6 | use sunshine_identity_client::{Identity, IdentityClient}; 7 | 8 | #[derive(Clone, Debug, Clap)] 9 | pub struct AccountCreateCommand { 10 | pub device: String, 11 | } 12 | 13 | impl AccountCreateCommand { 14 | pub async fn exec>(&self, client: &C) -> Result<()> 15 | where 16 | N::Runtime: Identity, 17 | ::AccountId: Ss58Codec, 18 | { 19 | let device: Ss58 = self.device.parse()?; 20 | client.create_account_for(&device.0).await?; 21 | Ok(()) 22 | } 23 | } 24 | 25 | #[derive(Clone, Debug, Clap)] 26 | pub struct AccountPasswordCommand; 27 | 28 | impl AccountPasswordCommand { 29 | pub async fn exec>(&self, client: &C) -> Result<()> 30 | where 31 | N::Runtime: Identity, 32 | { 33 | let password = ask_for_new_password(8)?; 34 | client.change_password(&password).await?; 35 | Ok(()) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /identity/cli/src/device.rs: -------------------------------------------------------------------------------- 1 | use clap::Clap; 2 | use substrate_subxt::sp_core::crypto::Ss58Codec; 3 | use substrate_subxt::system::System; 4 | use sunshine_cli_utils::client::crypto::ss58::Ss58; 5 | use sunshine_cli_utils::{Node, Result}; 6 | use sunshine_identity_client::{resolve, Identifier, Identity, IdentityClient}; 7 | 8 | #[derive(Clone, Debug, Clap)] 9 | pub struct DeviceAddCommand { 10 | pub device: String, 11 | } 12 | 13 | impl DeviceAddCommand { 14 | pub async fn exec>(&self, client: &C) -> Result<()> 15 | where 16 | N::Runtime: Identity, 17 | ::AccountId: Ss58Codec, 18 | { 19 | let device: Ss58 = self.device.parse()?; 20 | client.add_key(&device.0).await?; 21 | Ok(()) 22 | } 23 | } 24 | 25 | #[derive(Clone, Debug, Clap)] 26 | pub struct DeviceRemoveCommand { 27 | pub device: String, 28 | } 29 | 30 | impl DeviceRemoveCommand { 31 | pub async fn exec>(&self, client: &C) -> Result<()> 32 | where 33 | N::Runtime: Identity, 34 | ::AccountId: Ss58Codec, 35 | { 36 | let device: Ss58 = self.device.parse()?; 37 | client.remove_key(&device.0).await?; 38 | Ok(()) 39 | } 40 | } 41 | 42 | #[derive(Clone, Debug, Clap)] 43 | pub struct DeviceListCommand { 44 | pub identifier: Option, 45 | } 46 | 47 | impl DeviceListCommand { 48 | pub async fn exec>(&self, client: &C) -> Result<()> 49 | where 50 | N::Runtime: Identity, 51 | ::AccountId: Ss58Codec, 52 | { 53 | let identifier: Option> = if let Some(identifier) = &self.identifier 54 | { 55 | Some(identifier.parse()?) 56 | } else { 57 | None 58 | }; 59 | let uid = resolve(client, identifier).await?; 60 | for key in client.fetch_keys(uid, None).await? { 61 | println!("{}", key.to_ss58check()); 62 | } 63 | Ok(()) 64 | } 65 | } 66 | 67 | #[derive(Clone, Debug, Clap)] 68 | pub struct DevicePaperkeyCommand; 69 | 70 | impl DevicePaperkeyCommand { 71 | pub async fn exec>(&self, client: &C) -> Result<()> 72 | where 73 | N::Runtime: Identity, 74 | { 75 | println!("Generating a new paper key."); 76 | let mnemonic = client.add_paperkey().await?; 77 | println!("Here is your secret paper key phrase:"); 78 | let words: Vec<_> = mnemonic.as_str().split(' ').collect(); 79 | println!(); 80 | println!("{}", words[..12].join(" ")); 81 | println!("{}", words[12..].join(" ")); 82 | println!(); 83 | println!("Write it down and keep somewhere safe."); 84 | Ok(()) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /identity/cli/src/id.rs: -------------------------------------------------------------------------------- 1 | use clap::Clap; 2 | use substrate_subxt::sp_core::crypto::Ss58Codec; 3 | use substrate_subxt::system::System; 4 | use sunshine_cli_utils::{Node, Result}; 5 | use sunshine_identity_client::{resolve, Identifier, Identity, IdentityClient, Service}; 6 | 7 | #[derive(Clone, Debug, Clap)] 8 | pub struct IdListCommand { 9 | pub identifier: Option, 10 | } 11 | 12 | impl IdListCommand { 13 | pub async fn exec>(&self, client: &C) -> Result<()> 14 | where 15 | N::Runtime: Identity, 16 | ::AccountId: Ss58Codec, 17 | { 18 | let identifier: Option> = if let Some(identifier) = &self.identifier 19 | { 20 | Some(identifier.parse()?) 21 | } else { 22 | None 23 | }; 24 | let uid = resolve(client, identifier).await?; 25 | println!("Your user id is {}", uid); 26 | for id in client.identity(uid).await? { 27 | println!("{}", id); 28 | } 29 | Ok(()) 30 | } 31 | } 32 | 33 | #[derive(Clone, Debug, Clap)] 34 | pub struct IdProveCommand { 35 | pub service: Service, 36 | } 37 | 38 | impl IdProveCommand { 39 | pub async fn exec>(&self, client: &C) -> Result<()> 40 | where 41 | N::Runtime: Identity, 42 | { 43 | println!("Claiming {}...", self.service); 44 | let instructions = self.service.cli_instructions(); 45 | let proof = client.prove_identity(self.service.clone()).await?; 46 | println!("{}", instructions); 47 | print!("{}", proof); 48 | Ok(()) 49 | } 50 | } 51 | 52 | #[derive(Clone, Debug, Clap)] 53 | pub struct IdRevokeCommand { 54 | pub service: Service, 55 | } 56 | 57 | impl IdRevokeCommand { 58 | pub async fn exec>(&self, client: &C) -> Result<()> 59 | where 60 | N::Runtime: Identity, 61 | { 62 | client.revoke_identity(self.service.clone()).await?; 63 | Ok(()) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /identity/cli/src/key.rs: -------------------------------------------------------------------------------- 1 | use clap::Clap; 2 | pub use sunshine_cli_utils::key::{KeyLockCommand, KeyUnlockCommand}; 3 | use sunshine_cli_utils::{set_key, Node, Result}; 4 | use sunshine_identity_client::{Identity, IdentityClient}; 5 | use textwrap::Wrapper; 6 | 7 | #[derive(Clone, Debug, Clap)] 8 | pub struct KeySetCommand { 9 | /// Overwrite existing keys. 10 | #[clap(short = 'f', long = "force")] 11 | pub force: bool, 12 | 13 | /// Suri. 14 | #[clap(long = "suri")] 15 | pub suri: Option, 16 | 17 | /// Paperkey. 18 | #[clap(long = "paperkey")] 19 | pub paperkey: bool, 20 | } 21 | 22 | impl KeySetCommand { 23 | pub async fn exec>(&self, client: &mut C) -> Result<()> 24 | where 25 | N::Runtime: Identity, 26 | { 27 | let account_id = set_key(client, self.paperkey, self.suri.as_deref(), self.force).await?; 28 | let account_id_str = account_id.to_string(); 29 | println!("Your device id is {}", &account_id_str); 30 | if let Some(uid) = client.fetch_uid(&account_id).await? { 31 | println!("Your user id is {}", uid); 32 | } else { 33 | let p = "Creating an account requires making a `create_account_for` \ 34 | transaction. Or transfering funds to your address. Your wallet \ 35 | contains insufficient funds for paying the transaction fee. Ask \ 36 | someone to scan the qr code with your device id to create an \ 37 | account for you."; 38 | println!("{}\n", Wrapper::with_termwidth().fill(p)); 39 | qr2term::print_qr(&account_id_str)?; 40 | } 41 | Ok(()) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /identity/cli/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod account; 2 | pub mod device; 3 | pub mod id; 4 | pub mod key; 5 | pub mod wallet; 6 | -------------------------------------------------------------------------------- /identity/cli/src/wallet.rs: -------------------------------------------------------------------------------- 1 | use clap::Clap; 2 | use core::fmt::{Debug, Display}; 3 | use substrate_subxt::balances::{AccountData, Balances, TransferCallExt, TransferEventExt}; 4 | use substrate_subxt::sp_core::crypto::Ss58Codec; 5 | use substrate_subxt::system::System; 6 | use substrate_subxt::{Runtime, SignedExtension, SignedExtra}; 7 | pub use sunshine_cli_utils::wallet::{TransferEventDecode, TransferEventFind}; 8 | use sunshine_cli_utils::{Node, Result}; 9 | use sunshine_identity_client::{resolve, Identifier, Identity, IdentityClient}; 10 | 11 | #[derive(Clone, Debug, Clap)] 12 | pub struct WalletBalanceCommand { 13 | pub identifier: Option, 14 | } 15 | 16 | impl WalletBalanceCommand { 17 | pub async fn exec>(&self, client: &C) -> Result<()> 18 | where 19 | N::Runtime: Identity + Balances, 20 | ::AccountId: Ss58Codec, 21 | N::Runtime: Identity::Balance>>, 22 | { 23 | let identifier: Option> = if let Some(identifier) = &self.identifier 24 | { 25 | Some(identifier.parse()?) 26 | } else { 27 | None 28 | }; 29 | let uid = resolve(client, identifier).await?; 30 | let account = client.fetch_account(uid).await?; 31 | println!("{:?}", account.free); 32 | Ok(()) 33 | } 34 | } 35 | 36 | #[derive(Clone, Debug, Clap)] 37 | pub struct WalletTransferCommand { 38 | pub identifier: String, 39 | pub amount: u128, 40 | } 41 | 42 | impl WalletTransferCommand { 43 | pub async fn exec>( 44 | &self, 45 | client: &C, 46 | ) -> Result<()> 47 | where 48 | N::Runtime: Identity + Balances, 49 | ::AccountId: Ss58Codec + Into<::Address>, 50 | <<::Extra as SignedExtra>::Extra as SignedExtension>::AdditionalSigned: 51 | Send + Sync, 52 | ::Balance: From + Display, 53 | { 54 | let identifier: Identifier = self.identifier.parse()?; 55 | let uid = resolve(client, Some(identifier)).await?; 56 | let keys = client.fetch_keys(uid, None).await?; 57 | let signer = client.chain_signer()?; 58 | let event = client 59 | .chain_client() 60 | .transfer_and_watch(&signer, &keys[0].clone().into(), self.amount.into()) 61 | .await? 62 | .transfer() 63 | .map_err(|_| TransferEventDecode)? 64 | .ok_or(TransferEventFind)?; 65 | println!("transfered {} to {}", event.amount, event.to.to_string()); 66 | Ok(()) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /identity/client/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sunshine-identity-client" 3 | version = "0.2.3" 4 | authors = ["David Craven "] 5 | edition = "2018" 6 | description = "Client for the identity module." 7 | license = "ISC" 8 | documentation = "https://docs.rs/sunshine-identity-client" 9 | repository = "https://github.com/sunshine-protocol/sunshine-identity" 10 | 11 | [dependencies] 12 | async-std = { version = "1.6.4", features = ["unstable"] } 13 | codec = { version = "1.3.0", package = "parity-scale-codec" } 14 | frame-support = "2.0.0" 15 | libipld = { version = "0.6.1", features = ["dag-json"] } 16 | log = "0.4.11" 17 | serde = { version = "1.0.116", features = ["derive"] } 18 | # TODO export error in libipld 19 | serde_json = "1.0.57" 20 | substrate-subxt = "0.12.0" 21 | sunshine-client-utils = { git = "https://github.com/sunshine-protocol/sunshine-core" } 22 | ureq = { version = "1.4.1", default-features = false, features = ["tls", "json"] } 23 | thiserror = "1.0.20" 24 | 25 | [dev-dependencies] 26 | async-std = { version = "1.6.4", features = ["attributes"] } 27 | test-client = { path = "../../bin/client", features = ["mock"] } 28 | -------------------------------------------------------------------------------- /identity/client/github-proof-instructions.md: -------------------------------------------------------------------------------- 1 | Please *publicly* post the following Gist, and name it '{gist_name}'. 2 | -------------------------------------------------------------------------------- /identity/client/github-proof-template.md: -------------------------------------------------------------------------------- 1 | ### sunshine identity proof 2 | 3 | I hereby claim: 4 | 5 | * I am {username} on github. 6 | * I am {uid} on the substrate chain with genesis hash {genesis}. 7 | * I have a public key {public} valid at block with hash {block}. 8 | 9 | To claim this, I am signing this object: 10 | 11 | ```json 12 | {object} 13 | ``` 14 | 15 | with the key {public}, yielding the signature: 16 | 17 | ``` 18 | {signature} 19 | ``` 20 | 21 | And finally, I am proving ownership of the github account by posting this as a gist. 22 | -------------------------------------------------------------------------------- /identity/client/src/claim.rs: -------------------------------------------------------------------------------- 1 | use crate::service::Service; 2 | use libipld::cbor::DagCborCodec; 3 | use libipld::cid::Cid; 4 | use libipld::codec::Codec as _; 5 | use libipld::DagCbor; 6 | use std::time::{Duration, UNIX_EPOCH}; 7 | use sunshine_client_utils::Result; 8 | 9 | #[derive(Clone, Debug, Eq, PartialEq, DagCbor)] 10 | pub struct Claim { 11 | claim: UnsignedClaim, 12 | signature: Vec, 13 | } 14 | 15 | impl Claim { 16 | pub fn new(claim: UnsignedClaim, signature: Vec) -> Self { 17 | Self { claim, signature } 18 | } 19 | 20 | pub fn claim(&self) -> &UnsignedClaim { 21 | &self.claim 22 | } 23 | 24 | pub fn signature(&self) -> &[u8] { 25 | &self.signature 26 | } 27 | } 28 | 29 | impl core::fmt::Display for Claim { 30 | fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { 31 | write!(f, "{:?}", self.claim().body) 32 | } 33 | } 34 | 35 | #[derive(Clone, Debug, Eq, PartialEq, DagCbor)] 36 | pub struct UnsignedClaim { 37 | /// The chain that this claim is valid for. 38 | pub genesis: Vec, 39 | /// The block at which the signing key was valid. 40 | pub block: Vec, 41 | /// The user that is making the claim. 42 | pub uid: u64, 43 | /// The public key used for signing the claim. 44 | pub public: String, 45 | /// The previous claim that the user made. 46 | pub prev: Option, 47 | /// The sequence number of the claim. 48 | pub seqno: u32, 49 | /// The time this claim was made at. 50 | pub ctime: u64, 51 | /// The time at which the claim becomes invalid. 52 | pub expire_in: u64, 53 | /// The claim. 54 | pub body: ClaimBody, 55 | } 56 | 57 | impl UnsignedClaim { 58 | pub fn expired(&self) -> bool { 59 | let expires_at = Duration::from_millis(self.ctime.saturating_add(self.expire_in)); 60 | UNIX_EPOCH.elapsed().unwrap() > expires_at 61 | } 62 | 63 | pub fn to_bytes(&self) -> Result> { 64 | Ok(DagCborCodec.encode(self)?.into_boxed_slice()) 65 | } 66 | } 67 | 68 | #[derive(Clone, Debug, Eq, PartialEq, DagCbor)] 69 | pub enum ClaimBody { 70 | Ownership(Service), 71 | Revoke(u32), 72 | } 73 | 74 | #[derive(Clone, Debug, Eq, PartialEq)] 75 | pub enum IdentityStatus { 76 | Expired, 77 | Revoked, 78 | ProofNotFound, 79 | Active(String), 80 | } 81 | 82 | impl core::fmt::Display for IdentityStatus { 83 | fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { 84 | match self { 85 | Self::Expired => write!(f, "expired"), 86 | Self::Revoked => write!(f, "revoked"), 87 | Self::ProofNotFound => write!(f, "proof not found"), 88 | Self::Active(proof) => write!(f, "{}", proof), 89 | } 90 | } 91 | } 92 | 93 | #[derive(Clone, Debug, Eq, PartialEq)] 94 | pub struct IdentityInfo { 95 | pub service: Service, 96 | pub claims: Vec, 97 | pub status: IdentityStatus, 98 | } 99 | 100 | impl core::fmt::Display for IdentityInfo { 101 | fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { 102 | write!(f, "{} {}", self.service, self.status) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /identity/client/src/client.rs: -------------------------------------------------------------------------------- 1 | use crate::claim::{Claim, ClaimBody, IdentityInfo, IdentityStatus, UnsignedClaim}; 2 | use crate::error::{InvalidClaim, NoAccount, NoBlockHash, ResolveFailure, RuntimeInvalid}; 3 | use crate::keystore::{Keystore, Mask}; 4 | use crate::service::Service; 5 | use crate::subxt::*; 6 | use codec::{Decode, Encode}; 7 | use core::convert::TryInto; 8 | use libipld::alias; 9 | use libipld::cache::Cache; 10 | use libipld::cbor::DagCborCodec; 11 | use libipld::cid::Cid; 12 | use libipld::store::Store; 13 | use std::collections::HashMap; 14 | use std::time::Duration; 15 | use std::time::UNIX_EPOCH; 16 | use substrate_subxt::sp_core::crypto::{Pair, Ss58Codec}; 17 | use substrate_subxt::sp_runtime::traits::{IdentifyAccount, SignedExtension, Verify}; 18 | use substrate_subxt::system::System; 19 | use substrate_subxt::{EventSubscription, EventsDecoder, Runtime, SignedExtra}; 20 | use sunshine_client_utils::crypto::{ 21 | bip39::Mnemonic, 22 | keychain::{KeyType, TypedPair}, 23 | secrecy::SecretString, 24 | signer::GenericSigner, 25 | }; 26 | use sunshine_client_utils::{Client, Node, OffchainConfig, Result, Signer}; 27 | 28 | async fn set_identity(client: &C, claim: Claim) -> Result<()> 29 | where 30 | N: Node, 31 | N::Runtime: Identity, 32 | <<::Extra as SignedExtra>::Extra as SignedExtension>::AdditionalSigned: Send + Sync, 33 | C: Client, 34 | C::OffchainClient: Cache, DagCborCodec, Claim>, 35 | { 36 | let alias = alias!(sunshine_keybase_chain); 37 | let prev = claim.claim().prev; 38 | let root = client.offchain_client().insert(claim).await?; 39 | let prev_cid = prev.clone().map(Into::into); 40 | let root_cid = root.clone().into(); 41 | client 42 | .chain_client() 43 | .set_identity_and_watch(&client.chain_signer()?, &prev_cid, &root_cid) 44 | .await? 45 | .identity_changed()?; 46 | client.offchain_client().alias(alias, Some(&root)).await?; 47 | Ok(()) 48 | } 49 | 50 | async fn fetch_identity(client: &C, uid: ::Uid) -> Result> 51 | where 52 | N: Node, 53 | N::Runtime: Identity, 54 | <<::Extra as SignedExtra>::Extra as SignedExtension>::AdditionalSigned: Send + Sync, 55 | C: Client, 56 | { 57 | Ok(client 58 | .chain_client() 59 | .identity(uid, None) 60 | .await? 61 | .map(Into::into)) 62 | } 63 | 64 | async fn create_claim( 65 | client: &C, 66 | body: ClaimBody, 67 | expire_in: Option, 68 | uid: ::Uid, 69 | ) -> Result 70 | where 71 | N: Node, 72 | N::Runtime: Identity, 73 | <<::Extra as SignedExtra>::Extra as SignedExtension>::AdditionalSigned: Send + Sync, 74 | C: Client, 75 | C::OffchainClient: Cache, DagCborCodec, Claim>, 76 | ::AccountId: Ss58Codec, 77 | { 78 | let genesis = client.chain_client().genesis().as_ref().to_vec(); 79 | let block = client 80 | .chain_client() 81 | .block_hash(None) 82 | .await? 83 | .ok_or(NoBlockHash)? 84 | .as_ref() 85 | .to_vec(); 86 | let prev = fetch_identity(client, uid).await?; 87 | let prev_seqno = if let Some(prev) = prev.as_ref() { 88 | client.offchain_client().get(prev).await?.claim().seqno 89 | } else { 90 | 0 91 | }; 92 | let public = client.signer()?.account_id().to_ss58check(); 93 | let expire_in = expire_in 94 | .unwrap_or_else(|| Duration::from_millis(u64::MAX)) 95 | .as_millis() as u64; 96 | let ctime = UNIX_EPOCH.elapsed().unwrap().as_millis() as u64; 97 | let claim = UnsignedClaim { 98 | genesis, 99 | block, 100 | uid: uid.into(), 101 | public, 102 | prev, 103 | seqno: prev_seqno + 1, 104 | ctime, 105 | expire_in, 106 | body, 107 | }; 108 | let signature = client.signer()?.sign(&claim.to_bytes()?); 109 | let signature = Encode::encode(&signature); 110 | Ok(Claim::new(claim, signature)) 111 | } 112 | 113 | async fn verify_claim(client: &C, uid: ::Uid, claim: &Claim) -> Result<()> 114 | where 115 | N: Node, 116 | N::Runtime: Identity, 117 | <<::Extra as SignedExtra>::Extra as SignedExtension>::AdditionalSigned: Send + Sync, 118 | C: Client, 119 | C::OffchainClient: Cache, DagCborCodec, Claim>, 120 | ::Signature: Decode, 121 | <::Signature as Verify>::Signer: IdentifyAccount::AccountId>, 122 | ::AccountId: Ss58Codec, 123 | { 124 | if claim.claim().uid != uid.into() { 125 | return Err(InvalidClaim("uid").into()); 126 | } 127 | if &claim.claim().genesis[..] != client.chain_client().genesis().as_ref() { 128 | return Err(InvalidClaim("genesis").into()); 129 | } 130 | let prev_seqno = if let Some(prev) = claim.claim().prev.as_ref() { 131 | client.offchain_client().get(prev).await?.claim().seqno 132 | } else { 133 | 0 134 | }; 135 | if claim.claim().seqno != prev_seqno + 1 { 136 | return Err(InvalidClaim("seqno").into()); 137 | } 138 | let block = Decode::decode(&mut &claim.claim().block[..])?; 139 | let keys = client.chain_client().keys(uid, Some(block)).await?; 140 | let key = keys 141 | .iter() 142 | .find(|k| k.to_ss58check() == claim.claim().public) 143 | .ok_or(InvalidClaim("key"))?; 144 | let bytes = claim.claim().to_bytes()?; 145 | let signature: ::Signature = Decode::decode(&mut claim.signature())?; 146 | if !signature.verify(&bytes[..], key) { 147 | return Err(InvalidClaim("signature").into()); 148 | } 149 | Ok(()) 150 | } 151 | 152 | pub async fn create_account_for(client: &C, key: &::AccountId) -> Result<()> 153 | where 154 | N: Node, 155 | N::Runtime: Identity, 156 | <<::Extra as SignedExtra>::Extra as SignedExtension>::AdditionalSigned: Send + Sync, 157 | C: Client, 158 | { 159 | client 160 | .chain_client() 161 | .create_account_for_and_watch(&client.chain_signer()?, key) 162 | .await? 163 | .account_created()?; 164 | Ok(()) 165 | } 166 | 167 | pub async fn add_paperkey(client: &C) -> Result 168 | where 169 | N: Node, 170 | N::Runtime: Identity, 171 | ::AccountId: Into<::Address>, 172 | <<::Extra as SignedExtra>::Extra as SignedExtension>::AdditionalSigned: Send + Sync, 173 | <::Signature as Verify>::Signer: From<::Public> 174 | + TryInto<::Public> 175 | + IdentifyAccount::AccountId> 176 | + Clone 177 | + Send 178 | + Sync, 179 | C: Client, 180 | K: KeyType, 181 | ::Signature: Into<::Signature>, 182 | { 183 | let mnemonic = Mnemonic::generate(24).expect("word count is a multiple of six; qed"); 184 | let key = TypedPair::::from_mnemonic(&mnemonic).expect("have enough entropy bits; qed"); 185 | let signer = GenericSigner::::new(key); 186 | add_key(client, signer.account_id()).await?; 187 | Ok(mnemonic) 188 | } 189 | 190 | pub async fn add_key(client: &C, key: &::AccountId) -> Result<()> 191 | where 192 | N: Node, 193 | N::Runtime: Identity, 194 | <<::Extra as SignedExtra>::Extra as SignedExtension>::AdditionalSigned: Send + Sync, 195 | C: Client, 196 | { 197 | client 198 | .chain_client() 199 | .add_key_and_watch(&client.chain_signer()?, key) 200 | .await? 201 | .key_added()?; 202 | Ok(()) 203 | } 204 | 205 | pub async fn remove_key(client: &C, key: &::AccountId) -> Result<()> 206 | where 207 | N: Node, 208 | N::Runtime: Identity, 209 | <<::Extra as SignedExtra>::Extra as SignedExtension>::AdditionalSigned: Send + Sync, 210 | C: Client, 211 | { 212 | client 213 | .chain_client() 214 | .remove_key_and_watch(&client.chain_signer()?, key) 215 | .await? 216 | .key_removed()?; 217 | Ok(()) 218 | } 219 | 220 | pub async fn change_password(client: &C, password: &SecretString) -> Result<()> 221 | where 222 | N: Node, 223 | N::Runtime: Identity, 224 | <<::Extra as SignedExtra>::Extra as SignedExtension>::AdditionalSigned: Send + Sync, 225 | C: Client>, 226 | K: KeyType, 227 | { 228 | let (mask, gen) = client.keystore().change_password_mask(password).await?; 229 | client 230 | .chain_client() 231 | .change_password_and_watch(&client.chain_signer()?, mask.as_ref(), gen) 232 | .await?; 233 | Ok(()) 234 | } 235 | 236 | pub async fn update_password(client: &mut C) -> Result<()> 237 | where 238 | N: Node, 239 | N::Runtime: Identity, 240 | <<::Extra as SignedExtra>::Extra as SignedExtension>::AdditionalSigned: Send + Sync, 241 | C: Client>, 242 | K: KeyType, 243 | { 244 | let uid = fetch_uid(client, &client.signer()?.account_id()) 245 | .await? 246 | .ok_or(NoAccount)?; 247 | let pgen = client.chain_client().password_gen(uid, None).await?; 248 | let gen = client.keystore().gen().await?; 249 | for g in gen..pgen { 250 | log::info!("Password change detected: reencrypting keystore"); 251 | let mask = client 252 | .chain_client() 253 | .password_mask(uid, g + 1, None) 254 | .await? 255 | .ok_or(RuntimeInvalid)?; 256 | client 257 | .keystore_mut() 258 | .apply_mask(&Mask::new(mask), g + 1) 259 | .await?; 260 | } 261 | Ok(()) 262 | } 263 | 264 | pub async fn subscribe_password_changes(client: &C) -> Result> 265 | where 266 | N: Node, 267 | N::Runtime: Identity, 268 | <<::Extra as SignedExtra>::Extra as SignedExtension>::AdditionalSigned: Send + Sync, 269 | C: Client, 270 | { 271 | let subscription = client.chain_client().subscribe_events().await.unwrap(); 272 | let mut decoder = EventsDecoder::::new(client.chain_client().metadata().clone()); 273 | decoder.with_identity(); 274 | let mut subscription = EventSubscription::::new(subscription, decoder); 275 | subscription.filter_event::>(); 276 | Ok(subscription) 277 | } 278 | 279 | pub async fn fetch_uid(client: &C, key: &::AccountId) -> Result::Uid>> 280 | where 281 | N: Node, 282 | N::Runtime: Identity, 283 | <<::Extra as SignedExtra>::Extra as SignedExtension>::AdditionalSigned: Send + Sync, 284 | C: Client, 285 | { 286 | Ok(client.chain_client().uid_lookup(key, None).await?) 287 | } 288 | 289 | pub async fn fetch_keys( 290 | client: &C, 291 | uid: ::Uid, 292 | hash: Option<::Hash>, 293 | ) -> Result::AccountId>> 294 | where 295 | N: Node, 296 | N::Runtime: Identity, 297 | <<::Extra as SignedExtra>::Extra as SignedExtension>::AdditionalSigned: Send + Sync, 298 | C: Client, 299 | { 300 | let keys = client.chain_client().keys(uid, hash).await?; 301 | if keys.is_empty() { 302 | return Err(ResolveFailure.into()); 303 | } 304 | Ok(keys) 305 | } 306 | 307 | pub async fn fetch_account(client: &C, uid: ::Uid) -> Result<::IdAccountData> 308 | where 309 | N: Node, 310 | N::Runtime: Identity, 311 | <<::Extra as SignedExtra>::Extra as SignedExtension>::AdditionalSigned: Send + Sync, 312 | C: Client, 313 | { 314 | Ok(client.chain_client().account(uid, None).await?) 315 | } 316 | 317 | pub async fn prove_identity(client: &C, service: Service) -> Result 318 | where 319 | N: Node, 320 | N::Runtime: Identity, 321 | <<::Extra as SignedExtra>::Extra as SignedExtension>::AdditionalSigned: Send + Sync, 322 | C: Client, 323 | C::OffchainClient: Cache, DagCborCodec, Claim>, 324 | ::AccountId: Ss58Codec, 325 | { 326 | let uid = fetch_uid(client, client.signer()?.account_id()) 327 | .await? 328 | .ok_or(NoAccount)?; 329 | let claim = create_claim(client, ClaimBody::Ownership(service.clone()), None, uid).await?; 330 | let proof = service.proof(&claim)?; 331 | set_identity(client, claim).await?; 332 | Ok(proof) 333 | } 334 | 335 | pub async fn revoke_identity(client: &C, service: Service) -> Result<()> 336 | where 337 | N: Node, 338 | N::Runtime: Identity, 339 | <<::Extra as SignedExtra>::Extra as SignedExtension>::AdditionalSigned: Send + Sync, 340 | C: Client, 341 | C::OffchainClient: Cache, DagCborCodec, Claim>, 342 | ::AccountId: Ss58Codec, 343 | ::Signature: Decode, 344 | <::Signature as Verify>::Signer: IdentifyAccount::AccountId>, 345 | { 346 | let uid = fetch_uid(client, client.signer()?.account_id()) 347 | .await? 348 | .ok_or(NoAccount)?; 349 | let id = identity(client, uid) 350 | .await? 351 | .into_iter() 352 | .find(|id| id.service == service && id.status != IdentityStatus::Revoked); 353 | if let Some(id) = id { 354 | let seqno = id.claims.last().unwrap().claim().seqno; 355 | let claim = create_claim(client, ClaimBody::Revoke(seqno), None, uid).await?; 356 | set_identity(client, claim).await?; 357 | } 358 | Ok(()) 359 | } 360 | 361 | pub async fn identity(client: &C, uid: ::Uid) -> Result> 362 | where 363 | N: Node, 364 | N::Runtime: Identity, 365 | <<::Extra as SignedExtra>::Extra as SignedExtension>::AdditionalSigned: Send + Sync, 366 | C: Client, 367 | C::OffchainClient: Cache, DagCborCodec, Claim>, 368 | ::AccountId: Ss58Codec, 369 | ::Signature: Decode, 370 | <::Signature as Verify>::Signer: IdentifyAccount::AccountId>, 371 | { 372 | let mut claims = vec![]; 373 | let mut next = fetch_identity(client, uid).await?; 374 | while let Some(cid) = next { 375 | let claim = client.offchain_client().get(&cid).await?; 376 | next = claim.claim().prev; 377 | verify_claim(client, uid, &claim).await?; 378 | claims.push(claim); 379 | } 380 | let mut ids = HashMap::>::new(); 381 | for claim in claims.iter().rev() { 382 | match claim.claim().body.clone() { 383 | ClaimBody::Ownership(service) => { 384 | ids.entry(service.clone()).or_default().push(claim.clone()); 385 | } 386 | ClaimBody::Revoke(seqno) => { 387 | let index = claims.len() - seqno as usize; 388 | if let Some(claim2) = claims.get(index) { 389 | if let ClaimBody::Ownership(service) = &claim2.claim().body { 390 | ids.entry(service.clone()).or_default().push(claim.clone()); 391 | } else { 392 | return Err(InvalidClaim("cannot revoke: claim is not revokable").into()); 393 | } 394 | } else { 395 | return Err(InvalidClaim("cannot revoke: claim not found").into()); 396 | } 397 | } 398 | } 399 | } 400 | 401 | let mut info = vec![]; 402 | for (service, claims) in ids.into_iter() { 403 | let mut status = IdentityStatus::ProofNotFound; 404 | let mut proof = None; 405 | for claim in &claims { 406 | match &claim.claim().body { 407 | ClaimBody::Ownership(_) => { 408 | if claim.claim().expired() { 409 | status = IdentityStatus::Expired; 410 | } else { 411 | status = IdentityStatus::ProofNotFound; 412 | proof = Some(claim); 413 | } 414 | } 415 | ClaimBody::Revoke(seqno) => { 416 | if let Some(p) = proof { 417 | if p.claim().seqno == *seqno { 418 | status = IdentityStatus::Revoked; 419 | proof = None; 420 | } 421 | } 422 | } 423 | } 424 | } 425 | if status == IdentityStatus::ProofNotFound { 426 | if let Some(proof) = proof { 427 | if let Ok(proof_url) = service.verify(proof.signature()).await { 428 | status = IdentityStatus::Active(proof_url); 429 | } 430 | } 431 | } 432 | info.push(IdentityInfo { 433 | service, 434 | claims, 435 | status, 436 | }); 437 | } 438 | Ok(info) 439 | } 440 | 441 | pub async fn resolve(client: &C, service: &Service) -> Result<::Uid> 442 | where 443 | N: Node, 444 | N::Runtime: Identity, 445 | <<::Extra as SignedExtra>::Extra as SignedExtension>::AdditionalSigned: Send + Sync, 446 | C: Client, 447 | C::OffchainClient: Cache, DagCborCodec, Claim>, 448 | ::AccountId: Ss58Codec, 449 | ::Signature: Decode, 450 | <::Signature as Verify>::Signer: IdentifyAccount::AccountId>, 451 | { 452 | let uids = service.resolve().await?; 453 | for uid in uids { 454 | let uid = if let Ok(uid) = uid.parse() { 455 | uid 456 | } else { 457 | continue; 458 | }; 459 | for id in identity(client, uid).await? { 460 | if &id.service == service { 461 | if let IdentityStatus::Active(_) = &id.status { 462 | return Ok(uid); 463 | } 464 | } 465 | } 466 | } 467 | Err(ResolveFailure.into()) 468 | } 469 | 470 | #[cfg(test)] 471 | mod tests { 472 | use super::*; 473 | use test_client::client::{AccountKeyring, Client as _, Node as _}; 474 | use test_client::identity::{IdentityClient, IdentityStatus, Service}; 475 | use test_client::{Client, Node}; 476 | 477 | #[async_std::test] 478 | async fn prove_identity() { 479 | let node = Node::new_mock(); 480 | let (client, _tmp) = Client::mock(&node, AccountKeyring::Alice).await; 481 | let account_id = AccountKeyring::Alice.to_account_id(); 482 | let uid = client.fetch_uid(&account_id).await.unwrap().unwrap(); 483 | let service = Service::Github("dvc94ch".into()); 484 | 485 | let ids = client.identity(uid).await.unwrap(); 486 | assert_eq!(ids.len(), 0); 487 | 488 | client.prove_identity(service.clone()).await.unwrap(); 489 | let ids = client.identity(uid).await.unwrap(); 490 | assert_eq!(ids.len(), 1); 491 | assert_eq!(&ids[0].service, &service); 492 | assert_eq!(ids[0].status, IdentityStatus::ProofNotFound); 493 | 494 | client.revoke_identity(service.clone()).await.unwrap(); 495 | let ids = client.identity(uid).await.unwrap(); 496 | assert_eq!(ids.len(), 1); 497 | assert_eq!(&ids[0].service, &service); 498 | assert_eq!(ids[0].status, IdentityStatus::Revoked); 499 | 500 | client.prove_identity(service.clone()).await.unwrap(); 501 | let ids = client.identity(uid).await.unwrap(); 502 | assert_eq!(ids.len(), 1); 503 | assert_eq!(&ids[0].service, &service); 504 | assert_eq!(ids[0].status, IdentityStatus::ProofNotFound); 505 | } 506 | 507 | #[async_std::test] 508 | async fn change_password() { 509 | let node = Node::new_mock(); 510 | let (mut client1, _tmp) = Client::mock(&node, AccountKeyring::Alice).await; 511 | let (client2, _tmp) = Client::mock(&node, AccountKeyring::Eve).await; 512 | 513 | let signer2 = client2.signer().unwrap(); 514 | client1.add_key(signer2.account_id()).await.unwrap(); 515 | let mut sub = client1.subscribe_password_changes().await.unwrap(); 516 | 517 | let password = SecretString::new("password2".to_string()); 518 | client2.change_password(&password).await.unwrap(); 519 | 520 | let event = sub.next().await; 521 | assert!(event.is_some()); 522 | client1.update_password().await.unwrap(); 523 | client1.lock().await.unwrap(); 524 | client1.unlock(&password).await.unwrap(); 525 | } 526 | 527 | #[async_std::test] 528 | async fn provision_device() { 529 | let node = Node::new_mock(); 530 | let (mut client1, _tmp) = Client::mock(&node, AccountKeyring::Alice).await; 531 | let (mut client2, _tmp) = Client::mock(&node, AccountKeyring::Eve).await; 532 | 533 | let password = SecretString::new("abcdefgh".to_string()); 534 | let (mask, gen) = client1 535 | .keystore_mut() 536 | .change_password_mask(&password) 537 | .await 538 | .unwrap(); 539 | client1.keystore_mut().apply_mask(&mask, gen).await.unwrap(); 540 | 541 | let (pass, gen) = client1.keystore().password().await.unwrap(); 542 | client2 543 | .keystore_mut() 544 | .provision_device(&pass, gen) 545 | .await 546 | .unwrap(); 547 | 548 | client2.lock().await.unwrap(); 549 | client2.unlock(&password).await.unwrap(); 550 | } 551 | } 552 | -------------------------------------------------------------------------------- /identity/client/src/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | #[derive(Debug, Error)] 4 | #[error("Failed to find account associated with key.")] 5 | pub struct NoAccount; 6 | 7 | #[derive(Debug, Error)] 8 | #[error("invalid claim {0}")] 9 | pub struct InvalidClaim(pub &'static str); 10 | 11 | #[derive(Debug, Error)] 12 | #[error("failed to resolve identity")] 13 | pub struct ResolveFailure; 14 | 15 | #[derive(Debug, Error)] 16 | #[error("proof not found")] 17 | pub struct ProofNotFound; 18 | 19 | #[derive(Debug, Error)] 20 | #[error("failed to get block hash")] 21 | pub struct NoBlockHash; 22 | 23 | #[derive(Debug, Error)] 24 | #[error("runtime invalid")] 25 | pub struct RuntimeInvalid; 26 | -------------------------------------------------------------------------------- /identity/client/src/github.rs: -------------------------------------------------------------------------------- 1 | use crate::error::ProofNotFound; 2 | use async_std::task; 3 | use serde::Deserialize; 4 | use std::collections::BTreeMap; 5 | use sunshine_client_utils::Result; 6 | 7 | #[derive(Deserialize)] 8 | struct Gist { 9 | html_url: String, 10 | files: BTreeMap, 11 | } 12 | 13 | #[derive(Deserialize)] 14 | struct GistFile { 15 | raw_url: String, 16 | } 17 | 18 | struct Proof { 19 | html_url: String, 20 | content: String, 21 | } 22 | 23 | const GIST_NAME: &str = "sunshine-identity-proof.md"; 24 | 25 | async fn find_proofs(user: &str) -> Result> { 26 | let uri = format!("https://api.github.com/users/{}/gists", user); 27 | let gists: Vec = 28 | task::spawn_blocking(move || ureq::get(&uri).call().into_json_deserialize()).await?; 29 | let mut proofs = Vec::with_capacity(gists.len()); 30 | let urls = gists.into_iter().filter_map(|mut g| { 31 | g.files 32 | .remove(GIST_NAME) 33 | .map(|file| (g.html_url, file.raw_url)) 34 | }); 35 | for (html_url, raw_url) in urls { 36 | let res = task::spawn_blocking(move || ureq::get(&raw_url).call()).await; 37 | let content = res.into_string()?; 38 | proofs.push(Proof { html_url, content }); 39 | } 40 | Ok(proofs) 41 | } 42 | 43 | pub async fn verify(user: &str, signature: &str) -> Result { 44 | Ok(find_proofs(user) 45 | .await? 46 | .into_iter() 47 | .filter_map(|proof| { 48 | if let Some(signature2) = proof.content.lines().nth(17) { 49 | if signature == signature2 { 50 | return Some(proof.html_url); 51 | } 52 | } 53 | None 54 | }) 55 | .next() 56 | .ok_or(ProofNotFound)?) 57 | } 58 | 59 | pub async fn resolve(user: &str) -> Result> { 60 | Ok(find_proofs(user) 61 | .await? 62 | .into_iter() 63 | .filter_map(|proof| { 64 | if let Some(line) = proof.content.lines().nth(5) { 65 | line.split_whitespace().nth(3).map(|s| s.to_string()) 66 | } else { 67 | None 68 | } 69 | }) 70 | .collect()) 71 | } 72 | 73 | pub fn proof( 74 | genesis: &str, 75 | block: &str, 76 | uid: &str, 77 | username: &str, 78 | public: &str, 79 | object: &str, 80 | signature: &str, 81 | ) -> String { 82 | format!( 83 | include_str!("../github-proof-template.md"), 84 | genesis = genesis, 85 | block = block, 86 | uid = uid, 87 | username = username, 88 | public = public, 89 | object = object, 90 | signature = signature, 91 | ) 92 | } 93 | 94 | pub fn cli_instructions() -> String { 95 | format!( 96 | include_str!("../github-proof-instructions.md"), 97 | gist_name = GIST_NAME, 98 | ) 99 | } 100 | -------------------------------------------------------------------------------- /identity/client/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod claim; 2 | mod client; 3 | mod error; 4 | mod github; 5 | mod service; 6 | mod subxt; 7 | mod utils; 8 | 9 | pub use claim::{Claim, IdentityInfo, IdentityStatus}; 10 | pub use service::{Service, ServiceParseError}; 11 | pub use subxt::*; 12 | pub use utils::{resolve, Identifier}; 13 | 14 | use codec::Decode; 15 | use libipld::cache::Cache; 16 | use libipld::cbor::DagCborCodec; 17 | use sp_core::crypto::{Pair, Ss58Codec}; 18 | use sp_runtime::traits::{IdentifyAccount, Verify}; 19 | use std::convert::TryInto; 20 | use substrate_subxt::{ 21 | sp_core, sp_runtime, system::System, EventSubscription, Runtime, SignedExtension, SignedExtra, 22 | }; 23 | use sunshine_client_utils::{ 24 | async_trait, 25 | crypto::{bip39::Mnemonic, keychain::KeyType, secrecy::SecretString}, 26 | keystore, Client, Node, OffchainConfig, Result, 27 | }; 28 | 29 | #[async_trait] 30 | pub trait IdentityClient: Client 31 | where 32 | N::Runtime: Identity, 33 | { 34 | async fn create_account_for(&self, key: &::AccountId) -> Result<()>; 35 | async fn add_paperkey(&self) -> Result; 36 | async fn add_key(&self, key: &::AccountId) -> Result<()>; 37 | async fn remove_key(&self, key: &::AccountId) -> Result<()>; 38 | async fn change_password(&self, password: &SecretString) -> Result<()>; 39 | async fn update_password(&mut self) -> Result<()>; 40 | async fn subscribe_password_changes(&self) -> Result>; 41 | async fn fetch_uid( 42 | &self, 43 | key: &::AccountId, 44 | ) -> Result::Uid>>; 45 | async fn fetch_keys( 46 | &self, 47 | uid: ::Uid, 48 | hash: Option<::Hash>, 49 | ) -> Result::AccountId>>; 50 | async fn fetch_account( 51 | &self, 52 | uid: ::Uid, 53 | ) -> Result<::IdAccountData>; 54 | async fn prove_identity(&self, service: Service) -> Result; 55 | async fn revoke_identity(&self, service: Service) -> Result<()>; 56 | async fn identity(&self, uid: ::Uid) -> Result>; 57 | async fn resolve(&self, service: &Service) -> Result<::Uid>; 58 | } 59 | 60 | #[async_trait] 61 | impl IdentityClient for C 62 | where 63 | N: Node, 64 | N::Runtime: Identity, 65 | <<::Extra as SignedExtra>::Extra as SignedExtension>::AdditionalSigned: Send + Sync, 66 | ::AccountId: Into<::Address> + Ss58Codec, 67 | ::Signature: From<<::Pair as Pair>::Signature> + Decode, 68 | <::Signature as Verify>::Signer: From<<::Pair as Pair>::Public> 69 | + TryInto<<::Pair as Pair>::Public> 70 | + IdentifyAccount::AccountId> 71 | + Clone 72 | + Send 73 | + Sync, 74 | C: Client>, 75 | C::OffchainClient: Cache, DagCborCodec, Claim>, 76 | K: KeyType + 'static, 77 | { 78 | async fn create_account_for(&self, key: &::AccountId) -> Result<()> { 79 | client::create_account_for(self, key).await 80 | } 81 | 82 | async fn add_paperkey(&self) -> Result { 83 | client::add_paperkey::<_, _, C::KeyType>(self).await 84 | } 85 | 86 | async fn add_key(&self, key: &::AccountId) -> Result<()> { 87 | client::add_key(self, key).await 88 | } 89 | 90 | async fn remove_key(&self, key: &::AccountId) -> Result<()> { 91 | client::remove_key(self, key).await 92 | } 93 | 94 | async fn change_password(&self, password: &SecretString) -> Result<()> { 95 | client::change_password(self, password).await 96 | } 97 | 98 | async fn update_password(&mut self) -> Result<()> { 99 | client::update_password(self).await 100 | } 101 | 102 | async fn subscribe_password_changes(&self) -> Result> { 103 | client::subscribe_password_changes(self).await 104 | } 105 | 106 | async fn fetch_uid(&self, key: &::AccountId) -> Result::Uid>> { 107 | client::fetch_uid(self, key).await 108 | } 109 | 110 | async fn fetch_keys( 111 | &self, 112 | uid: ::Uid, 113 | hash: Option<::Hash>, 114 | ) -> Result::AccountId>> { 115 | client::fetch_keys(self, uid, hash).await 116 | } 117 | 118 | async fn fetch_account(&self, uid: ::Uid) -> Result<::IdAccountData> { 119 | client::fetch_account(self, uid).await 120 | } 121 | 122 | async fn prove_identity(&self, service: Service) -> Result { 123 | client::prove_identity(self, service).await 124 | } 125 | 126 | async fn revoke_identity(&self, service: Service) -> Result<()> { 127 | client::revoke_identity(self, service).await 128 | } 129 | 130 | async fn identity(&self, uid: ::Uid) -> Result> { 131 | client::identity(self, uid).await 132 | } 133 | 134 | async fn resolve(&self, service: &Service) -> Result<::Uid> { 135 | client::resolve(self, service).await 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /identity/client/src/service.rs: -------------------------------------------------------------------------------- 1 | use crate::claim::Claim; 2 | use crate::github; 3 | use core::str::FromStr; 4 | use libipld::cbor::DagCborCodec; 5 | use libipld::codec::Codec; 6 | use libipld::json::DagJsonCodec; 7 | use libipld::multibase::{encode, Base}; 8 | use libipld::{DagCbor, Ipld}; 9 | use sunshine_client_utils::Result; 10 | use thiserror::Error; 11 | 12 | #[derive(Clone, Debug, Eq, PartialEq, Hash, DagCbor)] 13 | pub enum Service { 14 | Github(String), 15 | } 16 | 17 | impl Service { 18 | pub fn username(&self) -> &str { 19 | match self { 20 | Self::Github(username) => &username, 21 | } 22 | } 23 | 24 | pub fn service(&self) -> &str { 25 | match self { 26 | Self::Github(_) => "github", 27 | } 28 | } 29 | 30 | pub async fn verify(&self, signature: &[u8]) -> Result { 31 | let signature = encode(Base::Base64, signature); 32 | match self { 33 | Self::Github(user) => github::verify(&user, &signature).await, 34 | } 35 | } 36 | 37 | pub async fn resolve(&self) -> Result> { 38 | match self { 39 | Self::Github(user) => github::resolve(&user).await, 40 | } 41 | } 42 | 43 | pub fn proof(&self, claim: &Claim) -> Result { 44 | let genesis = encode(Base::Base64, &claim.claim().genesis); 45 | let block = encode(Base::Base64, &claim.claim().block); 46 | let uid = claim.claim().uid.to_string(); 47 | let public = &claim.claim().public; 48 | let signature = encode(Base::Base64, claim.signature()); 49 | 50 | let bytes = DagCborCodec.encode(claim.claim())?; 51 | let ipld: Ipld = DagCborCodec.decode(&bytes)?; 52 | let bytes = DagJsonCodec.encode(&ipld)?; 53 | let object = std::str::from_utf8(&bytes).expect("json codec returns valid utf8"); 54 | 55 | Ok(match self { 56 | Self::Github(user) => { 57 | github::proof(&genesis, &block, &uid, &user, &public, &object, &signature) 58 | } 59 | }) 60 | } 61 | 62 | pub fn cli_instructions(&self) -> String { 63 | match self { 64 | Self::Github(_) => github::cli_instructions(), 65 | } 66 | } 67 | } 68 | 69 | impl core::fmt::Display for Service { 70 | fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { 71 | write!(f, "{}@{}", self.username(), self.service()) 72 | } 73 | } 74 | 75 | impl FromStr for Service { 76 | type Err = ServiceParseError; 77 | 78 | fn from_str(string: &str) -> core::result::Result { 79 | let mut parts = string.split('@'); 80 | let username = parts.next().ok_or(ServiceParseError::Invalid)?; 81 | if username.is_empty() { 82 | return Err(ServiceParseError::Invalid); 83 | } 84 | let service = parts.next().ok_or(ServiceParseError::Invalid)?; 85 | if service.is_empty() { 86 | return Err(ServiceParseError::Invalid); 87 | } 88 | if parts.next().is_some() { 89 | return Err(ServiceParseError::Invalid); 90 | } 91 | match service { 92 | "github" => Ok(Self::Github(username.into())), 93 | _ => Err(ServiceParseError::Unknown(service.into())), 94 | } 95 | } 96 | } 97 | 98 | #[derive(Debug, Error, Eq, PartialEq)] 99 | pub enum ServiceParseError { 100 | #[error("Expected a service description of the form username@service.")] 101 | Invalid, 102 | #[error("Unknown service '{0}'")] 103 | Unknown(String), 104 | } 105 | -------------------------------------------------------------------------------- /identity/client/src/subxt.rs: -------------------------------------------------------------------------------- 1 | //! Subxt calls. 2 | use codec::{Decode, Encode, FullCodec}; 3 | use core::fmt::Display; 4 | use frame_support::Parameter; 5 | use libipld::cid::Cid; 6 | use std::str::FromStr; 7 | use substrate_subxt::sp_runtime::traits::{CheckedAdd, Member}; 8 | use substrate_subxt::system::{System, SystemEventsDecoder}; 9 | use substrate_subxt::{module, Call, Event, Store}; 10 | 11 | #[module] 12 | pub trait Identity: System { 13 | type Uid: Parameter + Member + Copy + Default + CheckedAdd + Into + FromStr + Display; 14 | 15 | type Cid: Parameter + Member + Default + From + Into; 16 | 17 | type Mask: Parameter + Member + Default; 18 | 19 | type Gen: Parameter + Member + Copy + Default + CheckedAdd + From + Into + Ord; 20 | 21 | type IdAccountData: Member + FullCodec + Clone + Default; 22 | } 23 | 24 | #[derive(Clone, Debug, Eq, Encode, PartialEq, Store)] 25 | pub struct UidLookupStore<'a, T: Identity> { 26 | #[store(returns = Option)] 27 | key: &'a ::AccountId, 28 | } 29 | 30 | #[derive(Clone, Debug, Eq, Encode, PartialEq, Store)] 31 | pub struct KeysStore { 32 | #[store(returns = Vec<::AccountId>)] 33 | uid: T::Uid, 34 | } 35 | 36 | #[derive(Clone, Debug, Eq, Encode, PartialEq, Store)] 37 | pub struct IdentityStore { 38 | #[store(returns = Option)] 39 | uid: T::Uid, 40 | } 41 | 42 | #[derive(Clone, Debug, Eq, Encode, PartialEq, Store)] 43 | pub struct PasswordGenStore { 44 | #[store(returns = T::Gen)] 45 | uid: T::Uid, 46 | } 47 | 48 | #[derive(Clone, Debug, Eq, Encode, PartialEq, Store)] 49 | pub struct PasswordMaskStore { 50 | #[store(returns = Option)] 51 | uid: T::Uid, 52 | gen: T::Gen, 53 | } 54 | 55 | #[derive(Clone, Debug, Eq, Encode, PartialEq, Store)] 56 | pub struct AccountStore { 57 | #[store(returns = T::IdAccountData)] 58 | uid: T::Uid, 59 | } 60 | 61 | #[derive(Call, Clone, Debug, Eq, Encode, PartialEq)] 62 | pub struct CreateAccountForCall<'a, T: Identity> { 63 | key: &'a ::AccountId, 64 | } 65 | 66 | #[derive(Call, Clone, Debug, Eq, Encode, PartialEq)] 67 | pub struct AddKeyCall<'a, T: Identity> { 68 | key: &'a ::AccountId, 69 | } 70 | 71 | #[derive(Call, Clone, Debug, Eq, Encode, PartialEq)] 72 | pub struct RemoveKeyCall<'a, T: Identity> { 73 | key: &'a ::AccountId, 74 | } 75 | 76 | #[derive(Call, Clone, Debug, Eq, Encode, PartialEq)] 77 | pub struct ChangePasswordCall<'a, T: Identity> { 78 | password_mask: &'a T::Mask, 79 | gen: T::Gen, 80 | } 81 | 82 | #[derive(Call, Clone, Debug, Eq, Encode, PartialEq)] 83 | pub struct SetIdentityCall<'a, T: Identity> { 84 | prev_cid: &'a Option, 85 | new_cid: &'a T::Cid, 86 | } 87 | 88 | #[derive(Clone, Debug, Decode, Eq, Event, PartialEq)] 89 | pub struct AccountCreatedEvent { 90 | uid: T::Uid, 91 | } 92 | 93 | #[derive(Clone, Debug, Decode, Eq, Event, PartialEq)] 94 | pub struct KeyAddedEvent { 95 | uid: T::Uid, 96 | key: ::AccountId, 97 | } 98 | 99 | #[derive(Clone, Debug, Decode, Eq, Event, PartialEq)] 100 | pub struct KeyRemovedEvent { 101 | uid: T::Uid, 102 | key: ::AccountId, 103 | } 104 | 105 | #[derive(Clone, Debug, Decode, Eq, Event, PartialEq)] 106 | pub struct IdentityChangedEvent { 107 | uid: T::Uid, 108 | cid: T::Cid, 109 | } 110 | 111 | #[derive(Clone, Debug, Decode, Eq, Event, PartialEq)] 112 | pub struct PasswordChangedEvent { 113 | uid: T::Uid, 114 | gen: T::Gen, 115 | mask: T::Mask, 116 | } 117 | -------------------------------------------------------------------------------- /identity/client/src/utils.rs: -------------------------------------------------------------------------------- 1 | use crate::error::NoAccount; 2 | use crate::service::{Service, ServiceParseError}; 3 | use crate::{Identity, IdentityClient}; 4 | use sp_core::crypto::Ss58Codec; 5 | use std::str::FromStr; 6 | use substrate_subxt::{sp_core, system::System}; 7 | use sunshine_client_utils::{crypto::ss58::Ss58, Node, Result}; 8 | 9 | pub async fn resolve( 10 | client: &C, 11 | identifier: Option>, 12 | ) -> Result<::Uid> 13 | where 14 | N: Node, 15 | N::Runtime: Identity, 16 | C: IdentityClient, 17 | { 18 | let identifier = if let Some(identifier) = identifier { 19 | identifier 20 | } else { 21 | Identifier::Account(client.signer()?.account_id().clone()) 22 | }; 23 | let uid = match identifier { 24 | Identifier::Uid(uid) => uid, 25 | Identifier::Account(account_id) => client.fetch_uid(&account_id).await?.ok_or(NoAccount)?, 26 | Identifier::Service(service) => client.resolve(&service).await?, 27 | }; 28 | Ok(uid) 29 | } 30 | 31 | #[derive(Clone, Debug, Eq, PartialEq)] 32 | pub enum Identifier { 33 | Uid(R::Uid), 34 | Account(R::AccountId), 35 | Service(Service), 36 | } 37 | 38 | impl core::fmt::Display for Identifier { 39 | fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { 40 | match self { 41 | Self::Uid(uid) => write!(f, "{}", uid), 42 | Self::Account(account_id) => write!(f, "{}", account_id.to_string()), 43 | Self::Service(service) => write!(f, "{}", service), 44 | } 45 | } 46 | } 47 | 48 | impl FromStr for Identifier 49 | where 50 | ::AccountId: Ss58Codec, 51 | { 52 | type Err = ServiceParseError; 53 | 54 | fn from_str(string: &str) -> core::result::Result { 55 | if let Ok(uid) = R::Uid::from_str(string) { 56 | Ok(Self::Uid(uid)) 57 | } else if let Ok(Ss58(account_id)) = Ss58::::from_str(string) { 58 | Ok(Self::Account(account_id)) 59 | } else { 60 | Ok(Self::Service(Service::from_str(string)?)) 61 | } 62 | } 63 | } 64 | 65 | #[cfg(test)] 66 | mod tests { 67 | use core::str::FromStr; 68 | use test_client::client::AccountKeyring; 69 | use test_client::identity::{Identifier, Service, ServiceParseError}; 70 | use test_client::Runtime; 71 | 72 | #[test] 73 | fn parse_identifer() { 74 | assert_eq!( 75 | Identifier::from_str("dvc94ch@github"), 76 | Ok(Identifier::::Service(Service::Github( 77 | "dvc94ch".into() 78 | ))) 79 | ); 80 | assert_eq!( 81 | Identifier::::from_str("dvc94ch@twitter"), 82 | Err(ServiceParseError::Unknown("twitter".into())) 83 | ); 84 | assert_eq!( 85 | Identifier::::from_str("@dvc94ch"), 86 | Err(ServiceParseError::Invalid) 87 | ); 88 | let alice = AccountKeyring::Alice.to_account_id(); 89 | assert_eq!( 90 | Identifier::::from_str(&alice.to_string()), 91 | Ok(Identifier::Account(alice)) 92 | ); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /identity/ffi/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sunshine-identity-ffi" 3 | version = "0.2.3" 4 | authors = ["Shady Khalifa "] 5 | edition = "2018" 6 | description = "FFI for the identity module." 7 | license = "ISC" 8 | documentation = "https://docs.rs/sunshine-identity-ffi" 9 | repository = "https://github.com/sunshine-protocol/sunshine-identity" 10 | 11 | [dependencies] 12 | substrate-subxt = "0.12.0" 13 | sunshine-client-utils = { git = "https://github.com/sunshine-protocol/sunshine-core" } 14 | sunshine-ffi-utils = { git = "https://github.com/sunshine-protocol/sunshine-core" } 15 | sunshine-identity-client = { version = "^0.2", path = "../client" } 16 | thiserror = "1.0.20" 17 | 18 | [dev-dependencies] 19 | test-client = { path = "../../bin/client" } 20 | 21 | [features] 22 | default = ["identity-key", "identity-wallet", "identity-device", "identity-id", "identity-account"] 23 | identity-key = [] 24 | identity-wallet = [] 25 | identity-device = [] 26 | identity-id = [] 27 | identity-account = [] 28 | -------------------------------------------------------------------------------- /identity/ffi/src/ffi.rs: -------------------------------------------------------------------------------- 1 | use std::marker::PhantomData; 2 | use substrate_subxt::{ 3 | balances::{AccountData, Balances, TransferCallExt, TransferEventExt}, 4 | sp_core::crypto::Ss58Codec, 5 | system::System, 6 | Runtime, SignedExtension, SignedExtra, 7 | }; 8 | use sunshine_client_utils::crypto::{ 9 | bip39::Mnemonic, 10 | keychain::TypedPair, 11 | keystore::Keystore, 12 | secrecy::{ExposeSecret, SecretString}, 13 | ss58::Ss58, 14 | }; 15 | use sunshine_client_utils::{Node, Result}; 16 | use sunshine_ffi_utils::async_std::sync::RwLock; 17 | use sunshine_identity_client::{resolve, Identifier, Identity, IdentityClient, Service}; 18 | use thiserror::Error; 19 | 20 | macro_rules! make { 21 | ($name: ident) => { 22 | #[derive(Clone, Debug)] 23 | pub struct $name<'a, C, N> 24 | where 25 | C: IdentityClient + Send + Sync, 26 | N: Node, 27 | N::Runtime: Identity, 28 | { 29 | client: &'a RwLock, 30 | _runtime: PhantomData, 31 | } 32 | 33 | impl<'a, C, N> $name<'a, C, N> 34 | where 35 | C: IdentityClient + Send + Sync, 36 | N: Node, 37 | N::Runtime: Identity, 38 | { 39 | pub fn new(client: &'a RwLock) -> Self { 40 | Self { 41 | client, 42 | _runtime: PhantomData, 43 | } 44 | } 45 | } 46 | }; 47 | ($($name: ident),+) => { 48 | $( 49 | make!($name); 50 | )+ 51 | } 52 | } 53 | 54 | make!(Key, Account, Device, ID, Wallet); 55 | 56 | impl<'a, C, N> Key<'a, C, N> 57 | where 58 | N: Node, 59 | N::Runtime: Identity, 60 | C: IdentityClient + Send + Sync, 61 | { 62 | pub async fn exists(&self) -> Result { 63 | self.client.read().await.keystore().is_initialized().await 64 | } 65 | 66 | pub async fn set( 67 | &self, 68 | password: &str, 69 | suri: Option<&str>, 70 | paperkey: Option<&str>, 71 | ) -> Result { 72 | let password = SecretString::new(password.to_string()); 73 | if password.expose_secret().len() < 8 { 74 | return Err(Error::PasswordTooShort.into()); 75 | } 76 | let dk = if let Some(paperkey) = paperkey { 77 | let mnemonic = Mnemonic::parse(paperkey)?; 78 | TypedPair::::from_mnemonic(&mnemonic)? 79 | } else if let Some(suri) = suri { 80 | TypedPair::::from_suri(suri)? 81 | } else { 82 | TypedPair::::generate().await 83 | }; 84 | 85 | self.client 86 | .write() 87 | .await 88 | .set_key(dk, &password, false) 89 | .await?; 90 | let account_id = self.client.read().await.signer()?.account_id().to_string(); 91 | Ok(account_id) 92 | } 93 | 94 | pub async fn uid(&self) -> Result { 95 | let client = self.client.read().await; 96 | let signer = client.signer()?; 97 | Ok(signer.account_id().to_string()) 98 | } 99 | 100 | pub async fn lock(&self) -> Result { 101 | self.client.write().await.lock().await?; 102 | Ok(true) 103 | } 104 | 105 | pub async fn unlock(&self, password: &str) -> Result { 106 | let password = SecretString::new(password.to_string()); 107 | self.client.write().await.unlock(&password).await?; 108 | Ok(true) 109 | } 110 | } 111 | 112 | impl<'a, C, N> Account<'a, C, N> 113 | where 114 | N: Node, 115 | N::Runtime: Identity, 116 | C: IdentityClient + Send + Sync, 117 | ::AccountId: Ss58Codec, 118 | { 119 | pub async fn create(&self, device: &str) -> Result { 120 | let device: Ss58 = device.parse()?; 121 | self.client 122 | .read() 123 | .await 124 | .create_account_for(&device.0) 125 | .await?; 126 | Ok(true) 127 | } 128 | 129 | pub async fn change_password(&self, password: &str) -> Result { 130 | let password = SecretString::new(password.to_string()); 131 | if password.expose_secret().len() < 8 { 132 | return Err(Error::PasswordTooShort.into()); 133 | } 134 | self.client.read().await.change_password(&password).await?; 135 | Ok(true) 136 | } 137 | } 138 | 139 | impl<'a, C, N> Device<'a, C, N> 140 | where 141 | N: Node, 142 | N::Runtime: Identity, 143 | C: IdentityClient + Send + Sync, 144 | ::AccountId: Ss58Codec, 145 | { 146 | pub async fn current(&self) -> Result { 147 | let client = self.client.read().await; 148 | let signer = client.signer()?; 149 | Ok(signer.account_id().to_string()) 150 | } 151 | 152 | pub async fn has_device_key(&self) -> Result { 153 | Ok(self.client.read().await.keystore().is_initialized().await?) 154 | } 155 | 156 | pub async fn add(&self, device: &str) -> Result { 157 | let device: Ss58 = device.parse()?; 158 | self.client.read().await.add_key(&device.0).await?; 159 | Ok(true) 160 | } 161 | 162 | pub async fn remove(&self, device: &str) -> Result { 163 | let device: Ss58 = device.parse()?; 164 | self.client.read().await.remove_key(&device.0).await?; 165 | Ok(true) 166 | } 167 | 168 | pub async fn list(&self, identifier: &str) -> Result> { 169 | let client = self.client.read().await; 170 | let identifier: Identifier = identifier.parse()?; 171 | let uid = resolve(&*client, Some(identifier)).await?; 172 | let list = client 173 | .fetch_keys(uid, None) 174 | .await? 175 | .into_iter() 176 | .map(|key| key.to_ss58check()) 177 | .collect(); 178 | Ok(list) 179 | } 180 | 181 | pub async fn paperkey(&self) -> Result { 182 | let mnemonic = self.client.read().await.add_paperkey().await?; 183 | Ok(mnemonic.as_str().into()) 184 | } 185 | } 186 | 187 | impl<'a, C, N> ID<'a, C, N> 188 | where 189 | N: Node, 190 | N::Runtime: Identity, 191 | C: IdentityClient + Send + Sync, 192 | ::AccountId: Ss58Codec, 193 | { 194 | pub async fn resolve(&self, identifier: &str) -> Result { 195 | let identifier: Identifier = identifier.parse()?; 196 | let client = self.client.read().await; 197 | let uid = resolve(&*client, Some(identifier)).await?; 198 | Ok(uid.to_string()) 199 | } 200 | 201 | pub async fn list(&self, identifier: &str) -> Result> { 202 | let client = self.client.read().await; 203 | let identifier: Identifier = identifier.parse()?; 204 | let uid = resolve(&*client, Some(identifier)).await?; 205 | let list = client 206 | .identity(uid) 207 | .await? 208 | .into_iter() 209 | .map(|id| id.to_string()) 210 | .collect(); 211 | Ok(list) 212 | } 213 | 214 | pub async fn prove(&self, service: &str) -> Result> { 215 | let service: Service = service.parse()?; 216 | let instructions = service.cli_instructions(); 217 | let proof = self.client.read().await.prove_identity(service).await?; 218 | Ok(vec![instructions, proof]) 219 | } 220 | 221 | pub async fn revoke(&self, service: &str) -> Result { 222 | let service: Service = service.parse()?; 223 | self.client.read().await.revoke_identity(service).await?; 224 | Ok(true) 225 | } 226 | } 227 | 228 | impl<'a, C, N> Wallet<'a, C, N> 229 | where 230 | N: Node, 231 | N::Runtime: Identity + Balances, 232 | C: IdentityClient + Send + Sync, 233 | N::Runtime: Identity::Balance>>, 234 | ::AccountId: Ss58Codec + Into<::Address>, 235 | <<::Extra as SignedExtra>::Extra as SignedExtension>::AdditionalSigned: 236 | Send + Sync, 237 | { 238 | pub async fn balance(&self, identifier: Option<&str>) -> Result<::Balance> { 239 | let client = self.client.read().await; 240 | let account_id: Identifier = if let Some(identifier) = identifier { 241 | identifier.parse()? 242 | } else { 243 | Identifier::Account(client.signer()?.account_id().clone()) 244 | }; 245 | let uid = resolve(&*client, Some(account_id)).await?; 246 | let account = client.fetch_account(uid).await?; 247 | Ok(account.free) 248 | } 249 | 250 | pub async fn transfer( 251 | &self, 252 | identifier: &str, 253 | amount: impl Into<::Balance>, 254 | ) -> Result<::Balance> { 255 | let client = self.client.read().await; 256 | let identifier: Identifier = identifier.parse()?; 257 | let signer = client.chain_signer()?; 258 | let uid = resolve(&*client, Some(identifier)).await?; 259 | let keys = client.fetch_keys(uid, None).await?; 260 | client 261 | .chain_client() 262 | .transfer_and_watch(&signer, &keys[0].clone().into(), amount.into()) 263 | .await? 264 | .transfer()? 265 | .ok_or(Error::TransferEventFind)?; 266 | 267 | self.balance(None).await 268 | } 269 | } 270 | 271 | #[derive(Debug, Error)] 272 | pub enum Error { 273 | #[error("password too short")] 274 | PasswordTooShort, 275 | #[error("transfer event not found")] 276 | TransferEventFind, 277 | } 278 | -------------------------------------------------------------------------------- /identity/ffi/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub use sunshine_ffi_utils as utils; 2 | 3 | #[doc(hidden)] 4 | pub mod ffi; 5 | 6 | #[doc(hidden)] 7 | #[cfg(feature = "identity-key")] 8 | #[macro_export] 9 | macro_rules! impl_identity_key_ffi { 10 | () => { 11 | use $crate::ffi::Key; 12 | gen_ffi! { 13 | /// Check if the Keystore is exist and initialized. 14 | /// 15 | /// this is useful if you want to check if there is an already created account or not. 16 | Key::exists => fn client_key_exists() -> bool; 17 | /// Set a new Key for this device if not already exist. 18 | /// you should call `client_has_device_key` first to see if you have already a key. 19 | /// 20 | /// suri is used for testing only. 21 | /// phrase is used to restore a backup 22 | /// returns a string that is the current device id 23 | Key::set => fn client_key_set( 24 | password: *const raw::c_char = cstr!(password), 25 | suri: *const raw::c_char = cstr!(suri, allow_null), 26 | paperkey: *const raw::c_char = cstr!(paperkey, allow_null) 27 | ) -> String; 28 | /// Lock your account 29 | /// return `true` if locked, and return an error message if something went wrong 30 | Key::lock => fn client_key_lock() -> bool; 31 | /// Unlock your account using the password 32 | /// return `true` when the account get unlocked, otherwise an error message returned 33 | Key::unlock => fn client_key_unlock(password: *const raw::c_char = cstr!(password)) -> bool; 34 | /// Get current UID as string (if any) 35 | /// otherwise null returned 36 | Key::uid => fn client_key_uid() -> Option; 37 | } 38 | } 39 | } 40 | 41 | #[doc(hidden)] 42 | #[cfg(not(feature = "identity-key"))] 43 | #[macro_export] 44 | macro_rules! impl_identity_key_ffi { 45 | () => {}; 46 | } 47 | 48 | #[doc(hidden)] 49 | #[cfg(feature = "identity-wallet")] 50 | #[macro_export] 51 | macro_rules! impl_identity_wallet_ffi { 52 | () => { 53 | use $crate::ffi::Wallet; 54 | gen_ffi! { 55 | /// Get the balance of an identifier. 56 | /// returns and string but normally it's a `u128` encoded as string. 57 | Wallet::balance => fn client_wallet_balance(identifier: *const raw::c_char = cstr!(identifier, allow_null)) -> String; 58 | /// Transfer tokens to another account using there `identifier` 59 | /// returns current account balance after the transaction. 60 | Wallet::transfer => fn client_wallet_transfer( 61 | to: *const raw::c_char = cstr!(to), 62 | amount: u64 = amount 63 | ) -> String; 64 | } 65 | }; 66 | } 67 | 68 | #[doc(hidden)] 69 | #[cfg(not(feature = "identity-wallet"))] 70 | #[macro_export] 71 | macro_rules! impl_identity_wallet_ffi { 72 | () => {}; 73 | } 74 | 75 | 76 | #[doc(hidden)] 77 | #[cfg(feature = "identity-account")] 78 | #[macro_export] 79 | macro_rules! impl_identity_account_ffi { 80 | () => { 81 | use $crate::ffi::Account; 82 | gen_ffi! { 83 | /// Creates Account for that device id. 84 | /// returns `true` if it got created. 85 | Account::create => fn client_account_create(device: *const raw::c_char = cstr!(device)) -> bool; 86 | /// Changes Current Account Password. 87 | /// returns `true` if it got updated. 88 | Account::change_password => fn client_account_change_password( 89 | to: *const raw::c_char = cstr!(to) 90 | ) -> bool; 91 | } 92 | }; 93 | } 94 | 95 | #[doc(hidden)] 96 | #[cfg(not(feature = "identity-account"))] 97 | #[macro_export] 98 | macro_rules! impl_identity_account_ffi { 99 | () => {}; 100 | } 101 | 102 | 103 | #[doc(hidden)] 104 | #[cfg(feature = "identity-device")] 105 | #[macro_export] 106 | macro_rules! impl_identity_device_ffi { 107 | () => { 108 | use $crate::ffi::Device; 109 | gen_ffi! { 110 | /// Check if the current client has a device key already or not 111 | Device::has_device_key => fn client_device_has_key() -> bool; 112 | /// Get current Device ID as string (if any) 113 | /// otherwise null returned 114 | Device::current => fn client_device_current() -> Option; 115 | /// add a new device to your account 116 | /// the `device` should be in the `ss58` format 117 | Device::add => fn client_device_add(device: *const raw::c_char = cstr!(device)) -> bool; 118 | /// remove a device from your account 119 | /// the `device` should be in the `ss58` fromat 120 | Device::remove => fn client_device_remove(device: *const raw::c_char = cstr!(device)) -> bool; 121 | /// get a list of devices that linked to that identifier 122 | /// returns list of devices ids in `ss58` fromat (as strings) or an error message 123 | Device::list => fn client_device_list(identifier: *const raw::c_char = cstr!(identifier)) -> Vec; 124 | /// Generate a new backup paper key that can be used to recover your account 125 | /// returns a string that contains the phrase, otherwise null if there is an error 126 | Device::paperkey => fn client_device_paperkey() -> Option; 127 | } 128 | }; 129 | } 130 | 131 | #[doc(hidden)] 132 | #[cfg(not(feature = "identity-device"))] 133 | #[macro_export] 134 | macro_rules! impl_identity_device_ffi { 135 | () => {}; 136 | } 137 | 138 | #[doc(hidden)] 139 | #[cfg(feature = "identity-id")] 140 | #[macro_export] 141 | macro_rules! impl_identity_id_ffi { 142 | () => { 143 | use $crate::ffi::ID; 144 | gen_ffi! { 145 | /// Get the `UID` of the provided identifier 146 | ID::resolve => fn client_id_resolve(identifier: *const raw::c_char = cstr!(identifier)) -> Option; 147 | /// get a list of identities of the provided identifier. 148 | ID::list => fn client_id_list(identifier: *const raw::c_char = cstr!(identifier)) -> Vec; 149 | /// prove the current account identity to a service. 150 | /// the service string should be in the format of `username@service` for example `shekohex@github` 151 | /// returns a pair (list of two values) the first element is the `instructions` of how to prove the identity 152 | /// the second element is the `proof` itself where you should follow the instructions and post it somewhere. 153 | /// otherwise and error returned as string. 154 | ID::prove => fn client_id_prove(service: *const raw::c_char = cstr!(service)) -> (String, String); 155 | /// revoke your identity from the provided service 156 | /// see `client_id_prove` for more information. 157 | /// returns `true` if the identity revoked. 158 | ID::revoke => fn client_id_revoke(service: *const raw::c_char = cstr!(service)) -> bool; 159 | 160 | } 161 | } 162 | } 163 | 164 | #[doc(hidden)] 165 | #[cfg(not(feature = "identity-id"))] 166 | #[macro_export] 167 | macro_rules! impl_identity_id_ffi { 168 | () => {}; 169 | } 170 | 171 | /// Generate the FFI for the provided runtime 172 | /// 173 | /// ### Example 174 | /// ``` 175 | /// use test_client::Client; 176 | /// use sunshine_identity_ffi::impl_ffi; 177 | /// 178 | /// impl_ffi!(client: Client); 179 | /// ``` 180 | #[macro_export] 181 | macro_rules! impl_ffi { 182 | () => { 183 | $crate::impl_identity_key_ffi!(); 184 | $crate::impl_identity_device_ffi!(); 185 | $crate::impl_identity_id_ffi!(); 186 | $crate::impl_identity_wallet_ffi!(); 187 | $crate::impl_identity_account_ffi!(); 188 | }; 189 | (client: $client: ty) => { 190 | use ::std::os::raw; 191 | #[allow(unused)] 192 | use $crate::utils::*; 193 | gen_ffi!(client = $client); 194 | $crate::impl_ffi!(); 195 | }; 196 | } 197 | -------------------------------------------------------------------------------- /identity/ffi/tests/impl_ffi_macro.rs: -------------------------------------------------------------------------------- 1 | use sunshine_identity_ffi::impl_ffi; 2 | use test_client::Client; 3 | 4 | // Test how the macro expands 5 | // cargo expand --package sunshine-identity-ffi --test impl_ffi_macro -- test_impl_ffi_macro 6 | #[test] 7 | fn test_impl_ffi_macro() { 8 | impl_ffi!(client: Client); 9 | } 10 | -------------------------------------------------------------------------------- /identity/pallet/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sunshine-identity-pallet" 3 | version = "0.2.0" 4 | authors = ["David Craven "] 5 | edition = "2018" 6 | description = "Identity runtime module." 7 | license = "ISC" 8 | documentation = "https://docs.rs/sunshine-identity-pallet" 9 | repository = "https://github.com/sunshine-protocol/sunshine-identity" 10 | 11 | [dependencies] 12 | codec = { package = "parity-scale-codec", version = "1.3.0", default-features = false, features = ["derive"] } 13 | frame-support = { version = "2.0.0", default-features = false } 14 | frame-system = { version = "2.0.0", default-features = false } 15 | sp-core = { version = "2.0.0", default-features = false } 16 | sp-io = { version = "2.0.0", default-features = false } 17 | sp-runtime = { version = "2.0.0", default-features = false } 18 | orml-utilities = { version = "0.2.0", default-features = false } 19 | 20 | [features] 21 | default = ["std"] 22 | std = [ 23 | "codec/std", 24 | "frame-support/std", 25 | "frame-system/std", 26 | ] 27 | -------------------------------------------------------------------------------- /identity/pallet/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Identity module. 2 | #![cfg_attr(not(feature = "std"), no_std)] 3 | 4 | use codec::FullCodec; 5 | use frame_support::dispatch::DispatchResult; 6 | use frame_support::traits::StoredMap; 7 | use frame_support::{decl_error, decl_event, decl_module, decl_storage, ensure, Parameter}; 8 | use frame_system::{ensure_signed, Trait as System}; 9 | use orml_utilities::OrderedSet; 10 | use sp_runtime::traits::{CheckedAdd, Member}; 11 | 12 | #[cfg(test)] 13 | mod mock; 14 | 15 | #[cfg(test)] 16 | mod tests; 17 | 18 | /// The pallet's configuration trait. 19 | pub trait Trait: System { 20 | /// User ID type. 21 | type Uid: Parameter + Member + Copy + Default + CheckedAdd + From; 22 | 23 | /// Cid type. 24 | type Cid: Parameter + Member; 25 | 26 | /// Mask type. 27 | type Mask: Parameter + Member; 28 | 29 | /// Generation type. 30 | type Gen: Parameter + Member + Copy + Default + CheckedAdd + From + Ord; 31 | 32 | /// Data to be associated with an account. 33 | type AccountData: Member + FullCodec + Clone + Default; 34 | 35 | /// The overarching event type. 36 | type Event: From> + Into<::Event>; 37 | } 38 | 39 | decl_storage! { 40 | trait Store for Module as IdentityModule { 41 | UidCounter: T::Uid; 42 | 43 | pub UidLookup get(fn key): map 44 | hasher(blake2_128_concat) ::AccountId 45 | => Option; 46 | 47 | pub Keys get(fn keys): map 48 | hasher(blake2_128_concat) T::Uid 49 | => OrderedSet<::AccountId>; 50 | 51 | pub Identity get(fn identity): map 52 | hasher(blake2_128_concat) T::Uid 53 | => Option; 54 | 55 | pub PasswordGen get(fn gen): map 56 | hasher(blake2_128_concat) T::Uid 57 | => T::Gen; 58 | 59 | pub PasswordMask get(fn mask): double_map 60 | hasher(blake2_128_concat) T::Uid, 61 | hasher(blake2_128_concat) T::Gen 62 | => Option; 63 | 64 | pub Account get(fn account): map 65 | hasher(blake2_128_concat) T::Uid 66 | => ::AccountData; 67 | } 68 | } 69 | 70 | decl_event!( 71 | pub enum Event 72 | where 73 | AccountId = ::AccountId, 74 | Uid = ::Uid, 75 | Cid = ::Cid, 76 | Mask = ::Mask, 77 | Gen = ::Gen, 78 | { 79 | AccountCreated(Uid), 80 | KeyAdded(Uid, AccountId), 81 | KeyRemoved(Uid, AccountId), 82 | IdentityChanged(Uid, Cid), 83 | PasswordChanged(Uid, Gen, Mask), 84 | } 85 | ); 86 | 87 | decl_error! { 88 | pub enum Error for Module { 89 | /// No account. 90 | NoAccount, 91 | /// Uid overflow. 92 | UidOverflow, 93 | /// Key in use. 94 | KeyInUse, 95 | /// Unauthorized to remove key. 96 | Unauthorized, 97 | /// Cant remove self. 98 | CantRemoveSelf, 99 | /// Prev cid missmatch. 100 | PrevCidMissmatch, 101 | /// Password gen overflow. 102 | PasswordGenOverflow, 103 | /// Password gen missmatch. 104 | PasswordGenMissmatch, 105 | } 106 | } 107 | 108 | decl_module! { 109 | /// The module declaration. 110 | pub struct Module for enum Call where origin: T::Origin { 111 | // Initialize errors. 112 | type Error = Error; 113 | 114 | // Initialize events. 115 | fn deposit_event() = default; 116 | 117 | /// Create account. 118 | #[weight = 0] 119 | pub fn create_account_for(origin, key: ::AccountId) -> DispatchResult { 120 | let _ = ensure_signed(origin)?; 121 | Self::ensure_key_unused(&key)?; 122 | 123 | Self::create_account(key)?; 124 | Ok(()) 125 | } 126 | 127 | /// Add a key. 128 | #[weight = 0] 129 | pub fn add_key(origin, key: ::AccountId) -> DispatchResult { 130 | let who = ensure_signed(origin)?; 131 | let uid = Self::ensure_uid(&who)?; 132 | Self::ensure_key_unused(&key)?; 133 | 134 | Self::add_key_to_uid(uid, key); 135 | Ok(()) 136 | } 137 | 138 | /// Remove a key. 139 | #[weight = 0] 140 | pub fn remove_key(origin, key: ::AccountId) -> DispatchResult { 141 | let who = ensure_signed(origin)?; 142 | let uid = Self::ensure_uid(&who)?; 143 | // Prevent user from locking himself out. 144 | ensure!(who != key, Error::::CantRemoveSelf); 145 | ensure!(>::get(&key) == Some(uid), Error::::Unauthorized); 146 | 147 | Self::remove_key_from_uid(uid, key); 148 | Ok(()) 149 | } 150 | 151 | /// Change password. 152 | #[weight = 0] 153 | pub fn change_password(origin, mask: T::Mask, gen: T::Gen) -> DispatchResult { 154 | let who = ensure_signed(origin)?; 155 | let uid = Self::ensure_uid(&who)?; 156 | ensure!( 157 | gen == >::get(uid) 158 | .checked_add(&1u8.into()) 159 | .ok_or(Error::::PasswordGenOverflow)?, 160 | Error::::PasswordGenMissmatch 161 | ); 162 | 163 | >::insert(uid, gen); 164 | >::insert(uid, gen, mask.clone()); 165 | Self::deposit_event(RawEvent::PasswordChanged(uid, gen, mask)); 166 | Ok(()) 167 | } 168 | 169 | /// Set the identity. 170 | #[weight = 0] 171 | pub fn set_identity(origin, prev_cid: Option, new_cid: T::Cid) -> DispatchResult { 172 | let who = ensure_signed(origin)?; 173 | let uid = Self::ensure_uid(&who)?; 174 | ensure!(>::get(uid) == prev_cid, Error::::PrevCidMissmatch); 175 | 176 | >::insert(uid, new_cid.clone()); 177 | Self::deposit_event(RawEvent::IdentityChanged(uid, new_cid)); 178 | Ok(()) 179 | } 180 | } 181 | } 182 | 183 | impl Module { 184 | fn ensure_uid(key: &::AccountId) -> Result> { 185 | let uid = >::get(&key).ok_or(Error::::NoAccount)?; 186 | if !>::get(uid).contains(key) { 187 | return Err(Error::::Unauthorized); 188 | } 189 | Ok(uid) 190 | } 191 | 192 | fn ensure_key_unused(key: &::AccountId) -> Result<(), Error> { 193 | if >::get(&key).is_some() { 194 | Err(Error::::KeyInUse) 195 | } else { 196 | Ok(()) 197 | } 198 | } 199 | 200 | fn create_account(key: ::AccountId) -> Result> { 201 | let uid = >::get(); 202 | let next_uid = uid 203 | .checked_add(&1u8.into()) 204 | .ok_or(Error::::UidOverflow)?; 205 | let gen = T::Gen::from(0u8); 206 | >::put(next_uid); 207 | >::insert(uid, gen); 208 | Self::deposit_event(RawEvent::AccountCreated(uid)); 209 | Self::add_key_to_uid(uid, key); 210 | Ok(uid) 211 | } 212 | 213 | fn add_key_to_uid(uid: T::Uid, key: ::AccountId) { 214 | >::insert(key.clone(), uid); 215 | >::mutate(uid, |keys| keys.insert(key.clone())); 216 | Self::deposit_event(RawEvent::KeyAdded(uid, key)); 217 | } 218 | 219 | fn remove_key_from_uid(uid: T::Uid, key: ::AccountId) { 220 | // The lookup can't be removed in case someone sends a transaction 221 | // to an old key or the same key being added to a different account 222 | // after being revoked. 223 | >::mutate(uid, |keys| keys.remove(&key)); 224 | Self::deposit_event(RawEvent::KeyRemoved(uid, key)); 225 | } 226 | } 227 | 228 | impl StoredMap<::AccountId, ::AccountData> for Module { 229 | fn get(k: &::AccountId) -> ::AccountData { 230 | if let Some(uid) = >::get(k) { 231 | >::get(&uid) 232 | } else { 233 | ::AccountData::default() 234 | } 235 | } 236 | 237 | fn is_explicit(k: &::AccountId) -> bool { 238 | >::get(k).is_some() 239 | } 240 | 241 | fn mutate( 242 | k: &::AccountId, 243 | f: impl FnOnce(&mut ::AccountData) -> R, 244 | ) -> R { 245 | if >::get(k).is_none() { 246 | Self::create_account(k.clone()).ok(); 247 | } 248 | if let Some(uid) = >::get(k) { 249 | >::mutate(&uid, f) 250 | } else { 251 | // This should only happen if uid overflows. 252 | f(&mut ::AccountData::default()) 253 | } 254 | } 255 | 256 | fn mutate_exists( 257 | k: &::AccountId, 258 | f: impl FnOnce(&mut Option<::AccountData>) -> R, 259 | ) -> R { 260 | if >::get(k).is_none() { 261 | Self::create_account(k.clone()).ok(); 262 | } 263 | if let Some(uid) = >::get(k) { 264 | >::mutate_exists(&uid, f) 265 | } else { 266 | // This should only happen if uid overflows. 267 | f(&mut None) 268 | } 269 | } 270 | 271 | fn try_mutate_exists( 272 | k: &::AccountId, 273 | f: impl FnOnce(&mut Option<::AccountData>) -> Result, 274 | ) -> Result { 275 | if >::get(k).is_none() { 276 | Self::create_account(k.clone()).ok(); 277 | } 278 | if let Some(uid) = >::get(k) { 279 | >::try_mutate_exists(&uid, f) 280 | } else { 281 | // This should only happen if uid overflows. 282 | f(&mut None) 283 | } 284 | } 285 | 286 | fn remove(k: &::AccountId) { 287 | if let Some(uid) = >::get(k) { 288 | >::remove(&uid); 289 | } 290 | } 291 | } 292 | -------------------------------------------------------------------------------- /identity/pallet/src/mock.rs: -------------------------------------------------------------------------------- 1 | use crate::{Module, Trait}; 2 | use frame_support::{impl_outer_origin, parameter_types, weights::Weight}; 3 | use frame_system as system; 4 | use sp_core::H256; 5 | use sp_runtime::{ 6 | testing::Header, 7 | traits::{BlakeTwo256, IdentityLookup}, 8 | Perbill, 9 | }; 10 | 11 | impl_outer_origin! { 12 | pub enum Origin for Test {} 13 | } 14 | 15 | #[derive(Clone, Eq, PartialEq)] 16 | pub struct Test; 17 | 18 | parameter_types! { 19 | pub const BlockHashCount: u64 = 250; 20 | pub const MaximumBlockWeight: Weight = 1024; 21 | pub const MaximumBlockLength: u32 = 2 * 1024; 22 | pub const AvailableBlockRatio: Perbill = Perbill::from_percent(75); 23 | } 24 | impl system::Trait for Test { 25 | type BaseCallFilter = (); 26 | type Origin = Origin; 27 | type Call = (); 28 | type Index = u64; 29 | type BlockNumber = u64; 30 | type Hash = H256; 31 | type Hashing = BlakeTwo256; 32 | type AccountId = u64; 33 | type Lookup = IdentityLookup; 34 | type Header = Header; 35 | type Event = (); 36 | type BlockHashCount = BlockHashCount; 37 | type MaximumBlockWeight = MaximumBlockWeight; 38 | type DbWeight = (); 39 | type BlockExecutionWeight = (); 40 | type ExtrinsicBaseWeight = (); 41 | type MaximumExtrinsicWeight = MaximumBlockWeight; 42 | type MaximumBlockLength = MaximumBlockLength; 43 | type AvailableBlockRatio = AvailableBlockRatio; 44 | type Version = (); 45 | type PalletInfo = (); 46 | type AccountData = (); 47 | type OnNewAccount = (); 48 | type OnKilledAccount = (); 49 | type SystemWeightInfo = (); 50 | } 51 | impl Trait for Test { 52 | type Uid = u8; 53 | type Mask = [u8; 32]; 54 | type Gen = u8; 55 | type Cid = u32; 56 | type AccountData = (); 57 | type Event = (); 58 | } 59 | pub type IdentityModule = Module; 60 | 61 | pub fn new_test_ext() -> sp_io::TestExternalities { 62 | system::GenesisConfig::default() 63 | .build_storage::() 64 | .unwrap() 65 | .into() 66 | } 67 | -------------------------------------------------------------------------------- /identity/pallet/src/tests.rs: -------------------------------------------------------------------------------- 1 | use crate::mock::*; 2 | use frame_support::assert_ok; 3 | 4 | #[test] 5 | fn set_identity() { 6 | new_test_ext().execute_with(|| { 7 | let key1 = Origin::signed(1); 8 | let key2 = Origin::signed(2); 9 | assert_ok!(IdentityModule::create_account_for(Origin::signed(0), 1)); 10 | assert_eq!(IdentityModule::identity(0), None); 11 | 12 | assert_ok!(IdentityModule::set_identity(key1.clone(), None, 42)); 13 | assert_eq!(IdentityModule::identity(0), Some(42)); 14 | 15 | assert_ok!(IdentityModule::add_key(key1.clone(), 2)); 16 | assert_ok!(IdentityModule::set_identity(key2.clone(), Some(42), 43)); 17 | assert_eq!(IdentityModule::identity(0), Some(43)); 18 | 19 | assert_ok!(IdentityModule::remove_key(key1, 2)); 20 | assert!(IdentityModule::set_identity(key2, Some(43), 44).is_err()); 21 | assert_eq!(IdentityModule::identity(0), Some(43)); 22 | }); 23 | } 24 | 25 | #[test] 26 | fn change_password() { 27 | new_test_ext().execute_with(|| { 28 | let key1 = Origin::signed(1); 29 | let key2 = Origin::signed(2); 30 | assert_ok!(IdentityModule::create_account_for(Origin::signed(0), 1)); 31 | assert_ok!(IdentityModule::add_key(key1, 2)); 32 | assert!(IdentityModule::change_password(key2.clone(), [0; 32], 0).is_err()); 33 | assert_ok!(IdentityModule::change_password(key2, [0; 32], 1)); 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /identity/utils/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sunshine-identity-utils" 3 | version = "0.2.0" 4 | authors = ["David Craven "] 5 | edition = "2018" 6 | description = "Utils shared by the runtime and the client." 7 | license = "ISC" 8 | documentation = "https://docs.rs/sunshine-identity-utils" 9 | repository = "https://github.com/sunshine-protocol/sunshine-identity" 10 | 11 | [features] 12 | default = ["std"] 13 | std = [ 14 | "codec/std", 15 | ] 16 | 17 | [dependencies] 18 | codec = { version = "1.3.0", package = "parity-scale-codec", default-features = false, features = ["derive"] } 19 | -------------------------------------------------------------------------------- /identity/utils/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(not(feature = "std"), no_std)] 2 | 3 | use codec::{Decode, Encode}; 4 | 5 | #[derive(Clone, Debug, Eq, PartialEq, Decode, Encode)] 6 | pub struct DeviceMaskData { 7 | pub mask: M, 8 | pub gen: G, 9 | } 10 | --------------------------------------------------------------------------------