├── .github ├── dependabot.yml └── workflows │ ├── client.yml │ ├── preferences.yml │ ├── release.yaml │ └── wallet.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── examples └── browser │ ├── .gitignore │ ├── README.md │ ├── bootstrap.js │ ├── index.html │ ├── index.js │ ├── package.json │ └── webpack.config.js └── packages ├── crw-client ├── Cargo.toml ├── README.md └── src │ ├── client.rs │ ├── error.rs │ ├── json.rs │ ├── lib.rs │ └── tx.rs ├── crw-preferences ├── Cargo.toml ├── Makefile ├── README.md ├── crw_preferences.h └── src │ ├── encrypted.rs │ ├── ffi.rs │ ├── io │ ├── mod.rs │ ├── native.rs │ └── wasm.rs │ ├── lib.rs │ ├── preferences.rs │ ├── unencrypted.rs │ └── wasm.rs └── crw-wallet ├── Cargo.toml ├── Makefile ├── README.md ├── ffi-binding.h └── src ├── crypto.rs ├── error.rs ├── ffi.rs ├── lib.rs └── wasm32_bindgen.rs /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: cargo 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | -------------------------------------------------------------------------------- /.github/workflows/client.yml: -------------------------------------------------------------------------------- 1 | name: crw-client 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | defaults: 10 | run: 11 | shell: bash 12 | working-directory: packages/crw-client 13 | 14 | jobs: 15 | build: 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v2 21 | 22 | - name: Install latest rust toolchain 23 | uses: actions-rs/toolchain@v1 24 | with: 25 | toolchain: stable 26 | default: true 27 | override: true 28 | 29 | - name: Build 30 | uses: actions-rs/cargo@v1 31 | with: 32 | command: build 33 | args: --release --all-features 34 | 35 | lints: 36 | name: Lints (fmt + clippy) 37 | runs-on: ubuntu-latest 38 | steps: 39 | - name: Checkout sources 40 | uses: actions/checkout@v2 41 | 42 | - name: Install stable toolchain 43 | uses: actions-rs/toolchain@v1 44 | with: 45 | toolchain: stable 46 | override: true 47 | components: rustfmt, clippy 48 | 49 | - name: Run cargo fmt 50 | run: cargo fmt --all -- --check 51 | - name: Run cargo clippy 52 | run: cargo clippy -- -D warnings 53 | -------------------------------------------------------------------------------- /.github/workflows/preferences.yml: -------------------------------------------------------------------------------- 1 | name: crw-preferences 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | defaults: 10 | run: 11 | shell: bash 12 | working-directory: packages/crw-preferences 13 | 14 | jobs: 15 | build: 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v2 21 | 22 | - name: Install latest rust toolchain 23 | uses: actions-rs/toolchain@v1 24 | with: 25 | toolchain: stable 26 | default: true 27 | override: true 28 | 29 | - name: Build 30 | env: 31 | CARGO_BIN_NAME: test 32 | uses: actions-rs/cargo@v1 33 | with: 34 | command: build 35 | args: --release --all-features 36 | 37 | lints: 38 | name: Lints (fmt + clippy) 39 | runs-on: ubuntu-latest 40 | steps: 41 | - name: Checkout sources 42 | uses: actions/checkout@v2 43 | 44 | - name: Install stable toolchain 45 | uses: actions-rs/toolchain@v1 46 | with: 47 | toolchain: stable 48 | override: true 49 | components: rustfmt, clippy 50 | 51 | - name: Run cargo fmt 52 | run: CARGO_BIN_NAME=test cargo fmt --all -- --check 53 | - name: Run cargo clippy 54 | run: CARGO_BIN_NAME=test cargo clippy -- -D warnings 55 | - name: Run cargo test 56 | run: CARGO_BIN_NAME=test cargo test --all-features 57 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Publish Packages 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v[0-9]+.[0-9]+.[0-9]+' 7 | 8 | jobs: 9 | publish: 10 | name: Publish packages on crates.io 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout 🛎️ 15 | uses: actions/checkout@v2 16 | 17 | - name: Setup Rust ⚙ 18 | uses: actions-rs/toolchain@v1 19 | with: 20 | profile: minimal 21 | toolchain: 1.61.0 22 | override: true 23 | components: rustfmt, clippy 24 | 25 | - name: Publish crw-wallet crates.io 📤 26 | uses: katyo/publish-crates@v1 27 | with: 28 | path: './packages/crw-wallet' 29 | registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }} 30 | 31 | - name: Publish crw-client crates.io 📤 32 | uses: katyo/publish-crates@v1 33 | with: 34 | path: './packages/crw-client' 35 | registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }} 36 | -------------------------------------------------------------------------------- /.github/workflows/wallet.yml: -------------------------------------------------------------------------------- 1 | name: crw-wallet 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | defaults: 10 | run: 11 | shell: bash 12 | working-directory: packages/crw-wallet 13 | 14 | jobs: 15 | build: 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v2 21 | 22 | - name: Install latest rust toolchain 23 | uses: actions-rs/toolchain@v1 24 | with: 25 | toolchain: stable 26 | default: true 27 | override: true 28 | 29 | - name: Build 30 | uses: actions-rs/cargo@v1 31 | with: 32 | command: build 33 | args: --release --all-features 34 | 35 | lints: 36 | name: Lints (fmt + clippy) 37 | runs-on: ubuntu-latest 38 | steps: 39 | - name: Checkout sources 40 | uses: actions/checkout@v2 41 | 42 | - name: Install stable toolchain 43 | uses: actions-rs/toolchain@v1 44 | with: 45 | toolchain: stable 46 | override: true 47 | components: rustfmt, clippy 48 | 49 | - name: Run cargo fmt 50 | run: cargo fmt --all -- --check 51 | - name: Run cargo clippy 52 | run: cargo clippy -- -D warnings 53 | - name: Run cargo test 54 | run: cargo test --all-features 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | packages/**/pkg 3 | packages/**/target 4 | Cargo.lock 5 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["packages/*"] 3 | 4 | [profile.release.package.crw-wallet] 5 | codegen-units = 1 6 | incremental = false 7 | 8 | [profile.release.package.crw-client] 9 | codegen-units = 1 10 | incremental = false 11 | 12 | [profile.release.package.crw-preferences] 13 | codegen-units = 1 14 | incremental = false 15 | 16 | [profile.release] 17 | rpath = false 18 | lto = true 19 | overflow-checks = true 20 | opt-level = 3 21 | debug = false 22 | debug-assertions = false 23 | 24 | [patch.crates-io] 25 | cosmos-sdk-proto = { git = "https://github.com/forbole/cosmos-rust", branch = "main"} 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cosmos Rust wallet 2 | 3 | A collection of packages to sign and send txs inside Cosmos-sdk based chains. 4 | 5 | ![GitHub](https://img.shields.io/github/license/forbole/cosmos-rust-wallet.svg) 6 | 7 | | Packages | Build | Crates.io | 8 | | ------------- | ------ | ------ | 9 | | crw-client | [![Package-client](https://github.com/forbole/cosmos-rust-wallet/actions/workflows/client.yml/badge.svg)](https://github.com/forbole/cosmos-rust-wallet/actions/workflows/client.yml)| [![crw-client on crates.io](https://img.shields.io/crates/v/crw-client.svg)](https://crates.io/crates/crw-client)| 10 | | crw-wallet | [![Package-wallet](https://github.com/forbole/cosmos-rust-wallet/actions/workflows/wallet.yml/badge.svg)](https://github.com/forbole/cosmos-rust-wallet/actions/workflows/wallet.yml)| [![crw-wallet on crates.io](https://img.shields.io/crates/v/crw-wallet.svg)](https://crates.io/crates/crw-wallet)| 11 | -------------------------------------------------------------------------------- /examples/browser/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | package-lock.json -------------------------------------------------------------------------------- /examples/browser/README.md: -------------------------------------------------------------------------------- 1 | # Browser example 2 | This folder contains an example on how the `crw-wallet` can be used from a javascript application. 3 | 4 | # Setup 5 | In order to use the `crw-wallet` you need to build it and generate the js glue code that interact with WASM. 6 | 7 | To do so you need to install the following tools: 8 | * [Rust](https://www.rust-lang.org/tools/install) 9 | * [wasm-pack](https://rustwasm.github.io/wasm-pack/installer/) 10 | 11 | After installing these tools move inside the `cosmos-rust-wallet/packages/crw-wallet` directory and run the following command: 12 | `wasm-pack build --release -- --features wasm-bindgen` this will build the wallet and prepare a node module 13 | inside a new directory **pkg**. 14 | 15 | Now go back to the `examples/browser` directory, install the required dependencies with `npm install` and 16 | finally launch the demo with `npm start`. 17 | 18 | Open a browser and navigate to `http://localhost:8080` to access the web application 19 | -------------------------------------------------------------------------------- /examples/browser/bootstrap.js: -------------------------------------------------------------------------------- 1 | // A dependency graph that contains some wasm code **must** all be imported 2 | // asynchronously. This `bootstrap.js` file does the single async import, so 3 | // that no one else needs to worry about it again. 4 | import("./index.js") 5 | .catch(e => console.error("Error importing `index.js`:", e)); 6 | -------------------------------------------------------------------------------- /examples/browser/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Web wallet demo 6 | 7 | 8 | 9 | 10 |

Web wallet demo

11 |
12 |

Mnemonic:

13 |

14 |
15 |
16 |

Address:

17 |

18 |
19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /examples/browser/index.js: -------------------------------------------------------------------------------- 1 | import {randomMnemonic, MnemonicWallet} from "crw-wallet"; 2 | 3 | const cosmos_dp = "m/44'/118'/0'/0/0"; 4 | const generate_btn = document.getElementById("generate_btn"); 5 | const mnemonic_container = document.getElementById("mnemonic"); 6 | const address_container = document.getElementById("address"); 7 | 8 | generate_btn.addEventListener("click", () => { 9 | const mnemonic = randomMnemonic(); 10 | const wallet = new MnemonicWallet(mnemonic, cosmos_dp); 11 | const address = wallet.getBech32Address("cosmos"); 12 | mnemonic_container.textContent = mnemonic; 13 | address_container.textContent = address; 14 | wallet.free(); 15 | }); -------------------------------------------------------------------------------- /examples/browser/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "create-wasm-app", 3 | "version": "0.1.0", 4 | "description": "create an app to consume rust-generated wasm packages", 5 | "main": "index.js", 6 | "bin": { 7 | "create-wasm-app": ".bin/create-wasm-app.js" 8 | }, 9 | "scripts": { 10 | "build": "webpack --config webpack.config.js", 11 | "start": "webpack-dev-server" 12 | }, 13 | "keywords": [ 14 | "webassembly", 15 | "wasm", 16 | "rust", 17 | "webpack" 18 | ], 19 | "author": "Ashley Williams ", 20 | "license": "(MIT OR Apache-2.0)", 21 | "bugs": { 22 | "url": "https://github.com/rustwasm/create-wasm-app/issues" 23 | }, 24 | "homepage": "https://github.com/rustwasm/create-wasm-app#readme", 25 | "dependencies": { 26 | "crw-wallet": "file:../../packages/crw-wallet/pkg" 27 | }, 28 | "devDependencies": { 29 | "copy-webpack-plugin": "^11.0.0", 30 | "webpack": "^5.74.0", 31 | "webpack-cli": "^4.10.0", 32 | "webpack-dev-server": "^4.10.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /examples/browser/webpack.config.js: -------------------------------------------------------------------------------- 1 | const CopyWebpackPlugin = require("copy-webpack-plugin"); 2 | const path = require('path'); 3 | 4 | module.exports = { 5 | entry: "./bootstrap.js", 6 | output: { 7 | path: path.resolve(__dirname, "dist"), 8 | filename: "bootstrap.js", 9 | }, 10 | experiments: { 11 | asyncWebAssembly: true, 12 | }, 13 | mode: "development", 14 | plugins: [ 15 | new CopyWebpackPlugin({ 16 | patterns: ['index.html'] 17 | }) 18 | ], 19 | }; 20 | -------------------------------------------------------------------------------- /packages/crw-client/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "crw-client" 3 | version = "0.1.0" 4 | authors = ["bragaz Result { 40 | let grpc_uri = grpc_addr.parse::()?; 41 | let grpc_channel = Channel::builder(grpc_uri); 42 | 43 | Ok(CosmosClient { 44 | grpc_channel, 45 | lcd_addr: lcd_addr.to_string(), 46 | }) 47 | } 48 | 49 | /// Gets the information of a full node. 50 | pub async fn node_info(&self) -> Result { 51 | let endpoint = format!("{}{}", self.lcd_addr, "/node_info"); 52 | let response = get(&endpoint) 53 | .await 54 | .map_err(|err| CosmosError::Lcd(err.to_string()))?; 55 | 56 | match response.status() { 57 | StatusCode::OK => { 58 | // Unwrap here is safe since we already knew that the response is good 59 | Ok(response.json::().await.unwrap().node_info) 60 | } 61 | status_code => Err(CosmosError::Lcd(status_code.to_string())), 62 | } 63 | } 64 | 65 | /// Returns the account data associated to the given address. 66 | pub async fn get_account_data(&self, address: &str) -> Result { 67 | // Create channel connection to the gRPC server 68 | let channel = self 69 | .grpc_channel 70 | .connect() 71 | .await 72 | .map_err(|err| CosmosError::Grpc(err.to_string()))?; 73 | 74 | // Create gRPC query auth client from channel 75 | let mut client = QueryClient::new(channel); 76 | 77 | // Build a new request 78 | let request = Request::new(QueryAccountRequest { 79 | address: address.to_owned(), 80 | }); 81 | 82 | // Send request and wait for response 83 | let response = client 84 | .account(request) 85 | .await 86 | .map_err(|err| CosmosError::Grpc(err.to_string()))? 87 | .into_inner(); 88 | 89 | // Decode response body into BaseAccount 90 | let base_account: BaseAccount = 91 | prost::Message::decode(response.account.unwrap().value.as_ref())?; 92 | 93 | Ok(base_account) 94 | } 95 | 96 | /// Broadcast a tx using the gRPC interface. 97 | pub async fn broadcast_tx( 98 | &self, 99 | tx: &Tx, 100 | mode: BroadcastMode, 101 | ) -> Result, CosmosError> { 102 | // Some buffers used to serialize the objects 103 | let mut serialized_body: Vec = Vec::new(); 104 | let mut serialized_auth: Vec = Vec::new(); 105 | let mut serialized_tx: Vec = Vec::new(); 106 | 107 | // Serialize the tx body and auth_info 108 | if let Some(body) = &tx.body { 109 | prost::Message::encode(body, &mut serialized_body)?; 110 | } 111 | if let Some(auth_info) = &tx.auth_info { 112 | prost::Message::encode(auth_info, &mut serialized_auth)?; 113 | } 114 | 115 | // Prepare and serialize the TxRaw 116 | let tx_raw = TxRaw { 117 | body_bytes: serialized_body, 118 | auth_info_bytes: serialized_auth, 119 | signatures: tx.signatures.clone(), 120 | }; 121 | prost::Message::encode(&tx_raw, &mut serialized_tx)?; 122 | 123 | // Open the channel and perform the actual gRPC BroadcastTxRequest 124 | let channel = self 125 | .grpc_channel 126 | .connect() 127 | .await 128 | .map_err(|err| CosmosError::Grpc(err.to_string()))?; 129 | let mut service = ServiceClient::new(channel); 130 | 131 | let request = Request::new(BroadcastTxRequest { 132 | tx_bytes: serialized_tx, 133 | mode: mode as i32, 134 | }); 135 | 136 | let response = service 137 | .broadcast_tx(request) 138 | .await 139 | .map_err(|e| CosmosError::Grpc(e.to_string()))? 140 | .into_inner(); 141 | 142 | Ok(response.tx_response) 143 | } 144 | } 145 | 146 | #[cfg(test)] 147 | mod tests { 148 | use super::*; 149 | use crate::tx::TxBuilder; 150 | use cosmos_sdk_proto::cosmos::{bank::v1beta1::MsgSend, base::v1beta1::Coin}; 151 | use crw_wallet::crypto::MnemonicWallet; 152 | 153 | static TEST_MNEMONIC: &str = "elephant luggage finger obscure nest smooth flag clay recycle unfair capital category organ bicycle gallery sight canyon hotel dutch skull today pink scale aisle"; 154 | static DESMOS_DERIVATION_PATH: &str = "m/44'/852'/0'/0/0"; 155 | 156 | #[actix_rt::test] 157 | async fn node_info() { 158 | let cosmos_client = 159 | CosmosClient::new("http://localhost:1317", "http://localhost:9090").unwrap(); 160 | 161 | let info = cosmos_client.node_info().await; 162 | 163 | assert!(info.is_ok()); 164 | assert_eq!("testchain", info.unwrap().network); 165 | } 166 | 167 | #[actix_rt::test] 168 | async fn broadcast_tx() { 169 | let wallet = MnemonicWallet::new(TEST_MNEMONIC, DESMOS_DERIVATION_PATH).unwrap(); 170 | 171 | let cosmos_client = 172 | CosmosClient::new("http://localhost:1317", "http://localhost:9090").unwrap(); 173 | 174 | let address = wallet.get_bech32_address("desmos").unwrap(); 175 | let account_data = cosmos_client.get_account_data(&address).await.unwrap(); 176 | 177 | let amount = Coin { 178 | denom: "stake".to_string(), 179 | amount: "10".to_string(), 180 | }; 181 | 182 | let msg_snd = MsgSend { 183 | from_address: address, 184 | to_address: "desmos18ek6mnlxj8sysrtvu60k5zj0re7s5n42yncner".to_string(), 185 | amount: vec![amount], 186 | }; 187 | 188 | let tx = TxBuilder::new("testchain") 189 | .memo("Test memo") 190 | .account_info(account_data.sequence, account_data.account_number) 191 | .timeout_height(0) 192 | .fee("stake", "5000", 300_000) 193 | .add_message("/cosmos.bank.v1beta1.Msg/Send", msg_snd) 194 | .unwrap() 195 | .sign(&wallet) 196 | .unwrap(); 197 | 198 | let res = cosmos_client 199 | .broadcast_tx(&tx, BroadcastMode::Block) 200 | .await 201 | .unwrap() 202 | .unwrap(); 203 | 204 | print!("{}", res.raw_log); 205 | assert_eq!(0, res.code); 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /packages/crw-client/src/error.rs: -------------------------------------------------------------------------------- 1 | use prost::{DecodeError, EncodeError}; 2 | use thiserror::Error; 3 | 4 | /// The various error that can be raised from [`super::client::CosmosClient`]. 5 | #[derive(Error, Debug, Eq, PartialEq)] 6 | pub enum CosmosError { 7 | #[error("Encoding error: {0}")] 8 | Encode(String), 9 | 10 | #[error("Decoding error: {0}")] 11 | Decode(String), 12 | 13 | #[error("Sign error: {0}")] 14 | Sign(String), 15 | 16 | #[error("gRPC error: {0}")] 17 | Grpc(String), 18 | 19 | #[error("LCD error: {0}")] 20 | Lcd(String), 21 | } 22 | 23 | /// The various error that can be raised from [`super::tx::TxBuilder`]. 24 | #[derive(Error, Debug, Clone, Eq, PartialEq)] 25 | pub enum TxBuildError { 26 | #[error("Encoding error: {0}")] 27 | Encode(String), 28 | 29 | #[error("Missing account information")] 30 | NoAccountInfo, 31 | 32 | #[error("Missing transaction fee")] 33 | NoFee, 34 | 35 | #[error("Sign error: {0}")] 36 | Sign(String), 37 | } 38 | 39 | impl From for CosmosError { 40 | fn from(e: EncodeError) -> Self { 41 | CosmosError::Encode(e.to_string()) 42 | } 43 | } 44 | 45 | impl From for CosmosError { 46 | fn from(e: DecodeError) -> Self { 47 | CosmosError::Decode(e.to_string()) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/crw-client/src/json.rs: -------------------------------------------------------------------------------- 1 | //! Module that contains the json types binding used to communicate with a cosmos based blockchain node. 2 | use serde::{Deserialize, Serialize}; 3 | 4 | /// NodeInfoResponse contains the response of the LCD request `/node_info`. 5 | #[derive(Clone, Serialize, Deserialize)] 6 | pub struct NodeInfoResponse { 7 | pub node_info: NodeInfo, 8 | } 9 | 10 | /// NodeInfo contains the information of a cosmos based blockchain node. 11 | #[derive(Clone, Serialize, Deserialize)] 12 | pub struct NodeInfo { 13 | pub id: String, 14 | pub listen_addr: String, 15 | pub network: String, 16 | pub version: String, 17 | pub moniker: String, 18 | } 19 | -------------------------------------------------------------------------------- /packages/crw-client/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod client; 2 | mod error; 3 | pub mod json; 4 | pub mod tx; 5 | 6 | pub use crate::error::{CosmosError, TxBuildError}; 7 | -------------------------------------------------------------------------------- /packages/crw-client/src/tx.rs: -------------------------------------------------------------------------------- 1 | //! Module to easily build and sign transactions for cosmos based blockchains. 2 | //! 3 | //! This module provides a facility to build and sign transactions for cosmos based blockchains. 4 | 5 | use crate::error::TxBuildError; 6 | use cosmos_sdk_proto::cosmos::{ 7 | base::v1beta1::Coin, 8 | tx::v1beta1::{ 9 | mode_info::{Single, Sum}, 10 | AuthInfo, Fee, ModeInfo, SignDoc, SignerInfo, Tx, TxBody, 11 | }, 12 | }; 13 | use crw_wallet::crypto::MnemonicWallet; 14 | use prost::EncodeError; 15 | use prost_types::Any; 16 | 17 | /// AccountInfo is a private structure which represents the information of the account 18 | /// that is performing the transaction. 19 | struct AccountInfo { 20 | pub sequence: u64, 21 | pub number: u64, 22 | } 23 | 24 | /// TxBuilder represents the single signer transaction builder. 25 | pub struct TxBuilder { 26 | chain_id: String, 27 | account_info: Option, 28 | tx_body: TxBody, 29 | fee: Option, 30 | } 31 | 32 | impl TxBuilder { 33 | /// Function to create a new `TxBuilder`. 34 | /// 35 | /// # Example 36 | /// 37 | /// This is a simple example of a cosmos Send transaction. 38 | /// 39 | ///``` 40 | /// use crw_wallet::crypto::MnemonicWallet; 41 | /// use cosmos_sdk_proto::cosmos::base::v1beta1::Coin; 42 | /// use cosmos_sdk_proto::cosmos::bank::v1beta1::MsgSend; 43 | /// use crw_client::tx::TxBuilder; 44 | /// 45 | /// let cosmos_derivation_path = "m/44'/118'/0'/0/0"; 46 | /// let (wallet, mnemonic) = MnemonicWallet::random(cosmos_derivation_path).unwrap(); 47 | /// 48 | /// let amount = Coin { 49 | /// denom: "stake".to_string(), 50 | /// amount: "10".to_string(), 51 | /// }; 52 | /// let msg_snd = MsgSend { 53 | /// // Get the bech32 address associated to the wallet 54 | /// from_address: wallet.get_bech32_address("cosmos").unwrap(), 55 | /// to_address: "cosmos18ek6mnlxj8sysrtvu60k5zj0re7s5n42yncner".to_string(), 56 | /// amount: vec![amount], 57 | /// }; 58 | /// 59 | /// let tx = TxBuilder::new("testchain") 60 | /// .memo("Test memo") 61 | /// .account_info(1, 5) 62 | /// .fee("stake", "10", 300_000) 63 | /// .timeout_height(1000) 64 | /// .add_message("/cosmos.bank.v1beta1.Msg/Send", msg_snd).unwrap() 65 | /// .sign(&wallet).unwrap(); 66 | ///``` 67 | pub fn new(chain_id: &str) -> TxBuilder { 68 | TxBuilder { 69 | chain_id: chain_id.to_string(), 70 | account_info: Option::None, 71 | tx_body: TxBody { 72 | messages: Vec::::new(), 73 | memo: "".to_string(), 74 | timeout_height: 0, 75 | extension_options: Vec::::new(), 76 | non_critical_extension_options: Vec::::new(), 77 | }, 78 | fee: Option::None, 79 | } 80 | } 81 | 82 | /// Sets the account information. 83 | pub fn account_info(mut self, sequence: u64, number: u64) -> Self { 84 | self.account_info = Some(AccountInfo { sequence, number }); 85 | self 86 | } 87 | 88 | /// Append a message to the transaction messages. 89 | pub fn add_message( 90 | self, 91 | msg_type: &str, 92 | msg: M, 93 | ) -> Result { 94 | let mut serialized: Vec = Vec::new(); 95 | 96 | prost::Message::encode(&msg, &mut serialized)?; 97 | 98 | Ok(self.add_message_raw(msg_type, serialized)) 99 | } 100 | 101 | fn add_message_raw(mut self, msg_type: &str, binary: Vec) -> Self { 102 | let data = Any { 103 | type_url: msg_type.to_owned(), 104 | value: binary, 105 | }; 106 | 107 | self.tx_body.messages.push(data); 108 | self 109 | } 110 | 111 | /// Sets the transaction memo. 112 | pub fn memo(mut self, memo: &str) -> Self { 113 | self.tx_body.memo = memo.to_string(); 114 | self 115 | } 116 | 117 | /// Sets the transaction timout height. 118 | pub fn timeout_height(mut self, timeout_height: u64) -> Self { 119 | self.tx_body.timeout_height = timeout_height; 120 | self 121 | } 122 | 123 | /// Sets the transaction fee. 124 | pub fn fee(mut self, denom: &str, amount: &str, gas_limit: u64) -> Self { 125 | let coin = Coin { 126 | denom: denom.to_string(), 127 | amount: amount.to_string(), 128 | }; 129 | 130 | self.fee = Some(Fee { 131 | amount: vec![coin], 132 | gas_limit, 133 | payer: "".to_string(), 134 | granter: "".to_string(), 135 | }); 136 | 137 | self 138 | } 139 | 140 | /// Generate the signed transaction using the provided wallet. 141 | /// 142 | /// The transaction will be signed following the `SIGN_MODE_DIRECT` specification. 143 | /// See [Cosmos adr-020](https://github.com/cosmos/cosmos-sdk/blob/master/docs/architecture/adr-020-protobuf-transaction-encoding.md) 144 | /// for more details. 145 | /// 146 | /// # Errors 147 | /// Returns an ['Err`] if one of the following cases: 148 | /// * If an error occur during the transaction serialization to protobuf 149 | /// * If an error occur during the transaction signature. 150 | pub fn sign(self, wallet: &MnemonicWallet) -> Result { 151 | if self.account_info.is_none() { 152 | return Result::Err(TxBuildError::NoAccountInfo); 153 | } 154 | 155 | if self.fee.is_none() { 156 | return Result::Err(TxBuildError::NoFee); 157 | } 158 | 159 | // Protobuf tx_body serialization 160 | let mut tx_body_buffer = Vec::new(); 161 | prost::Message::encode(&self.tx_body, &mut tx_body_buffer)?; 162 | 163 | let mut serialized_key: Vec = Vec::new(); 164 | prost::Message::encode(&wallet.get_pub_key().to_bytes(), &mut serialized_key)?; 165 | 166 | // TODO extract a better key type (not an Any type) 167 | let public_key_any = Any { 168 | type_url: "/cosmos.crypto.secp256k1.PubKey".to_string(), 169 | value: serialized_key, 170 | }; 171 | 172 | // Signer specifications 173 | let single_signer = Single { mode: 1 }; 174 | let single_signer_specifier = Some(Sum::Single(single_signer)); 175 | let broadcast_mode = Some(ModeInfo { 176 | sum: single_signer_specifier, 177 | }); 178 | 179 | // Building signer's info 180 | let signer_info = SignerInfo { 181 | public_key: Some(public_key_any), 182 | mode_info: broadcast_mode, 183 | sequence: self.account_info.as_ref().unwrap().sequence, 184 | }; 185 | 186 | let auth_info = AuthInfo { 187 | signer_infos: vec![signer_info], 188 | fee: Some(self.fee.as_ref().unwrap().clone()), 189 | }; 190 | 191 | // Protobuf auth_info serialization 192 | let mut auth_buffer = Vec::new(); 193 | prost::Message::encode(&auth_info, &mut auth_buffer)?; 194 | let sign_doc = SignDoc { 195 | body_bytes: tx_body_buffer, 196 | auth_info_bytes: auth_buffer, 197 | chain_id: self.chain_id, 198 | account_number: self.account_info.as_ref().unwrap().number, 199 | }; 200 | 201 | // Protobuf sign_doc serialization 202 | let mut sign_doc_buffer = Vec::new(); 203 | prost::Message::encode(&sign_doc, &mut sign_doc_buffer)?; 204 | 205 | // sign the doc buffer 206 | let signature = wallet 207 | .sign(&sign_doc_buffer) 208 | .map_err(|err| TxBuildError::Sign(err.to_string()))?; 209 | 210 | // compose the raw tx 211 | Result::Ok(Tx { 212 | body: Some(self.tx_body), 213 | auth_info: Some(auth_info), 214 | signatures: vec![signature], 215 | }) 216 | } 217 | } 218 | 219 | impl From for TxBuildError { 220 | fn from(e: EncodeError) -> Self { 221 | TxBuildError::Encode(e.to_string()) 222 | } 223 | } 224 | 225 | #[cfg(test)] 226 | mod tests { 227 | use crate::tx::{TxBuildError, TxBuilder}; 228 | use cosmos_sdk_proto::cosmos::bank::v1beta1::MsgSend; 229 | use cosmos_sdk_proto::cosmos::base::v1beta1::Coin; 230 | use crw_wallet::crypto::MnemonicWallet; 231 | 232 | static TEST_MNEMONIC: &str = "elephant luggage finger obscure nest smooth flag clay recycle unfair capital category organ bicycle gallery sight canyon hotel dutch skull today pink scale aisle"; 233 | static DESMOS_DERIVATION_PATH: &str = "m/44'/852'/0'/0/0"; 234 | 235 | #[test] 236 | fn test_missing_fee() { 237 | let wallet = MnemonicWallet::new(TEST_MNEMONIC, DESMOS_DERIVATION_PATH).unwrap(); 238 | 239 | let amount = Coin { 240 | denom: "stake".to_string(), 241 | amount: "10".to_string(), 242 | }; 243 | let msg_snd = MsgSend { 244 | from_address: wallet.get_bech32_address("desmos").unwrap(), 245 | to_address: "desmos18ek6mnlxj8sysrtvu60k5zj0re7s5n42yncner".to_string(), 246 | amount: vec![amount], 247 | }; 248 | 249 | let tx_builder = TxBuilder::new("testchain") 250 | .memo("Test memo") 251 | .account_info(0, 0) 252 | .timeout_height(0); 253 | 254 | let sign_result = tx_builder 255 | .add_message("/cosmos.bank.v1beta1.Msg/Send", msg_snd) 256 | .unwrap() 257 | .sign(&wallet); 258 | 259 | assert!(sign_result.is_err()); 260 | assert_eq!(TxBuildError::NoFee, sign_result.err().unwrap()); 261 | } 262 | 263 | #[test] 264 | fn test_missing_account() { 265 | let wallet = MnemonicWallet::new(TEST_MNEMONIC, DESMOS_DERIVATION_PATH).unwrap(); 266 | 267 | let amount = Coin { 268 | denom: "stake".to_string(), 269 | amount: "10".to_string(), 270 | }; 271 | let msg_snd = MsgSend { 272 | from_address: wallet.get_bech32_address("desmos").unwrap(), 273 | to_address: "desmos18ek6mnlxj8sysrtvu60k5zj0re7s5n42yncner".to_string(), 274 | amount: vec![amount], 275 | }; 276 | 277 | let sign_result = TxBuilder::new("testchain") 278 | .memo("Test memo") 279 | .fee("stake", "10", 300_000) 280 | .timeout_height(0) 281 | .add_message("/cosmos.bank.v1beta1.Msg/Send", msg_snd) 282 | .unwrap() 283 | .sign(&wallet); 284 | 285 | assert!(sign_result.is_err()); 286 | assert_eq!(TxBuildError::NoAccountInfo, sign_result.err().unwrap()); 287 | } 288 | 289 | #[test] 290 | fn test_sign() { 291 | let wallet = MnemonicWallet::new(TEST_MNEMONIC, DESMOS_DERIVATION_PATH).unwrap(); 292 | 293 | let amount = Coin { 294 | denom: "stake".to_string(), 295 | amount: "10".to_string(), 296 | }; 297 | let msg_snd = MsgSend { 298 | from_address: wallet.get_bech32_address("desmos").unwrap(), 299 | to_address: "desmos18ek6mnlxj8sysrtvu60k5zj0re7s5n42yncner".to_string(), 300 | amount: vec![amount], 301 | }; 302 | 303 | let tx = TxBuilder::new("testchain") 304 | .memo("Test memo") 305 | .account_info(1, 5) 306 | .fee("stake", "10", 300_000) 307 | .timeout_height(1000) 308 | .add_message("/cosmos.bank.v1beta1.Msg/Send", msg_snd) 309 | .unwrap() 310 | .sign(&wallet) 311 | .unwrap(); 312 | 313 | let tx_body = tx.body.unwrap(); 314 | let auth_info = tx.auth_info.unwrap(); 315 | 316 | assert_eq!("Test memo", &tx_body.memo); 317 | assert_eq!(1000, tx_body.timeout_height); 318 | 319 | // Should be 1 since TxBuilder support only single sign. 320 | assert_eq!(1, tx.signatures.len()); 321 | assert_eq!(1, auth_info.signer_infos.len()); 322 | // Check that the sequence is the same passed to account_info 323 | assert_eq!(1, auth_info.signer_infos[0].sequence); 324 | } 325 | } 326 | -------------------------------------------------------------------------------- /packages/crw-preferences/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "crw-preferences" 3 | version = "0.1.0" 4 | authors = ["Manuel "] 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib", "staticlib", "lib"] 9 | doctest = false 10 | 11 | [dependencies] 12 | cocoon = "0.3.0" 13 | serde = { version = "1.0.126", features = ["derive"] } 14 | serde_json = "1.0.64" 15 | bincode = "1.3.3" 16 | cfg-if = "1.0.0" 17 | thiserror = "1.0.25" 18 | base64 = "0.20.0" 19 | ffi_helpers = { version = "0.3.0", optional = true } 20 | libc = { version = "0.2.94", optional = true } 21 | 22 | 23 | [target.'cfg(not(target_arch = "wasm32"))'.dependencies] 24 | dirs = "4.0.0" 25 | once_cell = "1.7.2" 26 | 27 | [target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies] 28 | wasm-bindgen = { version = "0.2.62", default-features = false, optional = true } 29 | rand = { version = "0.8.5", optional = true} 30 | web-sys = { version = "0.3.51", optional = true, features = ["Window", "Storage"] } 31 | 32 | [target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies.bindgen] 33 | version = "0.2.74" 34 | optional = true 35 | package = "wasm-bindgen" 36 | 37 | [features] 38 | js = ["web-sys", "bindgen"] 39 | ffi = ["ffi_helpers", "libc"] 40 | -------------------------------------------------------------------------------- /packages/crw-preferences/Makefile: -------------------------------------------------------------------------------- 1 | current_dir := $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))) 2 | uid := $(shell id -u) 3 | guid := $(shell id -g) 4 | rust_version := 1.52.1 5 | osx_sdk := 11.1 6 | ios_sdk := 14.4 7 | android_ndk := r21e 8 | 9 | lint: 10 | cargo fmt 11 | cargo clippy -- -D warnings 12 | 13 | clean: 14 | rm -Rf $(current_dir)/target 15 | rm -Rf $(current_dir)/pkg 16 | 17 | build-linux: 18 | @echo "Building crw-preferences for linux" 19 | docker run -u $(uid):$(guid) --rm -v $(current_dir):/workdir forbole/rust-builder:$(rust_version) \ 20 | cargo build --release --target=x86_64-unknown-linux-gnu --features ffi 21 | 22 | build-windows: 23 | @echo "Building crw-preferences for windows" 24 | docker run -u $(uid):$(guid) --rm -v $(current_dir):/workdir forbole/windows-rust-builder:$(rust_version) \ 25 | cargo build --release --target=x86_64-pc-windows-gnu --features ffi 26 | 27 | build-osx: 28 | @echo "Building crw-preferences for mac" 29 | docker run -u $(uid):$(guid) --rm -v $(current_dir):/workdir forbole/osx-rust-builder:$(rust_version)-$(osx_sdk) \ 30 | cargo build --release --target=x86_64-apple-darwin --features ffi 31 | 32 | build-android-aarch64: 33 | @echo "Building crw-preferences for android-aarch64" 34 | docker run -u $(uid):$(guid) --rm -v $(current_dir):/workdir forbole/android-rust-builder:$(rust_version)-$(android_ndk) \ 35 | cargo build --release --target=aarch64-linux-android --features ffi 36 | 37 | build-android-armv7: 38 | @echo "Building crw-preferences for android-armv7" 39 | docker run -u $(uid):$(guid) --rm -v $(current_dir):/workdir forbole/android-rust-builder:$(rust_version)-$(android_ndk) \ 40 | cargo build --release --target=armv7-linux-androideabi --features ffi 41 | 42 | build-android-x86_64: 43 | @echo "Building crw-preferences for android-x86_64 (Emulator)" 44 | docker run -u $(uid):$(guid) --rm -v $(current_dir):/workdir forbole/android-rust-builder:$(rust_version)-$(android_ndk) \ 45 | cargo build --release --target=x86_64-linux-android --features ffi 46 | 47 | build-android-i686: 48 | @echo "Building crw-preferences for android-i686 (Emulator)" 49 | docker run -u $(uid):$(guid) --rm -v $(current_dir):/workdir android-rust-builder:$(rust_version)-$(android_ndk) \ 50 | cargo build --release --target=i686-linux-android --features ffi 51 | 52 | build-android: build-android-armv7 build-android-aarch64 build-android-x86_64 build-android-i686 53 | 54 | build-ios-aarch64: 55 | @echo "Building crw-preferences for iOS aarch64" 56 | docker run -u $(uid):$(guid) -e IOS_ARCH=arm64 --rm -v $(current_dir):/workdir forbole/ios-rust-builder:$(rust_version)-$(ios_sdk) \ 57 | cargo build --release --target=aarch64-apple-ios --features ffi 58 | 59 | build-ios-x86_64: 60 | @echo "Building crw-preferences for iOS x86_64 (Emulator)" 61 | docker run -u $(uid):$(guid) -e IOS_ARCH=x86_64 --rm -v $(current_dir):/workdir forbole/ios-rust-builder:$(rust_version)-$(ios_sdk) \ 62 | cargo build --release --target=x86_64-apple-ios --features ffi 63 | 64 | build-ios: build-ios-aarch64 build-ios-x86_64 65 | 66 | build-wasm: 67 | @echo "Building crw-preferences for web" 68 | docker run -u $(uid):$(guid) --rm -v $(current_dir):/workdir forbole/wasm-rust-builder:$(rust_version) \ 69 | wasm-pack build --release -- --features js -v 70 | 71 | all: build-linux build-windows build-osx build-android build-wasm -------------------------------------------------------------------------------- /packages/crw-preferences/README.md: -------------------------------------------------------------------------------- 1 | # Preferences package 2 | This package take care of storing a set of preferences into the device storage. 3 | The preferences sets are stored in different locations based on the device os. The table below shows 4 | where the preferences are stored based on the device's os. 5 | 6 | |Platform | Location | 7 | | ------- | ---------------- | 8 | | Linux | $XDG_CONFIG_HOME/{PREFERENCES_APP_DIR} or $HOME/.config/{PREFERENCES_APP_DIR} | 9 | | macOS | $HOME/Library/Application Support/{PREFERENCES_APP_DIR} | 10 | | Windows | C:\Users\\$USER\AppData\Roaming\{PREFERENCES_APP_DIR} | 11 | | Android | {PREFERENCES_APP_DIR} | 12 | | iOS | {PREFERENCES_APP_DIR} | 13 | | Web | LocalStorage | 14 | 15 | `PREFERENCES_APP_DIR` is the value provided with the `set_preferences_app_dir`. 16 | 17 | **NOTE:** Since in iOS and Android is not possible to know the 18 | application data directory the full path must be provided from the user 19 | using the `set_preferences_app_dir` function. 20 | 21 | -------------------------------------------------------------------------------- /packages/crw-preferences/crw_preferences.h: -------------------------------------------------------------------------------- 1 | #ifndef CRW_PREFERENCES_H 2 | #define CRW_PREFERENCES_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | /** 11 | * @brief Sets the application directory where will be stored the configurations. 12 | * @param name On windows, macOS and linux should be only the name of the directory that will 13 | * be created inside the current user configurations directory. 14 | * On Android instead since is not possible to obtain the `appData` directory at runtime 15 | * must be an absolute path to a directory where the application can read and write. 16 | * @return Returns 0 on success -1 on error. 17 | * In case of error, the error cause can be obtained using the error_message_utf8 18 | * function. 19 | */ 20 | int set_preferences_app_dir(const char *name); 21 | 22 | /** 23 | * @brief Checks if exist a preferences with the provided name. 24 | * @param name The preference name. 25 | * @return Return true if the preference exist, false otherwise. 26 | */ 27 | bool preferences_exist(const char *name); 28 | 29 | /** 30 | * @brief Deletes a preferences with the provided name. 31 | * @param name Name of the preference to delete. 32 | */ 33 | void preferences_delete(const char *name); 34 | 35 | /** 36 | * @brief Creates a new preferences with the provided name or 37 | * loads a previously created preferences with the same name. 38 | * @param name The preferences name, can contains only ascii alphanumeric chars or -, _. 39 | * @return Returns a valid pointer on success or nullptr if an error occurred. 40 | * In case of error, the error cause can be obtained using the error_message_utf8 41 | * function. 42 | */ 43 | void *preferences(const char *name); 44 | 45 | /** 46 | * @brief Creates a new encrypted preferences with the provided name or 47 | * loads a previously created preferences with the same name. 48 | * @param name The preferences name, can contains only ascii alphanumeric chars or -, _. 49 | * @param password The password used to secure the preferences. 50 | * @return Returns a valid pointer on success or nullptr if an error occurred. 51 | * In case of error, the error cause can be obtained using the error_message_utf8 52 | * function. 53 | */ 54 | void *encrypted_preferences(const char *name, 55 | const char *password); 56 | 57 | /** 58 | * @brief Release all the resources owned by a preferences instance. 59 | * @param preferences Pointer to the preference instance to free. 60 | */ 61 | void preferences_free(void *preferences); 62 | 63 | /** 64 | * @brief Gets an i32 from the preferences. 65 | * @param preferences pointer to the preferences from which will be extracted the value. 66 | * @param key name of the preference that will be loaded. 67 | * @param out pointer where will be stored the value. 68 | * @return Returns 0 on success -1 if the requested value is not 69 | * present into the preferences or -2 if one or more of the provided arguments is invalid. 70 | * In case of error, the error cause can be obtained using the error_message_utf8 71 | * function. 72 | */ 73 | int preferences_get_i32(const void *preferences, const char *key, int32_t *out); 74 | 75 | /** 76 | * @brief Puts an i32 into the preferences. 77 | * @param preferences pointer to the preferences where will be stored the value. 78 | * @param key name of the preference that will be stored. 79 | * @param value the value that will be stored. 80 | * @return Returns 0 on success or -1 on error. 81 | * In case of error, the error cause can be obtained using the error_message_utf8 82 | * function. 83 | */ 84 | int preferences_put_i32(void *preferences, const char *key, int32_t value); 85 | 86 | /** 87 | * @brief Gets a string from the preferences. 88 | * @param preferences pointer to the preferences from which will be extracted the value. 89 | * @param key name of the preference that will be loaded. 90 | * @param out_buf pointer where will be stored the value. 91 | * @param buf_len maximum number of bytes that can be used from `out_buf` 92 | * @return Returns the number of bytes that would have been written if `out_buf` 93 | * had been sufficiently large,0 if the value is not present into the preferences or -1 on error. 94 | * In case of error, the error cause can be obtained using the error_message_utf8 95 | * function. 96 | */ 97 | int preferences_get_string(const void *preferences, 98 | const char *key, 99 | unsigned char *out_buf, 100 | size_t len); 101 | 102 | /** 103 | * @brief Puts a string into the preferences. 104 | * @param preferences pointer to the preferences from which will be extracted the value. 105 | * @param key name of the preference that will be loaded. 106 | * @param value the value that will be stored. 107 | * @return Returns 0 on success -1 on error. 108 | * In case of error, the error cause can be obtained using the error_message_utf8 109 | * function. 110 | */ 111 | int preferences_put_string(void *preferences, const char *key, const char *value); 112 | 113 | /** 114 | * @brief Gets a bool from the preferences. 115 | * @param preferences pointer to the preferences from which will be extracted the value. 116 | * @param key name of the preference that will be loaded. 117 | * @param out pointer where will be stored the value. 118 | * @return Returns 0 on success -1 if the requested value is not present into the 119 | * preferences or -2 if one or more of the provided arguments is invalid. 120 | * In case of error, the error cause can be obtained using the error_message_utf8 121 | * function. 122 | */ 123 | int preferences_get_bool(const void *preferences, const char *key, bool *out); 124 | 125 | /** 126 | * @brief Puts a bool into the preferences. 127 | * @param preferences pointer to the preferences where will be stored the value. 128 | * @param key name of the preference that will be stored. 129 | * @param value the value that will be stored. 130 | * @return Returns 0 on success or -1 on error. 131 | * In case of error, the error cause can be obtained using the error_message_utf8 132 | * function. 133 | */ 134 | int preferences_put_bool(void *preferences, const char *key, bool value); 135 | 136 | /** 137 | * @brief Gets an array of bytes from the preferences. 138 | * @param preferences pointer to the preferences from which will be extracted the value. 139 | * @param key name of the preference that will be loaded. 140 | * @param out_buf pointer where will be stored the value. 141 | * @param buf_len maximum number of bytes that can be used from `out_buf` 142 | * @return Returns the number of bytes that would have been written if `out_buf` 143 | * had been sufficiently large, 0 if the value is not present into the preferences or -1 on error. 144 | * In case of error, the error cause can be obtained using the error_message_utf8 145 | * function. 146 | */ 147 | int preferences_get_bytes(const void *preferences, 148 | const char *key, 149 | uint8_t *out_buf, 150 | size_t buf_len); 151 | 152 | /** 153 | * @brief Store an array of bytes into the preferences. 154 | * @param preferences pointer to the preferences from which will be extracted the value. 155 | * @param key name of the preference that will be stored. 156 | * @param value array that will be stored into the preferences. 157 | * @param len length of `value`. 158 | * @return Returns 0 on success, -1 on error. 159 | * In case of error, the error cause can be obtained using the error_message_utf8 160 | * function. 161 | */ 162 | int preferences_put_bytes(void *preferences, 163 | const char *key, 164 | const uint8_t *value, 165 | size_t len); 166 | 167 | /** 168 | * @brief Delete all the preferences currently loaded from the provided 169 | * preferences instance. 170 | * @param preferences pointer to the preferences instance. 171 | * @return Returns 0 on success or -1 on error. 172 | * In case of error, the error cause can be obtained using the error_message_utf8 173 | * function. 174 | */ 175 | int preferences_clear(void *preferences); 176 | 177 | /** 178 | * @brief Delete all the preferences currently loaded and also the one stored 179 | * into the device storage from the provided preferences instance 180 | * @param preferences pointer to the preferences instance. 181 | * @return Returns 0 on success or -1 on error. 182 | * In case of error, the error cause can be obtained using the error_message_utf8 183 | * function. 184 | */ 185 | int preferences_erase(void *preferences); 186 | 187 | /** 188 | * @brief Saves the preferences into the device disk. 189 | * @param preferences pointer to the preferences instance. 190 | * @return Returns 0 on success or -1 on error. 191 | * In case of error, the error cause can be obtained using the error_message_utf8 192 | * function. 193 | */ 194 | int preferences_save(void *preferences); 195 | 196 | /** 197 | * @brief Clears the last error. 198 | */ 199 | void clear_last_error(); 200 | 201 | /** 202 | * @brief Gets the last error message length. 203 | */ 204 | int last_error_length(); 205 | 206 | /** 207 | * @brief Gets the last error message as UTF-8 encoded string. 208 | * @param out_buf: Pointer where will be stored the error message. 209 | * @param buf_size: Size of out_buf. 210 | * @return Returns the number of bytes wrote into out_buf or -1 on error. 211 | */ 212 | int error_message_utf8(char *out_buf, int buf_size); 213 | 214 | #endif /* CRW_PREFERENCES_H */ 215 | -------------------------------------------------------------------------------- /packages/crw-preferences/src/encrypted.rs: -------------------------------------------------------------------------------- 1 | //! Module that provides an implementation of [Preferences] that saves the values encrypted into 2 | //! the device storage. 3 | //! The data are securely stored into the device storage using the Chacha20Poly1305 algorithm. 4 | 5 | use crate::io; 6 | use crate::io::IoError; 7 | use crate::preferences::{Preferences, PreferencesError, Result}; 8 | use base64::DecodeError; 9 | use cocoon::{Cocoon, Error as CocoonErr}; 10 | use serde::{Deserialize, Serialize}; 11 | use std::collections::HashMap; 12 | use std::result::Result as StdResult; 13 | use thiserror::Error; 14 | 15 | #[derive(Error, Debug)] 16 | pub enum EncryptedPreferencesError { 17 | #[error("error while decrypting the data")] 18 | DecryptionFailed, 19 | // Wrapper to the preferences error. 20 | #[error("preferences error: `{0}`")] 21 | Preferences(Box), 22 | } 23 | 24 | #[derive(Serialize, Deserialize, Debug)] 25 | enum Value { 26 | I32(i32), 27 | Bool(bool), 28 | String(String), 29 | Bin(Vec), 30 | } 31 | 32 | pub struct EncryptedPreferences { 33 | name: String, 34 | password: String, 35 | data: HashMap, 36 | } 37 | 38 | impl EncryptedPreferences { 39 | /// Loads a preferences set from the device disk, if the preferences set don't exist into the 40 | /// device disk will be created a new empty one. 41 | /// 42 | /// * `password` - the password used decrypt the preferences set. 43 | /// * `name` - the preferences set name. 44 | /// 45 | /// # Errors 46 | /// This function can return the following errors: 47 | /// * [EncryptedPreferencesError::DecryptionFailed] if the provided password is not valid or the 48 | /// data is corrupted. 49 | /// * [PreferencesError::InvalidName] if the provided name contains non ascii alphanumeric chars 50 | /// * [PreferencesError::DeserializationError] if the data inside the disc is not valid. 51 | /// * [PreferencesError::IO] if an error occurred while reading the data from the device storage. 52 | fn load_from_disk( 53 | password: &str, 54 | name: &str, 55 | ) -> StdResult, EncryptedPreferencesError> { 56 | let read_result = io::load(name); 57 | 58 | if read_result.is_err() { 59 | let err = read_result.err().unwrap(); 60 | return match err { 61 | IoError::EmptyData => Ok(HashMap::new()), 62 | IoError::InvalidName(s) => Err(EncryptedPreferencesError::from( 63 | PreferencesError::InvalidName(s), 64 | )), 65 | _ => Err(EncryptedPreferencesError::from(PreferencesError::IO(err))), 66 | }; 67 | } 68 | let base64_data = read_result.unwrap(); 69 | 70 | // Get the data as binary 71 | let encrypted = base64::decode(base64_data) 72 | .map_err(|_| EncryptedPreferencesError::from(PreferencesError::DeserializationError))?; 73 | 74 | // Decrypt the binary data 75 | let cocoon = Cocoon::new(password.as_bytes()); 76 | let decrypted = cocoon.unwrap(&encrypted).map_err(|e| match e { 77 | CocoonErr::Cryptography => EncryptedPreferencesError::DecryptionFailed, 78 | _ => EncryptedPreferencesError::from(PreferencesError::DeserializationError), 79 | })?; 80 | 81 | // Deserialize the values 82 | bincode::deserialize::>(&decrypted) 83 | .map_err(|_| EncryptedPreferencesError::from(PreferencesError::DeserializationError)) 84 | } 85 | 86 | /// Creates a new encrypted preferences set with the provided `name`. 87 | /// If already exist a preferences set with the provided name will be loaded the previous one. 88 | /// 89 | /// * `name` - The preferences name, can contains only ascii alphanumeric chars or -, _. 90 | /// 91 | /// # Errors 92 | /// This function can return the following errors: 93 | /// * [EncryptedPreferencesError::DecryptionFailed] if the provided password is not valid or the 94 | /// data is corrupted. 95 | /// * [PreferencesError::InvalidName] if the provided name contains non ascii alphanumeric chars 96 | /// * [PreferencesError::DeserializationError] if the data inside the disc is not valid. 97 | /// * [PreferencesError::IO] if an error occurred while reading the data from the device storage. 98 | /// 99 | pub fn new( 100 | password: &str, 101 | name: &str, 102 | ) -> StdResult { 103 | Ok(EncryptedPreferences { 104 | name: name.to_owned(), 105 | password: password.to_owned(), 106 | data: EncryptedPreferences::load_from_disk(password, name)?, 107 | }) 108 | } 109 | } 110 | 111 | impl Preferences for EncryptedPreferences { 112 | fn get_i32(&self, key: &str) -> Option { 113 | self.data.get(key).and_then(|v| match v { 114 | Value::I32(i32) => Some(i32.to_owned()), 115 | _ => None, 116 | }) 117 | } 118 | 119 | fn put_i32(&mut self, key: &str, value: i32) -> Result<()> { 120 | self.data.insert(key.to_owned(), Value::I32(value)); 121 | Ok(()) 122 | } 123 | 124 | fn get_str(&self, key: &str) -> Option { 125 | self.data.get(key).and_then(|v| match v { 126 | Value::String(string) => Some(string.to_owned()), 127 | _ => None, 128 | }) 129 | } 130 | 131 | fn put_str(&mut self, key: &str, value: String) -> Result<()> { 132 | self.data.insert(key.to_owned(), Value::String(value)); 133 | Ok(()) 134 | } 135 | 136 | fn get_bool(&self, key: &str) -> Option { 137 | self.data.get(key).and_then(|v| match v { 138 | Value::Bool(bool) => Some(bool.to_owned()), 139 | _ => None, 140 | }) 141 | } 142 | 143 | fn put_bool(&mut self, key: &str, value: bool) -> Result<()> { 144 | self.data.insert(key.to_owned(), Value::Bool(value)); 145 | Ok(()) 146 | } 147 | 148 | fn get_bytes(&self, key: &str) -> Option> { 149 | self.data.get(key).and_then(|v| match v { 150 | Value::Bin(bin) => Some(bin.to_owned()), 151 | _ => None, 152 | }) 153 | } 154 | 155 | fn put_bytes(&mut self, key: &str, value: Vec) -> Result<()> { 156 | self.data.insert(key.to_owned(), Value::Bin(value)); 157 | Ok(()) 158 | } 159 | 160 | fn clear(&mut self) { 161 | self.data.clear() 162 | } 163 | 164 | fn erase(&mut self) { 165 | self.clear(); 166 | io::erase(&self.name); 167 | } 168 | 169 | fn save(&self) -> Result<()> { 170 | let serialized = 171 | bincode::serialize(&self.data).map_err(|_| PreferencesError::SerializationError)?; 172 | 173 | let storage = Cocoon::new(self.password.as_ref()); 174 | let encrypted = storage 175 | .wrap(&serialized) 176 | .map(base64::encode) 177 | .map_err(|_| PreferencesError::SerializationError)?; 178 | 179 | io::save(&self.name, &encrypted)?; 180 | Ok(()) 181 | } 182 | } 183 | 184 | impl From for PreferencesError { 185 | fn from(_: DecodeError) -> Self { 186 | PreferencesError::DeserializationError 187 | } 188 | } 189 | 190 | impl From for EncryptedPreferencesError { 191 | fn from(e: PreferencesError) -> Self { 192 | EncryptedPreferencesError::Preferences(Box::new(e)) 193 | } 194 | } 195 | 196 | #[cfg(test)] 197 | mod test { 198 | use crate::encrypted::EncryptedPreferences; 199 | use crate::preferences; 200 | use crate::preferences::Preferences; 201 | 202 | #[test] 203 | pub fn test_creation() { 204 | let encrypted = EncryptedPreferences::new("password", "test"); 205 | 206 | assert!(encrypted.is_ok()); 207 | encrypted.unwrap().erase(); 208 | } 209 | 210 | #[test] 211 | pub fn test_invalid_names() { 212 | // Check invalid names 213 | assert!(EncryptedPreferences::new("", "test.").is_err()); 214 | assert!(EncryptedPreferences::new("", "test\\").is_err()); 215 | assert!(EncryptedPreferences::new("", "test//").is_err()); 216 | assert!(EncryptedPreferences::new("", "test with spaces").is_err()); 217 | // Test empty 218 | assert!(EncryptedPreferences::new("", "").is_err()); 219 | } 220 | 221 | #[test] 222 | pub fn test_data_read_write() { 223 | let set_name = "rwenc"; 224 | let password = "password"; 225 | 226 | let test_vec: Vec = vec![12, 13, 54, 42]; 227 | let mut preferences = EncryptedPreferences::new(password, set_name).unwrap(); 228 | assert!(preferences.put_i32("i32", 42).is_ok()); 229 | assert!(preferences 230 | .put_str( 231 | "str", 232 | "some very long string with more than 32 bytes mf".to_owned() 233 | ) 234 | .is_ok()); 235 | assert!(preferences.put_bool("bool", true).is_ok()); 236 | assert!(preferences.put_bytes("bin", test_vec.clone()).is_ok()); 237 | 238 | // Write data to disk 239 | preferences.save().unwrap(); 240 | 241 | // Create a new one that reads from the saved preferences 242 | let mut preferences = EncryptedPreferences::new(password, set_name).unwrap(); 243 | let i32_result = preferences.get_i32("i32"); 244 | let str_result = preferences.get_str("str"); 245 | let bool_result = preferences.get_bool("bool"); 246 | let binary_result = preferences.get_bytes("bin"); 247 | 248 | // Delete the file from the disk to avoid that some date remain on the disk if the 249 | // test fails. 250 | preferences.erase(); 251 | assert_eq!(42, i32_result.unwrap()); 252 | assert_eq!( 253 | "some very long string with more than 32 bytes mf", 254 | str_result.unwrap() 255 | ); 256 | assert_eq!(true, bool_result.unwrap()); 257 | assert_eq!(test_vec, binary_result.unwrap()); 258 | } 259 | 260 | #[test] 261 | pub fn test_exist() { 262 | let set_name = "encrypted-exist"; 263 | 264 | let mut p = EncryptedPreferences::new("password", set_name).unwrap(); 265 | p.save().unwrap(); 266 | 267 | assert!(preferences::exist(set_name)); 268 | 269 | p.erase(); 270 | } 271 | } 272 | -------------------------------------------------------------------------------- /packages/crw-preferences/src/ffi.rs: -------------------------------------------------------------------------------- 1 | //! Module that provides the FFI to access the preferences from other programming languages. 2 | 3 | use crate::encrypted::EncryptedPreferences; 4 | use crate::preferences; 5 | use crate::preferences::Preferences; 6 | use crate::unencrypted::UnencryptedPreferences; 7 | use ffi_helpers; 8 | use libc::{c_char, c_int, c_uchar, c_void}; 9 | use std::ptr::null_mut; 10 | use std::slice; 11 | 12 | // Macro to export the ffi_helpers's functions used to access the error message from other programming languages. 13 | export_error_handling_functions!(); 14 | 15 | /// Macro that converts a raw C string to a &str breaking the code flow if the provided string 16 | /// is a null ptr or an invalid UTF-8 string. 17 | /// Other then breaking the code flow this macro also update a global variable that 18 | /// contains an error message that tells the error cause. 19 | /// 20 | /// # Example 21 | /// 22 | /// For example we want to create a function that computes the length of a c string 23 | /// and returns 0 if the provided string is invalid. 24 | /// 25 | /// ``` 26 | /// use libc::c_char; 27 | /// pub fn c_string_length(str: *const c_char) -> usize { 28 | /// let str = !check_str(str, 0); 29 | /// str.len() 30 | /// } 31 | /// ``` 32 | /// 33 | /// 34 | macro_rules! check_str { 35 | ($ptr:expr, $rc:expr) => {{ 36 | use std::ffi::CStr; 37 | if $ptr.is_null() { 38 | return $rc; 39 | } 40 | let c_str: &CStr = unsafe { CStr::from_ptr($ptr) }; 41 | let str_slice = c_str.to_str(); 42 | if let Err(e) = str_slice { 43 | ffi_helpers::update_last_error(e); 44 | return $rc; 45 | } 46 | str_slice.unwrap() 47 | }}; 48 | } 49 | 50 | fn box_to_c_ptr(preferences: T) -> *mut c_void { 51 | let boxed: Box = Box::new(preferences); 52 | Box::into_raw(Box::new(boxed)) as *mut c_void 53 | } 54 | 55 | unsafe fn unbox_from_c_ptr(ptr: *mut c_void) -> Box> { 56 | let ptr = unwrap_ptr_mut(ptr); 57 | Box::from_raw(ptr) 58 | } 59 | 60 | fn unwrap_ptr(ptr: *const c_void) -> *const Box { 61 | ptr as *const Box 62 | } 63 | 64 | fn unwrap_ptr_mut(ptr: *mut c_void) -> *mut Box { 65 | ptr as *mut Box 66 | } 67 | /// Sets the application directory where will be stored the configurations. 68 | /// On windows, macOS and linux `dir` should be only the name of the directory that will 69 | /// be created inside the current user app configurations directory. 70 | /// On Android instead since is not possible to obtain the `appData` directory at runtime 71 | /// `dir` must be an absolute path to a directory where the application can read and write. 72 | /// 73 | /// Returns 0 on success or -1 on error. 74 | #[no_mangle] 75 | pub extern "C" fn set_preferences_app_dir(dir: *const c_char) -> c_int { 76 | let path = check_str!(dir, -1); 77 | 78 | crate::preferences::set_preferences_app_dir(path); 79 | 80 | 0 81 | } 82 | 83 | /// Check if exist a preference with the provided `name`. 84 | #[no_mangle] 85 | pub extern "C" fn preferences_exist(name: *const c_char) -> bool { 86 | let name = check_str!(name, false); 87 | 88 | preferences::exist(name) 89 | } 90 | 91 | /// Deletes a preferences from the device storage. 92 | #[no_mangle] 93 | pub extern "C" fn preferences_delete(name: *const c_char) { 94 | let name = check_str!(name, ()); 95 | 96 | preferences::delete(name); 97 | } 98 | 99 | /// Creates a new preferences with the provided name or load an already existing preferences 100 | /// with the provided name. 101 | /// 102 | /// * `name` - The preferences name, can contains only ascii alphanumeric chars or -, _. 103 | /// 104 | /// Returns a valid pointer on success or nullptr if an error occurred. 105 | #[no_mangle] 106 | pub extern "C" fn preferences(name: *const c_char) -> *mut c_void { 107 | let name = check_str!(name, null_mut()); 108 | 109 | match UnencryptedPreferences::new(name) { 110 | Err(e) => { 111 | ffi_helpers::update_last_error(e); 112 | return null_mut(); 113 | } 114 | Ok(p) => box_to_c_ptr(p), 115 | } 116 | } 117 | 118 | /// Creates a new encrypted preferences with the provided name or load an already existing preferences 119 | /// with the provided name. 120 | /// 121 | /// * `name` - The preferences name, can contains only ascii alphanumeric chars or -, _. 122 | /// * `password` - The password used to secure the preferences. 123 | /// 124 | /// Returns a valid pointer on success or nullptr if an error occurred. 125 | #[no_mangle] 126 | pub extern "C" fn encrypted_preferences( 127 | name: *const c_char, 128 | password: *const c_char, 129 | ) -> *mut c_void { 130 | let name = check_str!(name, null_mut()); 131 | let password = check_str!(password, null_mut()); 132 | 133 | match EncryptedPreferences::new(password, name) { 134 | Err(e) => { 135 | ffi_helpers::update_last_error(e); 136 | return null_mut(); 137 | } 138 | Ok(p) => box_to_c_ptr(p), 139 | } 140 | } 141 | 142 | /// Release all the resources owned by a preferences instance. 143 | #[no_mangle] 144 | pub extern "C" fn preferences_free(preferences: *mut c_void) { 145 | if preferences.is_null() { 146 | return; 147 | } 148 | 149 | // Reclaim the boxed preferences to destroy. 150 | unsafe { unbox_from_c_ptr(preferences) }; 151 | } 152 | 153 | /// Gets an i32 from the preferences. 154 | /// 155 | /// * `preferences` - pointer to the preferences from which will be extracted the value. 156 | /// * `key` - name of the preference that will be loaded. 157 | /// * `out` - pointer where will be stored the value. 158 | /// 159 | /// Returns 0 on success -1 if the requested value is not present into the preferences or -2 160 | /// if one or more of the provided arguments is invalid. 161 | #[no_mangle] 162 | pub extern "C" fn preferences_get_i32( 163 | preferences: *const c_void, 164 | key: *const c_char, 165 | out: *mut i32, 166 | ) -> c_int { 167 | if preferences.is_null() || out.is_null() { 168 | return -2; 169 | } 170 | 171 | let key = check_str!(key, -2); 172 | let preferences = unsafe { unwrap_ptr(preferences).as_ref().unwrap() }; 173 | match preferences.get_i32(key) { 174 | Some(i) => { 175 | unsafe { *out = i }; 176 | 0 177 | } 178 | None => -1, 179 | } 180 | } 181 | 182 | /// Puts an i32 into the preferences. 183 | /// 184 | /// * `preferences` - pointer to the preferences where will be stored the value. 185 | /// * `key` - name of the preference that will be stored. 186 | /// * `value` - the value that will be stored. 187 | /// 188 | /// Returns 0 on success or -1 on error. 189 | #[no_mangle] 190 | pub extern "C" fn preferences_put_i32( 191 | preferences: *mut c_void, 192 | key: *const c_char, 193 | value: i32, 194 | ) -> c_int { 195 | if preferences.is_null() { 196 | return -1; 197 | } 198 | 199 | let key = check_str!(key, -1); 200 | let preferences = unsafe { unwrap_ptr_mut(preferences).as_mut().unwrap() }; 201 | match preferences.put_i32(key, value) { 202 | Ok(_) => 0, 203 | Err(e) => { 204 | ffi_helpers::update_last_error(e); 205 | -1 206 | } 207 | } 208 | } 209 | 210 | /// Gets a string from the preferences. 211 | /// 212 | /// * `preferences` - pointer to the preferences from which will be extracted the value. 213 | /// * `key` - name of the preference that will be loaded. 214 | /// * `out_buf` - pointer where will be stored the value. 215 | /// * `buf_len` - maximum number of bytes that can be used from `out_buf` 216 | /// 217 | /// Returns the number of bytes that would have been written if `out_buf` had been sufficiently large, 218 | /// 0 if the value is not present into the preferences or -1 on error. 219 | #[no_mangle] 220 | pub extern "C" fn preferences_get_string( 221 | preferences: *const c_void, 222 | key: *const c_char, 223 | out_buf: *mut c_uchar, 224 | len: usize, 225 | ) -> c_int { 226 | if preferences.is_null() { 227 | return -1; 228 | } 229 | 230 | let key = check_str!(key, -1); 231 | let preferences = unsafe { unwrap_ptr(preferences).as_ref().unwrap() }; 232 | match preferences.get_str(key) { 233 | Some(s) => { 234 | let bytes = s.as_bytes(); 235 | // Use bytes len instead of the string since in UTF-8 strings the length can be 236 | // different from the number of bytes that represents the string. 237 | if bytes.len() <= len { 238 | let out_slice: &mut [u8] = 239 | unsafe { slice::from_raw_parts_mut(out_buf as *mut u8, bytes.len()) }; 240 | out_slice.copy_from_slice(s.as_bytes()) 241 | } 242 | s.len() as c_int 243 | } 244 | None => 0, 245 | } 246 | } 247 | 248 | /// Puts a string into the preferences. 249 | /// 250 | /// * `preferences` - pointer to the preferences from which will be extracted the value. 251 | /// * `key` - name of the preference that will be loaded. 252 | /// * `value` - the value that will be stored. 253 | /// 254 | /// Returns 0 on success -1 on error. 255 | #[no_mangle] 256 | pub extern "C" fn preferences_put_string( 257 | preferences: *mut c_void, 258 | key: *const c_char, 259 | value: *const c_char, 260 | ) -> c_int { 261 | if preferences.is_null() { 262 | return -1; 263 | } 264 | 265 | let key = check_str!(key, -1); 266 | let value = check_str!(value, -1); 267 | let preferences = unsafe { unwrap_ptr_mut(preferences).as_mut().unwrap() }; 268 | 269 | match preferences.put_str(key, value.to_owned()) { 270 | Ok(_) => 0, 271 | Err(e) => { 272 | ffi_helpers::update_last_error(e); 273 | -1 274 | } 275 | } 276 | } 277 | 278 | /// Gets a bool from the preferences. 279 | /// 280 | /// * `preferences` - pointer to the preferences from which will be extracted the value. 281 | /// * `key` - name of the preference that will be loaded. 282 | /// * `out` - pointer where will be stored the value. 283 | /// 284 | /// Returns 0 on success -1 if the requested value is not present into the preferences or -2 285 | /// if one or more of the provided arguments is invalid. 286 | #[no_mangle] 287 | pub extern "C" fn preferences_get_bool( 288 | preferences: *const c_void, 289 | key: *const c_char, 290 | out: *mut bool, 291 | ) -> c_int { 292 | if preferences.is_null() || out.is_null() { 293 | return -2; 294 | } 295 | 296 | let key = check_str!(key, -2); 297 | let preferences = unsafe { unwrap_ptr(preferences).as_ref().unwrap() }; 298 | match preferences.get_bool(key) { 299 | Some(b) => { 300 | unsafe { *out = b }; 301 | 0 302 | } 303 | None => -1, 304 | } 305 | } 306 | 307 | /// Puts a bool into the preferences. 308 | /// 309 | /// * `preferences` - pointer to the preferences where will be stored the value. 310 | /// * `key` - name of the preference that will be stored. 311 | /// * `value` - the value that will be stored. 312 | /// 313 | /// Returns 0 on success or -1 on error. 314 | #[no_mangle] 315 | pub extern "C" fn preferences_put_bool( 316 | preferences: *mut c_void, 317 | key: *const c_char, 318 | value: bool, 319 | ) -> c_int { 320 | if preferences.is_null() { 321 | return -1; 322 | } 323 | 324 | let key = check_str!(key, -1); 325 | let preferences = unsafe { unwrap_ptr_mut(preferences).as_mut().unwrap() }; 326 | match preferences.put_bool(key, value) { 327 | Ok(_) => 0, 328 | Err(e) => { 329 | ffi_helpers::update_last_error(e); 330 | -1 331 | } 332 | } 333 | } 334 | 335 | /// Gets an array of bytes from the preferences. 336 | /// 337 | /// * `preferences` - pointer to the preferences from which will be extracted the value. 338 | /// * `key` - name of the preference that will be loaded. 339 | /// * `out_buf` - pointer where will be stored the value. 340 | /// * `buf_len` - maximum number of bytes that can be used from `out_buf` 341 | /// 342 | /// Returns the number of bytes that would have been written if `out_buf` had been sufficiently large, 343 | /// 0 if the value is not present into the preferences or -1 on error. 344 | #[no_mangle] 345 | pub extern "C" fn preferences_get_bytes( 346 | preferences: *const c_void, 347 | key: *const c_char, 348 | out_buf: *mut u8, 349 | buf_len: usize, 350 | ) -> c_int { 351 | if preferences.is_null() || out_buf.is_null() { 352 | return -1; 353 | } 354 | 355 | let key = check_str!(key, -1); 356 | let preferences = unsafe { unwrap_ptr(preferences).as_ref().unwrap() }; 357 | 358 | match preferences.get_bytes(key) { 359 | Some(v) => { 360 | if v.len() <= buf_len { 361 | // The buffer is large enough copy the vec to the dest buffer 362 | let dest: &mut [u8] = unsafe { slice::from_raw_parts_mut(out_buf, v.len()) }; 363 | dest.copy_from_slice(v.as_slice()); 364 | } 365 | v.len() as c_int 366 | } 367 | None => 0, 368 | } 369 | } 370 | 371 | /// Puts an array of bytes into the preferences. 372 | /// 373 | /// * `preferences` - pointer to the preferences from which will be extracted the value. 374 | /// * `key` - name of the preference that will be stored. 375 | /// * `value` - array that will be stored into the preferences. 376 | /// * `len` - length of `value`. 377 | /// 378 | /// Return 0 on on success, -1 on error. 379 | #[no_mangle] 380 | pub extern "C" fn preferences_put_bytes( 381 | preferences: *mut c_void, 382 | key: *const c_char, 383 | value: *const u8, 384 | len: usize, 385 | ) -> c_int { 386 | if preferences.is_null() || value.is_null() { 387 | return -1; 388 | } 389 | 390 | let key = check_str!(key, -1); 391 | let preferences = unsafe { unwrap_ptr_mut(preferences).as_mut().unwrap() }; 392 | let value = unsafe { slice::from_raw_parts(value, len) }; 393 | 394 | match preferences.put_bytes(key, value.to_owned()) { 395 | Ok(_) => 0, 396 | Err(e) => { 397 | ffi_helpers::update_last_error(e); 398 | -1 399 | } 400 | } 401 | } 402 | 403 | /// Delete all the preferences currently loaded from the provided preferences instance. 404 | /// 405 | /// * `preferences` - pointer to the preferences instance. 406 | /// 407 | /// Returns 0 on success or -1 on error. 408 | #[no_mangle] 409 | pub extern "C" fn preferences_clear(preferences: *mut c_void) -> c_int { 410 | if preferences.is_null() { 411 | return -1; 412 | } 413 | 414 | let preferences = unsafe { unwrap_ptr_mut(preferences).as_mut().unwrap() }; 415 | preferences.clear(); 416 | 0 417 | } 418 | 419 | /// Delete all the preferences currently loaded and also the one stored into the 420 | /// device storage from the provided preferences instance 421 | /// 422 | /// * `preferences` - pointer to the preferences instance. 423 | /// 424 | /// Returns 0 on success or -1 on error. 425 | #[no_mangle] 426 | pub extern "C" fn preferences_erase(preferences: *mut c_void) -> c_int { 427 | if preferences.is_null() { 428 | return -1; 429 | } 430 | 431 | let preferences = unsafe { unwrap_ptr_mut(preferences).as_mut().unwrap() }; 432 | preferences.erase(); 433 | 0 434 | } 435 | 436 | /// Saves the preferences into the device disk. 437 | /// 438 | /// * `preferences` - pointer to the preferences instance. 439 | /// 440 | /// Returns 0 on success or -1 on error. 441 | #[no_mangle] 442 | pub extern "C" fn preferences_save(preferences: *mut c_void) -> c_int { 443 | if preferences.is_null() { 444 | return -1; 445 | } 446 | 447 | let preferences = unsafe { unwrap_ptr_mut(preferences).as_mut().unwrap() }; 448 | match preferences.save() { 449 | Ok(_) => 0, 450 | Err(e) => { 451 | ffi_helpers::update_last_error(e); 452 | -1 453 | } 454 | } 455 | } 456 | 457 | #[cfg(test)] 458 | mod tests { 459 | use crate::ffi::{ 460 | encrypted_preferences, preferences, preferences_erase, preferences_free, 461 | preferences_get_bool, preferences_get_bytes, preferences_get_i32, preferences_get_string, 462 | preferences_put_bool, preferences_put_bytes, preferences_put_i32, preferences_put_string, 463 | preferences_save, 464 | }; 465 | use std::ffi::CString; 466 | 467 | #[test] 468 | fn test_preferences_creation() { 469 | let preferences_name = CString::new("creation").unwrap(); 470 | 471 | let raw_preferences = preferences(preferences_name.as_ptr()); 472 | assert!(!raw_preferences.is_null()); 473 | 474 | preferences_erase(raw_preferences); 475 | preferences_free(raw_preferences); 476 | } 477 | 478 | #[test] 479 | fn test_encrypted_preferences_creation() { 480 | let preferences_name = CString::new("encrypted").unwrap(); 481 | let preferences_password = CString::new("password").unwrap(); 482 | 483 | let raw_preferences = 484 | encrypted_preferences(preferences_name.as_ptr(), preferences_password.as_ptr()); 485 | assert!(!raw_preferences.is_null()); 486 | 487 | preferences_erase(raw_preferences); 488 | preferences_free(raw_preferences); 489 | } 490 | 491 | #[test] 492 | fn test_put_i32() { 493 | let preferences_name = CString::new("ffi").unwrap(); 494 | let raw_preferences = preferences(preferences_name.as_ptr()); 495 | assert!(!raw_preferences.is_null()); 496 | 497 | let i32_key = CString::new("i32").unwrap(); 498 | let insert_rc = preferences_put_i32(raw_preferences, i32_key.as_ptr(), 42); 499 | assert_eq!(0, insert_rc); 500 | 501 | let save_rc = preferences_save(raw_preferences); 502 | assert_eq!(0, save_rc); 503 | 504 | let mut read_val = 0; 505 | let read_rc = preferences_get_i32(raw_preferences, i32_key.as_ptr(), &mut read_val); 506 | assert_eq!(0, read_rc); 507 | assert_eq!(42, read_val); 508 | 509 | let erase_rc = preferences_erase(raw_preferences); 510 | assert_eq!(0, erase_rc); 511 | 512 | preferences_free(raw_preferences); 513 | } 514 | 515 | #[test] 516 | fn test_put_string() { 517 | let preferences_name = CString::new("string").unwrap(); 518 | let raw_preferences = preferences(preferences_name.as_ptr()); 519 | assert!(!raw_preferences.is_null()); 520 | 521 | let str_key = CString::new("str").unwrap(); 522 | let str_value = CString::new("value").unwrap(); 523 | let insert_rc = 524 | preferences_put_string(raw_preferences, str_key.as_ptr(), str_value.as_ptr()); 525 | assert_eq!(0, insert_rc); 526 | 527 | let save_rc = preferences_save(raw_preferences); 528 | assert_eq!(0, save_rc); 529 | 530 | let mut str_bytes = [0u8; 32]; 531 | let read_rc = preferences_get_string( 532 | raw_preferences, 533 | str_key.as_ptr(), 534 | str_bytes.as_mut_ptr(), 535 | str_bytes.len(), 536 | ); 537 | assert_eq!(5, read_rc); 538 | let str = String::from_utf8(str_bytes[0..5].to_vec()).unwrap(); 539 | assert_eq!("value", str); 540 | 541 | let erase_rc = preferences_erase(raw_preferences); 542 | assert_eq!(0, erase_rc); 543 | 544 | preferences_free(raw_preferences); 545 | } 546 | 547 | #[test] 548 | fn test_put_bool() { 549 | let preferences_name = CString::new("bool").unwrap(); 550 | let raw_preferences = preferences(preferences_name.as_ptr()); 551 | assert!(!raw_preferences.is_null()); 552 | 553 | let key = CString::new("bool").unwrap(); 554 | let insert_rc = preferences_put_bool(raw_preferences, key.as_ptr(), true); 555 | assert_eq!(0, insert_rc); 556 | 557 | let save_rc = preferences_save(raw_preferences); 558 | assert_eq!(0, save_rc); 559 | 560 | let mut read_bool = false; 561 | let read_rc = preferences_get_bool(raw_preferences, key.as_ptr(), &mut read_bool); 562 | assert_eq!(0, read_rc); 563 | assert_eq!(true, read_bool); 564 | 565 | let erase_rc = preferences_erase(raw_preferences); 566 | assert_eq!(0, erase_rc); 567 | 568 | preferences_free(raw_preferences); 569 | } 570 | 571 | #[test] 572 | fn test_put_binary() { 573 | let preferences_name = CString::new("binary").unwrap(); 574 | let raw_preferences = preferences(preferences_name.as_ptr()); 575 | assert!(!raw_preferences.is_null()); 576 | 577 | let key = CString::new("bin").unwrap(); 578 | let bin = [1u8, 2, 4, 5]; 579 | let insert_rc = 580 | preferences_put_bytes(raw_preferences, key.as_ptr(), bin.as_ptr(), bin.len()); 581 | assert_eq!(0, insert_rc); 582 | 583 | let mut read_buf = [0u8; 10]; 584 | let read_rc = preferences_get_bytes( 585 | raw_preferences, 586 | key.as_ptr(), 587 | read_buf.as_mut_ptr(), 588 | read_buf.len(), 589 | ); 590 | assert_eq!(4, read_rc); 591 | assert_eq!(bin, read_buf[0..4]); 592 | 593 | let erase_rc = preferences_erase(raw_preferences); 594 | assert_eq!(0, erase_rc); 595 | 596 | preferences_free(raw_preferences); 597 | } 598 | } 599 | -------------------------------------------------------------------------------- /packages/crw-preferences/src/io/mod.rs: -------------------------------------------------------------------------------- 1 | //! This crate provides a set of functions to save and load the preferences from the storage of the 2 | //! following devices: 3 | //! * windows 4 | //! * macOS 5 | //! * linux 6 | //! * android 7 | //! * ios 8 | //! * wasm32 on browser 9 | 10 | use std::io::{Error as StdIoError, Error}; 11 | use thiserror::Error; 12 | #[cfg(not(target_arch = "wasm32"))] 13 | mod native; 14 | #[cfg(not(target_arch = "wasm32"))] 15 | use native as sys; 16 | 17 | #[cfg(not(target_arch = "wasm32"))] 18 | pub use native::set_preferences_app_dir; 19 | 20 | #[cfg(all(target_arch = "wasm32", target_os = "unknown", feature = "js",))] 21 | mod wasm; 22 | #[cfg(all(target_arch = "wasm32", target_os = "unknown", feature = "js"))] 23 | use wasm as sys; 24 | 25 | /// Struct that represents a generic I/O error. 26 | #[derive(Error, Debug)] 27 | pub enum IoError { 28 | #[error("io error `{0}`")] 29 | Std(StdIoError), 30 | #[error("the preferences app directory was not initialized")] 31 | EmptyPreferencesPath, 32 | #[error("invalid name `{0}`")] 33 | InvalidName(String), 34 | #[error("the preferences are empty")] 35 | EmptyData, 36 | #[error("error while reading the data")] 37 | Read, 38 | #[error("error while writing the data")] 39 | Write, 40 | #[error("i/o operation not supported `{0}`")] 41 | Unsupported(String), 42 | #[error("unknown i/o error `{0}`")] 43 | Unknown(String), 44 | } 45 | 46 | pub type Result = std::result::Result; 47 | 48 | /// Functions to check if a key used to access the storage is valid. 49 | /// 50 | /// * `name` - The that will be checked. 51 | fn is_name_valid(name: &str) -> bool { 52 | !name.is_empty() 53 | && name 54 | .chars() 55 | .all(|c| c.is_ascii_alphanumeric() || ['-', '_'].contains(&c)) 56 | } 57 | 58 | /// Loads the string representation of a preferences set. 59 | /// 60 | /// * `name` - key that uniquely identify the preferences set that will be loaded. 61 | /// The `name` key can contain only ascii alphanumeric characters or -, _. 62 | /// 63 | /// # Errors 64 | /// This function returns one of the following errors: 65 | /// * [IoError::Read] - if an error occurred while reading the data from the device storage 66 | /// * [IoError::EmptyData] - if the data associated to the provided `name` is empty 67 | /// * [IoError::Unsupported] - if the device don't supports this operation 68 | pub fn load(name: &str) -> Result { 69 | if is_name_valid(name) { 70 | sys::load(name) 71 | } else { 72 | Err(IoError::InvalidName(name.to_owned())) 73 | } 74 | } 75 | 76 | /// Saves the string representation of preferences set into the device storage. 77 | /// 78 | /// * `name` - key that uniquely identify the preferences set that will be saved. 79 | /// The `name` key can contain only ascii alphanumeric characters or -, _. 80 | /// * `data` - the preferences set that will be stored as a string. 81 | /// 82 | /// # Errors 83 | /// This function can returns one of the following errors: 84 | /// * [IoError::Write] - if an error occur while writing the data into the device storage 85 | /// * [IoError::Unsupported] - if the device don't supports this operation 86 | pub fn save(name: &str, data: &str) -> Result<()> { 87 | if is_name_valid(name) { 88 | sys::save(name, data) 89 | } else { 90 | Err(IoError::InvalidName(name.to_owned())) 91 | } 92 | } 93 | 94 | /// Erase a preferences set stored into the device memory. 95 | pub fn erase(name: &str) { 96 | if is_name_valid(name) { 97 | sys::erase(name) 98 | } 99 | } 100 | 101 | /// Check if exist a preferences set withe the provided name into the device storage. 102 | pub fn exist(name: &str) -> bool { 103 | if !is_name_valid(name) { 104 | return false; 105 | } 106 | 107 | sys::exist(name) 108 | } 109 | 110 | impl From for IoError { 111 | fn from(e: Error) -> Self { 112 | IoError::Std(e) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /packages/crw-preferences/src/io/native.rs: -------------------------------------------------------------------------------- 1 | //! Module that provides the functions to read and write the preferences from the device storage for the 2 | //! following os: 3 | //! * windows 4 | //! * macOS 5 | //! * linux 6 | //! * android 7 | //! * ios 8 | 9 | use crate::io::{IoError, Result}; 10 | use once_cell::sync::Lazy; 11 | use std::fs; 12 | use std::fs::File; 13 | use std::path::PathBuf; 14 | use std::sync::Mutex; 15 | 16 | /// Global variable that contains the directory name where will be stored the configurations files. 17 | static PREFERENCES_APP_DIR: Lazy> = Lazy::new(|| { 18 | let str = option_env!("CARGO_BIN_NAME").unwrap_or("").to_owned(); 19 | Mutex::new(str) 20 | }); 21 | 22 | /// Gets a file with the provided name from the application configuration directory. 23 | /// 24 | /// * `name` - the name of the file requested from the user. 25 | /// * `create` - if true creates also the file if not exist. 26 | /// 27 | /// The file is located inside the application config directory that depends on the target device OS. 28 | /// 29 | /// |Platform | Example | 30 | /// | ------- | -----------------------------------------------------------------------------------------------------------| 31 | /// | Linux | `$XDG_CONFIG_HOME`/{PREFERENCES_APP_DIR}/{name} or `$HOME/.config/{PREFERENCES_APP_DIR}/{name}` | 32 | /// | macOS | `$HOME`/Library/Application Support/{PREFERENCES_APP_DIR}/{name} | 33 | /// | Windows | C:\Users\`$USER`\AppData\Roaming\{PREFERENCES_APP_DIR}/{name} | 34 | /// | Android | {PREFERENCES_APP_DIR}/{name} | 35 | /// | iOS | {PREFERENCES_APP_DIR}/{name} | 36 | /// 37 | /// # Errors 38 | /// 39 | /// This function return an [std::io::Error] if the file can't be created inside the configuration directory. 40 | fn get_config_file(name: &str, create: bool) -> Result { 41 | cfg_if! { 42 | if #[cfg(test)] { 43 | // In test mode just use the current working directory. 44 | let mut config_dir = PathBuf::new(); 45 | } 46 | else if #[cfg(any(target_os = "android", target_os = "ios"))] { 47 | // On android or ios we can't obtain the path at runtime so the app dir must be an 48 | // absolute path to the directory where will be stored the configurations. 49 | let dir = PREFERENCES_APP_DIR.lock().unwrap(); 50 | if dir.is_empty() { 51 | return Err(IoError::EmptyPreferencesPath); 52 | } 53 | let mut config_dir = PathBuf::from(dir.as_str()); 54 | } 55 | else { 56 | // The application name is resolved as compile from the cargo project name. 57 | let dir = PREFERENCES_APP_DIR.lock().unwrap(); 58 | if dir.is_empty() { 59 | return Err(IoError::EmptyPreferencesPath); 60 | } 61 | let mut config_dir = dirs::config_dir().unwrap(); 62 | // Append the binary name to the default config dir 63 | config_dir.push(dir.as_str()); 64 | // Check if the directory exists, if not create it. 65 | if !config_dir.exists() { 66 | fs::create_dir_all(config_dir.as_path())?; 67 | } 68 | } 69 | } 70 | 71 | // Append the name provided from the user 72 | config_dir.push(name); 73 | // Check if the file exists, if not create an empty one. 74 | if create && !config_dir.exists() { 75 | File::create(config_dir.as_path())?; 76 | } 77 | // Returns the config file path. 78 | Ok(config_dir) 79 | } 80 | 81 | /// Sets the application directory where will be stored the configurations. 82 | /// On windows, macOS and linux `dir` should be only the name of the directory that will 83 | /// be create inside the current user app configurations directory. 84 | /// On Android and iOS instead since is not possible to obtain the path where the application 85 | /// can read and write `dir` must be an absolute path to a directory accessible from the application. 86 | pub fn set_preferences_app_dir(dir: &str) { 87 | let mut str = PREFERENCES_APP_DIR.lock().unwrap(); 88 | str.clear(); 89 | str.push_str(dir); 90 | } 91 | 92 | /// Loads the string representation of a preferences set. 93 | /// 94 | /// * `name` - name of the file from which will be loaded the preferences. 95 | /// 96 | /// # Errors 97 | /// This function can returns one of the following errors: 98 | /// * [IoError::Read] if the file with the provided `name` can't be read 99 | /// * [IoError::EmptyData] if the file is empty 100 | pub fn load(name: &str) -> Result { 101 | let config_file = get_config_file(name, true)?; 102 | let content = fs::read_to_string(config_file)?; 103 | 104 | if content.is_empty() { 105 | Err(IoError::EmptyData) 106 | } else { 107 | Ok(content) 108 | } 109 | } 110 | 111 | /// Saves the string representation of preferences set into the device storage. 112 | /// 113 | /// * `name` - Name of the file where will be stored the data. 114 | /// * `data` - The string that will be stored inside the file. 115 | /// 116 | /// # Errors 117 | /// This function returns [IoError::Write] if can't write to the file with the provided `name`. 118 | pub fn save(name: &str, data: &str) -> Result<()> { 119 | let config_file = get_config_file(name, true)?; 120 | fs::write(config_file, data)?; 121 | Ok(()) 122 | } 123 | 124 | /// Deletes the file with the provide `name` from the device storage. 125 | pub fn erase(name: &str) { 126 | let path = get_config_file(name, false); 127 | if let Ok(path) = path { 128 | if path.exists() { 129 | // Make the compiler happy, an error here should never occur. 130 | let _ = fs::remove_file(path); 131 | } 132 | } 133 | } 134 | 135 | /// Check if exist a preferences set with the provided `name` into the device storage. 136 | pub fn exist(name: &str) -> bool { 137 | let path = get_config_file(name, false); 138 | 139 | if let Ok(p) = path { 140 | p.exists() 141 | } else { 142 | false 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /packages/crw-preferences/src/io/wasm.rs: -------------------------------------------------------------------------------- 1 | //! Module that provides the functions to read and write the preferences from the browser local storage. 2 | 3 | use crate::io::{IoError, Result}; 4 | use web_sys::{Storage, Window}; 5 | 6 | /// Gets the browser `LocalStorage` instance. 7 | /// 8 | /// # Errors 9 | /// This function returns [IoError::Unsupported] if the browser don't support the LocalStorage API 10 | /// or the global window object was not found. 11 | fn get_storage() -> Result { 12 | let window = web_sys::window().ok_or(IoError::Unsupported( 13 | "global `window` object not found".to_owned(), 14 | ))?; 15 | 16 | Ok(Window::local_storage(&window) 17 | .map_err(|_| IoError::Unsupported("Local storage not supported".to_owned()))? 18 | .ok_or(IoError::Unsupported("Local storage is null".to_owned()))?) 19 | } 20 | 21 | /// Loads the string representation of a preferences set from the browser `LocalStorage`. 22 | /// 23 | /// * `name` - key that uniquely identify the preferences set that will be loaded. 24 | pub fn load(name: &str) -> Result { 25 | let storage = get_storage()?; 26 | 27 | Storage::get_item(&storage, name) 28 | .map_err(|_| IoError::Read)? 29 | .ok_or(IoError::EmptyData) 30 | .and_then(|s| { 31 | if s.is_empty() { 32 | Err(IoError::EmptyData) 33 | } else { 34 | Ok(s) 35 | } 36 | }) 37 | } 38 | 39 | /// Saves the string representation of preferences set into the browser `LocalStorage`. 40 | /// 41 | /// * `name` - key that uniquely identify the preferences set that will be saved. 42 | /// * `value` - the preferences set that will be saved into the browser localStorage. 43 | /// 44 | /// # Errors 45 | /// This function returns [Err(IoError::Unsupported)] if the browser don't support the LocalStorage API 46 | /// or [Err(IoError::Write)] if an error occur when writing the data to the browser local storage. 47 | pub fn save(name: &str, value: &str) -> Result<()> { 48 | let storage = get_storage()?; 49 | 50 | Storage::set_item(&storage, name, value).map_err(|_| IoError::Write) 51 | } 52 | 53 | /// Deletes the data from the browser `LocalStorage` 54 | pub fn erase(name: &str) { 55 | let storage = get_storage(); 56 | 57 | if let Ok(storage) = storage { 58 | // Make the compiler happy, an error here should never occur. 59 | let _ = Storage::set_item(&storage, name, ""); 60 | } 61 | } 62 | 63 | /// Check if there are existent preferences set into the browser `LocalStorage`. 64 | pub fn exist(name: &str) -> bool { 65 | let storage = get_storage(); 66 | 67 | return if storage.is_err() { 68 | false 69 | } else { 70 | let storage = storage.unwrap(); 71 | let item_result = Storage::get_item(&storage, name); 72 | 73 | if item_result.is_err() { 74 | false 75 | } else { 76 | let item = item_result.unwrap().unwrap_or("".to_owned()); 77 | !item.is_empty() 78 | } 79 | }; 80 | } 81 | -------------------------------------------------------------------------------- /packages/crw-preferences/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Crate that provides a set of utility to store preferences into the device storage. 2 | //! The preferences are organized in sets, each one identified by an unique name. 3 | //! 4 | //! The values that can be saved into a preference set are: 5 | //! * `i32` 6 | //! * `bool` 7 | //! * `str` 8 | //! * `Vec` 9 | 10 | #[macro_use] 11 | extern crate cfg_if; 12 | 13 | #[cfg(feature = "ffi")] 14 | #[macro_use] 15 | extern crate ffi_helpers; 16 | 17 | pub mod encrypted; 18 | mod io; 19 | pub mod preferences; 20 | pub mod unencrypted; 21 | 22 | #[cfg(all(target_arch = "wasm32", target_os = "unknown", feature = "js",))] 23 | pub mod wasm; 24 | 25 | #[cfg(feature = "ffi")] 26 | pub mod ffi; 27 | -------------------------------------------------------------------------------- /packages/crw-preferences/src/preferences.rs: -------------------------------------------------------------------------------- 1 | //! Module that provides the generic trait to store and load preferences from the device storage. 2 | 3 | use crate::io; 4 | use crate::io::IoError; 5 | use std::result; 6 | use thiserror::Error; 7 | 8 | #[cfg(not(target_arch = "wasm32"))] 9 | pub use crate::io::set_preferences_app_dir; 10 | 11 | pub type Result = result::Result; 12 | 13 | #[derive(Error, Debug)] 14 | pub enum PreferencesError { 15 | #[error("invalid preference name `{0}`")] 16 | InvalidName(String), 17 | #[error("i/o error `{0}`")] 18 | IO(#[from] IoError), 19 | #[error("error while deserializing the preferences")] 20 | DeserializationError, 21 | #[error("error while serializing the preferences")] 22 | SerializationError, 23 | } 24 | 25 | /// Trait that represents a generic preferences set. 26 | pub trait Preferences { 27 | /// Gets a i32 from the preferences. 28 | /// 29 | /// * `key` - The name of the preference to retrieve. 30 | fn get_i32(&self, key: &str) -> Option; 31 | 32 | /// Store a i32 into the preferences. 33 | /// 34 | /// * `key` - The name of the preference that will be stored. 35 | /// * `value` - The value that will be stored into the preferences. 36 | fn put_i32(&mut self, key: &str, value: i32) -> Result<()>; 37 | 38 | /// Gets a string from the preferences. 39 | /// 40 | /// * `key` - name of the preference that will be loaded. 41 | fn get_str(&self, key: &str) -> Option; 42 | 43 | /// Store a string into the preferences. 44 | /// 45 | /// * `key` - The name of the preference that will be stored. 46 | /// * `value` - The value that will be stored into the preferences. 47 | fn put_str(&mut self, key: &str, value: String) -> Result<()>; 48 | 49 | /// Gets a boolean from the preferences. 50 | /// 51 | /// * `key` - name of the preference that will be loaded. 52 | fn get_bool(&self, key: &str) -> Option; 53 | 54 | /// Store a boolean into the preferences. 55 | /// 56 | /// * `key` - The name of the preference that will be stored. 57 | /// * `value` - The value that will be stored into the preferences. 58 | fn put_bool(&mut self, key: &str, value: bool) -> Result<()>; 59 | 60 | /// Gets an array of bytes from the preferences. 61 | /// 62 | /// * `key` - name of the preference that will be loaded. 63 | fn get_bytes(&self, key: &str) -> Option>; 64 | 65 | /// Store an array of bytes into the preferences. 66 | /// 67 | /// * `key` - The name of the preference that will be stored. 68 | /// * `value` - The array that will be stored into the preferences. 69 | fn put_bytes(&mut self, key: &str, value: Vec) -> Result<()>; 70 | 71 | /// Delete all the preferences currently loaded. 72 | fn clear(&mut self); 73 | 74 | /// Delete all the preferences currently loaded and also the one stored into the 75 | /// device storage. 76 | fn erase(&mut self); 77 | 78 | /// Saves the preferences into the device disk. 79 | fn save(&self) -> Result<()>; 80 | } 81 | 82 | /// Deletes a preferences set from the device storage 83 | pub fn delete(name: &str) { 84 | io::erase(name); 85 | } 86 | 87 | /// Checks if there are existent preferences set with the provided `name`. 88 | pub fn exist(name: &str) -> bool { 89 | io::exist(name) 90 | } 91 | -------------------------------------------------------------------------------- /packages/crw-preferences/src/unencrypted.rs: -------------------------------------------------------------------------------- 1 | //! Module that provides an implementation of [Preferences] that saves the value into the device 2 | //! storage. 3 | 4 | use crate::io; 5 | use crate::io::IoError; 6 | use crate::preferences::{Preferences, PreferencesError, Result}; 7 | use serde_json::{Map, Value}; 8 | 9 | pub struct UnencryptedPreferences { 10 | name: String, 11 | data: Map, 12 | } 13 | 14 | impl UnencryptedPreferences { 15 | /// Loads the json preferences from the device storage. 16 | /// 17 | /// * `name` - The preferences set name. 18 | /// 19 | /// # Errors 20 | /// This function can return one of the following errors: 21 | /// * [PreferencesError::DeserializationError] if the data loaded from the disk is not valid. 22 | /// * [PreferencesError::IO] if an error occurred while reading the data from the device storage. 23 | fn load_from_disk(name: &str) -> Result> { 24 | let disk_data = io::load(name); 25 | 26 | if disk_data.is_err() { 27 | let err = disk_data.err().unwrap(); 28 | return match err { 29 | IoError::EmptyData => Ok(Map::new()), 30 | IoError::InvalidName(s) => Err(PreferencesError::InvalidName(s)), 31 | _ => Err(PreferencesError::IO(err)), 32 | }; 33 | } 34 | 35 | serde_json::from_str(&disk_data.unwrap()) 36 | .map_err(|_| PreferencesError::DeserializationError) 37 | } 38 | 39 | /// Writes the data as json to the device storage. 40 | /// 41 | /// * `name` - The preference set name. 42 | /// * `data` - The data that will be wrote to the device storage. 43 | /// 44 | /// # Errors 45 | /// This function returns the following errors 46 | /// * [PreferencesError::IO] - if an error occurs while writing the data to the device storage 47 | /// * [PreferencesError::SerializationError] - if an error occurs while serializing the data. 48 | fn write_to_disk(name: &str, data: &Map) -> Result<()> { 49 | let json = 50 | serde_json::to_string(&data).map_err(|_| PreferencesError::SerializationError)?; 51 | io::save(name, &json)?; 52 | 53 | Ok(()) 54 | } 55 | 56 | /// Creates a new preferences set with the provided `name`. 57 | /// If already exist a preferences set with the provided name will be loaded the previous one. 58 | /// 59 | /// * `name` - The preferences name, can contains only ascii alphanumeric chars or -, _. 60 | /// 61 | /// # Errors 62 | /// This function returns [PreferencesError::InvalidName] if the provided name contains 63 | /// non ascii alphanumeric chars or [PreferencesError::DeserializationError] if the data 64 | /// associated with the provided name are invalid. 65 | /// 66 | pub fn new(name: &str) -> Result { 67 | let data = UnencryptedPreferences::load_from_disk(name)?; 68 | 69 | Ok(UnencryptedPreferences { 70 | name: name.to_owned(), 71 | data, 72 | }) 73 | } 74 | } 75 | 76 | impl Preferences for UnencryptedPreferences { 77 | fn get_i32(&self, key: &str) -> Option { 78 | self.data.get(key).and_then(|v| { 79 | v.as_i64().and_then(|i| { 80 | if i >= (i32::MIN as i64) && i <= (i32::MAX as i64) { 81 | Some(i as i32) 82 | } else { 83 | None 84 | } 85 | }) 86 | }) 87 | } 88 | 89 | fn put_i32(&mut self, key: &str, value: i32) -> Result<()> { 90 | self.data.insert(key.to_owned(), Value::from(value)); 91 | Ok(()) 92 | } 93 | 94 | fn get_str(&self, key: &str) -> Option { 95 | self.data 96 | .get(key) 97 | .and_then(|v| v.as_str().map(|s| s.to_owned())) 98 | } 99 | 100 | fn put_str(&mut self, key: &str, value: String) -> Result<()> { 101 | self.data.insert(key.to_owned(), Value::from(value)); 102 | Ok(()) 103 | } 104 | 105 | fn get_bool(&self, key: &str) -> Option { 106 | self.data.get(key).and_then(|v| v.as_bool()) 107 | } 108 | 109 | fn put_bool(&mut self, key: &str, value: bool) -> Result<()> { 110 | self.data.insert(key.to_owned(), Value::from(value)); 111 | Ok(()) 112 | } 113 | 114 | fn get_bytes(&self, key: &str) -> Option> { 115 | self.data.get(key).and_then(|v| v.as_array()).map(|v| { 116 | let mut vector: Vec = Vec::with_capacity(v.len()); 117 | for value in v { 118 | if value.is_u64() { 119 | vector.push(value.as_u64().unwrap() as u8) 120 | } 121 | } 122 | vector 123 | }) 124 | } 125 | 126 | fn put_bytes(&mut self, key: &str, value: Vec) -> Result<()> { 127 | self.data.insert(key.to_owned(), Value::from(value)); 128 | Ok(()) 129 | } 130 | 131 | fn clear(&mut self) { 132 | self.data.clear() 133 | } 134 | 135 | fn erase(&mut self) { 136 | self.clear(); 137 | io::erase(&self.name) 138 | } 139 | 140 | fn save(&self) -> Result<()> { 141 | UnencryptedPreferences::write_to_disk(&self.name, &self.data) 142 | } 143 | } 144 | 145 | #[cfg(test)] 146 | mod tests { 147 | use crate::preferences; 148 | use crate::preferences::Preferences; 149 | use crate::unencrypted::UnencryptedPreferences; 150 | 151 | #[test] 152 | pub fn test_preferences_save() { 153 | let mut test_preferences = UnencryptedPreferences::new("save").unwrap(); 154 | test_preferences 155 | .put_str("data", "some simple data".to_owned()) 156 | .unwrap(); 157 | 158 | test_preferences.save().unwrap(); 159 | 160 | assert_eq!( 161 | "some simple data", 162 | test_preferences.get_str("data").unwrap() 163 | ); 164 | test_preferences.erase(); 165 | } 166 | 167 | #[test] 168 | pub fn test_invalid_names() { 169 | // Check invalid names 170 | assert!(UnencryptedPreferences::new("test.").is_err()); 171 | assert!(UnencryptedPreferences::new("test\\").is_err()); 172 | assert!(UnencryptedPreferences::new("test//").is_err()); 173 | assert!(UnencryptedPreferences::new("test with spaces").is_err()); 174 | // Test empty 175 | assert!(UnencryptedPreferences::new("").is_err()); 176 | } 177 | 178 | #[test] 179 | pub fn test_data_read_write() { 180 | let set_name = "rw"; 181 | let test_vec: Vec = vec![12, 13, 54, 42]; 182 | let mut preferences = UnencryptedPreferences::new(set_name).unwrap(); 183 | assert!(preferences.put_i32("i32", 42).is_ok()); 184 | assert!(preferences.put_str("str", "str".to_owned()).is_ok()); 185 | assert!(preferences.put_bool("bool", true).is_ok()); 186 | assert!(preferences.put_bytes("bin", test_vec.clone()).is_ok()); 187 | 188 | // Write data to disk 189 | preferences.save().unwrap(); 190 | 191 | // Create a new one that reads from the saved preferences 192 | let mut preferences = UnencryptedPreferences::new(set_name).unwrap(); 193 | let i32_result = preferences.get_i32("i32"); 194 | let str_result = preferences.get_str("str"); 195 | let bool_result = preferences.get_bool("bool"); 196 | let binary_result = preferences.get_bytes("bin"); 197 | 198 | // Delete the file from the disk to avoid that some date remain on the disk if the 199 | // test fails. 200 | preferences.erase(); 201 | assert_eq!(42, i32_result.unwrap()); 202 | assert_eq!("str", str_result.unwrap()); 203 | assert_eq!(true, bool_result.unwrap()); 204 | assert_eq!(test_vec, binary_result.unwrap()); 205 | } 206 | 207 | #[test] 208 | pub fn test_exist() { 209 | let set_name = "unencrypted-exist"; 210 | 211 | let mut p = UnencryptedPreferences::new(set_name).unwrap(); 212 | p.save().unwrap(); 213 | 214 | assert!(preferences::exist(set_name)); 215 | 216 | p.erase(); 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /packages/crw-preferences/src/wasm.rs: -------------------------------------------------------------------------------- 1 | //! Module that provides a wrapper to expose a [Preferences] to a js application. 2 | 3 | extern crate bindgen as wasm_bindgen; 4 | 5 | use crate::encrypted::{EncryptedPreferences, EncryptedPreferencesError}; 6 | use crate::preferences; 7 | use crate::preferences::{Preferences, PreferencesError}; 8 | use crate::unencrypted::UnencryptedPreferences; 9 | use wasm_bindgen::prelude::*; 10 | 11 | #[wasm_bindgen(js_name = Preferences)] 12 | pub struct PreferencesWrapper { 13 | container: Box, 14 | } 15 | 16 | #[wasm_bindgen(js_class = Preferences)] 17 | impl PreferencesWrapper { 18 | #[wasm_bindgen(js_name = "getI32")] 19 | pub fn get_i32(&self, key: &str) -> Option { 20 | self.container.get_i32(key) 21 | } 22 | 23 | #[wasm_bindgen(js_name = "putI32")] 24 | pub fn put_i32(&mut self, key: &str, value: i32) -> Result<(), JsValue> { 25 | Ok(self.container.put_i32(key, value)?) 26 | } 27 | 28 | #[wasm_bindgen(js_name = "getStr")] 29 | pub fn get_str(&self, key: &str) -> Option { 30 | self.container.get_str(key) 31 | } 32 | 33 | #[wasm_bindgen(js_name = "putStr")] 34 | pub fn put_str(&mut self, key: &str, value: &str) -> Result<(), JsValue> { 35 | Ok(self.container.put_str(key, value.to_owned())?) 36 | } 37 | 38 | #[wasm_bindgen(js_name = "getBool")] 39 | pub fn get_bool(&self, key: &str) -> Option { 40 | self.container.get_bool(key) 41 | } 42 | 43 | #[wasm_bindgen(js_name = "putBool")] 44 | pub fn put_bool(&mut self, key: &str, value: bool) -> Result<(), JsValue> { 45 | Ok(self.container.put_bool(key, value)?) 46 | } 47 | 48 | #[wasm_bindgen(js_name = "getBytes")] 49 | pub fn get_bytes(&self, key: &str) -> Option> { 50 | self.container.get_bytes(key) 51 | } 52 | 53 | #[wasm_bindgen(js_name = "putBytes")] 54 | pub fn put_bytes(&mut self, key: &str, value: Vec) -> Result<(), JsValue> { 55 | Ok(self.container.put_bytes(key, value)?) 56 | } 57 | 58 | pub fn clear(&mut self) { 59 | self.container.clear() 60 | } 61 | 62 | pub fn erase(&mut self) { 63 | self.container.erase(); 64 | } 65 | 66 | pub fn save(&self) -> Result<(), JsValue> { 67 | Ok(self.container.save()?) 68 | } 69 | } 70 | 71 | #[wasm_bindgen] 72 | pub fn exist(name: &str) -> bool { 73 | preferences::exist(name) 74 | } 75 | 76 | #[wasm_bindgen] 77 | pub fn delete(name: &str) { 78 | preferences::delete(name); 79 | } 80 | 81 | #[wasm_bindgen(js_name = "preferences")] 82 | pub fn preferences(name: &str) -> Result { 83 | UnencryptedPreferences::new(name) 84 | .map(|container| PreferencesWrapper { 85 | container: Box::new(container), 86 | }) 87 | .map_err(JsValue::from) 88 | } 89 | 90 | #[wasm_bindgen(js_name = "encryptedPreferences")] 91 | pub fn encrypted_preferences(password: &str, name: &str) -> Result { 92 | EncryptedPreferences::new(password, name) 93 | .map(|container| PreferencesWrapper { 94 | container: Box::new(container), 95 | }) 96 | .map_err(JsValue::from) 97 | } 98 | 99 | impl From for JsValue { 100 | fn from(e: PreferencesError) -> Self { 101 | JsValue::from(e.to_string()) 102 | } 103 | } 104 | 105 | impl From for JsValue { 106 | fn from(e: EncryptedPreferencesError) -> Self { 107 | JsValue::from(e.to_string()) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /packages/crw-wallet/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "crw-wallet" 3 | version = "0.1.0" 4 | authors = ["bragaz ", "Manuel Turetta "] 5 | edition = "2018" 6 | description = "Wallet package of cosmos-rust-wallet to create a wallet and sign txs" 7 | license = "Apache-2.0" 8 | repository = "https://github.com/forbole/cosmos-rust-wallet" 9 | keywords = ["blockchain", "cosmos", "cosmos-rust-wallet"] 10 | 11 | [lib] 12 | crate-type = ["cdylib", "lib"] 13 | 14 | [dependencies] 15 | bech32 = { version = "0.9.0" } 16 | bitcoin = { version = "0.29.1" } 17 | hdpath = {version = "0.6.0", features = ["with-bitcoin"] } 18 | k256 = { version = "0.11.4", features = ["ecdsa-core", "ecdsa", "sha256"]} 19 | ripemd = { version = "0.1.1" } 20 | serde = { version = "1.0", features = ["derive"] } 21 | sha2 = { version = "0.10.2" } 22 | tiny-bip39 = { version = "1.0.0", default-features = false } 23 | thiserror = "1.0.24" 24 | wasm-bindgen-futures = { version = "0.4.21", optional = true} 25 | parking_lot = { version = "0.12.1", default-features = false, optional = true } 26 | rand = { version = "0.8.5", optional = true } 27 | libc = { version = "0.2.94", optional = true } 28 | ffi_helpers = { version = "0.3.0", optional = true } 29 | getrandom = { version = "0.2.8", optional = true } 30 | 31 | [dependencies.bindgen] 32 | version = "0.2.70" 33 | optional = true 34 | package = "wasm-bindgen" 35 | 36 | [dev-dependencies] 37 | wasm-bindgen-test = "0.3.20" 38 | actix-rt = "2.0.2" 39 | hex = "0.4.3" 40 | 41 | [features] 42 | default = [] 43 | wasm-bindgen = ["bindgen", "wasm-bindgen-futures", "parking_lot", "rand", "getrandom/js"] 44 | ffi = ["libc", "ffi_helpers"] -------------------------------------------------------------------------------- /packages/crw-wallet/Makefile: -------------------------------------------------------------------------------- 1 | current_dir := $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))) 2 | uid := $(shell id -u) 3 | guid := $(shell id -g) 4 | rust_version := 1.52.1 5 | osx_sdk := 11.1 6 | ios_sdk := 14.4 7 | android_ndk := r21e 8 | 9 | lint: 10 | cargo fmt 11 | cargo clippy -- -D warnings 12 | 13 | clean: 14 | rm -Rf $(current_dir)/target 15 | rm -Rf $(current_dir)/pkg 16 | 17 | build-linux: 18 | @echo "Building crw-wallet for linux" 19 | docker run -u $(uid):$(guid) --rm -v $(current_dir):/workdir forbole/rust-builder:$(rust_version) \ 20 | cargo build --release --target=x86_64-unknown-linux-gnu --features ffi 21 | 22 | build-windows: 23 | @echo "Building crw-wallet for windows" 24 | docker run -u $(uid):$(guid) --rm -v $(current_dir):/workdir forbole/windows-rust-builder:$(rust_version) \ 25 | cargo build --release --target=x86_64-pc-windows-gnu --features ffi 26 | 27 | build-osx: 28 | @echo "Building crw-wallet for mac" 29 | docker run -u $(uid):$(guid) --rm -v $(current_dir):/workdir forbole/osx-rust-builder:$(rust_version)-$(osx_sdk) \ 30 | cargo build --release --target=x86_64-apple-darwin --features ffi 31 | 32 | build-android-aarch64: 33 | @echo "Building crw-wallet for android-aarch64" 34 | docker run -u $(uid):$(guid) --rm -v $(current_dir):/workdir forbole/android-rust-builder:$(rust_version)-$(android_ndk) \ 35 | cargo build --release --target=aarch64-linux-android --features ffi 36 | 37 | build-android-armv7: 38 | @echo "Building crw-wallet for android-armv7" 39 | docker run -u $(uid):$(guid) --rm -v $(current_dir):/workdir forbole/android-rust-builder:$(rust_version)-$(android_ndk) \ 40 | cargo build --release --target=armv7-linux-androideabi --features ffi 41 | 42 | build-android-x86_64: 43 | @echo "Building crw-wallet for android-x86_64 (Emulator)" 44 | docker run -u $(uid):$(guid) --rm -v $(current_dir):/workdir forbole/android-rust-builder:$(rust_version)-$(android_ndk) \ 45 | cargo build --release --target=x86_64-linux-android --features ffi 46 | 47 | build-android: build-android-armv7 build-android-aarch64 build-android-x86_64 48 | 49 | build-ios-aarch64: 50 | @echo "Building crw-wallet for iOS aarch64" 51 | docker run -u $(uid):$(guid) -e IOS_ARCH=arm64 --rm -v $(current_dir):/workdir forbole/ios-rust-builder:$(rust_version)-$(ios_sdk) \ 52 | cargo build --release --target=aarch64-apple-ios --features ffi 53 | 54 | build-ios-x86_64: 55 | @echo "Building crw-wallet for iOS x86_64 (Emulator)" 56 | docker run -u $(uid):$(guid) -e IOS_ARCH=x86_64 --rm -v $(current_dir):/workdir forbole/ios-rust-builder:$(rust_version)-$(ios_sdk) \ 57 | cargo build --release --target=x86_64-apple-ios --features ffi 58 | 59 | build-ios: build-ios-aarch64 build-ios-x86_64 60 | 61 | build-wasm: 62 | @echo "Building crw-wallet for web" 63 | docker run -u $(uid):$(guid) --rm -v $(current_dir):/workdir forbole/wasm-rust-builder:$(rust_version) \ 64 | wasm-pack build --release -- --features wasm-bindgen -v 65 | 66 | all: build-linux build-windows build-osx build-android build-ios build-wasm -------------------------------------------------------------------------------- /packages/crw-wallet/README.md: -------------------------------------------------------------------------------- 1 | # Wallet Package 2 | The Wallet package contains all the things needed to create a `wallet` from a mnemonic phrase and use it 3 | to sign a tx. 4 | 5 | This package can also be compiled to WASM and used on the browser. 6 | 7 | ### Import wallet from mnemonic 8 | ````rust 9 | let cosmos_dp = "m/44'/118'/0'/0/0"; 10 | let mnemonic = "battle call once stool three mammal hybrid list sign field athlete amateur cinnamon eagle shell erupt voyage hero assist maple matrix maximum able barrel"; 11 | 12 | let wallet = MnemonicWallet::new(mnemonic, cosmos_dp).unwrap(); 13 | ```` 14 | 15 | ### Wallet from random mnemonic 16 | ````rust 17 | let cosmos_dp = "m/44'/118'/0'/0/0"; 18 | 19 | let (wallet, mnemonic) = MnemonicWallet::random(cosmos_dp).unwrap(); 20 | ```` 21 | 22 | ### Sign tx 23 | ````rust 24 | fn sign_tx_example() { 25 | let cosmos_dp = "m/44'/118'/0'/0/0"; 26 | 27 | let (wallet, mnemonic) = MnemonicWallet::random(cosmos_dp).unwrap(); 28 | 29 | // ... Prepare the transaction 30 | 31 | let serialized_transaction: Vec = ...; 32 | 33 | let signature = wallet.sign(&serialized_transaction).unwrap(); 34 | } 35 | ```` -------------------------------------------------------------------------------- /packages/crw-wallet/ffi-binding.h: -------------------------------------------------------------------------------- 1 | /*** 2 | * @brief This C header file exposes the FFI defined inside the ffi crate. 3 | */ 4 | 5 | #include 6 | 7 | typedef struct wallet wallet_t; 8 | 9 | /** 10 | * @brief Struct that represents a signature. 11 | */ 12 | typedef struct { 13 | /** 14 | * @brief The length of the signature. 15 | */ 16 | uint32_t len; 17 | /** 18 | * @brief The data signature. 19 | */ 20 | uint8_t* data; 21 | } signature_t; 22 | 23 | /** 24 | * @brief Creates a random 24 words mnemonic. 25 | * @return Returns the generated mnemonic or NULL in case of error. 26 | * The caller must take care of releasing the returned mnemonic with the 27 | * cstring_free function. 28 | * In case of error the error cause can be obtained using the error_message_utf8 29 | * function. 30 | */ 31 | char* wallet_random_mnemonic(); 32 | 33 | /** 34 | * @brief Free a string. 35 | * @param str: Pointer to the string to free. 36 | */ 37 | void cstring_free(char* str); 38 | 39 | /** 40 | * @brief Derive a Secp256k1 key pair from the given mnemonic and derivation_path. 41 | * @param mnemonic: The wallet mnemonic. 42 | * @param derivation_path: The derivation path used to derive the keys from the mnemonic. 43 | * @return Returns a pointer to a valid wallet or NULL on error. 44 | * The caller must take care of freeing the returned wallet instance with 45 | * the wallet_free function. 46 | * In case of error the error cause can be obtained using the error_message_utf8 47 | * function. 48 | */ 49 | wallet_t* wallet_from_mnemonic(const char* mnemonic, const char* derivation_path); 50 | 51 | /** 52 | * @brief Free a wallet instance. 53 | * @param wallet: The wallet to free. 54 | */ 55 | void wallet_free(wallet_t* wallet); 56 | 57 | /** 58 | * @brief Gets the bec32 address associated to the wallet. 59 | * @param wallet: Pointer to the wallet instance. 60 | * @param hrp: The address human readable part. 61 | * @return Returns the bech32 address associated to the wallet on success or 62 | * NULL on error. 63 | * The caller must take care of freeing the returned address with the 64 | * cstring_free function. 65 | * In case of error the error cause can be obtained using the error_message_utf8 66 | * function. 67 | */ 68 | char* wallet_get_bech32_address(wallet_t *wallet, const char* hrp); 69 | 70 | /** 71 | * @brief Gets secp256 public key from the wallet. 72 | * @param wallet: Pointer to the wallet instance. 73 | * @param compressed: a value != 0 to get the public key in compressed format, 74 | * 0 to get in uncompressed format. 75 | * @param out_buffer: Pointer where will be stored the public key 76 | * @param size: Size of out_buffer. 77 | * @return Returns the number of bytes wrote inside out_buffer on success, 78 | * -1 if the provided arguments are invalid or -2 if the public key don't fit 79 | * into out_buffer. 80 | */ 81 | int wallet_get_public_key(wallet_t *wallet, uint32_t compressed, uint8_t *out_buffer, int size); 82 | 83 | /** 84 | * @brief Performs the signature of the provided data. 85 | * @param wallet: Pointer to the wallet instance. 86 | * @param data: The data to sign. 87 | * @param len: The length of the data to sign. 88 | * @return Returns a pointer to a signature_t instance on success, NULL on error. 89 | * The caller must take care of freeing the returned signature with the 90 | * wallet_sign_free function. 91 | * In case of error the error cause can be obtained using the error_message_utf8 92 | * function. 93 | */ 94 | signature_t* wallet_sign(wallet_t *wallet, const uint8_t* data, uint32_t len); 95 | 96 | /** 97 | * @brief Free a signature instance. 98 | * @param signature: Pointer to the signature to free. 99 | */ 100 | void wallet_sign_free(signature_t* signature); 101 | 102 | /** 103 | * @brief Clears the last error. 104 | */ 105 | void clear_last_error(); 106 | 107 | /** 108 | * @brief Gets the last error message length. 109 | */ 110 | int last_error_length(); 111 | 112 | /** 113 | * @brief Gets the last error message as UTF-8 encoded string. 114 | * @param out_buf: Pointer where will be stored the error message. 115 | * @param buf_size: Size of out_buf. 116 | * @return Returns the number of bytes wrote into out_buf or -1 on error. 117 | */ 118 | int error_message_utf8(char *out_buf, int buf_size); 119 | -------------------------------------------------------------------------------- /packages/crw-wallet/src/crypto.rs: -------------------------------------------------------------------------------- 1 | //! Utility to create an in memory Secp256k1 wallet from a BIP-32 mnemonic. 2 | //! 3 | //! This module contains functions to generate a Secp256k1 key pair from a BIP-32 mnemonic and 4 | //! sign a generic [`Vec`] payload. 5 | 6 | use crate::WalletError; 7 | use bech32::{ToBase32, Variant::Bech32}; 8 | use bip39::{Language, Mnemonic, MnemonicType, Seed}; 9 | use bitcoin::util::bip32::ChildNumber; 10 | use bitcoin::{ 11 | network::constants::Network, 12 | secp256k1::Secp256k1, 13 | util::bip32::{ExtendedPrivKey, ExtendedPubKey}, 14 | PublicKey, 15 | }; 16 | use hdpath::StandardHDPath; 17 | use k256::ecdsa::{signature::Signer, Signature, SigningKey}; 18 | use sha2::{Digest, Sha256}; 19 | use std::convert::TryFrom; 20 | 21 | /// Represents a Secp256k1 key pair. 22 | #[derive(Clone)] 23 | struct Keychain { 24 | pub ext_public_key: ExtendedPubKey, 25 | pub ext_private_key: ExtendedPrivKey, 26 | } 27 | 28 | /// Facility used to manage a Secp256k1 key pair and generate signatures. 29 | #[derive(Clone)] 30 | pub struct MnemonicWallet { 31 | mnemonic: Mnemonic, 32 | derivation_path: String, 33 | keychain: Keychain, 34 | } 35 | 36 | impl MnemonicWallet { 37 | /// Derive a Secp256k1 key pair from the given `mnemonic_phrase` and `derivation_path`. 38 | /// 39 | /// # Errors 40 | /// 41 | /// Returns an [`Err`] if the provided `mnemonic_phrase` or `derivation_path` is invalid. 42 | /// 43 | /// # Examples 44 | /// 45 | /// ``` 46 | /// use crw_wallet::crypto::MnemonicWallet; 47 | /// 48 | /// let cosmos_dp = "m/44'/118'/0'/0/0"; 49 | /// let mnemonic = "battle call once stool three mammal hybrid list sign field athlete amateur cinnamon eagle shell erupt voyage hero assist maple matrix maximum able barrel"; 50 | /// 51 | /// let wallet = MnemonicWallet::new(mnemonic, cosmos_dp).unwrap(); 52 | /// ``` 53 | pub fn new( 54 | mnemonic_phrase: &str, 55 | derivation_path: &str, 56 | ) -> Result { 57 | // Create mnemonic and generate seed from it 58 | let mnemonic = Mnemonic::from_phrase(mnemonic_phrase, Language::English) 59 | .map_err(|err| WalletError::Mnemonic(err.to_string()))?; 60 | 61 | let seed = Seed::new(&mnemonic, ""); 62 | 63 | // Set hd_path for master_key generation 64 | let hd_path = StandardHDPath::try_from(derivation_path) 65 | .map_err(|_| WalletError::DerivationPath(derivation_path.to_string()))?; 66 | 67 | let keychain = MnemonicWallet::generate_keychain(hd_path, seed)?; 68 | 69 | Ok(MnemonicWallet { 70 | mnemonic, 71 | keychain, 72 | derivation_path: derivation_path.to_owned(), 73 | }) 74 | } 75 | 76 | /// Generates a random mnemonic phrase and derive a Secp256k1 key pair from it. 77 | /// 78 | /// # Errors 79 | /// 80 | /// Returns an [`Err`] if the provided `derivation_path` is invalid. 81 | /// 82 | /// # Examples 83 | /// 84 | /// ``` 85 | /// use crw_wallet::crypto::MnemonicWallet; 86 | /// 87 | /// let cosmos_dp = "m/44'/118'/0'/0/0"; 88 | /// 89 | /// let (wallet, mnemonic) = MnemonicWallet::random(cosmos_dp).unwrap(); 90 | /// ``` 91 | pub fn random(derivation_path: &str) -> Result<(MnemonicWallet, String), WalletError> { 92 | let mnemonic = Mnemonic::new(MnemonicType::Words24, Language::English); 93 | let phrase = mnemonic.phrase().to_owned(); 94 | 95 | Ok((MnemonicWallet::new(&phrase, derivation_path)?, phrase)) 96 | } 97 | 98 | /// Changes the derivation path used used to derive the key pair from the mnemonic. 99 | /// This function force the regenerations of the wallet internal keypair. 100 | /// 101 | /// # Errors 102 | /// 103 | /// Returns an [`Err`] if the provided `derivation_path` is invalid. 104 | pub fn set_derivation_path(&mut self, derivation_path: &str) -> Result<(), WalletError> { 105 | // Update only if the derivation path is different. 106 | if derivation_path == self.derivation_path { 107 | return Ok(()); 108 | } 109 | 110 | let seed = Seed::new(&self.mnemonic, ""); 111 | 112 | // Set hd_path for master_key generation 113 | let hd_path = StandardHDPath::try_from(derivation_path) 114 | .map_err(|_| WalletError::DerivationPath(derivation_path.to_string()))?; 115 | 116 | // Regenerate the keychain with the new derivation path 117 | let keychain = MnemonicWallet::generate_keychain(hd_path, seed)?; 118 | 119 | // Update the wallet. 120 | self.keychain = keychain; 121 | self.derivation_path = derivation_path.to_string(); 122 | 123 | Ok(()) 124 | } 125 | 126 | /// Utility function to generate the Secp256k1 keypair. 127 | fn generate_keychain(hd_path: StandardHDPath, seed: Seed) -> Result { 128 | let private_key = ExtendedPrivKey::new_master(Network::Bitcoin, seed.as_bytes()) 129 | .and_then(|priv_key| { 130 | let child_nubers: Vec = vec![ 131 | ChildNumber::Hardened { 132 | index: hd_path.purpose().as_value().as_number(), 133 | }, 134 | ChildNumber::Hardened { 135 | index: hd_path.coin_type(), 136 | }, 137 | ChildNumber::Hardened { 138 | index: hd_path.account(), 139 | }, 140 | ChildNumber::Normal { 141 | index: hd_path.change(), 142 | }, 143 | ChildNumber::Normal { 144 | index: hd_path.index(), 145 | }, 146 | ]; 147 | priv_key.derive_priv(&Secp256k1::new(), &child_nubers) 148 | }) 149 | .map_err(|err| WalletError::PrivateKey(err.to_string()))?; 150 | 151 | let public_key = ExtendedPubKey::from_priv(&Secp256k1::new(), &private_key); 152 | 153 | Ok(Keychain { 154 | ext_private_key: private_key, 155 | ext_public_key: public_key, 156 | }) 157 | } 158 | 159 | /// Gets the public key derived from the mnemonic. 160 | pub fn get_pub_key(&self) -> PublicKey { 161 | let compressed_public_key = self.keychain.ext_public_key.public_key.serialize(); 162 | PublicKey::from_slice(&compressed_public_key).unwrap() 163 | } 164 | 165 | /// Gets the bech32 address derived from the mnemonic and the provided 166 | /// human readable part. 167 | /// 168 | /// # Errors 169 | /// Returns an an [`Err`] in one of this cases: 170 | /// * If the hrp contains both uppercase and lowercase characters. 171 | /// * If the hrp contains any non-ASCII characters (outside 33..=126). 172 | /// * If the hrp is outside 1..83 characters long. 173 | pub fn get_bech32_address(&self, hrp: &str) -> Result { 174 | let mut hasher = Sha256::new(); 175 | let pub_key_bytes = self.get_pub_key().to_bytes(); 176 | hasher.update(pub_key_bytes); 177 | 178 | // Read hash digest over the public key bytes & consume hasher 179 | let pk_hash = hasher.finalize(); 180 | 181 | // Insert the hash result in the ripdem hash function 182 | let mut rip_hasher = ripemd::Ripemd160::default(); 183 | rip_hasher.update(pk_hash); 184 | let rip_result = rip_hasher.finalize(); 185 | 186 | let address_bytes = rip_result.to_vec(); 187 | 188 | let bech32_address = bech32::encode(hrp, address_bytes.to_base32(), Bech32) 189 | .map_err(|err| WalletError::Hrp(err.to_string()))?; 190 | 191 | Ok(bech32_address) 192 | } 193 | 194 | /// Returns the signature of the provided data. 195 | pub fn sign(&self, data: &[u8]) -> Result, WalletError> { 196 | if data.is_empty() { 197 | return Result::Ok(Vec::new()); 198 | } 199 | // Get the sign key from the private key 200 | let sign_key = 201 | SigningKey::from_bytes(&self.keychain.ext_private_key.private_key.secret_bytes()) 202 | .unwrap(); 203 | 204 | // Sign the data provided data 205 | let signature: Signature = sign_key 206 | .try_sign(data) 207 | .map_err(|err| WalletError::Sign(err.to_string()))?; 208 | 209 | Ok(signature.as_ref().to_vec()) 210 | } 211 | } 212 | 213 | #[cfg(test)] 214 | mod tests { 215 | use super::*; 216 | use hex; 217 | 218 | static DESMOS_DERIVATION_PATH: &str = "m/44'/852'/0'/0/0"; 219 | static COSMOS_DERIVATION_PATH: &str = "m/44'/118'/0'/0/0"; 220 | static TEST_MNEMONIC: &str = "battle call once stool three mammal hybrid list sign field athlete amateur cinnamon eagle shell erupt voyage hero assist maple matrix maximum able barrel"; 221 | 222 | #[test] 223 | fn initialization_with_valid_mnemonic_and_derivation_path() { 224 | let result = MnemonicWallet::new(TEST_MNEMONIC, DESMOS_DERIVATION_PATH); 225 | 226 | assert!(result.is_ok()) 227 | } 228 | 229 | #[test] 230 | fn initialize_with_invalid_mnemonic() { 231 | let result = MnemonicWallet::new("an invalid mnemonic", DESMOS_DERIVATION_PATH); 232 | 233 | assert!(result.is_err()); 234 | 235 | assert!(match result.err().unwrap() { 236 | WalletError::Mnemonic(_) => true, 237 | _ => false, 238 | }); 239 | } 240 | 241 | #[test] 242 | fn initialize_with_invalid_derivation_path() { 243 | let result = MnemonicWallet::new(TEST_MNEMONIC, ""); 244 | 245 | assert!(result.is_err()); 246 | 247 | assert!(match result.err().unwrap() { 248 | WalletError::DerivationPath(_) => true, 249 | _ => false, 250 | }); 251 | } 252 | 253 | #[test] 254 | fn initialize_random_wallet() { 255 | let result = MnemonicWallet::random(DESMOS_DERIVATION_PATH); 256 | 257 | assert!(result.is_ok()); 258 | let (_, mnemonic) = result.unwrap(); 259 | 260 | assert!(!mnemonic.is_empty()); 261 | } 262 | 263 | #[test] 264 | fn initialize_random_wallet_with_invalid_dp() { 265 | let result = MnemonicWallet::random(""); 266 | 267 | assert!(result.is_err()); 268 | let wallet_error = result.err().unwrap(); 269 | 270 | assert!(match wallet_error { 271 | WalletError::DerivationPath(_) => true, 272 | _ => false, 273 | }); 274 | } 275 | 276 | #[test] 277 | fn desmos_bech32_address() { 278 | let wallet = MnemonicWallet::new(TEST_MNEMONIC, DESMOS_DERIVATION_PATH).unwrap(); 279 | 280 | let address = wallet.get_bech32_address("desmos"); 281 | 282 | assert!(address.is_ok()); 283 | assert_eq!( 284 | address.unwrap(), 285 | "desmos1k8u92hx3k33a5vgppkyzq6m4frxx7ewnlkyjrh" 286 | ); 287 | } 288 | 289 | #[test] 290 | fn cosmos_bech32_address() { 291 | let wallet = MnemonicWallet::new(TEST_MNEMONIC, COSMOS_DERIVATION_PATH).unwrap(); 292 | 293 | let address = wallet.get_bech32_address("cosmos"); 294 | 295 | assert!(address.is_ok()); 296 | assert_eq!( 297 | address.unwrap(), 298 | "cosmos1dzczdka6wpzwvmawpps7tf8047gkft0e5cupun" 299 | ); 300 | } 301 | 302 | #[test] 303 | fn empty_sign() { 304 | let wallet = MnemonicWallet::new(TEST_MNEMONIC, COSMOS_DERIVATION_PATH).unwrap(); 305 | 306 | let empty: Vec = Vec::new(); 307 | let result = wallet.sign(&empty); 308 | 309 | assert!(result.is_ok()); 310 | assert!(result.unwrap().is_empty()); 311 | } 312 | 313 | #[test] 314 | fn sign() { 315 | let ref_hex_signature = "ce0558eb2f0847d4e58b29ca45f0a2a8764395b52c829888fa017aaf5b8b2e695e47aac9fe1cf77a66a1ba872d8a7e5302d31874b686973c0a5c196cca707667"; 316 | let wallet = MnemonicWallet::new(TEST_MNEMONIC, DESMOS_DERIVATION_PATH).unwrap(); 317 | 318 | let data = "some simple data".as_bytes(); 319 | let result = wallet.sign(data).unwrap(); 320 | 321 | let sign_hex = hex::encode(result); 322 | 323 | assert_eq!(ref_hex_signature, sign_hex); 324 | } 325 | } 326 | -------------------------------------------------------------------------------- /packages/crw-wallet/src/error.rs: -------------------------------------------------------------------------------- 1 | //! This file defines the various errors raised by the wallet. 2 | use thiserror::Error; 3 | 4 | #[derive(Error, Debug, Clone)] 5 | pub enum WalletError { 6 | #[error("sign error: {0}")] 7 | Sign(String), 8 | 9 | #[error("mnemonic error: {0}")] 10 | Mnemonic(String), 11 | 12 | #[error("invalid derivation path: {0}")] 13 | DerivationPath(String), 14 | 15 | #[error("private key error: {0}")] 16 | PrivateKey(String), 17 | 18 | #[error("invalid human readable path {0}")] 19 | Hrp(String), 20 | } 21 | -------------------------------------------------------------------------------- /packages/crw-wallet/src/ffi.rs: -------------------------------------------------------------------------------- 1 | //! Provides the FFI to interact with [`MnemonicWallet`] from other programming languages. 2 | use crate::crypto::MnemonicWallet; 3 | use crate::WalletError; 4 | use bip39::{Language, Mnemonic, MnemonicType}; 5 | use libc::{c_char, c_int, c_uchar, c_uint, size_t}; 6 | use std::ffi::{CStr, CString}; 7 | use std::ptr::null_mut; 8 | use std::{mem, slice}; 9 | 10 | #[repr(C)] 11 | pub struct Signature { 12 | len: c_uint, 13 | data: *mut c_uchar, 14 | } 15 | 16 | /// Release a string returned from rust. 17 | #[no_mangle] 18 | pub extern "C" fn cstring_free(s: *mut c_char) { 19 | unsafe { 20 | if s.is_null() { 21 | return; 22 | } 23 | CString::from_raw(s) 24 | }; 25 | } 26 | 27 | /// Generates a random mnemonic of 24 words. 28 | /// The returned mnemonic must be freed using the [`cstring_free`] function to avoid memory leaks. 29 | /// 30 | /// # Errors 31 | /// This function returns a nullptr in case of error and store the error cause in a local thread 32 | /// global variable that can be accessed using the [error_message_utf8](ffi_helpers::error_handling::error_message_utf8) function. 33 | #[no_mangle] 34 | pub extern "C" fn wallet_random_mnemonic() -> *mut c_char { 35 | let mnemonic = Mnemonic::new(MnemonicType::Words24, Language::English); 36 | let phrase = mnemonic.phrase().to_owned(); 37 | 38 | let phrase_c_str = match CString::new(phrase) { 39 | Ok(s) => s, 40 | Err(e) => { 41 | ffi_helpers::update_last_error(e); 42 | return null_mut(); 43 | } 44 | }; 45 | 46 | phrase_c_str.into_raw() 47 | } 48 | 49 | /// Derive a Secp256k1 key pair from the given mnemonic_phrase and derivation_path. 50 | /// The returned [`MnemonicWallet`] ptr must be freed using the [`wallet_free`] function to avoid memory 51 | /// leaks. 52 | /// 53 | /// # Errors 54 | /// This function returns a nullptr in case of error and store the error cause in a local thread 55 | /// global variable that can be accessed using the [error_message_utf8](ffi_helpers::error_handling::error_message_utf8) function. 56 | #[no_mangle] 57 | pub extern "C" fn wallet_from_mnemonic( 58 | mnemonic: *const c_char, 59 | derivation_path: *const c_char, 60 | ) -> *mut MnemonicWallet { 61 | if mnemonic.is_null() { 62 | ffi_helpers::update_last_error(WalletError::Mnemonic("null mnemonic ptr".to_owned())); 63 | return null_mut(); 64 | } 65 | if derivation_path.is_null() { 66 | ffi_helpers::update_last_error(WalletError::DerivationPath( 67 | "null derivation path ptr".to_owned(), 68 | )); 69 | return null_mut(); 70 | } 71 | 72 | let mnemonic_c_str = unsafe { CStr::from_ptr(mnemonic).to_string_lossy() }; 73 | let dp_c_str = unsafe { CStr::from_ptr(derivation_path).to_string_lossy() }; 74 | 75 | let wallet = match MnemonicWallet::new(mnemonic_c_str.as_ref(), dp_c_str.as_ref()) { 76 | Ok(w) => w, 77 | Err(e) => { 78 | ffi_helpers::update_last_error(e); 79 | return null_mut(); 80 | } 81 | }; 82 | 83 | Box::into_raw(Box::new(wallet)) 84 | } 85 | 86 | /// Deallocate a [`MnemonicWallet`] instance. 87 | #[no_mangle] 88 | pub extern "C" fn wallet_free(ptr: *mut MnemonicWallet) { 89 | if ptr.is_null() { 90 | return; 91 | } 92 | 93 | Box::from(ptr); 94 | } 95 | 96 | /// Gets the bech32 address derived from the mnemonic and the provided human readable part. 97 | /// 98 | /// # Errors 99 | /// This function returns a nullptr in case of error and store the error cause in a local thread 100 | /// global variable that can be accessed using the [error_message_utf8](ffi_helpers::error_handling::error_message_utf8) function. 101 | #[no_mangle] 102 | pub extern "C" fn wallet_get_bech32_address( 103 | ptr: *const MnemonicWallet, 104 | hrp: *const c_char, 105 | ) -> *mut c_char { 106 | null_pointer_check!(ptr); 107 | 108 | if hrp.is_null() { 109 | ffi_helpers::update_last_error(WalletError::Hrp("received null hrp".to_owned())); 110 | return null_mut(); 111 | } 112 | 113 | let hrp_cstr = unsafe { CStr::from_ptr(hrp).to_string_lossy() }; 114 | 115 | let address = unsafe { 116 | match ptr.as_ref().unwrap().get_bech32_address(hrp_cstr.as_ref()) { 117 | Ok(a) => a, 118 | Err(e) => { 119 | ffi_helpers::update_last_error(e); 120 | return null_mut(); 121 | } 122 | } 123 | }; 124 | 125 | let address_c_str = match CString::new(address) { 126 | Ok(s) => s, 127 | Err(e) => { 128 | ffi_helpers::update_last_error(e); 129 | return null_mut(); 130 | } 131 | }; 132 | 133 | address_c_str.into_raw() 134 | } 135 | 136 | /// Gets the wallet public key. 137 | /// This function returns the number of bytes copied into `out_buffer`. 138 | /// 139 | /// # Errors 140 | /// Returns -1 if the provided arguments are invalid or -2 if the public key don't fit into `out_buffer`. 141 | #[no_mangle] 142 | pub extern "C" fn wallet_get_public_key( 143 | ptr: *const MnemonicWallet, 144 | compressed: c_uint, 145 | out_buffer: *mut c_uchar, 146 | size: size_t, 147 | ) -> c_int { 148 | if ptr.is_null() || out_buffer.is_null() || size <= 0 { 149 | return -1; 150 | } 151 | 152 | let pub_key = unsafe { 153 | let key = ptr.as_ref().unwrap().get_pub_key().inner; 154 | 155 | if compressed != 0 { 156 | key.serialize().to_vec() 157 | } else { 158 | key.serialize_uncompressed().to_vec() 159 | } 160 | }; 161 | 162 | if pub_key.len() > size { 163 | return -2; 164 | } 165 | 166 | let out_buf = unsafe { slice::from_raw_parts_mut(out_buffer, pub_key.len()) }; 167 | 168 | out_buf.copy_from_slice(&pub_key); 169 | pub_key.len() as i32 170 | } 171 | 172 | /// Generates a signature of the provided data. 173 | /// The returned [`Signature`] pointer must bee freed using the [`wallet_sign_free`] function 174 | /// to avoid memory leaks. 175 | /// 176 | /// # Errors 177 | /// This function returns a nullptr in case of error and store the error cause in a local thread 178 | /// global variable that can be accessed using the [error_message_utf8](ffi_helpers::error_handling::error_message_utf8) function. 179 | #[no_mangle] 180 | pub extern "C" fn wallet_sign( 181 | ptr: *const MnemonicWallet, 182 | data: *const c_uchar, 183 | data_len: c_uint, 184 | ) -> *mut Signature { 185 | null_pointer_check!(ptr); 186 | null_pointer_check!(data); 187 | 188 | let signature = unsafe { 189 | let data = std::slice::from_raw_parts(data, data_len as usize); 190 | match ptr.as_ref().unwrap().sign(data) { 191 | Ok(s) => { 192 | let mut sign = s.to_owned(); 193 | let ptr = sign.as_mut_ptr(); 194 | let vec_len = s.len() as c_uint; 195 | // Prevent deallocation from rust, the array now can be reached only from the ptr variable. 196 | mem::forget(sign); 197 | 198 | Signature { 199 | len: vec_len, 200 | data: ptr, 201 | } 202 | } 203 | Err(e) => { 204 | ffi_helpers::update_last_error(e.to_owned()); 205 | return null_mut(); 206 | } 207 | } 208 | }; 209 | 210 | Box::into_raw(Box::from(signature)) 211 | } 212 | 213 | /// Deallocate a [`Signature`] instance. 214 | #[no_mangle] 215 | pub extern "C" fn wallet_sign_free(ptr: *mut Signature) { 216 | if ptr.is_null() { 217 | return; 218 | } 219 | 220 | unsafe { 221 | let signature = ptr.as_ref().unwrap(); 222 | 223 | drop(Vec::from_raw_parts( 224 | signature.data, 225 | signature.len as usize, 226 | signature.len as usize, 227 | )); 228 | }; 229 | 230 | Box::from(ptr); 231 | } 232 | 233 | // Macro to export the ffi_helpers's functions used to access the error message from other programming languages. 234 | export_error_handling_functions!(); 235 | 236 | #[cfg(test)] 237 | mod tests { 238 | use crate::ffi::{ 239 | cstring_free, wallet_free, wallet_from_mnemonic, wallet_get_bech32_address, 240 | wallet_get_public_key, wallet_random_mnemonic, wallet_sign, wallet_sign_free, 241 | }; 242 | use ffi_helpers::error_handling::error_message; 243 | use std::ffi::CString; 244 | use std::mem; 245 | use std::ptr::null_mut; 246 | 247 | static COSMOS_DERIVATION_PATH: &str = "m/44'/118'/0'/0/0"; 248 | static TEST_MNEMONIC: &str = "battle call once stool three mammal hybrid list sign field athlete amateur cinnamon eagle shell erupt voyage hero assist maple matrix maximum able barrel"; 249 | 250 | #[test] 251 | fn test_random_mnemonic() { 252 | let mnemonic = wallet_random_mnemonic(); 253 | assert!(!mnemonic.is_null()); 254 | 255 | let c_str = unsafe { CString::from_raw(mnemonic) }; 256 | let string = c_str.to_string_lossy().to_string(); 257 | let phrases: Vec<&str> = string.split(" ").collect(); 258 | assert_eq!(24, phrases.len()); 259 | } 260 | 261 | #[test] 262 | fn initialization_with_valid_mnemonic_and_derivation_path() { 263 | let c_mnemonic = CString::new(TEST_MNEMONIC).unwrap().into_raw(); 264 | let c_dp = CString::new(COSMOS_DERIVATION_PATH).unwrap().into_raw(); 265 | let wallet = wallet_from_mnemonic(c_mnemonic, c_dp); 266 | 267 | assert!(!wallet.is_null()); 268 | wallet_free(wallet); 269 | cstring_free(c_mnemonic); 270 | cstring_free(c_dp); 271 | } 272 | 273 | #[test] 274 | fn initialize_with_invalid_mnemonic() { 275 | let c_mnemonic = CString::new("invalid mnemonic").unwrap().into_raw(); 276 | let c_dp = CString::new(COSMOS_DERIVATION_PATH).unwrap().into_raw(); 277 | let wallet = wallet_from_mnemonic(c_mnemonic, c_dp); 278 | 279 | assert!(wallet.is_null()); 280 | 281 | let error_msg = error_message(); 282 | assert!(error_msg.is_some()); 283 | 284 | cstring_free(c_mnemonic); 285 | cstring_free(c_dp); 286 | } 287 | 288 | #[test] 289 | fn initialize_with_null_mnemonic() { 290 | let c_dp = CString::new(COSMOS_DERIVATION_PATH).unwrap().into_raw(); 291 | let wallet = wallet_from_mnemonic(null_mut(), c_dp); 292 | 293 | assert!(wallet.is_null()); 294 | let error_msg = error_message(); 295 | assert!(error_msg.is_some()); 296 | 297 | cstring_free(c_dp); 298 | } 299 | 300 | #[test] 301 | fn initialize_with_invalid_derivation_path() { 302 | let c_mnemonic = CString::new("invalid mnemonic").unwrap().into_raw(); 303 | let c_dp = CString::new(COSMOS_DERIVATION_PATH).unwrap().into_raw(); 304 | 305 | let wallet = wallet_from_mnemonic(c_mnemonic, c_dp); 306 | 307 | assert!(wallet.is_null()); 308 | 309 | let error_msg = error_message(); 310 | assert!(error_msg.is_some()); 311 | 312 | cstring_free(c_mnemonic); 313 | cstring_free(c_dp); 314 | } 315 | 316 | #[test] 317 | fn initialize_with_null_derivation_path() { 318 | let c_mnemonic = CString::new(TEST_MNEMONIC).unwrap().into_raw(); 319 | let wallet = wallet_from_mnemonic(c_mnemonic, null_mut()); 320 | 321 | assert!(wallet.is_null()); 322 | let error_msg = error_message(); 323 | assert!(error_msg.is_some()); 324 | 325 | cstring_free(c_mnemonic); 326 | } 327 | 328 | #[test] 329 | fn bech32_address() { 330 | let c_mnemonic = CString::new(TEST_MNEMONIC).unwrap().into_raw(); 331 | let c_dp = CString::new(COSMOS_DERIVATION_PATH).unwrap().into_raw(); 332 | let wallet = wallet_from_mnemonic(c_mnemonic, c_dp); 333 | 334 | let hrp = CString::new("cosmos").unwrap().into_raw(); 335 | let address = wallet_get_bech32_address(wallet, hrp); 336 | let c_address = unsafe { CString::from_raw(address) }; 337 | 338 | assert!(!address.is_null()); 339 | assert_eq!( 340 | c_address.to_string_lossy().as_ref(), 341 | "cosmos1dzczdka6wpzwvmawpps7tf8047gkft0e5cupun" 342 | ); 343 | 344 | wallet_free(wallet); 345 | cstring_free(c_mnemonic); 346 | cstring_free(c_dp); 347 | } 348 | 349 | #[test] 350 | fn bech32_address_with_null_hrp() { 351 | let c_mnemonic = CString::new(TEST_MNEMONIC).unwrap().into_raw(); 352 | let c_dp = CString::new(COSMOS_DERIVATION_PATH).unwrap().into_raw(); 353 | let wallet = wallet_from_mnemonic(c_mnemonic, c_dp); 354 | 355 | let address = wallet_get_bech32_address(wallet, null_mut()); 356 | let error_msg = error_message(); 357 | assert!(address.is_null()); 358 | assert!(error_msg.is_some()); 359 | 360 | wallet_free(wallet); 361 | cstring_free(c_mnemonic); 362 | cstring_free(c_dp); 363 | } 364 | 365 | #[test] 366 | fn get_public_key() { 367 | let ref_public_key = "048b3f1f48e4dbc68287473da1a76d81bd827aac22622b7da5f351e2580d14b2823fe447037648f5d83b11dd2ea88e06db6c452b5376aa4c70e7a8c9c7b13cf39a"; 368 | let c_mnemonic = CString::new(TEST_MNEMONIC).unwrap().into_raw(); 369 | let c_dp = CString::new(COSMOS_DERIVATION_PATH).unwrap().into_raw(); 370 | let wallet = wallet_from_mnemonic(c_mnemonic, c_dp); 371 | 372 | let mut out_buffer: [u8; 65] = [0; 65]; 373 | let copied_bytes = 374 | wallet_get_public_key(wallet, 0, out_buffer.as_mut_ptr(), out_buffer.len()); 375 | assert_eq!(65, copied_bytes); 376 | 377 | let pub_key_hex = hex::encode(out_buffer); 378 | assert_eq!(ref_public_key, pub_key_hex); 379 | 380 | wallet_free(wallet); 381 | cstring_free(c_mnemonic); 382 | cstring_free(c_dp); 383 | } 384 | 385 | #[test] 386 | fn get_public_key_compressed() { 387 | let ref_public_key = "028b3f1f48e4dbc68287473da1a76d81bd827aac22622b7da5f351e2580d14b282"; 388 | let c_mnemonic = CString::new(TEST_MNEMONIC).unwrap().into_raw(); 389 | let c_dp = CString::new(COSMOS_DERIVATION_PATH).unwrap().into_raw(); 390 | let wallet = wallet_from_mnemonic(c_mnemonic, c_dp); 391 | 392 | let mut out_buffer: [u8; 33] = [0; 33]; 393 | let copied_bytes = 394 | wallet_get_public_key(wallet, 1, out_buffer.as_mut_ptr(), out_buffer.len()); 395 | assert_eq!(33, copied_bytes); 396 | 397 | let pub_key_hex = hex::encode(out_buffer); 398 | assert_eq!(ref_public_key, pub_key_hex); 399 | 400 | wallet_free(wallet); 401 | cstring_free(c_mnemonic); 402 | cstring_free(c_dp); 403 | } 404 | 405 | #[test] 406 | fn get_public_key_invalid_args() { 407 | let c_mnemonic = CString::new(TEST_MNEMONIC).unwrap().into_raw(); 408 | let c_dp = CString::new(COSMOS_DERIVATION_PATH).unwrap().into_raw(); 409 | let wallet = wallet_from_mnemonic(c_mnemonic, c_dp); 410 | let mut out_buffer: [u8; 64] = [0; 64]; 411 | 412 | let copied_bytes = wallet_get_public_key(null_mut(), 0, null_mut(), 0); 413 | assert_eq!(-1, copied_bytes); 414 | 415 | let copied_bytes = wallet_get_public_key(wallet, 0, null_mut(), 0); 416 | assert_eq!(-1, copied_bytes); 417 | 418 | let copied_bytes = wallet_get_public_key(null_mut(), 0, out_buffer.as_mut_ptr(), 0); 419 | assert_eq!(-1, copied_bytes); 420 | 421 | let copied_bytes = wallet_get_public_key(wallet, 0, out_buffer.as_mut_ptr(), 0); 422 | assert_eq!(-1, copied_bytes); 423 | 424 | let copied_bytes = wallet_get_public_key(wallet, 0, out_buffer.as_mut_ptr(), 2); 425 | assert_eq!(-2, copied_bytes); 426 | 427 | wallet_free(wallet); 428 | cstring_free(c_mnemonic); 429 | cstring_free(c_dp); 430 | } 431 | 432 | #[test] 433 | fn empty_sign() { 434 | let c_mnemonic = CString::new(TEST_MNEMONIC).unwrap().into_raw(); 435 | let c_dp = CString::new(COSMOS_DERIVATION_PATH).unwrap().into_raw(); 436 | let wallet = wallet_from_mnemonic(c_mnemonic, c_dp); 437 | 438 | let empty: Vec = Vec::new(); 439 | let signature = wallet_sign(wallet, empty.as_ptr(), empty.len() as u32); 440 | 441 | assert!(!signature.is_null()); 442 | assert_eq!(0, unsafe { signature.as_ref() }.unwrap().len); 443 | 444 | wallet_sign_free(signature); 445 | wallet_free(wallet); 446 | cstring_free(c_mnemonic); 447 | cstring_free(c_dp); 448 | } 449 | 450 | #[test] 451 | fn sign_null() { 452 | let c_mnemonic = CString::new(TEST_MNEMONIC).unwrap().into_raw(); 453 | let c_dp = CString::new(COSMOS_DERIVATION_PATH).unwrap().into_raw(); 454 | let wallet = wallet_from_mnemonic(c_mnemonic, c_dp); 455 | 456 | let signature = wallet_sign(wallet, null_mut(), 12); 457 | let error = error_message(); 458 | 459 | assert!(signature.is_null()); 460 | assert!(error.is_some()); 461 | 462 | wallet_free(wallet); 463 | cstring_free(c_mnemonic); 464 | cstring_free(c_dp); 465 | } 466 | 467 | #[test] 468 | fn sign() { 469 | let ref_hex_signature = "5590171f32520497dd9ca07a3f03ef69ceff972471821902ebe31532d7f13be51021b7c8849431340fe6e91321987a90ffe5598d5e87fe4d55acf1bb90a000e9"; 470 | let c_mnemonic = CString::new(TEST_MNEMONIC).unwrap().into_raw(); 471 | let c_dp = CString::new(COSMOS_DERIVATION_PATH).unwrap().into_raw(); 472 | let wallet = wallet_from_mnemonic(c_mnemonic, c_dp); 473 | 474 | let data = "some simple data".as_bytes(); 475 | let signature = wallet_sign(wallet, data.as_ptr(), data.len() as u32); 476 | 477 | assert!(!signature.is_null()); 478 | let sign_ref = unsafe { signature.as_ref().unwrap() }; 479 | let signature_vec = unsafe { 480 | Vec::from_raw_parts(sign_ref.data, sign_ref.len as usize, sign_ref.len as usize) 481 | }; 482 | let sign_hex = hex::encode(&signature_vec); 483 | assert_eq!(ref_hex_signature, sign_hex); 484 | 485 | // Forget since will be freed from wallet_sign_free 486 | mem::forget(signature_vec); 487 | wallet_sign_free(signature); 488 | wallet_free(wallet); 489 | cstring_free(c_mnemonic); 490 | cstring_free(c_dp); 491 | } 492 | } 493 | -------------------------------------------------------------------------------- /packages/crw-wallet/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "ffi")] 2 | #[macro_use] 3 | extern crate ffi_helpers; 4 | 5 | pub mod crypto; 6 | mod error; 7 | pub use crate::error::WalletError; 8 | 9 | #[cfg(feature = "wasm-bindgen")] 10 | pub mod wasm32_bindgen; 11 | 12 | #[cfg(feature = "ffi")] 13 | pub mod ffi; 14 | -------------------------------------------------------------------------------- /packages/crw-wallet/src/wasm32_bindgen.rs: -------------------------------------------------------------------------------- 1 | //! Implementation for WASM via wasm-bindgen 2 | 3 | use crate::crypto::MnemonicWallet; 4 | extern crate bindgen as wasm_bindgen; 5 | use bip39::{Language, Mnemonic, MnemonicType}; 6 | use wasm_bindgen::prelude::*; 7 | 8 | #[wasm_bindgen(js_name = MnemonicWallet)] 9 | pub struct JsMnemonicWallet { 10 | wallet: MnemonicWallet, 11 | } 12 | 13 | #[wasm_bindgen(js_class = MnemonicWallet)] 14 | impl JsMnemonicWallet { 15 | #[wasm_bindgen(constructor)] 16 | pub fn new(mnemonic: &str, derivation_path: &str) -> Result { 17 | return Ok(JsMnemonicWallet { 18 | wallet: MnemonicWallet::new(mnemonic, derivation_path) 19 | .map_err(|e| JsValue::from(e.to_string()))?, 20 | }); 21 | } 22 | 23 | #[wasm_bindgen(js_name = setDerivationPath)] 24 | pub fn set_derivation_path(&mut self, new_derivation_path: &str) -> Result<(), JsValue> { 25 | self.wallet 26 | .set_derivation_path(new_derivation_path) 27 | .map_err(|e| JsValue::from(e.to_string()))?; 28 | Ok(()) 29 | } 30 | 31 | #[wasm_bindgen(js_name = getBech32Address)] 32 | pub fn get_bech32_address(&self, hrp: &str) -> Result { 33 | Ok(self 34 | .wallet 35 | .get_bech32_address(hrp) 36 | .map_err(|e| JsValue::from(e.to_string()))?) 37 | } 38 | 39 | #[wasm_bindgen(js_name = getPubKey)] 40 | pub fn get_pub_key(&self, compressed: Option) -> Vec { 41 | return if compressed.unwrap_or(true) { 42 | self.wallet.get_pub_key().inner.serialize().to_vec() 43 | } else { 44 | self.wallet 45 | .get_pub_key() 46 | .inner 47 | .serialize_uncompressed() 48 | .to_vec() 49 | }; 50 | } 51 | 52 | pub fn sign(&self, data: Vec) -> Result, JsValue> { 53 | Ok(self 54 | .wallet 55 | .sign(&data) 56 | .map_err(|e| JsValue::from(e.to_string()))?) 57 | } 58 | } 59 | 60 | #[wasm_bindgen(js_name = randomMnemonic)] 61 | pub fn random_mnemonic() -> String { 62 | let mnemonic = Mnemonic::new(MnemonicType::Words24, Language::English); 63 | return mnemonic.phrase().to_string(); 64 | } 65 | --------------------------------------------------------------------------------