├── .cargo └── config.toml ├── .dockerignore ├── .github └── workflows │ ├── audit-check-on-push.yaml │ ├── docker.yaml │ └── rust.yaml ├── .gitignore ├── Cargo.toml ├── Dockerfile ├── LICENSE ├── README.md ├── app └── index.html ├── rust-toolchain ├── src ├── healthz.rs ├── keys.rs ├── lib.rs ├── main.rs ├── opts.rs ├── wallet.rs └── wallet │ ├── address.rs │ ├── asset_balance.rs │ ├── assets.rs │ ├── blind.rs │ ├── data.rs │ ├── dir.rs │ ├── drain_to.rs │ ├── go_online.rs │ ├── invoice.rs │ ├── issue.rs │ ├── issue │ └── rgb20.rs │ ├── refresh.rs │ ├── send.rs │ ├── transfers.rs │ ├── unspents.rs │ └── utxos.rs └── test-mocks ├── docker-compose.yml └── start_services.sh /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [net] 2 | git-fetch-with-cli = true 3 | 4 | [registries.crates-io] 5 | protocol = "sparse" -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | test-mock/** 2 | target/** 3 | -------------------------------------------------------------------------------- /.github/workflows/audit-check-on-push.yaml: -------------------------------------------------------------------------------- 1 | name: Security audit 2 | on: 3 | schedule: 4 | - cron: '0 0 * * *' 5 | push: 6 | paths: 7 | - '**/Cargo.toml' 8 | - '**/Cargo.lock' 9 | jobs: 10 | security_audit: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v1 14 | - uses: rustsec/audit-check@v1 15 | with: 16 | token: ${{ secrets.GITHUB_TOKEN }} 17 | -------------------------------------------------------------------------------- /.github/workflows/docker.yaml: -------------------------------------------------------------------------------- 1 | name: Docker container image 2 | 3 | on: 4 | push: 5 | branches-ignore: 6 | - '**' 7 | tags: 8 | - 'v*' 9 | 10 | jobs: 11 | docker: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - 15 | name: Set up QEMU 16 | uses: docker/setup-qemu-action@v2 17 | - 18 | name: Set up Docker Buildx 19 | uses: docker/setup-buildx-action@v2 20 | - 21 | name: Checkout 22 | uses: actions/checkout@v2 23 | - 24 | name: Set up metadata 25 | uses: docker/metadata-action@v4 26 | id: meta 27 | with: 28 | images: ghcr.io/${{ github.repository }} 29 | tags: | 30 | type=semver,pattern={{version}} 31 | type=semver,pattern={{major}}.{{minor}} 32 | - 33 | name: Login to GHCR. 34 | uses: docker/login-action@v2 35 | with: 36 | registry: ghcr.io 37 | username: ${{ github.actor }} 38 | password: ${{ secrets.GITHUB_TOKEN }} 39 | - 40 | name: Build and push a docker image 41 | uses: docker/build-push-action@v3 42 | with: 43 | context: . 44 | push: true 45 | platforms: linux/amd64,linux/arm64 46 | tags: ${{ steps.meta.outputs.tags }} 47 | labels: ${{ steps.meta.outputs.labels }} 48 | -------------------------------------------------------------------------------- /.github/workflows/rust.yaml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | lint: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v3 18 | 19 | - uses: Swatinem/rust-cache@v2 20 | 21 | - run: rustup component add clippy 22 | 23 | - name: Lint 24 | run: cargo clippy -- -D warnings 25 | 26 | format: 27 | runs-on: ubuntu-latest 28 | 29 | steps: 30 | - uses: actions/checkout@v3 31 | 32 | - uses: Swatinem/rust-cache@v2 33 | 34 | - run: rustup component add rustfmt 35 | 36 | - name: fmt 37 | run: cargo fmt --all -- --check 38 | 39 | test: 40 | runs-on: ubuntu-latest 41 | 42 | steps: 43 | - uses: actions/checkout@v3 44 | 45 | - uses: Swatinem/rust-cache@v2 46 | 47 | - run: mkdir /tmp/shiro-wallet 48 | 49 | - run: bash test-mocks/start_services.sh 50 | 51 | - name: Run tarpaulin 52 | env: 53 | RUST_TEST_THREADS: 1 54 | BITCOIN_NETWORK_NAME: regtest 55 | run: | 56 | cargo install cargo-tarpaulin 57 | cargo tarpaulin --release --out Xml 58 | 59 | - name: Upload to Codecov 60 | uses: codecov/codecov-action@v3 61 | with: 62 | fail_ci_if_error: true 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 7 | Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | test-mocks/tmp/ 12 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "shiro-backend" 3 | version = "0.4.7" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | actix-web = "4.2.1" 10 | actix-cors = "0.6.4" 11 | actix-files = "0.6.2" 12 | clap = { version = "4.0.15", features = ["derive", "env"] } 13 | rand = "0.8.5" 14 | rgb-lib = "=0.2.0-alpha.2" 15 | serde = { version = "1.0", features = ["derive"] } 16 | serde_json = "1.0" 17 | 18 | [dev-dependencies] 19 | actix-rt = "2.1.0" 20 | async-trait = "0.1.59" 21 | once_cell = "1.16.0" 22 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rust:1.66-slim-bullseye as builder 2 | RUN apt-get update \ 3 | && apt-get install -y libcrypt1-dev libssl-dev g++ pkg-config git \ 4 | && rm -fr /var/lib/apt/lists/* \ 5 | && mkdir -p /tmp/shiro-wallet 6 | WORKDIR /usr/src/myapp 7 | COPY . . 8 | RUN cargo install --path . 9 | 10 | FROM debian:bullseye-slim 11 | ARG BITCOIN_NETWORK_NAME=testnet 12 | ARG ELECTRUM_URL=127.0.0.1:50001 13 | ARG RGB_PROXY_URL=http://proxy.rgbtools.org 14 | 15 | RUN apt-get update \ 16 | && apt-get install -y libcrypt1 libssl1.1 libstdc++6 \ 17 | && rm -fr /var/lib/apt/lists/* \ 18 | && mkdir -p /tmp/shiro-wallet 19 | COPY --from=builder /usr/local/cargo/bin/shiro-backend /usr/local/bin/shiro-backend 20 | ENV BITCOIN_NETWORK_NAME=${BITCOIN_NETWORK_NAME} 21 | ENV ELECTRUM_URL=${ELECTRUM_URL} 22 | ENV RGB_PROXY_URL=${RGB_PROXY_URL} 23 | CMD ["shiro-backend"] 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 diamondhands-rgb 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # shiro-backend 2 | Wallet server for the RGB protocol (that is defined LNP/BP) 3 | 4 | # Prequisite 5 | 6 | * Electrum and bitcoin-node that sync on your bitcoin network. (`mainnet`, `testnet`, `signet`, or `regtest`). 7 | * rgb-proxy-server. One of the rgb-proxy-server is https://github.com/grunch/rgb-proxy-server . 8 | * Some more shared libraries like libcrypt1.so. 9 | 10 | And you want to develop Shiro-backend itself. 11 | 12 | # How to run 13 | 14 | ## Docker 15 | 16 | You shall specify `{network_name}`. (`mainnet`, `testnet`, `signet`, or `regtest`). 17 | 18 | ``` 19 | docker run -d -p 8080:8080 -e BITCOIN_NETWORK={network_name} ghcr.io/diamondhands-dev/shiro-backend:latest 20 | ``` 21 | 22 | ## From sources 23 | 24 | ``` 25 | git clone https://github.com/diamondhands-dev/shiro-backend.git 26 | cd shiro-backend 27 | cargo install --path . 28 | export BITCOIN_NETWORK_NAME={network_name} 29 | export ELECTRUM_URL={electrum_url} 30 | export RGB_PROXY_URL={rgb_proxy_url} 31 | mkdir -p /tmp/shiro-wallet 32 | 33 | shiro-backend 34 | ``` 35 | 36 | Environment variables will be fixed for your runtime environment. 37 | 38 | # How to test 39 | 40 | ## Prequisite 41 | 42 | * Linux runs on 64bit architecture. 43 | * You'll get a build error on 32bit versions of Linux (like i386, armv7). 44 | * I've not checked yet on Windows, macOS. I guess Shiro-backend won't work with them. 45 | * Rust/Cargo (>= 1.66.0 ). 46 | * C++ toolchain (Shiro itself doesn't need but some dependencies require it). 47 | * Some additional `*.a` library files. It depends your development environment. 48 | 49 | ## Steps. 50 | 51 | ``` 52 | git clone https://github.com/diamondhands-dev/shiro-backend.git 53 | cd shiro-backend 54 | mkdir -p /tmp/shiro-wallet 55 | test-mocks/start_services.sh 56 | RUST_TEST_THREADS=1 BITCOIN_NETWORK_NAME={network_name} cargo test 57 | ``` 58 | 59 | # How to release docker images 60 | 61 | ``` 62 | git tag v{semantic version} 63 | git push origin v{semantic version} 64 | ``` 65 | 66 | A GitHub action will be build new image with tags. 67 | 68 | You'll find the rule of `{semantic version}` is https://semver.org/ 69 | 70 | # Help wanted? 71 | 72 | https://github.com/diamondhands-dev/shiro-backend/issues 73 | -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /rust-toolchain: -------------------------------------------------------------------------------- 1 | 1.72.0 2 | -------------------------------------------------------------------------------- /src/healthz.rs: -------------------------------------------------------------------------------- 1 | use actix_web::{get, Responder}; 2 | 3 | #[get("/healthz")] 4 | pub async fn get() -> impl Responder { 5 | "" 6 | } 7 | 8 | #[cfg(test)] 9 | mod tests { 10 | use super::*; 11 | 12 | use actix_web::{test, App}; 13 | 14 | #[actix_web::test] 15 | async fn test_healthz() { 16 | let app = test::init_service(App::new().service(get)).await; 17 | let req = test::TestRequest::get().uri("/healthz").to_request(); 18 | 19 | let resp = test::call_service(&app, req).await; 20 | println!("{:?}", resp); 21 | 22 | assert!(resp.status().is_success()); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/keys.rs: -------------------------------------------------------------------------------- 1 | use actix_web::{post, put, web, HttpResponse, Responder}; 2 | use rgb_lib::keys::{generate_keys, restore_keys}; 3 | use serde::Deserialize; 4 | use serde::Serialize; 5 | 6 | #[derive(Serialize, Deserialize)] 7 | pub struct KeyGenParams {} 8 | 9 | #[derive(Serialize, Deserialize)] 10 | struct KeyGenResult { 11 | mnemonic: String, 12 | xpub: String, 13 | xpub_fingerprint: String, 14 | } 15 | 16 | #[derive(Serialize, Deserialize)] 17 | pub struct KeyRestoreParams { 18 | mnemonic: String, 19 | } 20 | 21 | #[post("/keys")] 22 | pub async fn post(params: web::Json) -> impl Responder { 23 | let network = shiro_backend::opts::get_bitcoin_network(); 24 | let result = restore_keys(network, params.mnemonic.clone()); 25 | match result { 26 | Result::Ok(keys) => HttpResponse::Ok().json(KeyGenResult { 27 | mnemonic: keys.mnemonic, 28 | xpub: keys.xpub, 29 | xpub_fingerprint: keys.xpub_fingerprint, 30 | }), 31 | Result::Err(_) => HttpResponse::BadRequest().body("Invalid mnemonic"), 32 | } 33 | } 34 | 35 | #[put("/keys")] 36 | pub async fn put(_params: web::Json) -> impl Responder { 37 | let network = shiro_backend::opts::get_bitcoin_network(); 38 | 39 | let keys = generate_keys(network); 40 | let result = KeyGenResult { 41 | mnemonic: keys.mnemonic, 42 | xpub: keys.xpub, 43 | xpub_fingerprint: keys.xpub_fingerprint, 44 | }; 45 | HttpResponse::Ok().json(result) 46 | } 47 | 48 | #[cfg(test)] 49 | mod tests { 50 | use super::*; 51 | use actix_web::{ 52 | http, 53 | test::{self, read_body_json}, 54 | App, 55 | }; 56 | 57 | #[actix_web::test] 58 | async fn test_post_with_no_json() { 59 | let app = test::init_service(App::new().service(post)).await; 60 | let req = test::TestRequest::post().uri("/keys").to_request(); 61 | 62 | let resp = test::call_service(&app, req).await; 63 | println!("{:?}", resp); 64 | 65 | assert_eq!(resp.status(), http::StatusCode::BAD_REQUEST); 66 | } 67 | 68 | #[actix_web::test] 69 | async fn test_post_with_bad_mnemonic() { 70 | let app = test::init_service(App::new().service(post)).await; 71 | let payload = KeyRestoreParams { 72 | mnemonic: ("save call film frog usual market noodle hope stomach chat word worry bad") 73 | .to_string(), 74 | }; 75 | let req = test::TestRequest::post() 76 | .uri("/keys") 77 | .set_json(payload) 78 | .to_request(); 79 | 80 | let resp = test::call_service(&app, req).await; 81 | println!("{:?}", resp); 82 | 83 | assert_eq!(resp.status(), http::StatusCode::BAD_REQUEST); 84 | } 85 | 86 | #[actix_web::test] 87 | async fn test_post() { 88 | let app = test::init_service(App::new().service(post)).await; 89 | let payload = KeyRestoreParams { 90 | mnemonic: ("save call film frog usual market noodle hope stomach chat word worry") 91 | .to_string(), 92 | }; 93 | let req = test::TestRequest::post() 94 | .uri("/keys") 95 | .set_json(payload) 96 | .to_request(); 97 | 98 | let result: KeyGenResult = test::call_and_read_body_json(&app, req).await; 99 | 100 | assert_eq!( 101 | result.mnemonic, 102 | ("save call film frog usual market noodle hope stomach chat word worry").to_string() 103 | ); 104 | assert_eq!(result.xpub, "tpubD6NzVbkrYhZ4YT9CY6kBTU8xYWq2GQPq4NYzaJer1CRrffVLwzYt5Rs3WhjZJGKaNaiN42JfgtnyGwHXc5n5oPbAUSbxwuwDqZci5kdAZHb".to_string()); 105 | assert_eq!(result.xpub_fingerprint, "60ec7707"); 106 | } 107 | 108 | #[actix_web::test] 109 | async fn test_put() { 110 | let payload = KeyGenParams {}; 111 | let app = test::init_service(App::new().service(put)).await; 112 | let req = test::TestRequest::put() 113 | .uri("/keys") 114 | .set_json(payload) 115 | .to_request(); 116 | 117 | let resp = test::call_service(&app, req).await; 118 | println!("{:?}", resp); 119 | 120 | assert!(resp.status().is_success()); 121 | let result: KeyGenResult = read_body_json(resp).await; 122 | assert_eq!(result.mnemonic.split(' ').count(), 12); 123 | assert!(result.xpub.starts_with("tpub")); 124 | assert_ne!(result.xpub_fingerprint, ""); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod opts; 2 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use crate::wallet::ShiroWallet; 2 | use actix_cors::Cors; 3 | use actix_web::{http::header, web, App, HttpServer}; 4 | use std::sync::Mutex; 5 | 6 | mod healthz; 7 | mod keys; 8 | mod wallet; 9 | 10 | #[actix_web::main] 11 | async fn main() -> std::io::Result<()> { 12 | HttpServer::new(|| { 13 | let shiro_wallet = Mutex::new(wallet::ShiroWallet::new()); 14 | let data = web::Data::new(shiro_wallet); 15 | let cors = Cors::default() 16 | .allow_any_origin() 17 | .send_wildcard() 18 | .allowed_methods(vec!["GET", "DELETE", "OPTIONS", "POST", "PUT"]) 19 | .allowed_headers(vec![ 20 | header::AUTHORIZATION, 21 | header::ACCEPT, 22 | header::ACCEPT_ENCODING, 23 | header::CONTENT_TYPE, 24 | header::CONTENT_LENGTH, 25 | ]) 26 | .max_age(3600); 27 | 28 | let frontend = actix_files::Files::new("/", "./app").index_file("index.html"); 29 | 30 | App::new() 31 | .app_data(data) 32 | .wrap(cors) 33 | .service(healthz::get) 34 | .service(keys::post) 35 | .service(keys::put) 36 | .service(wallet::address::get) 37 | .service(wallet::invoice::put) 38 | .service(wallet::asset_balance::get) 39 | .service(wallet::assets::put) 40 | .service(wallet::blind::put) 41 | .service(wallet::data::get) 42 | .service(wallet::dir::get) 43 | .service(wallet::drain_to::put) 44 | .service(wallet::go_online::put) 45 | .service(wallet::issue::rgb20::put) 46 | .service(wallet::refresh::post) 47 | .service(wallet::send::post) 48 | .service(wallet::put) 49 | .service(wallet::transfers::delete) 50 | .service(wallet::transfers::put) 51 | .service(wallet::unspents::put) 52 | .service(wallet::utxos::put) 53 | .service(frontend) 54 | }) 55 | .bind("0.0.0.0:8080")? 56 | .workers(1) 57 | .run() 58 | .await 59 | } 60 | 61 | #[cfg(test)] 62 | mod tests { 63 | use super::*; 64 | 65 | use actix_web::{test, web, App}; 66 | use once_cell::sync::Lazy; 67 | use serde::Deserialize; 68 | use serde::Serialize; 69 | 70 | pub static PROXY_ENDPOINT: Lazy = 71 | Lazy::new(|| "rpc://127.0.0.1:3000/json-rpc".to_string()); 72 | 73 | #[derive(Serialize, Deserialize)] 74 | pub struct OnlineResult { 75 | pub id: String, 76 | pub electrum_url: String, 77 | pub proxy_url: String, 78 | } 79 | 80 | #[actix_web::test] 81 | async fn test_root() { 82 | let shiro_wallet = Mutex::new(ShiroWallet::new()); 83 | let app = test::init_service(App::new().app_data(web::Data::new(shiro_wallet))).await; 84 | let req = test::TestRequest::get().uri("/").to_request(); 85 | 86 | let resp = test::call_service(&app, req).await; 87 | println!("{:?}", resp); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/opts.rs: -------------------------------------------------------------------------------- 1 | use clap::builder::TypedValueParser as _; 2 | use clap::Parser; 3 | use rgb_lib::wallet::WalletData; 4 | 5 | #[derive(Parser, Debug)] 6 | #[command(author, version, about, long_about = None)] 7 | pub struct Args { 8 | /// Path to data_dir 9 | #[arg(long, default_value = "/tmp/shiro-wallet")] 10 | data_dir: String, 11 | 12 | /// Name of BitcoinNetwork 13 | #[arg( 14 | env = "BITCOIN_NETWORK_NAME", 15 | short, 16 | long, 17 | default_value_t = parser::BitcoinNetwork::Mainnet, 18 | value_parser = clap::builder::PossibleValuesParser::new(["mainnet","testnet","regtest","signet"]) 19 | .map(|s| s.parse::().unwrap()), 20 | )] 21 | network_name: parser::BitcoinNetwork, 22 | 23 | #[arg( 24 | long, 25 | default_value_t = parser::DatabaseType::Sqlite, 26 | value_parser = clap::builder::PossibleValuesParser::new(["sqlite"]) 27 | .map(|s| s.parse::().unwrap()), 28 | )] 29 | database_type: parser::DatabaseType, 30 | 31 | #[arg( 32 | env = "ELECTRUM_URL", 33 | long = "electrum-url", 34 | default_value = "127.0.0.1:50001" 35 | )] 36 | pub electrum_url: String, 37 | 38 | #[arg( 39 | env = "RGB_PROXY_URL", 40 | long = "proxy-url", 41 | default_value = "http://proxy.rgbtools.org" 42 | )] 43 | pub proxy_url: String, 44 | 45 | #[arg(long = "show-output")] 46 | show_output: bool, 47 | 48 | #[arg(long, default_value_t = true)] 49 | pub skip_consistency_check: bool, 50 | } 51 | 52 | pub fn get_args() -> Args { 53 | Args::parse() 54 | } 55 | 56 | mod parser { 57 | #[derive(Copy, Clone, PartialEq, Eq, Debug)] 58 | pub enum BitcoinNetwork { 59 | Mainnet, 60 | Testnet, 61 | Regtest, 62 | Signet, 63 | } 64 | 65 | impl std::fmt::Display for BitcoinNetwork { 66 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 67 | let s = match self { 68 | Self::Mainnet => "mainnet", 69 | Self::Testnet => "testnet", 70 | Self::Regtest => "regtest", 71 | Self::Signet => "signet", 72 | }; 73 | s.fmt(f) 74 | } 75 | } 76 | 77 | impl std::str::FromStr for BitcoinNetwork { 78 | type Err = String; 79 | fn from_str(s: &str) -> Result { 80 | match s { 81 | "mainnet" => Ok(Self::Mainnet), 82 | "testnet" => Ok(Self::Testnet), 83 | "regtest" => Ok(Self::Regtest), 84 | "signet" => Ok(Self::Signet), 85 | _ => Err(format!("Unknown bitcoin network: {s}")), 86 | } 87 | } 88 | } 89 | 90 | #[derive(Copy, Clone, PartialEq, Eq, Debug)] 91 | pub enum DatabaseType { 92 | Sqlite, 93 | } 94 | 95 | impl std::fmt::Display for DatabaseType { 96 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 97 | let s = match self { 98 | Self::Sqlite => "sqlite", 99 | }; 100 | s.fmt(f) 101 | } 102 | } 103 | 104 | impl std::str::FromStr for DatabaseType { 105 | type Err = String; 106 | fn from_str(s: &str) -> Result { 107 | match s { 108 | "sqlite" => Ok(Self::Sqlite), 109 | _ => Err(format!("Unknown database type: {s}")), 110 | } 111 | } 112 | } 113 | } 114 | 115 | pub fn get_bitcoin_network() -> rgb_lib::BitcoinNetwork { 116 | get_wallet_data().bitcoin_network 117 | } 118 | 119 | pub fn get_wallet_data() -> rgb_lib::wallet::WalletData { 120 | let args = Args::parse(); 121 | 122 | let bitcoin_network = if args.network_name == parser::BitcoinNetwork::Mainnet { 123 | rgb_lib::BitcoinNetwork::Mainnet 124 | } else if args.network_name == parser::BitcoinNetwork::Testnet { 125 | rgb_lib::BitcoinNetwork::Testnet 126 | } else if args.network_name == parser::BitcoinNetwork::Regtest { 127 | rgb_lib::BitcoinNetwork::Regtest 128 | } else if args.network_name == parser::BitcoinNetwork::Signet { 129 | rgb_lib::BitcoinNetwork::Signet 130 | } else { 131 | panic!("Internal error: an wrong args.network_name should be checked in the parser.") 132 | }; 133 | 134 | let database_type = if args.database_type == parser::DatabaseType::Sqlite { 135 | rgb_lib::wallet::DatabaseType::Sqlite 136 | } else { 137 | panic!("Internal error: an wrong args.database_type should be checked in the parser.") 138 | }; 139 | 140 | WalletData { 141 | data_dir: args.data_dir, 142 | bitcoin_network, 143 | database_type, 144 | pubkey: "".to_string(), 145 | mnemonic: None, 146 | } 147 | } 148 | 149 | #[cfg(test)] 150 | mod tests { 151 | use super::parser::*; 152 | 153 | #[test] 154 | fn test_database_type() { 155 | assert_eq!(format!("{}", DatabaseType::Sqlite), "sqlite") 156 | } 157 | 158 | #[test] 159 | fn test_bitcoinnetwork_mainnet() { 160 | assert_eq!(format!("{}", BitcoinNetwork::Mainnet), "mainnet") 161 | } 162 | 163 | #[test] 164 | fn test_bitcoinnetwork_testnet() { 165 | assert_eq!(format!("{}", BitcoinNetwork::Testnet), "testnet") 166 | } 167 | 168 | #[test] 169 | fn test_bitcoinnetwork_regtest() { 170 | assert_eq!(format!("{}", BitcoinNetwork::Regtest), "regtest") 171 | } 172 | 173 | #[test] 174 | fn test_bitcoinnetwork_signet() { 175 | assert_eq!(format!("{}", BitcoinNetwork::Signet), "signet") 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /src/wallet.rs: -------------------------------------------------------------------------------- 1 | use actix_web::{put, web, HttpResponse, Responder}; 2 | use rgb_lib::wallet::{Online, Wallet, WalletData}; 3 | use serde::Deserialize; 4 | use serde::Serialize; 5 | use std::sync::Mutex; 6 | 7 | pub mod address; 8 | pub mod asset_balance; 9 | pub mod assets; 10 | pub mod blind; 11 | pub mod data; 12 | pub mod dir; 13 | pub mod drain_to; 14 | pub mod go_online; 15 | pub mod invoice; 16 | pub mod issue; 17 | pub mod refresh; 18 | pub mod send; 19 | pub mod transfers; 20 | pub mod unspents; 21 | pub mod utxos; 22 | 23 | pub struct ShiroWallet { 24 | pub wallet: Option, 25 | pub online: Option, 26 | } 27 | 28 | impl ShiroWallet { 29 | pub fn new() -> ShiroWallet { 30 | ShiroWallet { 31 | wallet: None, 32 | online: None, 33 | } 34 | } 35 | 36 | #[allow(dead_code)] 37 | pub fn get_online(&mut self) -> Option { 38 | self.online.clone() 39 | } 40 | } 41 | 42 | #[derive(Serialize, Deserialize)] 43 | pub struct WalletParams { 44 | mnemonic: String, 45 | pubkey: String, 46 | } 47 | 48 | #[derive(Deserialize, Serialize)] 49 | pub struct Balance { 50 | settled: String, 51 | future: String, 52 | spendable: String, 53 | } 54 | 55 | impl From for Balance { 56 | fn from(origin: rgb_lib::wallet::Balance) -> Balance { 57 | Balance { 58 | settled: origin.settled.to_string(), 59 | future: origin.future.to_string(), 60 | spendable: origin.spendable.to_string(), 61 | } 62 | } 63 | } 64 | 65 | #[allow(clippy::await_holding_lock)] 66 | #[put("/wallet")] 67 | pub async fn put( 68 | params: web::Json, 69 | data: web::Data>, 70 | ) -> impl Responder { 71 | let mut shiro_wallet = data.lock().unwrap(); 72 | match shiro_wallet.wallet { 73 | Some(_) => HttpResponse::BadRequest().body("wallet already created"), 74 | None => { 75 | let base_data = shiro_backend::opts::get_wallet_data(); 76 | let wallet_data = WalletData { 77 | data_dir: base_data.data_dir, 78 | bitcoin_network: base_data.bitcoin_network, 79 | database_type: base_data.database_type, 80 | pubkey: params.pubkey.clone(), 81 | mnemonic: Some(params.mnemonic.clone()), 82 | }; 83 | match actix_web::rt::task::spawn_blocking(move || Wallet::new(wallet_data).unwrap()) 84 | .await 85 | { 86 | Ok(wallet) => { 87 | shiro_wallet.wallet = Some(wallet); 88 | HttpResponse::Ok().json(params) 89 | } 90 | Err(err) => HttpResponse::BadRequest().body(format!("{}", err)), 91 | } 92 | } 93 | } 94 | } 95 | 96 | #[cfg(test)] 97 | mod tests { 98 | use super::*; 99 | 100 | use actix_web::{http, test, web, App}; 101 | use std::process::{Command, Stdio}; 102 | 103 | extern crate rand; 104 | use rand::seq::SliceRandom; 105 | 106 | pub fn gen_fake_ticker() -> String { 107 | const BASE_STR: &str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; 108 | let mut rng = &mut rand::thread_rng(); 109 | String::from_utf8( 110 | BASE_STR 111 | .as_bytes() 112 | .choose_multiple(&mut rng, 8) 113 | .cloned() 114 | .collect(), 115 | ) 116 | .unwrap() 117 | } 118 | 119 | fn _bitcoin_cli() -> [String; 9] { 120 | [ 121 | "-f".to_string(), 122 | "test-mocks/docker-compose.yml".to_string(), 123 | "exec".to_string(), 124 | "-T".to_string(), 125 | "-u".to_string(), 126 | "blits".to_string(), 127 | "bitcoind".to_string(), 128 | "bitcoin-cli".to_string(), 129 | "-regtest".to_string(), 130 | ] 131 | } 132 | 133 | pub fn fund_wallet(address: String) { 134 | let status = Command::new("docker-compose") 135 | .stdin(Stdio::null()) 136 | .stdout(Stdio::null()) 137 | .stderr(Stdio::null()) 138 | .args(_bitcoin_cli()) 139 | .arg("-rpcwallet=miner") 140 | .arg("sendtoaddress") 141 | .arg(address) 142 | .arg("1") 143 | .status() 144 | .expect("failed to fund wallet"); 145 | assert!(status.success()); 146 | } 147 | 148 | #[actix_web::test] 149 | async fn test_put_failed() { 150 | let shiro_wallet = Mutex::new(ShiroWallet::new()); 151 | let app = test::init_service( 152 | App::new() 153 | .app_data(web::Data::new(shiro_wallet)) 154 | .service(put), 155 | ) 156 | .await; 157 | let wallet_params = WalletParams { 158 | mnemonic: "".to_string(), 159 | pubkey: "".to_string(), 160 | }; 161 | let req = test::TestRequest::put() 162 | .uri("/wallet") 163 | .set_json(wallet_params) 164 | .to_request(); 165 | let resp = test::call_service(&app, req).await; 166 | println!("{:?}", resp); 167 | assert_eq!(resp.status(), http::StatusCode::BAD_REQUEST); 168 | } 169 | 170 | #[actix_web::test] 171 | async fn test_put_bad() { 172 | let shiro_wallet = Mutex::new(ShiroWallet::new()); 173 | let app = test::init_service( 174 | App::new() 175 | .app_data(web::Data::new(shiro_wallet)) 176 | .service(put), 177 | ) 178 | .await; 179 | let wallet_params = WalletParams { 180 | mnemonic: "save call film frog usual market noodle hope stomach chat word worry bad".to_string(), 181 | pubkey: "tpubD6NzVbkrYhZ4YT9CY6kBTU8xYWq2GQPq4NYzaJer1CRrffVLwzYt5Rs3WhjZJGKaNaiN42JfgtnyGwHXc5n5oPbAUSbxwuwDqZci5kdAZHb".to_string(), 182 | }; 183 | let req = test::TestRequest::put() 184 | .uri("/wallet") 185 | .set_json(wallet_params) 186 | .to_request(); 187 | let resp = test::call_service(&app, req).await; 188 | println!("{:?}", resp); 189 | assert_eq!(resp.status(), http::StatusCode::BAD_REQUEST); 190 | } 191 | 192 | #[actix_web::test] 193 | async fn test_put() { 194 | let shiro_wallet = Mutex::new(ShiroWallet::new()); 195 | let app = test::init_service( 196 | App::new() 197 | .app_data(web::Data::new(shiro_wallet)) 198 | .service(put), 199 | ) 200 | .await; 201 | let wallet_params = WalletParams { 202 | mnemonic: "save call film frog usual market noodle hope stomach chat word worry".to_string(), 203 | pubkey: "tpubD6NzVbkrYhZ4YT9CY6kBTU8xYWq2GQPq4NYzaJer1CRrffVLwzYt5Rs3WhjZJGKaNaiN42JfgtnyGwHXc5n5oPbAUSbxwuwDqZci5kdAZHb".to_string(), 204 | }; 205 | let req = test::TestRequest::put() 206 | .uri("/wallet") 207 | .set_json(wallet_params) 208 | .to_request(); 209 | let resp = test::call_service(&app, req).await; 210 | println!("{:?}", resp); 211 | assert!(resp.status().is_success()); 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /src/wallet/address.rs: -------------------------------------------------------------------------------- 1 | use crate::ShiroWallet; 2 | use actix_web::{get, web, HttpResponse, Responder}; 3 | use serde::Deserialize; 4 | use serde::Serialize; 5 | use std::sync::Mutex; 6 | 7 | #[derive(Serialize, Deserialize)] 8 | pub struct AddressResult { 9 | pub(crate) new_address: String, 10 | } 11 | 12 | #[allow(clippy::await_holding_lock)] 13 | #[get("/wallet/address")] 14 | pub async fn get(data: web::Data>) -> impl Responder { 15 | let shiro_wallet = data.lock().unwrap(); 16 | match &shiro_wallet.wallet { 17 | Some(wallet) => { 18 | let address = wallet.get_address(); 19 | HttpResponse::Ok().json(AddressResult { 20 | new_address: address, 21 | }) 22 | } 23 | None => HttpResponse::BadRequest().body("wallet should be created first"), 24 | } 25 | } 26 | 27 | #[cfg(test)] 28 | mod tests { 29 | use super::*; 30 | 31 | use actix_web::{test, web, App}; 32 | 33 | #[actix_web::test] 34 | async fn test_get() { 35 | let shiro_wallet = Mutex::new(ShiroWallet::new()); 36 | let app = test::init_service( 37 | App::new() 38 | .app_data(web::Data::new(shiro_wallet)) 39 | .service(get) 40 | .service(crate::wallet::put), 41 | ) 42 | .await; 43 | 44 | let wallet_params = crate::wallet::WalletParams { 45 | mnemonic: "save call film frog usual market noodle hope stomach chat word worry".to_string(), 46 | pubkey: "tpubD6NzVbkrYhZ4YT9CY6kBTU8xYWq2GQPq4NYzaJer1CRrffVLwzYt5Rs3WhjZJGKaNaiN42JfgtnyGwHXc5n5oPbAUSbxwuwDqZci5kdAZHb".to_string(), 47 | }; 48 | let wallet_req = test::TestRequest::put() 49 | .uri("/wallet") 50 | .set_json(wallet_params) 51 | .to_request(); 52 | let wallet_resp = test::call_service(&app, wallet_req).await; 53 | println!("{:?}", wallet_resp); 54 | assert!(wallet_resp.status().is_success()); 55 | 56 | let req = test::TestRequest::get().uri("/wallet/address").to_request(); 57 | let resp = test::call_service(&app, req).await; 58 | println!("{:?}", resp); 59 | assert!(resp.status().is_success()); 60 | let body: AddressResult = test::read_body_json(resp).await; 61 | assert!(body.new_address.starts_with("bcrt1")); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/wallet/asset_balance.rs: -------------------------------------------------------------------------------- 1 | use crate::{wallet::Balance, ShiroWallet}; 2 | use actix_web::{get, web, HttpResponse, Responder}; 3 | use serde::Deserialize; 4 | use serde::Serialize; 5 | use std::sync::Mutex; 6 | 7 | #[derive(Serialize, Deserialize)] 8 | pub struct AssetBalanceParams { 9 | asset_id: String, 10 | } 11 | 12 | #[get("/wallet/asset_balance")] 13 | pub async fn get( 14 | params: web::Json, 15 | data: web::Data>, 16 | ) -> impl Responder { 17 | if data.lock().unwrap().wallet.is_some() { 18 | match actix_web::rt::task::spawn_blocking(move || { 19 | data.lock() 20 | .unwrap() 21 | .wallet 22 | .as_mut() 23 | .unwrap() 24 | .get_asset_balance(params.asset_id.clone()) 25 | }) 26 | .await 27 | .unwrap() 28 | { 29 | Ok(balance) => HttpResponse::Ok().json(Balance::from(balance)), 30 | Err(e) => HttpResponse::BadRequest().body(e.to_string()), 31 | } 32 | } else { 33 | HttpResponse::BadRequest().body("wallet should be created first") 34 | } 35 | } 36 | 37 | #[cfg(test)] 38 | mod tests { 39 | use super::*; 40 | 41 | use crate::wallet::{ 42 | address::AddressResult, 43 | go_online::GoOnlineParams, 44 | issue::rgb20::{Rgb20Params, Rgb20Result}, 45 | tests::{fund_wallet, gen_fake_ticker}, 46 | utxos::UtxosParams, 47 | }; 48 | use actix_web::{test, web, App}; 49 | use rgb_lib::generate_keys; 50 | 51 | #[actix_web::test] 52 | async fn test_get() { 53 | let shiro_wallet = Mutex::new(ShiroWallet::new()); 54 | let app = test::init_service( 55 | App::new() 56 | .app_data(web::Data::new(shiro_wallet)) 57 | .service(crate::wallet::put) 58 | .service(crate::wallet::address::get) 59 | .service(crate::wallet::utxos::put) 60 | .service(crate::wallet::go_online::put) 61 | .service(crate::wallet::issue::rgb20::put) 62 | .service(get), 63 | ) 64 | .await; 65 | 66 | { 67 | let keys = generate_keys(rgb_lib::BitcoinNetwork::Regtest); 68 | let params = crate::wallet::WalletParams { 69 | mnemonic: keys.mnemonic, 70 | pubkey: keys.xpub, 71 | }; 72 | let req = test::TestRequest::put() 73 | .uri("/wallet") 74 | .set_json(params) 75 | .to_request(); 76 | let resp = test::call_service(&app, req).await; 77 | println!("{:?}", resp); 78 | assert!(resp.status().is_success()); 79 | } 80 | let address: AddressResult = { 81 | let req = test::TestRequest::get().uri("/wallet/address").to_request(); 82 | let resp = test::call_service(&app, req).await; 83 | println!("{:?}", resp); 84 | assert!(resp.status().is_success()); 85 | test::read_body_json(resp).await 86 | }; 87 | fund_wallet(address.new_address.clone()); 88 | { 89 | let params = GoOnlineParams::new(true, "127.0.0.1:50001".to_string()); 90 | let req = test::TestRequest::put() 91 | .uri("/wallet/go_online") 92 | .set_json(params) 93 | .to_request(); 94 | let resp = test::call_service(&app, req).await; 95 | println!("{:?}", resp); 96 | assert!(resp.status().is_success()); 97 | } 98 | { 99 | let params = UtxosParams::new(false, Some(1), None, 1.0); 100 | let req = test::TestRequest::put() 101 | .uri("/wallet/utxos") 102 | .set_json(params) 103 | .to_request(); 104 | let resp = test::call_service(&app, req).await; 105 | println!("{:?}", resp); 106 | assert!(resp.status().is_success()); 107 | } 108 | let rgb20_result: Rgb20Result = { 109 | let params = Rgb20Params { 110 | ticker: gen_fake_ticker(), 111 | name: "Fake Monacoin".to_string(), 112 | presision: 8, 113 | amounts: vec![100.to_string()], 114 | }; 115 | let req = test::TestRequest::put() 116 | .uri("/wallet/issue/rgb20") 117 | .set_json(params) 118 | .to_request(); 119 | test::call_and_read_body_json(&app, req).await 120 | }; 121 | let params = AssetBalanceParams { 122 | asset_id: rgb20_result.asset_id, 123 | }; 124 | let req = test::TestRequest::get() 125 | .uri("/wallet/asset_balance") 126 | .set_json(params) 127 | .to_request(); 128 | let asset_balance_result: Balance = test::call_and_read_body_json(&app, req).await; 129 | assert_eq!(asset_balance_result.settled, "100"); 130 | assert_eq!(asset_balance_result.future, "100"); 131 | assert_eq!(asset_balance_result.spendable, "100"); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/wallet/assets.rs: -------------------------------------------------------------------------------- 1 | use crate::{wallet::Balance, ShiroWallet}; 2 | use actix_web::{put, web, HttpResponse, Responder}; 3 | use rgb_lib::wallet::AssetIface; 4 | use serde::Deserialize; 5 | use serde::Serialize; 6 | use std::sync::Mutex; 7 | 8 | #[derive(Deserialize, Serialize)] 9 | pub struct AssetsParams { 10 | filter_asset_types: Vec, 11 | } 12 | 13 | #[derive(Deserialize, Serialize)] 14 | pub struct Media { 15 | file_path: String, 16 | mime: String, 17 | } 18 | 19 | impl From for Media { 20 | fn from(x: rgb_lib::wallet::Media) -> Media { 21 | Media { 22 | file_path: x.file_path, 23 | mime: x.mime, 24 | } 25 | } 26 | } 27 | 28 | #[derive(Deserialize, Serialize)] 29 | pub struct AssetRgb20 { 30 | asset_id: String, 31 | ticker: String, 32 | name: String, 33 | precision: u8, 34 | balance: Balance, 35 | } 36 | 37 | impl From for AssetRgb20 { 38 | fn from(origin: rgb_lib::wallet::AssetRgb20) -> AssetRgb20 { 39 | AssetRgb20 { 40 | asset_id: origin.asset_id, 41 | ticker: origin.ticker, 42 | name: origin.name, 43 | precision: origin.precision, 44 | balance: Balance::from(origin.balance), 45 | } 46 | } 47 | } 48 | 49 | #[derive(Deserialize, Serialize)] 50 | pub struct AssetRgb25 { 51 | asset_id: String, 52 | name: String, 53 | precision: u8, 54 | description: Option, 55 | balance: Balance, 56 | data_paths: Vec, 57 | } 58 | 59 | impl From for AssetRgb25 { 60 | fn from(origin: rgb_lib::wallet::AssetRgb25) -> AssetRgb25 { 61 | AssetRgb25 { 62 | asset_id: origin.asset_id, 63 | name: origin.name, 64 | precision: origin.precision, 65 | description: origin.description, 66 | balance: Balance::from(origin.balance), 67 | data_paths: origin 68 | .data_paths 69 | .into_iter() 70 | .map(Media::from) 71 | .collect::>(), 72 | } 73 | } 74 | } 75 | 76 | #[derive(Deserialize, Serialize)] 77 | pub struct Assets { 78 | rgb20: Option>, 79 | rgb25: Option>, 80 | } 81 | 82 | impl From for Assets { 83 | fn from(x: rgb_lib::wallet::Assets) -> Assets { 84 | Assets { 85 | rgb20: x.rgb20.map(|vec| { 86 | vec.into_iter() 87 | .map(AssetRgb20::from) 88 | .collect::>() 89 | }), 90 | rgb25: x.rgb25.map(|vec| { 91 | vec.into_iter() 92 | .map(AssetRgb25::from) 93 | .collect::>() 94 | }), 95 | } 96 | } 97 | } 98 | 99 | #[derive(Deserialize, Serialize)] 100 | pub struct AssetsResult { 101 | assets: Assets, 102 | } 103 | 104 | #[put("/wallet/assets")] 105 | pub async fn put( 106 | params: web::Json, 107 | data: web::Data>, 108 | ) -> impl Responder { 109 | if data.lock().unwrap().wallet.is_some() { 110 | match actix_web::rt::task::spawn_blocking(move || { 111 | data.lock() 112 | .unwrap() 113 | .wallet 114 | .as_mut() 115 | .unwrap() 116 | .list_assets(params.filter_asset_types.clone()) 117 | }) 118 | .await 119 | .unwrap() 120 | { 121 | Ok(assets) => HttpResponse::Ok().json(AssetsResult { 122 | assets: Assets::from(assets), 123 | }), 124 | Err(e) => HttpResponse::BadRequest().body(e.to_string()), 125 | } 126 | } else { 127 | HttpResponse::BadRequest().body("wallet should be created first") 128 | } 129 | } 130 | 131 | #[cfg(test)] 132 | mod tests { 133 | use super::*; 134 | 135 | use actix_web::{test, web, App}; 136 | use rgb_lib::generate_keys; 137 | 138 | #[actix_web::test] 139 | async fn test_get() { 140 | let shiro_wallet = Mutex::new(ShiroWallet::new()); 141 | let app = test::init_service( 142 | App::new() 143 | .app_data(web::Data::new(shiro_wallet)) 144 | .service(put) 145 | .service(crate::wallet::put), 146 | ) 147 | .await; 148 | 149 | { 150 | let keys = generate_keys(rgb_lib::BitcoinNetwork::Regtest); 151 | let wallet_params = crate::wallet::WalletParams { 152 | mnemonic: keys.mnemonic, 153 | pubkey: keys.xpub, 154 | }; 155 | let req = test::TestRequest::put() 156 | .uri("/wallet") 157 | .set_json(wallet_params) 158 | .to_request(); 159 | let resp = test::call_service(&app, req).await; 160 | println!("{:?}", resp); 161 | assert!(resp.status().is_success()); 162 | } 163 | { 164 | let params = AssetsParams { 165 | filter_asset_types: vec![], 166 | }; 167 | let req = test::TestRequest::put() 168 | .uri("/wallet/assets") 169 | .set_json(params) 170 | .to_request(); 171 | let resp = test::call_service(&app, req).await; 172 | println!("{:?}", resp); 173 | assert!(resp.status().is_success()); 174 | } 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /src/wallet/blind.rs: -------------------------------------------------------------------------------- 1 | use crate::ShiroWallet; 2 | use actix_web::{put, web, HttpResponse, Responder}; 3 | use serde::Deserialize; 4 | use serde::Serialize; 5 | use std::sync::Mutex; 6 | 7 | #[derive(Clone, Serialize, Deserialize)] 8 | pub struct BlindParams { 9 | asset_id: Option, 10 | amount: Option, 11 | duration_seconds: Option, 12 | transport_endpoints: Vec, 13 | } 14 | 15 | pub struct BlindParamsForLib { 16 | asset_id: Option, 17 | amount: Option, 18 | duration_seconds: Option, 19 | transport_endpoints: Vec, 20 | } 21 | 22 | impl From for BlindParamsForLib { 23 | fn from(x: BlindParams) -> BlindParamsForLib { 24 | BlindParamsForLib { 25 | asset_id: x.asset_id, 26 | amount: x.amount.map(|str| str.parse::().unwrap()), 27 | duration_seconds: x.duration_seconds, 28 | transport_endpoints: x.transport_endpoints, 29 | } 30 | } 31 | } 32 | 33 | #[derive(Serialize, Deserialize)] 34 | pub struct BlindData { 35 | invoice: String, 36 | blinded_utxo: String, 37 | blinding_secret: String, 38 | expiration_timestamp: Option, 39 | } 40 | 41 | impl From for BlindData { 42 | fn from(x: rgb_lib::wallet::BlindData) -> BlindData { 43 | BlindData { 44 | invoice: x.invoice, 45 | blinded_utxo: x.blinded_utxo, 46 | blinding_secret: x.blinding_secret.to_string(), 47 | expiration_timestamp: x.expiration_timestamp.map(|x| x.to_string()), 48 | } 49 | } 50 | } 51 | 52 | #[put("/wallet/blind")] 53 | pub async fn put( 54 | params: web::Json, 55 | data: web::Data>, 56 | ) -> impl Responder { 57 | if data.lock().unwrap().wallet.is_some() { 58 | match actix_web::rt::task::spawn_blocking(move || { 59 | let params = BlindParamsForLib::from(params.clone()); 60 | data.lock().unwrap().wallet.as_mut().unwrap().blind( 61 | params.asset_id.clone(), 62 | params.amount, 63 | params.duration_seconds, 64 | params.transport_endpoints, 65 | ) 66 | }) 67 | .await 68 | .unwrap() 69 | { 70 | Ok(blind_data) => HttpResponse::Ok().json(BlindData::from(blind_data)), 71 | Err(e) => HttpResponse::BadRequest().body(e.to_string()), 72 | } 73 | } else { 74 | HttpResponse::BadRequest().body("wallet should be created first") 75 | } 76 | } 77 | 78 | #[cfg(test)] 79 | mod tests { 80 | use super::*; 81 | 82 | use crate::tests::PROXY_ENDPOINT; 83 | use crate::wallet::{ 84 | address::AddressResult, 85 | go_online::GoOnlineParams, 86 | issue::rgb20::{Rgb20Params, Rgb20Result}, 87 | tests::fund_wallet, 88 | utxos::UtxosParams, 89 | }; 90 | use actix_web::{test, web, App}; 91 | use rgb_lib::generate_keys; 92 | 93 | #[actix_web::test] 94 | async fn test_put() { 95 | let shiro_wallet = Mutex::new(ShiroWallet::new()); 96 | let app = test::init_service( 97 | App::new() 98 | .app_data(web::Data::new(shiro_wallet)) 99 | .service(crate::wallet::put) 100 | .service(crate::wallet::address::get) 101 | .service(crate::wallet::utxos::put) 102 | .service(crate::wallet::go_online::put) 103 | .service(crate::wallet::issue::rgb20::put) 104 | .service(put), 105 | ) 106 | .await; 107 | 108 | { 109 | let keys = generate_keys(rgb_lib::BitcoinNetwork::Regtest); 110 | let params = crate::wallet::WalletParams { 111 | mnemonic: keys.mnemonic, 112 | pubkey: keys.xpub, 113 | }; 114 | let req = test::TestRequest::put() 115 | .uri("/wallet") 116 | .set_json(params) 117 | .to_request(); 118 | let resp = test::call_service(&app, req).await; 119 | println!("{:?}", resp); 120 | assert!(resp.status().is_success()); 121 | } 122 | let address: AddressResult = { 123 | let req = test::TestRequest::get().uri("/wallet/address").to_request(); 124 | let resp = test::call_service(&app, req).await; 125 | println!("{:?}", resp); 126 | assert!(resp.status().is_success()); 127 | test::read_body_json(resp).await 128 | }; 129 | fund_wallet(address.new_address); 130 | { 131 | let params = GoOnlineParams::new(true, "127.0.0.1:50001".to_string()); 132 | let req = test::TestRequest::put() 133 | .uri("/wallet/go_online") 134 | .set_json(params) 135 | .to_request(); 136 | let resp = test::call_service(&app, req).await; 137 | println!("{:?}", resp); 138 | assert!(resp.status().is_success()); 139 | } 140 | { 141 | let params = UtxosParams::new(true, Some(1), None, 1.0); 142 | let req = test::TestRequest::put() 143 | .uri("/wallet/utxos") 144 | .set_json(params) 145 | .to_request(); 146 | let resp = test::call_service(&app, req).await; 147 | println!("{:?}", resp); 148 | assert!(resp.status().is_success()); 149 | } 150 | let rgb20_result: Rgb20Result = { 151 | let params = Rgb20Params { 152 | ticker: "FAKEMONA".to_string(), 153 | name: "Fake Monacoin".to_string(), 154 | presision: 8, 155 | amounts: vec![100.to_string()], 156 | }; 157 | let req = test::TestRequest::put() 158 | .uri("/wallet/issue/rgb20") 159 | .set_json(params) 160 | .to_request(); 161 | test::call_and_read_body_json(&app, req).await 162 | }; 163 | let params = BlindParams { 164 | asset_id: Some(rgb20_result.asset_id), 165 | amount: Some("10".to_string()), 166 | duration_seconds: Some(10), 167 | transport_endpoints: vec![PROXY_ENDPOINT.clone()], 168 | }; 169 | let req = test::TestRequest::put() 170 | .uri("/wallet/blind") 171 | .set_json(params) 172 | .to_request(); 173 | let resp = test::call_service(&app, req).await; 174 | println!("{:?}", resp); 175 | assert!(resp.status().is_success()); 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /src/wallet/data.rs: -------------------------------------------------------------------------------- 1 | use crate::wallet::ShiroWallet; 2 | use actix_web::{get, web, HttpResponse, Responder}; 3 | use rgb_lib::wallet::DatabaseType; 4 | use rgb_lib::BitcoinNetwork; 5 | use serde::Deserialize; 6 | use serde::Serialize; 7 | use std::sync::Mutex; 8 | 9 | #[derive(Serialize, Deserialize)] 10 | pub struct WalletDataResponse { 11 | /// Directory where the wallet directory is to be created 12 | pub data_dir: String, 13 | /// Bitcoin network for the wallet 14 | pub bitcoin_network: String, 15 | /// Database type for the wallet 16 | pub database_type: String, 17 | /// Wallet xpub 18 | pub pubkey: String, 19 | /// Wallet mnemonic phrase 20 | pub mnemonic: String, 21 | } 22 | 23 | #[get("/wallet/data")] 24 | pub async fn get(data: web::Data>) -> impl Responder { 25 | let shiro_wallet = data.lock().unwrap(); 26 | match &shiro_wallet.wallet { 27 | Some(wallet) => { 28 | let wdata = wallet.get_wallet_data(); 29 | HttpResponse::Ok().json(WalletDataResponse { 30 | data_dir: wdata.data_dir.clone(), 31 | bitcoin_network: match wdata.bitcoin_network { 32 | BitcoinNetwork::Mainnet => "mainnet", 33 | BitcoinNetwork::Testnet => "testnet", 34 | BitcoinNetwork::Regtest => "regtest", 35 | BitcoinNetwork::Signet => "signet", 36 | } 37 | .to_string(), 38 | database_type: match wdata.database_type { 39 | DatabaseType::Sqlite => "sqlite", 40 | } 41 | .to_string(), 42 | pubkey: wdata.pubkey.clone(), 43 | mnemonic: if let Some(mnemonic) = &wdata.mnemonic { 44 | mnemonic.clone() 45 | } else { 46 | "".to_string() 47 | }, 48 | }) 49 | } 50 | None => HttpResponse::BadRequest().body("wallet doesn't be initialized"), 51 | } 52 | } 53 | 54 | #[cfg(test)] 55 | mod tests { 56 | use super::*; 57 | use actix_web::{http, test, App}; 58 | 59 | use crate::wallet::ShiroWallet; 60 | 61 | #[actix_web::test] 62 | async fn test_get_failed() { 63 | let shiro_wallet = Mutex::new(ShiroWallet::new()); 64 | let app = test::init_service( 65 | App::new() 66 | .app_data(web::Data::new(shiro_wallet)) 67 | .service(get) 68 | .service(crate::wallet::put), 69 | ) 70 | .await; 71 | let wallet_params = crate::wallet::WalletParams { 72 | mnemonic: "".to_string(), 73 | pubkey: "".to_string(), 74 | }; 75 | let wallet_req = test::TestRequest::put() 76 | .uri("/wallet") 77 | .set_json(wallet_params) 78 | .to_request(); 79 | let wallet_resp = test::call_service(&app, wallet_req).await; 80 | println!("{:?}", wallet_resp); 81 | assert_eq!(wallet_resp.status(), http::StatusCode::BAD_REQUEST); 82 | 83 | let req = test::TestRequest::get().uri("/wallet/data").to_request(); 84 | 85 | let resp = test::call_service(&app, req).await; 86 | println!("{:?}", resp); 87 | 88 | assert_eq!(resp.status(), http::StatusCode::BAD_REQUEST); 89 | } 90 | 91 | #[actix_web::test] 92 | async fn test_get() { 93 | let shiro_wallet = Mutex::new(ShiroWallet::new()); 94 | let app = test::init_service( 95 | App::new() 96 | .app_data(web::Data::new(shiro_wallet)) 97 | .service(get) 98 | .service(crate::wallet::put), 99 | ) 100 | .await; 101 | let wallet_params = crate::wallet::WalletParams { 102 | mnemonic: "save call film frog usual market noodle hope stomach chat word worry".to_string(), 103 | pubkey: "xpub661MyMwAqRbcGexM5um6FYobDPjNH1tmWjxhDkbhfHfxvNpdsmhnvzCDGfemmmNLagBTSSno9nxvaknvDDvqux8sQqrfGPGzFc2JKnf4KL9".to_string(), 104 | }; 105 | let wallet_req = test::TestRequest::put() 106 | .uri("/wallet") 107 | .set_json(wallet_params) 108 | .to_request(); 109 | let wallet_resp = test::call_service(&app, wallet_req).await; 110 | println!("{:?}", wallet_resp); 111 | assert!(wallet_resp.status().is_success()); 112 | 113 | let req = test::TestRequest::get().uri("/wallet/data").to_request(); 114 | 115 | let resp = test::call_service(&app, req).await; 116 | println!("{:?}", resp); 117 | 118 | assert!(resp.status().is_success()); 119 | let body: WalletDataResponse = test::read_body_json(resp).await; 120 | assert_eq!(body.data_dir, "/tmp/shiro-wallet"); 121 | assert_eq!(body.bitcoin_network, "regtest"); 122 | assert_eq!(body.database_type, "sqlite"); 123 | assert_eq!(body.pubkey, "xpub661MyMwAqRbcGexM5um6FYobDPjNH1tmWjxhDkbhfHfxvNpdsmhnvzCDGfemmmNLagBTSSno9nxvaknvDDvqux8sQqrfGPGzFc2JKnf4KL9"); 124 | assert_eq!( 125 | body.mnemonic, 126 | "save call film frog usual market noodle hope stomach chat word worry" 127 | ); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/wallet/dir.rs: -------------------------------------------------------------------------------- 1 | use crate::ShiroWallet; 2 | use actix_web::{get, web, HttpResponse, Responder}; 3 | use serde::Deserialize; 4 | use serde::Serialize; 5 | use std::sync::Mutex; 6 | 7 | #[derive(Serialize, Deserialize)] 8 | pub struct WalletDir { 9 | wallet_dir: String, 10 | } 11 | 12 | #[get("/wallet/dir")] 13 | pub async fn get(data: web::Data>) -> impl Responder { 14 | let shiro_wallet = data.lock().unwrap(); 15 | match &shiro_wallet.wallet { 16 | Some(wallet) => HttpResponse::Ok().json(WalletDir { 17 | wallet_dir: wallet.get_wallet_data().data_dir, 18 | }), 19 | None => HttpResponse::BadRequest().body("wallet data has not been provided"), 20 | } 21 | } 22 | 23 | #[cfg(test)] 24 | mod tests { 25 | use super::*; 26 | use actix_web::{http, test, App}; 27 | 28 | #[actix_web::test] 29 | async fn test_get_failed() { 30 | let shiro_wallet = Mutex::new(ShiroWallet::new()); 31 | let app = test::init_service( 32 | App::new() 33 | .app_data(web::Data::new(shiro_wallet)) 34 | .service(get) 35 | .service(crate::wallet::put), 36 | ) 37 | .await; 38 | let wallet_params = crate::wallet::WalletParams { 39 | mnemonic: "".to_string(), 40 | pubkey: "".to_string(), 41 | }; 42 | let wallet_req = test::TestRequest::put() 43 | .uri("/wallet") 44 | .set_json(wallet_params) 45 | .to_request(); 46 | let wallet_resp = test::call_service(&app, wallet_req).await; 47 | println!("{:?}", wallet_resp); 48 | assert_eq!(wallet_resp.status(), http::StatusCode::BAD_REQUEST); 49 | 50 | let req = test::TestRequest::get().uri("/wallet/dir").to_request(); 51 | 52 | let resp = test::call_service(&app, req).await; 53 | println!("{:?}", resp); 54 | 55 | assert_eq!(wallet_resp.status(), http::StatusCode::BAD_REQUEST); 56 | } 57 | 58 | #[actix_web::test] 59 | async fn test_get() { 60 | let shiro_wallet = Mutex::new(ShiroWallet::new()); 61 | let app = test::init_service( 62 | App::new() 63 | .app_data(web::Data::new(shiro_wallet)) 64 | .service(get) 65 | .service(crate::wallet::put), 66 | ) 67 | .await; 68 | let wallet_params = crate::wallet::WalletParams { 69 | mnemonic: "save call film frog usual market noodle hope stomach chat word worry".to_string(), 70 | pubkey: "tpubD6NzVbkrYhZ4YT9CY6kBTU8xYWq2GQPq4NYzaJer1CRrffVLwzYt5Rs3WhjZJGKaNaiN42JfgtnyGwHXc5n5oPbAUSbxwuwDqZci5kdAZHb".to_string(), 71 | }; 72 | let wallet_req = test::TestRequest::put() 73 | .uri("/wallet") 74 | .set_json(wallet_params) 75 | .to_request(); 76 | let wallet_resp = test::call_service(&app, wallet_req).await; 77 | println!("{:?}", wallet_resp); 78 | assert!(wallet_resp.status().is_success()); 79 | 80 | let req = test::TestRequest::get().uri("/wallet/dir").to_request(); 81 | 82 | let resp = test::call_service(&app, req).await; 83 | println!("{:?}", resp); 84 | 85 | assert!(wallet_resp.status().is_success()); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/wallet/drain_to.rs: -------------------------------------------------------------------------------- 1 | use crate::ShiroWallet; 2 | use actix_web::{put, web, HttpResponse, Responder}; 3 | use serde::Deserialize; 4 | use serde::Serialize; 5 | use std::sync::Mutex; 6 | 7 | #[derive(Serialize, Deserialize)] 8 | pub struct DrainToParams { 9 | address: String, 10 | destroy_assets: bool, 11 | fee_rate: f32, 12 | } 13 | 14 | #[derive(Serialize, Deserialize)] 15 | pub struct DrainToResult { 16 | txid: String, 17 | } 18 | 19 | #[put("/wallet/drain_to")] 20 | pub async fn put( 21 | params: web::Json, 22 | data: web::Data>, 23 | ) -> impl Responder { 24 | if data.lock().unwrap().wallet.is_some() { 25 | if data.lock().unwrap().online.is_some() { 26 | match actix_web::rt::task::spawn_blocking(move || { 27 | let mut shiro_wallet = data.lock().unwrap(); 28 | let online = shiro_wallet.get_online().unwrap(); 29 | shiro_wallet.wallet.as_mut().unwrap().drain_to( 30 | online, 31 | params.address.clone(), 32 | params.destroy_assets, 33 | params.fee_rate, 34 | ) 35 | }) 36 | .await 37 | .unwrap() 38 | { 39 | Ok(txid) => HttpResponse::Ok().json(DrainToResult { txid }), 40 | Err(e) => HttpResponse::BadRequest().body(e.to_string()), 41 | } 42 | } else { 43 | HttpResponse::BadRequest().body("wallet should be online") 44 | } 45 | } else { 46 | HttpResponse::BadRequest().body("wallet should be created first") 47 | } 48 | } 49 | 50 | #[cfg(test)] 51 | mod tests { 52 | use super::*; 53 | 54 | use crate::wallet::{address::AddressResult, go_online::GoOnlineParams, WalletParams}; 55 | use actix_web::{http, test, web, App}; 56 | use rgb_lib::generate_keys; 57 | 58 | #[actix_web::test] 59 | async fn test_put_failed() { 60 | let shiro_wallet = Mutex::new(ShiroWallet::new()); 61 | let app = test::init_service( 62 | App::new() 63 | .app_data(web::Data::new(shiro_wallet)) 64 | .service(put) 65 | .service(crate::wallet::go_online::put) 66 | .service(crate::wallet::address::get) 67 | .service(crate::wallet::put), 68 | ) 69 | .await; 70 | 71 | { 72 | let keys = generate_keys(rgb_lib::BitcoinNetwork::Regtest); 73 | let wallet_params = WalletParams { 74 | mnemonic: keys.mnemonic, 75 | pubkey: keys.xpub, 76 | }; 77 | let wallet_req = test::TestRequest::put() 78 | .uri("/wallet") 79 | .set_json(wallet_params) 80 | .to_request(); 81 | let wallet_resp = test::call_service(&app, wallet_req).await; 82 | println!("{:?}", wallet_resp); 83 | assert!(wallet_resp.status().is_success()); 84 | } 85 | { 86 | let params = GoOnlineParams::new(true, "127.0.0.1:50001".to_string()); 87 | let req = test::TestRequest::put() 88 | .uri("/wallet/go_online") 89 | .set_json(params) 90 | .to_request(); 91 | let resp = test::call_service(&app, req).await; 92 | println!("{:?}", resp); 93 | assert!(resp.status().is_success()); 94 | } 95 | let address: AddressResult = { 96 | let req = test::TestRequest::get().uri("/wallet/address").to_request(); 97 | let resp = test::call_service(&app, req).await; 98 | println!("{:?}", resp); 99 | assert!(resp.status().is_success()); 100 | test::read_body_json(resp).await 101 | }; 102 | { 103 | let params = DrainToParams { 104 | address: address.new_address, 105 | destroy_assets: false, 106 | fee_rate: 0.0, 107 | }; 108 | let req = test::TestRequest::put() 109 | .uri("/wallet/drain_to") 110 | .set_json(params) 111 | .to_request(); 112 | let resp = test::call_service(&app, req).await; 113 | println!("{:?}", resp); 114 | assert_eq!(resp.status(), http::StatusCode::BAD_REQUEST); 115 | } 116 | } 117 | 118 | #[actix_web::test] 119 | #[ignore] 120 | async fn test_put_success() {} 121 | } 122 | -------------------------------------------------------------------------------- /src/wallet/go_online.rs: -------------------------------------------------------------------------------- 1 | use crate::ShiroWallet; 2 | use actix_web::{put, web, HttpResponse, Responder}; 3 | use serde::Deserialize; 4 | use serde::Serialize; 5 | use std::sync::Mutex; 6 | 7 | #[derive(Serialize, Deserialize)] 8 | pub struct GoOnlineParams { 9 | skip_consistency_check: bool, 10 | electrum_url: String, 11 | } 12 | 13 | impl GoOnlineParams { 14 | #[allow(dead_code)] 15 | pub fn new(skip_consistency_check: bool, electrum_url: String) -> GoOnlineParams { 16 | GoOnlineParams { 17 | skip_consistency_check, 18 | electrum_url, 19 | } 20 | } 21 | } 22 | 23 | #[derive(Serialize, Deserialize)] 24 | pub struct GoOnlineResult {} 25 | 26 | #[put("/wallet/go_online")] 27 | pub async fn put( 28 | params: web::Json, 29 | data: web::Data>, 30 | ) -> impl Responder { 31 | if data.lock().unwrap().wallet.is_some() { 32 | match actix_web::rt::task::spawn_blocking(move || { 33 | let mut shiro_wallet = data.lock().unwrap(); 34 | let result = shiro_wallet 35 | .wallet 36 | .as_mut() 37 | .unwrap() 38 | .go_online(params.skip_consistency_check, params.electrum_url.clone()); 39 | shiro_wallet.online = Some(result.unwrap()) 40 | }) 41 | .await 42 | { 43 | Ok(_) => HttpResponse::Ok().json(GoOnlineResult {}), 44 | Err(e) => HttpResponse::BadRequest().body(e.to_string()), 45 | } 46 | } else { 47 | HttpResponse::BadRequest().body("wallet should be created first") 48 | } 49 | } 50 | 51 | #[cfg(test)] 52 | mod tests { 53 | use super::*; 54 | 55 | use actix_web::{test, web, App}; 56 | use rgb_lib::generate_keys; 57 | 58 | #[actix_web::test] 59 | async fn test_put() { 60 | let shiro_wallet = Mutex::new(ShiroWallet::new()); 61 | let app = test::init_service( 62 | App::new() 63 | .app_data(web::Data::new(shiro_wallet)) 64 | .service(put) 65 | .service(crate::wallet::put), 66 | ) 67 | .await; 68 | 69 | let wallet_params = crate::wallet::WalletParams { 70 | mnemonic: "save call film frog usual market noodle hope stomach chat word worry".to_string(), 71 | pubkey: "tpubD6NzVbkrYhZ4YT9CY6kBTU8xYWq2GQPq4NYzaJer1CRrffVLwzYt5Rs3WhjZJGKaNaiN42JfgtnyGwHXc5n5oPbAUSbxwuwDqZci5kdAZHb".to_string(), 72 | }; 73 | let wallet_req = test::TestRequest::put() 74 | .uri("/wallet") 75 | .set_json(wallet_params) 76 | .to_request(); 77 | let wallet_resp = test::call_service(&app, wallet_req).await; 78 | println!("{:?}", wallet_resp); 79 | assert!(wallet_resp.status().is_success()); 80 | 81 | let params = GoOnlineParams { 82 | skip_consistency_check: true, 83 | electrum_url: "127.0.0.1:50001".to_string(), 84 | }; 85 | let req = test::TestRequest::put() 86 | .uri("/wallet/go_online") 87 | .set_json(params) 88 | .to_request(); 89 | let resp = test::call_service(&app, req).await; 90 | println!("{:?}", resp); 91 | assert!(resp.status().is_success()); 92 | } 93 | 94 | #[actix_web::test] 95 | async fn test_put_again() { 96 | let shiro_wallet = Mutex::new(ShiroWallet::new()); 97 | let app = test::init_service( 98 | App::new() 99 | .app_data(web::Data::new(shiro_wallet)) 100 | .service(put) 101 | .service(crate::wallet::put), 102 | ) 103 | .await; 104 | 105 | let keys = generate_keys(rgb_lib::BitcoinNetwork::Regtest); 106 | let wallet_params = crate::wallet::WalletParams { 107 | mnemonic: keys.mnemonic, 108 | pubkey: keys.xpub, 109 | }; 110 | let wallet_req = test::TestRequest::put() 111 | .uri("/wallet") 112 | .set_json(wallet_params) 113 | .to_request(); 114 | let wallet_resp = test::call_service(&app, wallet_req).await; 115 | println!("{:?}", wallet_resp); 116 | assert!(wallet_resp.status().is_success()); 117 | 118 | let params = GoOnlineParams { 119 | skip_consistency_check: true, 120 | electrum_url: "127.0.0.1:50001".to_string(), 121 | }; 122 | let req = test::TestRequest::put() 123 | .uri("/wallet/go_online") 124 | .set_json(params) 125 | .to_request(); 126 | let resp = test::call_service(&app, req).await; 127 | println!("{:?}", resp); 128 | assert!(resp.status().is_success()); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/wallet/invoice.rs: -------------------------------------------------------------------------------- 1 | use actix_web::{put, web, HttpResponse, Responder}; 2 | use rgb_lib::wallet::{Invoice, InvoiceData}; 3 | use serde::Deserialize; 4 | use serde::Serialize; 5 | 6 | #[derive(Deserialize, Serialize)] 7 | pub struct RgbInvoice { 8 | invoice_string: String, 9 | } 10 | 11 | #[put("/wallet/invoice")] 12 | pub async fn put(params: web::Json) -> impl Responder { 13 | let decoded = Invoice::new(params.invoice_string.clone()); 14 | match decoded { 15 | Ok(invoice) => HttpResponse::Ok().json(InvoiceData { 16 | asset_iface: invoice.invoice_data().asset_iface, 17 | blinded_utxo: invoice.invoice_data().blinded_utxo, 18 | asset_id: invoice.invoice_data().asset_id, 19 | amount: invoice.invoice_data().amount, 20 | expiration_timestamp: invoice.invoice_data().expiration_timestamp, 21 | transport_endpoints: invoice.invoice_data().transport_endpoints, 22 | }), 23 | Err(e) => HttpResponse::BadRequest().body(e.to_string()), 24 | } 25 | } 26 | 27 | #[cfg(test)] 28 | mod tests { 29 | use super::*; 30 | 31 | use actix_web::{http, test, App}; 32 | use rgb_lib::wallet::BlindedUTXO; 33 | 34 | #[actix_web::test] 35 | async fn test_put_invoice() { 36 | let app = test::init_service(App::new().service(put)).await; 37 | let payload = RgbInvoice { 38 | invoice_string: ("rgb:9usESnQYgX2KWNycD3cYRGddBc65uDC6gPeHjV9XzbHU/RGB20/10+DLrwJdhSdhhhxrUGZudY6C6ubbdPn14SZ1FLuTT3nUER?expiry=1694222774&endpoints=rpc://127.0.0.1:3000/json-rpc") 39 | .to_string(), 40 | }; 41 | let req = test::TestRequest::put() 42 | .uri("/wallet/invoice") 43 | .set_json(payload) 44 | .to_request(); 45 | let resp = test::call_service(&app, req).await; 46 | println!("{:?}", resp); 47 | assert!(resp.status().is_success()); 48 | let body: InvoiceData = test::read_body_json(resp).await; 49 | let result = BlindedUTXO::new(body.blinded_utxo); 50 | assert!(result.is_ok()); 51 | } 52 | 53 | #[actix_web::test] 54 | async fn test_put_with_bad_request() { 55 | let app = test::init_service(App::new().service(put)).await; 56 | let payload = RgbInvoice { 57 | invoice_string: ("helloRGB").to_string(), 58 | }; 59 | let req = test::TestRequest::put() 60 | .uri("/wallet/invoice") 61 | .set_json(payload) 62 | .to_request(); 63 | let resp = test::call_service(&app, req).await; 64 | println!("{:?}", resp); 65 | assert_eq!(resp.status(), http::StatusCode::BAD_REQUEST); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/wallet/issue.rs: -------------------------------------------------------------------------------- 1 | pub mod rgb20; 2 | -------------------------------------------------------------------------------- /src/wallet/issue/rgb20.rs: -------------------------------------------------------------------------------- 1 | use crate::{wallet::Balance, ShiroWallet}; 2 | use actix_web::{put, web, HttpResponse, Responder}; 3 | use serde::Deserialize; 4 | use serde::Serialize; 5 | use std::sync::Mutex; 6 | 7 | #[derive(Serialize, Deserialize)] 8 | pub struct Rgb20Params { 9 | pub ticker: String, 10 | pub name: String, 11 | pub presision: u8, 12 | pub amounts: Vec, 13 | } 14 | 15 | #[derive(Serialize, Deserialize)] 16 | pub struct Rgb20Result { 17 | pub asset_id: String, 18 | pub ticker: String, 19 | pub name: String, 20 | pub presision: u8, 21 | pub balance: Balance, 22 | } 23 | 24 | #[put("/wallet/issue/rgb20")] 25 | pub async fn put( 26 | params: web::Json, 27 | data: web::Data>, 28 | ) -> impl Responder { 29 | if data.lock().unwrap().wallet.is_some() { 30 | if data.lock().unwrap().online.is_some() { 31 | match actix_web::rt::task::spawn_blocking(move || { 32 | let mut shiro_wallet = data.lock().unwrap(); 33 | let online = shiro_wallet.get_online().unwrap(); 34 | shiro_wallet.wallet.as_mut().unwrap().issue_asset_rgb20( 35 | online, 36 | params.ticker.clone(), 37 | params.name.clone(), 38 | params.presision, 39 | params 40 | .amounts 41 | .clone() 42 | .into_iter() 43 | .flat_map(|str| str.parse::()) 44 | .collect(), 45 | ) 46 | }) 47 | .await 48 | .unwrap() 49 | { 50 | Ok(asset) => HttpResponse::Ok().json(Rgb20Result { 51 | asset_id: asset.asset_id, 52 | ticker: asset.ticker, 53 | name: asset.name, 54 | presision: asset.precision, 55 | balance: asset.balance.into(), 56 | }), 57 | Err(e) => { 58 | println!("{:#?}", e); 59 | HttpResponse::BadRequest().body(e.to_string()) 60 | } 61 | } 62 | } else { 63 | HttpResponse::BadRequest().body("wallet should be online") 64 | } 65 | } else { 66 | HttpResponse::BadRequest().body("wallet should be created first") 67 | } 68 | } 69 | 70 | #[cfg(test)] 71 | mod tests { 72 | use super::*; 73 | 74 | use crate::wallet::{ 75 | address::AddressResult, go_online::GoOnlineParams, tests::fund_wallet, utxos::UtxosParams, 76 | }; 77 | use actix_web::{http, test, web, App}; 78 | use rgb_lib::generate_keys; 79 | 80 | #[actix_web::test] 81 | async fn test_put() { 82 | let shiro_wallet = Mutex::new(ShiroWallet::new()); 83 | let app = test::init_service( 84 | App::new() 85 | .app_data(web::Data::new(shiro_wallet)) 86 | .service(put) 87 | .service(crate::wallet::put) 88 | .service(crate::wallet::address::get) 89 | .service(crate::wallet::utxos::put) 90 | .service(crate::wallet::go_online::put), 91 | ) 92 | .await; 93 | 94 | { 95 | let keys = generate_keys(rgb_lib::BitcoinNetwork::Regtest); 96 | let params = crate::wallet::WalletParams { 97 | mnemonic: keys.mnemonic, 98 | pubkey: keys.xpub, 99 | }; 100 | let req = test::TestRequest::put() 101 | .uri("/wallet") 102 | .set_json(params) 103 | .to_request(); 104 | let resp = test::call_service(&app, req).await; 105 | println!("{:?}", resp); 106 | assert!(resp.status().is_success()); 107 | } 108 | let address: AddressResult = { 109 | let req = test::TestRequest::get().uri("/wallet/address").to_request(); 110 | let resp = test::call_service(&app, req).await; 111 | println!("{:?}", resp); 112 | assert!(resp.status().is_success()); 113 | test::read_body_json(resp).await 114 | }; 115 | fund_wallet(address.new_address.clone()); 116 | { 117 | let params = GoOnlineParams::new(true, "127.0.0.1:50001".to_string()); 118 | let req = test::TestRequest::put() 119 | .uri("/wallet/go_online") 120 | .set_json(params) 121 | .to_request(); 122 | let resp = test::call_service(&app, req).await; 123 | println!("{:?}", resp); 124 | assert!(resp.status().is_success()); 125 | } 126 | { 127 | let params = UtxosParams::new(false, Some(1), None, 1.0); 128 | let req = test::TestRequest::put() 129 | .uri("/wallet/utxos") 130 | .set_json(params) 131 | .to_request(); 132 | let resp = test::call_service(&app, req).await; 133 | println!("{:?}", resp); 134 | assert!(resp.status().is_success()); 135 | } 136 | { 137 | let params = Rgb20Params { 138 | ticker: "FAKEMONA".to_string(), 139 | name: "Fake Monacoin".to_string(), 140 | presision: 8, 141 | amounts: vec![100.to_string()], 142 | }; 143 | let req = test::TestRequest::put() 144 | .uri("/wallet/issue/rgb20") 145 | .set_json(params) 146 | .to_request(); 147 | let resp = test::call_service(&app, req).await; 148 | println!("{:?}", resp); 149 | assert!(resp.status().is_success()); 150 | } 151 | { 152 | let params = Rgb20Params { 153 | ticker: "FAKEMONA".to_string(), 154 | name: "Fake Monacoin".to_string(), 155 | presision: 8, 156 | amounts: vec!["".to_string()], 157 | }; 158 | let req = test::TestRequest::put() 159 | .uri("/wallet/issue/rgb20") 160 | .set_json(params) 161 | .to_request(); 162 | let resp = test::call_service(&app, req).await; 163 | println!("{:?}", resp); 164 | assert_eq!(resp.status(), http::StatusCode::BAD_REQUEST); 165 | } 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/wallet/refresh.rs: -------------------------------------------------------------------------------- 1 | use crate::ShiroWallet; 2 | use actix_web::{post, web, HttpResponse, Responder}; 3 | use rgb_lib::wallet::RefreshTransferStatus; 4 | use serde::Deserialize; 5 | use serde::Serialize; 6 | use std::sync::Mutex; 7 | 8 | #[derive(Serialize, Deserialize)] 9 | pub struct RefreshParams { 10 | asset_id: Option, 11 | filter: Vec, 12 | } 13 | 14 | #[derive(Deserialize, Serialize)] 15 | struct RefreshFilter { 16 | status: String, 17 | incoming: bool, 18 | } 19 | 20 | impl RefreshFilter { 21 | fn conv(&self) -> rgb_lib::wallet::RefreshFilter { 22 | rgb_lib::wallet::RefreshFilter { 23 | status: match self.status.as_str() { 24 | "WaitingCounterparty" => RefreshTransferStatus::WaitingCounterparty, 25 | "WaitingConfirmations" => RefreshTransferStatus::WaitingConfirmations, 26 | &_ => panic!("Unknown status"), 27 | }, 28 | incoming: self.incoming, 29 | } 30 | } 31 | } 32 | 33 | #[derive(Serialize, Deserialize)] 34 | pub struct RefreshResult { 35 | result: bool, 36 | } 37 | 38 | #[post("/wallet/refresh")] 39 | pub async fn post( 40 | params: web::Json, 41 | data: web::Data>, 42 | ) -> impl Responder { 43 | if data.lock().unwrap().wallet.is_some() { 44 | if data.lock().unwrap().online.is_some() { 45 | match actix_web::rt::task::spawn_blocking(move || { 46 | let mut shiro_wallet = data.lock().unwrap(); 47 | let online = shiro_wallet.get_online().unwrap(); 48 | shiro_wallet.wallet.as_mut().unwrap().refresh( 49 | online, 50 | params.asset_id.clone(), 51 | params 52 | .filter 53 | .iter() 54 | .map(|x| x.conv()) 55 | .collect::>(), 56 | ) 57 | }) 58 | .await 59 | .unwrap() 60 | { 61 | Ok(result) => HttpResponse::Ok().json(RefreshResult { result }), 62 | Err(e) => HttpResponse::BadRequest().body(e.to_string()), 63 | } 64 | } else { 65 | HttpResponse::BadRequest().body("wallet should be online") 66 | } 67 | } else { 68 | HttpResponse::BadRequest().body("wallet should be created first") 69 | } 70 | } 71 | 72 | #[cfg(test)] 73 | mod tests { 74 | use super::*; 75 | 76 | use crate::wallet::{ 77 | address::AddressResult, 78 | go_online::GoOnlineParams, 79 | issue::rgb20::{Rgb20Params, Rgb20Result}, 80 | tests::{fund_wallet, gen_fake_ticker}, 81 | utxos::UtxosParams, 82 | }; 83 | use actix_web::{test, web, App}; 84 | use rgb_lib::generate_keys; 85 | 86 | #[actix_web::test] 87 | async fn test_post() { 88 | let shiro_wallet = Mutex::new(ShiroWallet::new()); 89 | let app = test::init_service( 90 | App::new() 91 | .app_data(web::Data::new(shiro_wallet)) 92 | .service(crate::wallet::put) 93 | .service(crate::wallet::address::get) 94 | .service(crate::wallet::utxos::put) 95 | .service(crate::wallet::go_online::put) 96 | .service(crate::wallet::issue::rgb20::put) 97 | .service(post), 98 | ) 99 | .await; 100 | 101 | { 102 | let keys = generate_keys(rgb_lib::BitcoinNetwork::Regtest); 103 | let params = crate::wallet::WalletParams { 104 | mnemonic: keys.mnemonic, 105 | pubkey: keys.xpub, 106 | }; 107 | let req = test::TestRequest::put() 108 | .uri("/wallet") 109 | .set_json(params) 110 | .to_request(); 111 | let resp = test::call_service(&app, req).await; 112 | println!("{:?}", resp); 113 | assert!(resp.status().is_success()); 114 | } 115 | let address: AddressResult = { 116 | let req = test::TestRequest::get().uri("/wallet/address").to_request(); 117 | let resp = test::call_service(&app, req).await; 118 | println!("{:?}", resp); 119 | assert!(resp.status().is_success()); 120 | test::read_body_json(resp).await 121 | }; 122 | fund_wallet(address.new_address.clone()); 123 | { 124 | let params = GoOnlineParams::new(true, "127.0.0.1:50001".to_string()); 125 | let req = test::TestRequest::put() 126 | .uri("/wallet/go_online") 127 | .set_json(params) 128 | .to_request(); 129 | let resp = test::call_service(&app, req).await; 130 | println!("{:?}", resp); 131 | assert!(resp.status().is_success()); 132 | } 133 | { 134 | let params = UtxosParams::new(false, Some(1), None, 1.0); 135 | let req = test::TestRequest::put() 136 | .uri("/wallet/utxos") 137 | .set_json(params) 138 | .to_request(); 139 | let resp = test::call_service(&app, req).await; 140 | println!("{:?}", resp); 141 | assert!(resp.status().is_success()); 142 | } 143 | let rgb20_result: Rgb20Result = { 144 | let params = Rgb20Params { 145 | ticker: gen_fake_ticker(), 146 | name: "Fake Monacoin".to_string(), 147 | presision: 8, 148 | amounts: vec![100.to_string()], 149 | }; 150 | let req = test::TestRequest::put() 151 | .uri("/wallet/issue/rgb20") 152 | .set_json(params) 153 | .to_request(); 154 | test::call_and_read_body_json(&app, req).await 155 | }; 156 | let params = RefreshParams { 157 | asset_id: Some(rgb20_result.asset_id), 158 | filter: vec![], 159 | }; 160 | let req = test::TestRequest::post() 161 | .uri("/wallet/refresh") 162 | .set_json(params) 163 | .to_request(); 164 | let res: RefreshResult = test::call_and_read_body_json(&app, req).await; 165 | assert!(!res.result); 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/wallet/send.rs: -------------------------------------------------------------------------------- 1 | use crate::ShiroWallet; 2 | use actix_web::{post, web, HttpResponse, Responder}; 3 | use serde::Deserialize; 4 | use serde::Serialize; 5 | use std::collections::HashMap; 6 | use std::sync::Mutex; 7 | 8 | #[derive(Serialize, Deserialize)] 9 | pub struct SendParams { 10 | recipient_map: HashMap>, 11 | donation: bool, 12 | fee_rate: f32, 13 | } 14 | 15 | #[derive(Serialize, Deserialize)] 16 | struct Recipient { 17 | blinded_utxo: String, 18 | amount: String, 19 | transport_endpoints: Vec, 20 | } 21 | 22 | impl Recipient { 23 | pub fn conv(&self) -> rgb_lib::wallet::Recipient { 24 | rgb_lib::wallet::Recipient { 25 | blinded_utxo: self.blinded_utxo.clone(), 26 | amount: str::parse::(&self.amount).unwrap(), 27 | transport_endpoints: self.transport_endpoints.clone(), 28 | } 29 | } 30 | } 31 | 32 | impl From for Recipient { 33 | fn from(x: rgb_lib::wallet::Recipient) -> Recipient { 34 | Recipient { 35 | blinded_utxo: x.blinded_utxo, 36 | amount: x.amount.to_string(), 37 | transport_endpoints: x.transport_endpoints, 38 | } 39 | } 40 | } 41 | 42 | #[derive(Serialize, Deserialize)] 43 | pub struct SendResult { 44 | txid: String, 45 | } 46 | 47 | #[post("/wallet/send")] 48 | pub async fn post( 49 | params: web::Json, 50 | data: web::Data>, 51 | ) -> impl Responder { 52 | if data.lock().unwrap().wallet.is_some() { 53 | if data.lock().unwrap().online.is_some() { 54 | match actix_web::rt::task::spawn_blocking(move || { 55 | let mut shiro_wallet = data.lock().unwrap(); 56 | let online = shiro_wallet.get_online().unwrap(); 57 | let recipient_map = params 58 | .recipient_map 59 | .iter() 60 | .map(|(psbt, recipients)| { 61 | ( 62 | psbt.clone(), 63 | recipients 64 | .iter() 65 | .map(|recipient| recipient.conv()) 66 | .collect::>(), 67 | ) 68 | }) 69 | .collect::>(); 70 | shiro_wallet.wallet.as_mut().unwrap().send( 71 | online, 72 | recipient_map, 73 | params.donation, 74 | params.fee_rate, 75 | ) 76 | }) 77 | .await 78 | .unwrap() 79 | { 80 | Ok(txid) => HttpResponse::Ok().json(SendResult { txid }), 81 | Err(e) => HttpResponse::BadRequest().body(e.to_string()), 82 | } 83 | } else { 84 | HttpResponse::BadRequest().body("wallet should be online") 85 | } 86 | } else { 87 | HttpResponse::BadRequest().body("wallet should be created first") 88 | } 89 | } 90 | 91 | #[cfg(test)] 92 | mod tests { 93 | use super::*; 94 | 95 | use crate::tests::PROXY_ENDPOINT; 96 | use crate::wallet::{ 97 | address::AddressResult, 98 | go_online::GoOnlineParams, 99 | issue::rgb20::{Rgb20Params, Rgb20Result}, 100 | tests::fund_wallet, 101 | utxos::UtxosParams, 102 | }; 103 | use actix_web::{test, web, App}; 104 | use rgb_lib::{ 105 | generate_keys, 106 | wallet::{Wallet, WalletData}, 107 | }; 108 | 109 | async fn get_blinded_utxo() -> String { 110 | let keys = generate_keys(rgb_lib::BitcoinNetwork::Regtest); 111 | let base_data = shiro_backend::opts::get_wallet_data(); 112 | let wallet_data = WalletData { 113 | data_dir: base_data.data_dir, 114 | bitcoin_network: base_data.bitcoin_network, 115 | database_type: base_data.database_type, 116 | pubkey: keys.xpub, 117 | mnemonic: Some(keys.mnemonic), 118 | }; 119 | actix_web::rt::task::spawn_blocking(move || { 120 | let mut wallet = Wallet::new(wallet_data).unwrap(); 121 | let address = wallet.get_address(); 122 | fund_wallet(address); 123 | let online = wallet 124 | .go_online(true, "127.0.0.1:50001".to_string()) 125 | .unwrap(); 126 | wallet 127 | .create_utxos(online, true, Some(1), None, 1.0) 128 | .unwrap(); 129 | let blind_data = wallet 130 | .blind(None, None, None, vec![PROXY_ENDPOINT.clone()]) 131 | .unwrap(); 132 | //let blind_data = wallet.blind(Some(asset_id), Some(10), None).unwrap(); 133 | blind_data.blinded_utxo 134 | }) 135 | .await 136 | .unwrap() 137 | } 138 | 139 | #[actix_web::test] 140 | async fn test_post() { 141 | let shiro_wallet = Mutex::new(ShiroWallet::new()); 142 | let app = test::init_service( 143 | App::new() 144 | .app_data(web::Data::new(shiro_wallet)) 145 | .service(crate::wallet::put) 146 | .service(crate::wallet::address::get) 147 | .service(crate::wallet::utxos::put) 148 | .service(crate::wallet::go_online::put) 149 | .service(crate::wallet::issue::rgb20::put) 150 | .service(crate::wallet::blind::put) 151 | .service(post), 152 | ) 153 | .await; 154 | 155 | { 156 | let keys = generate_keys(rgb_lib::BitcoinNetwork::Regtest); 157 | let params = crate::wallet::WalletParams { 158 | mnemonic: keys.mnemonic, 159 | pubkey: keys.xpub, 160 | }; 161 | let req = test::TestRequest::put() 162 | .uri("/wallet") 163 | .set_json(params) 164 | .to_request(); 165 | let resp = test::call_service(&app, req).await; 166 | println!("{:?}", resp); 167 | assert!(resp.status().is_success()); 168 | } 169 | let address: AddressResult = { 170 | let req = test::TestRequest::get().uri("/wallet/address").to_request(); 171 | let resp = test::call_service(&app, req).await; 172 | println!("{:?}", resp); 173 | assert!(resp.status().is_success()); 174 | test::read_body_json(resp).await 175 | }; 176 | fund_wallet(address.new_address.clone()); 177 | fund_wallet(address.new_address); 178 | { 179 | let params = GoOnlineParams::new(true, "127.0.0.1:50001".to_string()); 180 | let req = test::TestRequest::put() 181 | .uri("/wallet/go_online") 182 | .set_json(params) 183 | .to_request(); 184 | let resp = test::call_service(&app, req).await; 185 | println!("{:?}", resp); 186 | assert!(resp.status().is_success()); 187 | } 188 | { 189 | let params = UtxosParams::new(true, Some(2), None, 1.0); 190 | let req = test::TestRequest::put() 191 | .uri("/wallet/utxos") 192 | .set_json(params) 193 | .to_request(); 194 | let resp = test::call_service(&app, req).await; 195 | assert!(resp.status().is_success()); 196 | } 197 | let rgb20_result: Rgb20Result = { 198 | let params = Rgb20Params { 199 | ticker: "FAKEMONA".to_string(), 200 | name: "Fake Monacoin".to_string(), 201 | presision: 7, 202 | amounts: vec![666.to_string()], 203 | }; 204 | let req = test::TestRequest::put() 205 | .uri("/wallet/issue/rgb20") 206 | .set_json(params) 207 | .to_request(); 208 | test::call_and_read_body_json(&app, req).await 209 | }; 210 | let blinded_utxo = get_blinded_utxo().await; 211 | let mut recipient_map = HashMap::new(); 212 | recipient_map.insert( 213 | rgb20_result.asset_id, 214 | vec![Recipient { 215 | blinded_utxo, 216 | amount: "10".to_string(), 217 | transport_endpoints: vec![PROXY_ENDPOINT.clone()], 218 | }], 219 | ); 220 | let params = SendParams { 221 | recipient_map, 222 | donation: false, 223 | fee_rate: 1.0, 224 | }; 225 | let req = test::TestRequest::post() 226 | .uri("/wallet/send") 227 | .set_json(params) 228 | .to_request(); 229 | let resp = test::call_service(&app, req).await; 230 | println!("{:?}", resp); 231 | assert!(resp.status().is_success()); 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /src/wallet/transfers.rs: -------------------------------------------------------------------------------- 1 | use crate::ShiroWallet; 2 | use actix_web::{delete, put, web, HttpResponse, Responder}; 3 | use rgb_lib::{ 4 | wallet::{Outpoint, TransferKind}, 5 | TransferStatus, 6 | }; 7 | use serde::Deserialize; 8 | use serde::Serialize; 9 | use std::sync::Mutex; 10 | 11 | #[derive(Serialize, Deserialize)] 12 | pub struct TransferParams { 13 | asset_id: String, 14 | } 15 | 16 | #[derive(Serialize, Deserialize)] 17 | pub struct Transfer { 18 | idx: String, 19 | created_at: String, 20 | updated_at: String, 21 | status: String, 22 | amount: String, 23 | kind: String, 24 | txid: Option, 25 | blinded_utxo: Option, 26 | unblinded_utxo: Option, 27 | change_utxo: Option, 28 | blinding_secret: Option, 29 | expiration: Option, 30 | } 31 | 32 | impl From for Transfer { 33 | fn from(x: rgb_lib::wallet::Transfer) -> Transfer { 34 | Transfer { 35 | idx: x.idx.to_string(), 36 | created_at: x.created_at.to_string(), 37 | updated_at: x.updated_at.to_string(), 38 | status: match x.status { 39 | TransferStatus::WaitingCounterparty => "WaitingCounterparty", 40 | TransferStatus::WaitingConfirmations => "WaitingConfirmations", 41 | TransferStatus::Settled => "Settled", 42 | TransferStatus::Failed => "Failed", 43 | } 44 | .to_string(), 45 | amount: x.amount.to_string(), 46 | kind: match x.kind { 47 | TransferKind::Issuance => "issuance", 48 | TransferKind::Receive => "receive", 49 | TransferKind::Send => "send", 50 | } 51 | .to_string(), 52 | txid: x.txid, 53 | blinded_utxo: x.blinded_utxo, 54 | unblinded_utxo: x.unblinded_utxo, 55 | change_utxo: x.change_utxo, 56 | blinding_secret: x.blinding_secret.map(|n| n.to_string()), 57 | expiration: x.expiration.map(|n| n.to_string()), 58 | } 59 | } 60 | } 61 | 62 | #[derive(Serialize, Deserialize)] 63 | pub struct TransferResult { 64 | transfers: Vec, 65 | } 66 | 67 | #[put("/wallet/transfers")] 68 | pub async fn put( 69 | params: web::Json, 70 | data: web::Data>, 71 | ) -> impl Responder { 72 | if data.lock().unwrap().wallet.is_some() { 73 | match actix_web::rt::task::spawn_blocking(move || { 74 | data.lock() 75 | .unwrap() 76 | .wallet 77 | .as_mut() 78 | .unwrap() 79 | .list_transfers(params.asset_id.clone()) 80 | }) 81 | .await 82 | .unwrap() 83 | { 84 | Ok(transfers) => HttpResponse::Ok().json( 85 | transfers 86 | .into_iter() 87 | .map(Transfer::from) 88 | .collect::>(), 89 | ), 90 | Err(e) => HttpResponse::BadRequest().body(e.to_string()), 91 | } 92 | } else { 93 | HttpResponse::BadRequest().body("wallet should be created first") 94 | } 95 | } 96 | 97 | #[derive(Deserialize, Serialize)] 98 | pub struct TransferDeleteParams { 99 | blinded_utxo: Option, 100 | txid: Option, 101 | no_asset_only: bool, 102 | } 103 | 104 | #[derive(Deserialize, Serialize)] 105 | pub struct TransferDeleteResult { 106 | transfers_changed: bool, 107 | } 108 | 109 | #[delete("/wallet/transfers")] 110 | pub async fn delete( 111 | params: web::Json, 112 | data: web::Data>, 113 | ) -> impl Responder { 114 | if data.lock().unwrap().wallet.is_some() { 115 | match actix_web::rt::task::spawn_blocking(move || { 116 | data.lock() 117 | .unwrap() 118 | .wallet 119 | .as_mut() 120 | .unwrap() 121 | .delete_transfers( 122 | params.blinded_utxo.clone(), 123 | params.txid.clone(), 124 | params.no_asset_only, 125 | ) 126 | }) 127 | .await 128 | .unwrap() 129 | { 130 | Ok(transfers_changed) => { 131 | HttpResponse::Ok().json(TransferDeleteResult { transfers_changed }) 132 | } 133 | Err(e) => HttpResponse::BadRequest().body(e.to_string()), 134 | } 135 | } else { 136 | HttpResponse::BadRequest().body("wallet should be created first") 137 | } 138 | } 139 | 140 | #[cfg(test)] 141 | mod tests { 142 | use super::*; 143 | 144 | use crate::wallet::{ 145 | address::AddressResult, 146 | go_online::GoOnlineParams, 147 | issue::rgb20::{Rgb20Params, Rgb20Result}, 148 | tests::fund_wallet, 149 | utxos::UtxosParams, 150 | }; 151 | use actix_web::{test, web, App}; 152 | use rgb_lib::generate_keys; 153 | 154 | #[actix_web::test] 155 | async fn test() { 156 | let shiro_wallet = Mutex::new(ShiroWallet::new()); 157 | let app = test::init_service( 158 | App::new() 159 | .app_data(web::Data::new(shiro_wallet)) 160 | .service(crate::wallet::put) 161 | .service(crate::wallet::address::get) 162 | .service(crate::wallet::utxos::put) 163 | .service(crate::wallet::go_online::put) 164 | .service(crate::wallet::issue::rgb20::put) 165 | .service(put), 166 | ) 167 | .await; 168 | 169 | { 170 | let keys = generate_keys(rgb_lib::BitcoinNetwork::Regtest); 171 | let params = crate::wallet::WalletParams { 172 | mnemonic: keys.mnemonic, 173 | pubkey: keys.xpub, 174 | }; 175 | let req = test::TestRequest::put() 176 | .uri("/wallet") 177 | .set_json(params) 178 | .to_request(); 179 | let resp = test::call_service(&app, req).await; 180 | println!("{:?}", resp); 181 | assert!(resp.status().is_success()); 182 | } 183 | let address: AddressResult = { 184 | let req = test::TestRequest::get().uri("/wallet/address").to_request(); 185 | let resp = test::call_service(&app, req).await; 186 | println!("{:?}", resp); 187 | assert!(resp.status().is_success()); 188 | test::read_body_json(resp).await 189 | }; 190 | fund_wallet(address.new_address); 191 | { 192 | let params = GoOnlineParams::new(true, "127.0.0.1:50001".to_string()); 193 | let req = test::TestRequest::put() 194 | .uri("/wallet/go_online") 195 | .set_json(params) 196 | .to_request(); 197 | let resp = test::call_service(&app, req).await; 198 | println!("{:?}", resp); 199 | assert!(resp.status().is_success()); 200 | } 201 | { 202 | let params = UtxosParams::new(true, Some(1), None, 1.0); 203 | let req = test::TestRequest::put() 204 | .uri("/wallet/utxos") 205 | .set_json(params) 206 | .to_request(); 207 | let resp = test::call_service(&app, req).await; 208 | println!("{:?}", resp); 209 | assert!(resp.status().is_success()); 210 | } 211 | let rgb20_result: Rgb20Result = { 212 | let params = Rgb20Params { 213 | ticker: "FAKEMONA".to_string(), 214 | name: "Fake Monacoin".to_string(), 215 | presision: 8, 216 | amounts: vec![100.to_string()], 217 | }; 218 | let req = test::TestRequest::put() 219 | .uri("/wallet/issue/rgb20") 220 | .set_json(params) 221 | .to_request(); 222 | test::call_and_read_body_json(&app, req).await 223 | }; 224 | let params = TransferParams { 225 | asset_id: rgb20_result.asset_id, 226 | }; 227 | let req = test::TestRequest::put() 228 | .uri("/wallet/transfers") 229 | .set_json(params) 230 | .to_request(); 231 | let resp = test::call_service(&app, req).await; 232 | assert!(resp.status().is_success()); 233 | println!("put {:?}", test::read_body(resp).await); 234 | { 235 | let params = TransferDeleteParams { 236 | blinded_utxo: None, 237 | txid: None, 238 | no_asset_only: false, 239 | }; 240 | let req = test::TestRequest::delete() 241 | .uri("/wallet/transfers") 242 | .set_json(params) 243 | .to_request(); 244 | let resp = test::call_service(&app, req).await; 245 | let status = resp.status().is_client_error(); 246 | println!("delete {:?}", test::read_body(resp).await); 247 | assert!(status); 248 | } 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /src/wallet/unspents.rs: -------------------------------------------------------------------------------- 1 | use crate::ShiroWallet; 2 | use actix_web::{put, web, HttpResponse, Responder}; 3 | use rgb_lib::wallet::Outpoint; 4 | use serde::Deserialize; 5 | use serde::Serialize; 6 | use std::sync::Mutex; 7 | 8 | #[derive(Serialize, Deserialize)] 9 | pub struct UnspentsParams { 10 | settled_only: bool, 11 | } 12 | 13 | #[derive(Serialize, Deserialize)] 14 | pub struct Utxo { 15 | outpoint: Outpoint, 16 | btc_amount: String, 17 | pub colorable: bool, 18 | } 19 | 20 | impl From for Utxo { 21 | fn from(x: rgb_lib::wallet::Utxo) -> Utxo { 22 | Utxo { 23 | outpoint: x.outpoint, 24 | btc_amount: x.btc_amount.to_string(), 25 | colorable: x.colorable, 26 | } 27 | } 28 | } 29 | 30 | #[derive(Serialize, Deserialize)] 31 | pub struct RgbAllocation { 32 | asset_id: Option, 33 | amount: String, 34 | settled: bool, 35 | } 36 | 37 | impl From for RgbAllocation { 38 | fn from(x: rgb_lib::wallet::RgbAllocation) -> RgbAllocation { 39 | RgbAllocation { 40 | asset_id: x.asset_id.clone(), 41 | amount: x.amount.to_string(), 42 | settled: x.settled, 43 | } 44 | } 45 | } 46 | #[derive(Serialize, Deserialize)] 47 | pub struct Unspent { 48 | utxo: Utxo, 49 | rgb_allocations: Vec, 50 | } 51 | 52 | impl Unspent { 53 | fn from(origin: rgb_lib::wallet::Unspent) -> Unspent { 54 | Unspent { 55 | utxo: Utxo::from(origin.utxo), 56 | rgb_allocations: origin 57 | .rgb_allocations 58 | .into_iter() 59 | .map(RgbAllocation::from) 60 | .collect::>(), 61 | } 62 | } 63 | } 64 | 65 | #[derive(Serialize, Deserialize)] 66 | pub struct UnspentsResult { 67 | unspents: Vec, 68 | } 69 | 70 | #[put("/wallet/unspents")] 71 | pub async fn put( 72 | params: web::Json, 73 | data: web::Data>, 74 | ) -> impl Responder { 75 | if data.lock().unwrap().wallet.is_some() { 76 | match actix_web::rt::task::spawn_blocking(move || { 77 | data.lock() 78 | .unwrap() 79 | .wallet 80 | .as_mut() 81 | .unwrap() 82 | .list_unspents(params.settled_only) 83 | }) 84 | .await 85 | .unwrap() 86 | { 87 | Ok(unspents) => HttpResponse::Ok().json(UnspentsResult { 88 | unspents: unspents 89 | .into_iter() 90 | .map(Unspent::from) 91 | .collect::>(), 92 | }), 93 | Err(e) => HttpResponse::BadRequest().body(e.to_string()), 94 | } 95 | } else { 96 | HttpResponse::BadRequest().body("wallet should be created first") 97 | } 98 | } 99 | 100 | #[cfg(test)] 101 | mod tests { 102 | use super::*; 103 | 104 | use actix_web::{test, web, App}; 105 | use rgb_lib::generate_keys; 106 | 107 | #[actix_web::test] 108 | async fn test_put() { 109 | let shiro_wallet = Mutex::new(ShiroWallet::new()); 110 | let app = test::init_service( 111 | App::new() 112 | .app_data(web::Data::new(shiro_wallet)) 113 | .service(put) 114 | .service(crate::wallet::put), 115 | ) 116 | .await; 117 | 118 | { 119 | let keys = generate_keys(rgb_lib::BitcoinNetwork::Regtest); 120 | let wallet_params = crate::wallet::WalletParams { 121 | mnemonic: keys.mnemonic, 122 | pubkey: keys.xpub, 123 | }; 124 | let req = test::TestRequest::put() 125 | .uri("/wallet") 126 | .set_json(wallet_params) 127 | .to_request(); 128 | let resp = test::call_service(&app, req).await; 129 | println!("{:?}", resp); 130 | assert!(resp.status().is_success()); 131 | } 132 | { 133 | let params = UnspentsParams { settled_only: true }; 134 | let req = test::TestRequest::put() 135 | .uri("/wallet/unspents") 136 | .set_json(params) 137 | .to_request(); 138 | let resp = test::call_service(&app, req).await; 139 | println!("{:?}", resp); 140 | assert!(resp.status().is_success()); 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/wallet/utxos.rs: -------------------------------------------------------------------------------- 1 | use crate::ShiroWallet; 2 | use actix_web::{put, web, HttpResponse, Responder}; 3 | use serde::Deserialize; 4 | use serde::Serialize; 5 | use std::sync::Mutex; 6 | 7 | #[derive(Serialize, Deserialize)] 8 | pub struct UtxosParams { 9 | up_to: bool, 10 | num: Option, 11 | size: Option, 12 | fee_rate: f32, 13 | } 14 | 15 | #[derive(Serialize, Deserialize)] 16 | pub struct UtxosResult { 17 | created_utxos: u8, 18 | } 19 | 20 | #[put("/wallet/utxos")] 21 | pub async fn put( 22 | params: web::Json, 23 | data: web::Data>, 24 | ) -> impl Responder { 25 | if data.lock().unwrap().wallet.is_some() { 26 | if data.lock().unwrap().online.is_some() { 27 | match actix_web::rt::task::spawn_blocking(move || { 28 | let mut shiro_wallet = data.lock().unwrap(); 29 | let online = shiro_wallet.get_online().unwrap(); 30 | shiro_wallet.wallet.as_mut().unwrap().create_utxos( 31 | online, 32 | params.up_to, 33 | params.num, 34 | params.size, 35 | params.fee_rate, 36 | ) 37 | }) 38 | .await 39 | .unwrap() 40 | { 41 | Ok(created_utxos) => HttpResponse::Ok().json(UtxosResult { created_utxos }), 42 | Err(e) => HttpResponse::BadRequest().body(e.to_string()), 43 | } 44 | } else { 45 | HttpResponse::BadRequest().body("wallet should be online") 46 | } 47 | } else { 48 | HttpResponse::BadRequest().body("wallet should be created first") 49 | } 50 | } 51 | 52 | #[cfg(test)] 53 | mod tests { 54 | use super::*; 55 | 56 | use crate::wallet::{address::AddressResult, go_online::GoOnlineParams, tests::fund_wallet}; 57 | use actix_web::{test, web, App}; 58 | use rgb_lib::generate_keys; 59 | 60 | impl UtxosParams { 61 | pub fn new(up_to: bool, num: Option, size: Option, fee_rate: f32) -> UtxosParams { 62 | UtxosParams { 63 | up_to, 64 | num, 65 | size, 66 | fee_rate, 67 | } 68 | } 69 | } 70 | 71 | #[actix_web::test] 72 | async fn test_put() { 73 | let shiro_wallet = Mutex::new(ShiroWallet::new()); 74 | let app = test::init_service( 75 | App::new() 76 | .app_data(web::Data::new(shiro_wallet)) 77 | .service(put) 78 | .service(crate::wallet::go_online::put) 79 | .service(crate::wallet::address::get) 80 | .service(crate::wallet::put), 81 | ) 82 | .await; 83 | 84 | { 85 | let keys = generate_keys(rgb_lib::BitcoinNetwork::Regtest); 86 | let params = crate::wallet::WalletParams { 87 | mnemonic: keys.mnemonic, 88 | pubkey: keys.xpub, 89 | }; 90 | let req = test::TestRequest::put() 91 | .uri("/wallet") 92 | .set_json(params) 93 | .to_request(); 94 | let resp = test::call_service(&app, req).await; 95 | println!("{:?}", resp); 96 | assert!(resp.status().is_success()); 97 | } 98 | let address: AddressResult = { 99 | let req = test::TestRequest::get().uri("/wallet/address").to_request(); 100 | let resp = test::call_service(&app, req).await; 101 | println!("{:?}", resp); 102 | assert!(resp.status().is_success()); 103 | test::read_body_json(resp).await 104 | }; 105 | fund_wallet(address.new_address.clone()); 106 | { 107 | let params = GoOnlineParams::new(true, "127.0.0.1:50001".to_string()); 108 | let req = test::TestRequest::put() 109 | .uri("/wallet/go_online") 110 | .set_json(params) 111 | .to_request(); 112 | let resp = test::call_service(&app, req).await; 113 | println!("{:?}", resp); 114 | assert!(resp.status().is_success()); 115 | } 116 | let params = UtxosParams { 117 | up_to: true, 118 | num: Some(1), 119 | size: None, 120 | fee_rate: 1.0, 121 | }; 122 | let req = test::TestRequest::put() 123 | .uri("/wallet/utxos") 124 | .set_json(params) 125 | .to_request(); 126 | let resp = test::call_service(&app, req).await; 127 | println!("{:?}", resp); 128 | assert!(resp.status().is_success()); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /test-mocks/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.2' 2 | 3 | services: 4 | bitcoind: 5 | container_name: rgb-bitcoind 6 | image: registry.gitlab.com/hashbeam/docker/bitcoind:25.0 7 | command: "-fallbackfee=0.0002" 8 | volumes: 9 | - ./tmp/bitcoin:/srv/app/.bitcoin 10 | electrs: 11 | container_name: rgb-electrs 12 | image: registry.gitlab.com/hashbeam/docker/electrs:0.9.14 13 | volumes: 14 | - ./tmp/electrs:/srv/app/db 15 | ports: 16 | - 50001:50001 17 | depends_on: 18 | - bitcoind 19 | proxy: 20 | image: ghcr.io/grunch/rgb-proxy-server:0.1.0 21 | ports: 22 | - 3000:3000 23 | -------------------------------------------------------------------------------- /test-mocks/start_services.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eu 3 | 4 | COMPOSE="docker-compose -f test-mocks/docker-compose.yml" 5 | TEST_DIR="./test-mocks/tmp" 6 | 7 | $COMPOSE down -v 8 | rm -rf $TEST_DIR 9 | mkdir -p $TEST_DIR 10 | $COMPOSE up -d 11 | 12 | # wait for bitcoind to be up 13 | until $COMPOSE logs bitcoind |grep 'Bound to'; do 14 | sleep 1 15 | done 16 | 17 | # prepare bitcoin funds 18 | BCLI="$COMPOSE exec -T -u blits bitcoind bitcoin-cli -regtest" 19 | $BCLI createwallet miner 20 | $BCLI -rpcwallet=miner -generate 103 21 | 22 | # wait for electrs to have completed startup 23 | until $COMPOSE logs electrs |grep 'finished full compaction'; do 24 | sleep 1 25 | done 26 | --------------------------------------------------------------------------------