├── .gitignore ├── Cargo.toml ├── README.md └── src ├── jwtclient.rs ├── api.rs ├── lib.rs └── types.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | .env 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ethers-fireblocks" 3 | version = "0.1.3-alpha.0" 4 | authors = ["Georgios Konstantopoulos "] 5 | edition = "2018" 6 | license = "MIT OR Apache-2.0" 7 | readme = "README.md" 8 | description = """ 9 | ethers-rs middleware and signer for Fireblocks' APIs 10 | """ 11 | 12 | 13 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 14 | 15 | [dependencies] 16 | ethers-core = { version="2.0.0", default-features = false } 17 | ethers-providers = { version="2.0.0", default-features = false } 18 | ethers-signers = { version="2.0.0", default-features = false } 19 | 20 | serde_json = "1.0.60" 21 | serde = "1.0.118" 22 | jsonwebtoken = "7.2.0" 23 | reqwest = { version = "0.11.4", default-features = false, features = ["json"] } 24 | thiserror = "1.0.22" 25 | rustc-hex = "2.1.0" 26 | digest = "0.9.0" 27 | sha2 = "0.9.2" 28 | async-trait = "0.1.42" 29 | rand = "0.8.5" 30 | 31 | [dev-dependencies] 32 | reqwest = { version = "0.11.4", default-features = false, features = ["json", "rustls"] } 33 | tokio = { version = "1.10.0", features = ["macros", "rt"] } 34 | once_cell = "1.5.2" 35 | 36 | [features] 37 | default = ["rustls"] 38 | openssl = ["ethers-providers/openssl", "reqwest/native-tls"] 39 | rustls = ["ethers-providers/rustls", "reqwest/rustls-tls"] 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #

ethers-fireblocks

2 | 3 | Provides [ethers](https://github.com/gakonst/ethers-rs)-compatible Signer and Middleware 4 | implementations for the [Fireblocks Vaults API](https://fireblocks.com). 5 | 6 | ## Documentation 7 | 8 | Clone the repository and run `cd ethers-fireblocks/ && cargo doc --open` 9 | 10 | ## Add ethers-fireblocks to your repository 11 | 12 | ```toml 13 | [dependencies] 14 | 15 | ethers-fireblocks = { git = "https://github.com/gakonst/ethers-fireblocks" } 16 | ``` 17 | 18 | To use the example, you must have the following env vars set: 19 | 20 | ``` 21 | export FIREBLOCKS_API_SECRET_PATH= 22 | export FIREBLOCKS_API_KEY= 23 | export FIREBLOCKS_SOURCE_VAULT_ACCOUNT= 24 | ``` 25 | 26 | ## Example Usage 27 | 28 | ```rust 29 | use ethers_core::types::{Address, TransactionRequest}; 30 | use ethers_fireblocks::{Config, FireblocksMiddleware, FireblocksSigner}; 31 | use ethers_providers::{Middleware, Provider}; 32 | use std::convert::TryFrom; 33 | 34 | #[tokio::main] 35 | async fn main() -> anyhow::Result<()> { 36 | let chain_id = 3; // Ropsten 37 | let cfg = Config::new( 38 | &std::env::var("FIREBLOCKS_API_SECRET_PATH").expect("fireblocks secret not set"), 39 | &std::env::var("FIREBLOCKS_API_KEY").expect("fireblocks api key not set"), 40 | &std::env::var("FIREBLOCKS_SOURCE_VAULT_ACCOUNT").expect("fireblocks source vault account not set"), 41 | chain_id, 42 | )?; 43 | 44 | // Create the signer (it can also be used with ethers_signers::Wallet) 45 | let mut signer = FireblocksSigner::new(cfg).await; 46 | 47 | // Instantiate an Ethers provider 48 | let provider = Provider::try_from("http://localhost:8545")?; 49 | // Wrap the provider with the fireblocks middleware 50 | let provider = FireblocksMiddleware::new(provider, signer); 51 | 52 | // Any state altering transactions issued will be signed using 53 | // Fireblocks. Wait for your push notification and approve on your phone... 54 | let address: Address = "cbe74e21b070a979b9d6426b11e876d4cb618daf".parse()?; 55 | let tx = TransactionRequest::new().to(address); 56 | let pending_tx = provider.send_transaction(tx, None).await?; 57 | // Everything else follows the normal ethers-rs APIs 58 | // e.g. we can get the receipt after 6 confs 59 | let receipt = pending_tx.confirmations(6).await?; 60 | 61 | Ok(()) 62 | } 63 | ``` 64 | 65 | ## Sandbox environment 66 | 67 | Fireblocks sandbox api is available at `https://sandbox-api.fireblocks.io` in contrast with test and production api available at `https://api.fireblocks.io`. By default `FireblocksSigner` connects to production url. You can override this behaviour (i.e. to connect to sandbox) by setting env var: 68 | ``` 69 | export FIREBLOCKS_API_URL_OVERRIDE="https://sandbox-api.fireblocks.io" 70 | ``` 71 | -------------------------------------------------------------------------------- /src/jwtclient.rs: -------------------------------------------------------------------------------- 1 | use jsonwebtoken::{errors as jwterrors, Algorithm, EncodingKey, Header}; 2 | 3 | use digest::Digest; 4 | use rand::Rng; 5 | use rustc_hex::ToHex; 6 | use serde::{Deserialize, Serialize}; 7 | use sha2::Sha256; 8 | use std::time::{SystemTime, UNIX_EPOCH}; 9 | use thiserror::Error; 10 | 11 | const EXPIRY: u64 = 55; 12 | 13 | #[derive(Debug, Clone)] 14 | pub struct JwtSigner { 15 | // TODO: Make this work with Zeroize/Secrecy 16 | pub key: EncodingKey, 17 | pub api_key: String, 18 | } 19 | 20 | impl JwtSigner { 21 | pub fn new(key: EncodingKey, api_key: &str) -> Self { 22 | Self { 23 | key, 24 | api_key: api_key.to_string(), 25 | } 26 | } 27 | 28 | pub fn sign(&self, path: &str, body: S) -> Result { 29 | let header = Header::new(Algorithm::RS256); 30 | let claims = Claims::new(path, &self.api_key, body)?; 31 | Ok(jsonwebtoken::encode(&header, &claims, &self.key)?) 32 | } 33 | } 34 | 35 | #[derive(Debug, Deserialize, Serialize)] 36 | /// JWT Claims as specified in https://docs.fireblocks.com/api/#signing-a-request 37 | struct Claims<'a> { 38 | /// The URI part of the request (e.g., /v1/transactions) 39 | uri: &'a str, 40 | /// Constantly increasing number. Usually, a timestamp can be used. 41 | nonce: u64, 42 | /// The time at which the JWT was issued, in seconds since Epoch. 43 | iat: u64, 44 | /// The expiration time on and after which the JWT must not be accepted for processing, in seconds since Epoch. Must be less than iat+30sec. 45 | exp: u64, 46 | /// The API key 47 | sub: &'a str, 48 | #[serde(rename = "bodyHash")] 49 | /// Hex-encoded SHA-256 hash of the raw HTTP request body. 50 | body_hash: String, 51 | } 52 | 53 | #[derive(Debug, Error)] 54 | pub enum JwtError { 55 | #[error("Could not serialize JWT body: {0}")] 56 | Json(#[from] serde_json::Error), 57 | #[error("Could not create JWT time: {0}")] 58 | Time(#[from] std::time::SystemTimeError), 59 | #[error(transparent)] 60 | Jwt(#[from] jwterrors::Error), 61 | } 62 | 63 | impl<'a> Claims<'a> { 64 | fn new(uri: &'a str, sub: &'a str, body: S) -> Result { 65 | // use millisecond precision to ensure that it's not reused 66 | let now = SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis() as u64; 67 | let mut rng = rand::thread_rng(); 68 | let nonce = rng.gen::(); 69 | let now = now / 1000; 70 | 71 | let body_hash = { 72 | let mut digest = Sha256::new(); 73 | digest.update(serde_json::to_vec(&body)?); 74 | digest.finalize().to_vec() 75 | }; 76 | 77 | Ok(Self { 78 | uri, 79 | sub, 80 | body_hash: body_hash.to_hex::(), 81 | nonce, 82 | iat: now, 83 | exp: now + EXPIRY, 84 | }) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/api.rs: -------------------------------------------------------------------------------- 1 | // TODO: This file can be extracted to a separate crate. 2 | use crate::{ 3 | jwtclient::JwtSigner, 4 | types::{ 5 | AccountDetails, AssetResponse, CreateTransactionResponse, CreateVaultRequest, 6 | CreateVaultResponse, DepositAddressResponse, TransactionArguments, TransactionDetails, 7 | VaultAccountPaginatedResponse, VaultAccountResponse, 8 | }, 9 | FireblocksError, Result, 10 | }; 11 | 12 | use jsonwebtoken::EncodingKey; 13 | use reqwest::{Client, RequestBuilder}; 14 | use serde::{de::DeserializeOwned, Serialize}; 15 | 16 | const FIREBLOCKS_API: &str = "https://api.fireblocks.io"; 17 | const VERSION: &str = "v1"; 18 | 19 | #[derive(Debug, Clone)] 20 | pub struct FireblocksClient { 21 | pub signer: JwtSigner, 22 | client: Client, 23 | url: String, 24 | version: String, 25 | } 26 | 27 | // This impl block contains the necessary API calls for interacting with Ethereum 28 | impl FireblocksClient { 29 | pub fn new(key: EncodingKey, api_key: &str, api_url_override: Option<&str>) -> Self { 30 | let api_url = match api_url_override { 31 | Some(url) => url, 32 | None => FIREBLOCKS_API, 33 | }; 34 | Self::new_with_url(key, api_key, api_url) 35 | } 36 | 37 | pub fn new_with_url(key: EncodingKey, api_key: &str, url: &str) -> Self { 38 | Self { 39 | signer: JwtSigner::new(key, api_key), 40 | client: Client::new(), 41 | url: url.to_owned(), 42 | version: VERSION.to_owned(), 43 | } 44 | } 45 | 46 | pub async fn create_transaction( 47 | &self, 48 | tx: TransactionArguments, 49 | ) -> Result { 50 | self.post("transactions", tx).await 51 | } 52 | 53 | pub async fn get_account_details( 54 | &self, 55 | asset_id: &str, 56 | account_id: &str, 57 | ) -> Result { 58 | self.get(&format!("vault/accounts/{}/{}", account_id, asset_id)) 59 | .await 60 | } 61 | 62 | pub async fn transaction(&self, txid: &str) -> Result { 63 | self.get(&format!("transactions/{}", txid)).await 64 | } 65 | } 66 | 67 | // This impl block contains the underlying GET/POST helpers for authing to fireblocks 68 | impl FireblocksClient { 69 | async fn get(&self, path: &str) -> Result { 70 | let path = format!("/{}/{}", self.version, path); 71 | let req = self.client.get(format!("{}{}", self.url, path)); 72 | self.send(&path, req, ()).await 73 | } 74 | 75 | async fn post(&self, path: &str, body: S) -> Result { 76 | let path = format!("/{}/{}", self.version, path); 77 | let req = self 78 | .client 79 | .post(format!("{}{}", self.url, path)) 80 | .json(&body); 81 | self.send(&path, req, body).await 82 | } 83 | 84 | async fn send( 85 | &self, 86 | path: &str, 87 | req: RequestBuilder, 88 | body: S, 89 | ) -> Result { 90 | let req = self.authed(path, req, body)?; 91 | let res = req.send().await?; 92 | let text = res.text().await?; 93 | let res: R = 94 | serde_json::from_str(&text).map_err(|err| FireblocksError::SerdeJson { err, text })?; 95 | Ok(res) 96 | } 97 | 98 | // Helper function which adds the necessary authorization headers to auth into the Fireblocks 99 | // API 100 | fn authed( 101 | &self, 102 | url: &str, 103 | req: RequestBuilder, 104 | body: S, 105 | ) -> Result { 106 | let jwt = self.signer.sign(url, body)?; 107 | Ok(req 108 | .header("X-API-Key", &self.signer.api_key) 109 | .bearer_auth(jwt)) 110 | } 111 | } 112 | 113 | // This impl block contains the rest of "nice to have" endpoints 114 | impl FireblocksClient { 115 | pub async fn vaults(&self) -> Result { 116 | self.get("vault/accounts_paged").await 117 | } 118 | 119 | pub async fn vault(&self, account_id: &str) -> Result { 120 | self.get(&format!("vault/accounts/{}", account_id)).await 121 | } 122 | 123 | pub async fn vault_wallet(&self, account_id: &str, asset_id: &str) -> Result { 124 | self.get(&format!("vault/accounts/{}/{}", account_id, asset_id)) 125 | .await 126 | } 127 | 128 | pub async fn new_vault(&self, req: CreateVaultRequest) -> Result { 129 | self.post("vault/accounts", req).await 130 | } 131 | 132 | pub async fn vault_addresses( 133 | &self, 134 | account_id: &str, 135 | asset_id: &str, 136 | ) -> Result> { 137 | self.get(&format!( 138 | "vault/accounts/{}/{}/addresses", 139 | account_id, asset_id 140 | )) 141 | .await 142 | } 143 | } 144 | 145 | #[cfg(test)] 146 | mod tests { 147 | use super::*; 148 | 149 | // this section implements method useful in tests 150 | impl FireblocksClient { 151 | pub fn url(&self) -> &str { 152 | &self.url 153 | } 154 | } 155 | 156 | #[tokio::test] 157 | async fn v1_api() { 158 | let fireblocks_key = std::env::var("FIREBLOCKS_API_SECRET_PATH").unwrap(); 159 | let api_key = std::env::var("FIREBLOCKS_API_KEY").expect("fireblocks api key not set"); 160 | 161 | let rsa_pem = std::fs::read(fireblocks_key).unwrap(); 162 | let key = EncodingKey::from_rsa_pem(&rsa_pem[..]).unwrap(); 163 | let client = FireblocksClient::new(key, &api_key, None); 164 | 165 | assert_eq!(client.url(), FIREBLOCKS_API); 166 | 167 | let _res = client.vaults().await.unwrap(); 168 | let _res = client.vault("1").await.unwrap(); 169 | let _res = client.vault_addresses("1", "ETH_TEST5").await.unwrap(); 170 | let _res = client.vault_wallet("1", "ETH_TEST5").await.unwrap(); 171 | let _res = client 172 | // Creating a vault does not require approval? 173 | .new_vault(CreateVaultRequest { 174 | name: "test-acc".to_owned(), 175 | customer_ref_id: None, 176 | hidden_on_ui: false, 177 | auto_fuel: false, 178 | }) 179 | .await 180 | .unwrap(); 181 | } 182 | 183 | // test api url 184 | } 185 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | mod jwtclient; 2 | pub mod types; 3 | use types::{TransactionArguments, TransactionDetails, TransactionStatus}; 4 | 5 | mod api; 6 | use api::FireblocksClient; 7 | 8 | use ethers_core::types::Address; 9 | use jsonwebtoken::EncodingKey; 10 | use std::{collections::HashMap, time::Instant}; 11 | use thiserror::Error; 12 | 13 | pub(crate) type Result = std::result::Result; 14 | 15 | #[derive(Debug, Error)] 16 | /// Fireblocks API related errors 17 | pub enum FireblocksError { 18 | #[error(transparent)] 19 | /// Thrown when JWT signing fails 20 | JwtError(#[from] jwtclient::JwtError), 21 | 22 | #[error(transparent)] 23 | /// Thrown when we cannot parse the RSA PEM file 24 | JwtParseError(#[from] jsonwebtoken::errors::Error), 25 | 26 | #[error(transparent)] 27 | /// Thrown when we cannot find the RSA PEM file 28 | IoError(#[from] std::io::Error), 29 | 30 | #[error(transparent)] 31 | /// Thrown when submitting a POST/GET request fails 32 | ReqwestError(#[from] reqwest::Error), 33 | 34 | #[error("Deserialization Error: {err}. Response: {text}")] 35 | /// Serde JSON Error 36 | SerdeJson { 37 | err: serde_json::Error, 38 | text: String, 39 | }, 40 | 41 | #[error( 42 | "Transaction was not completed successfully. Final Status: {:?}. Sub status: {1}", 43 | 0 44 | )] 45 | /// Thrown when a transaction submission or message signing fails 46 | TxError(TransactionStatus, String), 47 | 48 | #[error("Could not parse data: {0}")] 49 | /// Thrown when parsing string as Ethereum data fails 50 | ParseError(String), 51 | 52 | #[error("Timed out while waiting for user to approve transaction")] 53 | Timeout, 54 | } 55 | 56 | #[derive(Debug, Clone)] 57 | /// FireblocksSigner is a [`Signer`](ethers_signers::Signer) which utilizes Fireblocks' 58 | /// MPC signing over its [API](https://docs.fireblocks.io/api) instead of a local private key. 59 | /// 60 | /// Note: Using FireblocksSigner as a signer WILL NOT take advantage of Fireblock's contextual 61 | /// policy engine and will only use the RAW signing functionalities. 62 | /// 63 | /// Consider using [`FireblocksMiddleware`](crate::FireblocksMiddleware) to have an integrated 64 | /// ethers [`Middleware`](eters_middleware::Middleware) experience. 65 | pub struct FireblocksSigner { 66 | fireblocks: FireblocksClient, 67 | account_ids: HashMap, 68 | chain_id: u64, 69 | address: Address, 70 | account_id: String, 71 | timeout: u128, 72 | } 73 | 74 | /// Configuration options for instantiating a [`FireblocksSigner`](FireblocksSigner) 75 | pub struct Config { 76 | /// The RSA key file. 77 | pub key: EncodingKey, 78 | /// The API key which was provided to you by fireblocks support 79 | pub api_key: String, 80 | /// The chain id of the network you are connecting to 81 | pub chain_id: u64, 82 | /// Your vault's account id. 83 | pub account_id: String, 84 | /// Timeout for reaching CONFIRMED state of transaction posted to Fireblocks 85 | pub timeout: u128, 86 | } 87 | 88 | impl Config { 89 | /// Instantiates the config file given a path to the RSA file as well as the rest of the config 90 | /// args. 91 | pub fn new>( 92 | key: T, 93 | api_key: &str, 94 | account_id: &str, 95 | chain_id: u64, 96 | timeout: u128, 97 | ) -> Result { 98 | let rsa_pem = std::fs::read(key.as_ref())?; 99 | let key = EncodingKey::from_rsa_pem(&rsa_pem)?; 100 | 101 | Ok(Self { 102 | key, 103 | chain_id, 104 | api_key: api_key.to_string(), 105 | account_id: account_id.to_string(), 106 | timeout, 107 | }) 108 | } 109 | } 110 | 111 | impl AsRef for FireblocksSigner { 112 | fn as_ref(&self) -> &FireblocksClient { 113 | &self.fireblocks 114 | } 115 | } 116 | 117 | impl FireblocksSigner { 118 | /// Instantiates a FireblocksSigner with the provided config 119 | pub async fn new(cfg: Config) -> Self { 120 | let local; 121 | let api_url_override = match std::env::var("FIREBLOCKS_API_URL_OVERRIDE") { 122 | Ok(string) => { 123 | local = string; 124 | Some(local.as_str()) 125 | } 126 | Err(_) => None, 127 | }; 128 | let fireblocks = FireblocksClient::new(cfg.key, &cfg.api_key, api_url_override); 129 | let asset_id = match cfg.chain_id { 130 | 1 => "ETH", 131 | 3 => "ETH_TEST", // Ropstein 132 | 5 => "ETH_TEST3", // Goerli 133 | 42 => "ETH_TEST2", 134 | 11155111 => "ETH_TEST5", // Sepolia 135 | _ => panic!("Unsupported chain_id"), 136 | }; 137 | 138 | let res = fireblocks 139 | .vault_addresses(&cfg.account_id, asset_id) 140 | .await 141 | .expect("could not get vault addrs"); 142 | 143 | Self { 144 | fireblocks, 145 | account_ids: HashMap::new(), 146 | chain_id: cfg.chain_id, 147 | address: res[0].address[2..] 148 | .parse() 149 | .expect("could not parse as address"), 150 | account_id: cfg.account_id, 151 | timeout: cfg.timeout, 152 | } 153 | } 154 | 155 | /// Sets the timeout duration in milliseconds. If the user does not approve a 156 | /// transaction within this time, the transaction request throws an error. 157 | pub fn timeout(&mut self, timeout_ms: u128) { 158 | self.timeout = timeout_ms; 159 | } 160 | 161 | /// Registers an Account ID to Address mapping. 162 | pub fn add_account(&mut self, account_id: String, address: Address) { 163 | self.account_ids.insert(address, account_id); 164 | } 165 | 166 | pub fn chain_id(&self) -> u64 { 167 | self.chain_id 168 | } 169 | 170 | pub fn address(&self) -> Address { 171 | self.address 172 | } 173 | 174 | pub async fn get_available(&self, asset_id: &str) -> Result { 175 | let account_details = self 176 | .fireblocks 177 | .get_account_details(asset_id, &self.account_id) 178 | .await?; 179 | 180 | Ok(account_details.available) 181 | } 182 | 183 | pub async fn handle_action(&self, args: TransactionArguments, func: F) -> Result 184 | where 185 | F: FnOnce(TransactionDetails) -> Result, 186 | { 187 | let res = self.fireblocks.create_transaction(args).await?; 188 | let start = Instant::now(); 189 | loop { 190 | if Instant::now().duration_since(start).as_millis() >= self.timeout { 191 | return Err(FireblocksError::Timeout); 192 | } 193 | 194 | let details = self.fireblocks.transaction(&res.id).await?; 195 | use TransactionStatus::*; 196 | // Loops in pending signature 197 | match details.status { 198 | COMPLETED => return func(details), 199 | BLOCKED | CANCELLED | FAILED => { 200 | return Err(FireblocksError::TxError(details.status, details.sub_status)) 201 | } 202 | _ => {} 203 | } 204 | } 205 | } 206 | } 207 | 208 | #[cfg(test)] 209 | mod tests { 210 | use super::*; 211 | 212 | #[tokio::test] 213 | async fn test_signer() { 214 | let config = Config::new( 215 | std::env::var("FIREBLOCKS_API_SECRET_PATH").unwrap(), 216 | &std::env::var("FIREBLOCKS_API_KEY").unwrap(), 217 | &std::env::var("FIREBLOCKS_SOURCE_VAULT_ACCOUNT").unwrap(), 218 | 11155111, 219 | 60_000, 220 | ) 221 | .unwrap(); 222 | FireblocksSigner::new(config).await; 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /src/types.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Debug, Serialize, Deserialize)] 4 | #[serde(rename_all = "camelCase")] 5 | pub struct VaultAccountPaginatedResponse { 6 | accounts: Vec, 7 | paging: Paging, 8 | previous_url: Option, 9 | next_url: Option, 10 | } 11 | 12 | #[derive(Debug, Serialize, Deserialize)] 13 | #[serde(rename_all = "camelCase")] 14 | pub struct VaultAccountResponse { 15 | id: String, 16 | name: String, 17 | #[serde(rename = "hiddenOnUI")] 18 | hidden_on_ui: bool, 19 | assets: Vec, 20 | customer_ref_id: Option, 21 | auto_fuel: bool, 22 | } 23 | 24 | #[derive(Debug, Serialize, Deserialize)] 25 | #[serde(rename_all = "camelCase")] 26 | pub struct Paging { 27 | before: Option, 28 | after: Option, 29 | } 30 | 31 | #[derive(Debug, Serialize, Deserialize)] 32 | #[serde(rename_all = "camelCase")] 33 | pub struct CreateVaultRequest { 34 | pub name: String, 35 | #[serde(rename = "hiddenOnUI")] 36 | pub hidden_on_ui: bool, 37 | #[serde(skip_serializing_if = "Option::is_none")] 38 | pub customer_ref_id: Option, 39 | // Field order matters :( 40 | #[serde(rename = "autoFuel")] 41 | pub auto_fuel: bool, 42 | } 43 | 44 | #[derive(Debug, Serialize, Deserialize)] 45 | #[serde(rename_all = "camelCase")] 46 | pub struct CreateVaultResponse { 47 | pub id: String, 48 | } 49 | 50 | #[derive(Debug, Serialize, Deserialize)] 51 | #[serde(rename_all = "camelCase")] 52 | pub struct AssetResponse { 53 | id: String, 54 | total: String, 55 | /// DEPRECATED 56 | balance: Option, 57 | #[serde(rename = "lockedAmount")] 58 | locked_amount: Option, 59 | available: Option, 60 | pending: Option, 61 | self_staked_cpu: Option, 62 | self_staked_network: Option, 63 | pending_refund_cpu: Option, 64 | pending_refund_network: Option, 65 | total_staked_cpu: Option, 66 | total_staked_network: Option, 67 | } 68 | 69 | #[derive(Debug, Serialize, Deserialize)] 70 | #[serde(rename_all = "camelCase")] 71 | // TODO: Figure out how to deserialize empty as None. 72 | pub struct DepositAddressResponse { 73 | #[serde(rename = "assetId")] 74 | pub asset_id: String, 75 | pub address: String, 76 | pub tag: Option, 77 | pub description: Option, 78 | #[serde(rename = "type")] 79 | pub kind: String, 80 | #[serde(rename = "legacyAddress")] 81 | pub legacy_address: Option, 82 | #[serde(rename = "customerRefId")] 83 | pub customer_ref_id: Option, 84 | #[serde(rename = "addressFormat")] 85 | pub address_format: Option, 86 | } 87 | 88 | // The APIs feel a bit weird: In trying to create a unified API, it might be good 89 | // to combine these options in enums 90 | #[derive(Debug, Serialize, Deserialize)] 91 | #[serde(rename_all = "camelCase")] 92 | pub struct TransactionArguments { 93 | #[serde(rename = "assetId")] 94 | pub asset_id: String, 95 | pub operation: TransactionOperation, 96 | pub source: TransferPeerPath, 97 | #[serde(skip_serializing_if = "Option::is_none")] 98 | pub destination: Option, 99 | #[serde(skip_serializing_if = "Option::is_none")] 100 | pub amount: Option, 101 | pub external_tx_id: Option, 102 | #[serde(skip_serializing_if = "Option::is_none")] 103 | pub gas_price: Option, 104 | #[serde(skip_serializing_if = "Option::is_none")] 105 | pub gas_limit: Option, 106 | pub note: String, 107 | #[serde(skip_serializing_if = "Option::is_none")] 108 | pub fee_level: Option, 109 | #[serde(skip_serializing_if = "Option::is_none")] 110 | pub extra_parameters: Option, 111 | } 112 | 113 | #[derive(Debug, Serialize, Deserialize)] 114 | #[serde(rename_all = "camelCase")] 115 | pub struct TransferPeerPath { 116 | #[serde(rename = "type")] 117 | pub peer_type: Option, 118 | pub id: Option, 119 | } 120 | 121 | #[derive(Debug, Serialize, Deserialize)] 122 | #[serde(rename_all = "camelCase")] 123 | pub struct DestinationTransferPeerPath { 124 | #[serde(rename = "type")] 125 | pub peer_type: PeerType, 126 | #[serde(skip_serializing_if = "Option::is_none")] 127 | pub id: Option, 128 | #[serde(skip_serializing_if = "Option::is_none")] 129 | pub one_time_address: Option, 130 | } 131 | 132 | #[derive(Debug, Serialize, Deserialize)] 133 | #[serde(rename_all = "camelCase")] 134 | pub struct OneTimeAddress { 135 | pub address: String, 136 | #[serde(skip_serializing_if = "Option::is_none")] 137 | pub tag: Option, 138 | } 139 | 140 | #[allow(non_camel_case_types)] 141 | #[derive(Debug, Serialize, Deserialize)] 142 | #[allow(clippy::upper_case_acronyms)] 143 | pub enum TransactionOperation { 144 | TRANSFER, 145 | RAW, 146 | CONTRACT_CALL, 147 | MINT, 148 | BURN, 149 | SUPPLY_TO_COMPOUND, 150 | REDEEM_FROM_COMPOUND, 151 | TYPED_MESSAGE, 152 | } 153 | 154 | #[allow(non_camel_case_types)] 155 | #[derive(Debug, Serialize, Deserialize)] 156 | #[allow(clippy::upper_case_acronyms)] 157 | pub enum PeerType { 158 | VAULT_ACCOUNT, 159 | EXCHANGE_ACCOUNT, 160 | INTERNAL_WALLET, 161 | EXTERNAL_WALLET, 162 | ONE_TIME_ADDRESS, 163 | NETWORK_CONNECTION, 164 | FIAT_ACCOUNT, 165 | COMPOUND, 166 | } 167 | 168 | #[allow(non_camel_case_types)] 169 | #[derive(Debug, Serialize, Deserialize)] 170 | #[allow(clippy::upper_case_acronyms)] 171 | pub enum FeeLevel { 172 | LOW, 173 | MEDIUM, 174 | HIGH, 175 | } 176 | 177 | #[derive(Debug, Serialize, Deserialize)] 178 | #[serde(rename_all = "camelCase")] 179 | pub struct CreateTransactionResponse { 180 | pub id: String, 181 | pub status: TransactionStatus, 182 | } 183 | 184 | #[allow(non_camel_case_types)] 185 | #[allow(clippy::upper_case_acronyms)] 186 | #[derive(Debug, Serialize, Deserialize)] 187 | pub enum TransactionStatus { 188 | SUBMITTED, 189 | QUEUED, 190 | PENDING_SIGNATURE, 191 | PENDING_AUTHORIZATION, 192 | PENDING_3RD_PARTY_MANUAL_APPROVAL, 193 | PENDING_3RD_PARTY, 194 | /** 195 | * @deprecated 196 | */ 197 | PENDING, 198 | BROADCASTING, 199 | CONFIRMING, 200 | /** 201 | * @deprecated Replaced by "COMPLETED" 202 | */ 203 | CONFIRMED, 204 | COMPLETED, 205 | PENDING_AML_SCREENING, 206 | PARTIALLY_COMPLETED, 207 | CANCELLING, 208 | CANCELLED, 209 | REJECTED, 210 | FAILED, 211 | TIMEOUT, 212 | BLOCKED, 213 | } 214 | 215 | #[derive(Debug, Serialize, Deserialize)] 216 | #[serde(rename_all = "camelCase")] 217 | pub struct TransactionDetails { 218 | pub id: String, 219 | pub asset_id: String, 220 | 221 | pub tx_hash: String, 222 | pub status: TransactionStatus, 223 | pub sub_status: String, 224 | 225 | pub signed_messages: Vec, 226 | } 227 | 228 | #[derive(Debug, Serialize, Deserialize)] 229 | #[serde(rename_all = "camelCase")] 230 | pub struct SignedMessageResponse { 231 | pub content: String, 232 | algorithm: String, 233 | derivation_path: Vec, 234 | pub signature: SignatureResponse, 235 | public_key: String, 236 | } 237 | 238 | #[derive(Debug, Serialize, Deserialize)] 239 | #[serde(rename_all = "camelCase")] 240 | pub struct SignatureResponse { 241 | pub full_sig: String, 242 | pub r: String, 243 | pub s: String, 244 | pub v: u64, 245 | } 246 | 247 | #[derive(Debug, Serialize, Deserialize)] 248 | #[serde(rename_all = "camelCase")] 249 | pub enum ExtraParameters { 250 | RawMessageData { messages: Vec }, 251 | ContractCallData (String), 252 | } 253 | 254 | #[derive(Debug, Serialize, Deserialize)] 255 | #[serde(rename_all = "camelCase")] 256 | pub struct RawMessageData { 257 | pub messages: Vec, 258 | } 259 | 260 | #[derive(Debug, Serialize, Deserialize)] 261 | #[serde(rename_all = "camelCase")] 262 | pub struct UnsignedMessage { 263 | pub content: String, 264 | pub r#type: MessageType, 265 | } 266 | 267 | #[allow(non_camel_case_types)] 268 | #[derive(Debug, Serialize, Deserialize)] 269 | #[allow(clippy::upper_case_acronyms)] 270 | pub enum MessageType { 271 | EIP191, 272 | EIP712, 273 | TIP191, 274 | BTC_MESSAGE, 275 | } 276 | 277 | #[derive(Debug, Serialize, Deserialize)] 278 | #[serde(rename_all = "camelCase")] 279 | pub struct AccountDetails { 280 | pub id: String, 281 | pub total: String, 282 | pub balance: String, 283 | pub locked_amount: String, 284 | pub available: String, 285 | pub pending: String, 286 | pub frozen: String, 287 | pub staked: String, 288 | pub block_height: String, 289 | } 290 | --------------------------------------------------------------------------------