├── .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 | 
6 |
7 | | Packages | Build | Crates.io |
8 | | ------------- | ------ | ------ |
9 | | crw-client | [](https://github.com/forbole/cosmos-rust-wallet/actions/workflows/client.yml)| [](https://crates.io/crates/crw-client)|
10 | | crw-wallet | [](https://github.com/forbole/cosmos-rust-wallet/actions/workflows/wallet.yml)| [](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 | This page contains webassembly and javascript content, please enable javascript in your browser.
9 |
10 | Web wallet demo
11 |
12 |
Mnemonic:
13 |
14 |
15 |
16 |
Address:
17 |
18 |
19 | Generate
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 |
--------------------------------------------------------------------------------