├── src ├── rgb │ ├── auction.rs │ ├── accept.rs │ ├── constants.rs │ ├── import.rs │ ├── proxy.rs │ └── cambria.rs ├── error.rs ├── lib.rs ├── bitcoin │ ├── assets.rs │ ├── wallet.rs │ ├── psbt.rs │ └── payment.rs ├── carbonado │ └── error.rs ├── regtest.rs ├── nostr.rs └── validators.rs ├── rust-toolchain.toml ├── .commands ├── .gitignore ├── .dockerignore ├── regtest ├── new_block.sh └── send_coins.sh ├── tests ├── scripts │ ├── new_blocks.sh │ ├── stop_node.sh │ ├── send_coins.sh │ ├── startup_node.sh │ └── check_headless.sh ├── _init.rs ├── rgb │ ├── web │ │ ├── stl_load.rs │ │ ├── stl_ids.rs │ │ ├── inspect.rs │ │ ├── utils.rs │ │ └── contracts.rs │ ├── integration │ │ ├── issue.rs │ │ ├── collectibles.rs │ │ ├── drain.rs │ │ ├── fungibles.rs │ │ ├── crdt.rs │ │ ├── udas.rs │ │ ├── proxy.rs │ │ ├── inspect.rs │ │ ├── watcher.rs │ │ ├── rbf.rs │ │ ├── states.rs │ │ └── dustless.rs │ └── unit │ │ ├── amount.rs │ │ ├── stl.rs │ │ ├── psbt.rs │ │ ├── issue.rs │ │ ├── stock.rs │ │ ├── invoice.rs │ │ └── utils.rs ├── rgb.rs ├── web_storage.rs ├── payjoin.rs ├── keys.rs ├── nostr.rs ├── wallet.rs ├── web_wallet.rs └── migration.rs ├── lib └── web │ ├── .gitignore │ ├── nostr.ts │ ├── index.ts │ ├── tsconfig.json │ ├── yarn.lock │ ├── package-lock.json │ ├── carbonado.ts │ ├── package.json │ ├── constants.ts │ ├── lightning.ts │ └── bitcoin.ts ├── docker ├── esplora │ ├── Dockerfile │ └── bitcoin.conf ├── rgb-proxy │ └── Dockerfile └── bitmask │ ├── Dockerfile │ └── Dockerfile.ST120 ├── .vscode ├── extensions.json └── settings.json ├── LICENSE ├── LICENSE-MIT ├── RGB_LIB_IDs.toml ├── docker-compose.yml ├── file_hashes.toml ├── .env ├── README.md ├── .github └── workflows │ └── rust.yaml └── Cargo.toml /src/rgb/auction.rs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "stable" 3 | -------------------------------------------------------------------------------- /.commands: -------------------------------------------------------------------------------- 1 | source .env 2 | alias node1="docker-compose exec -T node1 cli" 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /pkg 3 | .DS_Store 4 | wasm-pack.log 5 | launch.json 6 | /build 7 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | target 2 | Dockerfile 3 | .dockerignore 4 | .git 5 | .gitignore 6 | rust-toolchain.toml 7 | -------------------------------------------------------------------------------- /regtest/new_block.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | docker-compose exec -T node1 cli -generate 1 4 | sleep 3 5 | -------------------------------------------------------------------------------- /tests/scripts/new_blocks.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | docker-compose exec -T node1 cli -generate 1 4 | sleep 3 5 | -------------------------------------------------------------------------------- /lib/web/.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | *.wasm 3 | *.d.ts 4 | *.d.ts.map 5 | *.wasm.d.ts 6 | node_modules 7 | LICENSE* 8 | README.md 9 | -------------------------------------------------------------------------------- /tests/scripts/stop_node.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | docker-compose stop node1 bitmaskd 4 | docker system prune -f 5 | docker volume prune -f 6 | -------------------------------------------------------------------------------- /regtest/send_coins.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | docker-compose exec -T node1 cli sendtoaddress $1 $2 4 | docker-compose exec -T node1 cli -generate 1 5 | sleep 3 6 | -------------------------------------------------------------------------------- /tests/scripts/send_coins.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | docker-compose exec -T node1 cli sendtoaddress $1 $2 4 | docker-compose exec -T node1 cli -generate 2 5 | sleep 3 6 | -------------------------------------------------------------------------------- /docker/esplora/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM blockstream/esplora:builder-bitcoin-regtest 2 | 3 | # Change configs 4 | COPY ./bitcoin.conf /data/.bitcoin.conf 5 | WORKDIR /srv/explorer 6 | -------------------------------------------------------------------------------- /tests/scripts/startup_node.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | docker-compose up -d node1 bitmaskd 4 | sleep 10 5 | docker-compose exec -T node1 cli loadwallet default 6 | docker-compose exec -T node1 cli -generate 500 7 | sleep 5 8 | -------------------------------------------------------------------------------- /tests/_init.rs: -------------------------------------------------------------------------------- 1 | #![cfg(not(target_arch = "wasm32"))] 2 | 3 | use anyhow::Result; 4 | use bitmask_core::regtest::init_fs; 5 | 6 | #[test] 7 | pub fn _init() -> Result<()> { 8 | init_fs()?; 9 | 10 | Ok(()) 11 | } 12 | -------------------------------------------------------------------------------- /docker/esplora/bitcoin.conf: -------------------------------------------------------------------------------- 1 | #connect=node1:18443 2 | acceptnonstdtxn=1 3 | txconfirmtarget=0 4 | regtest=1 5 | dustrelayfee=0 6 | [regtest] 7 | server=1 8 | listen=1 9 | blocknotify=pkill -USR1 electrs 10 | fallbackfee=0.00001 11 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "rust-lang.rust-analyzer", 4 | "tamasfe.even-better-toml", 5 | "serayuzgur.crates", 6 | "wayou.vscode-todo-highlight", 7 | "redhat.vscode-yaml" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | use crate::{bitcoin::BitcoinKeysError, rgb::IssueError}; 4 | 5 | #[derive(Error, Debug)] 6 | pub enum BitMaskCoreError { 7 | /// Bitcoin Keys Error 8 | #[error(transparent)] 9 | BitcoinKeysError(#[from] BitcoinKeysError), 10 | /// RGB Issuer Operation Error 11 | #[error(transparent)] 12 | RgbIssueError(#[from] IssueError), 13 | } 14 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate amplify; 3 | 4 | pub mod bitcoin; 5 | pub mod carbonado; 6 | pub mod constants; 7 | pub mod error; 8 | pub mod lightning; 9 | pub mod nostr; 10 | pub mod proxy; 11 | #[cfg(not(target_arch = "wasm32"))] 12 | pub mod regtest; 13 | pub mod rgb; 14 | pub mod structs; 15 | pub mod util; 16 | pub mod validators; 17 | #[cfg(target_arch = "wasm32")] 18 | pub mod web; 19 | -------------------------------------------------------------------------------- /docker/rgb-proxy/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18-bullseye 2 | ARG BUILDER_SRC=/srv/src/ 3 | ARG SOURCE_CODE=https://github.com/grunch/rgb-proxy-server.git 4 | ARG VERSION=main 5 | 6 | RUN apt-get -y update \ 7 | && apt-get -y install tini git \ 8 | && apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* 9 | 10 | WORKDIR $BUILDER_SRC 11 | RUN git clone $SOURCE_CODE $BUILDER_SRC 12 | RUN git checkout $VERSION 13 | 14 | RUN npm install 15 | RUN npm run build 16 | 17 | ENV NODE_ENV="production" 18 | 19 | EXPOSE 3000/tcp 20 | VOLUME ["/root/.npm", "/root/.rgb-proxy-server"] 21 | 22 | CMD ["tini", "--", "npm", "run", "start"] 23 | -------------------------------------------------------------------------------- /lib/web/nostr.ts: -------------------------------------------------------------------------------- 1 | // Methods meant to work with LNDHubX defined within the web::nostr module from bitmask-core: 2 | // https://github.com/diba-io/bitmask-core/blob/development/src/web.rs 3 | 4 | import * as BMC from "./bitmask_core"; 5 | 6 | export interface Response { 7 | status: string; 8 | } 9 | 10 | export const newNostrPubkey = async ( 11 | pubkey: string, 12 | token: string 13 | ): Promise => JSON.parse(await BMC.new_nostr_pubkey(pubkey, token)); 14 | 15 | export const updateNostrPubkey = async ( 16 | pubkey: string, 17 | token: string 18 | ): Promise => 19 | JSON.parse(await BMC.update_nostr_pubkey(pubkey, token)); 20 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "rust-lang.rust-analyzer", 3 | "editor.formatOnSave": true, 4 | "editor.tabSize": 4, 5 | "files.autoSave": "onFocusChange", 6 | "files.insertFinalNewline": true, 7 | "files.trimTrailingWhitespace": true, 8 | "rust-analyzer.check.allTargets": true, 9 | "rust-analyzer.cargo.features": "all", // Enable only for desktop 10 | // "rust-analyzer.cargo.target": "wasm32-unknown-unknown", // Enable only for web 11 | // "rust-analyzer.check.noDefaultFeatures": true, // Enable for web 12 | // "rust-analyzer.runnables.extraArgs": ["--release"], // Enable for web 13 | "[toml]": { 14 | "editor.defaultFormatter": "tamasfe.even-better-toml" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/rgb/accept.rs: -------------------------------------------------------------------------------- 1 | use rgbstd::{ 2 | containers::Transfer, 3 | persistence::{Inventory, Stock}, 4 | resolvers::ResolveHeight, 5 | validation::Status, 6 | }; 7 | 8 | #[derive(Clone, Eq, PartialEq, Debug, Display, Error, From)] 9 | #[display(doc_comments)] 10 | // TODO: Complete errors 11 | pub enum InventoryError {} 12 | 13 | pub fn accept_transfer( 14 | transfer: Transfer, 15 | force: bool, 16 | mut stock: Stock, 17 | resolver: &mut R, 18 | ) -> Result 19 | where 20 | R::Error: 'static, 21 | { 22 | let status = stock 23 | .accept_transfer(transfer, resolver, force) 24 | .expect("accept transfer failed"); 25 | Ok(status) 26 | } 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This software is licensed under [Apache 2.0](LICENSE-APACHE) or 2 | [MIT](LICENSE-MIT), at your option. 3 | 4 | Some files retain their own copyright notice, however, for full authorship 5 | information, see version control history. 6 | 7 | Except as otherwise noted in individual files, all files in this repository are 8 | licensed under the Apache License, Version 2.0 or the MIT license , at your option. 11 | 12 | You may not use, copy, modify, merge, publish, distribute, sublicense, and/or 13 | sell copies of this software or any files in this repository except in 14 | accordance with one or both of these licenses. 15 | -------------------------------------------------------------------------------- /lib/web/index.ts: -------------------------------------------------------------------------------- 1 | import * as BMC from "./bitmask_core"; 2 | export * from "./bitmask_core"; // kludge :c 3 | 4 | import * as bitcoin from "./bitcoin"; 5 | import * as carbonado from "./carbonado"; 6 | import * as constants from "./constants"; 7 | import * as lightning from "./lightning"; 8 | import * as nostr from "./nostr"; 9 | import * as rgb from "./rgb"; 10 | 11 | export * as bitcoin from "./bitcoin"; 12 | export * as carbonado from "./carbonado"; 13 | export * as constants from "./constants"; 14 | export * as lightning from "./lightning"; 15 | export * as nostr from "./nostr"; 16 | export * as rgb from "./rgb"; 17 | 18 | export default { 19 | BMC, 20 | bitcoin, 21 | carbonado, 22 | constants, 23 | lightning, 24 | nostr, 25 | rgb, 26 | }; 27 | -------------------------------------------------------------------------------- /lib/web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "esModuleInterop": true, 5 | "forceConsistentCasingInFileNames": true, 6 | "skipLibCheck": true, 7 | "checkJs": true, 8 | "allowJs": true, 9 | "declaration": true, 10 | "declarationMap": true, 11 | "allowSyntheticDefaultImports": true, 12 | "types": ["node"], 13 | "lib": ["ES6", "DOM"], 14 | "target": "ES6", 15 | "module": "CommonJS", 16 | "moduleResolution": "Node", 17 | "outDir": ".", 18 | "declarationDir": "." 19 | }, 20 | 21 | "files": [ 22 | "./bitcoin.ts", 23 | "./carbonado.ts", 24 | "./constants.ts", 25 | "./index.ts", 26 | "./lightning.ts", 27 | "./nostr.ts", 28 | "./rgb.ts" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /tests/rgb/web/stl_load.rs: -------------------------------------------------------------------------------- 1 | #![cfg(target_arch = "wasm32")] 2 | use bp::bc::stl::bp_tx_stl; 3 | use rgb::interface::{rgb20_stl, rgb21_stl}; 4 | use rgbstd::stl::rgb_contract_stl; 5 | use strict_types::stl::std_stl; 6 | 7 | use wasm_bindgen_test::*; 8 | 9 | use bitmask_core::web::set_panic_hook; 10 | 11 | wasm_bindgen_test_configure!(run_in_browser); 12 | 13 | #[wasm_bindgen_test] 14 | async fn allow_load_stl() { 15 | set_panic_hook(); 16 | std_stl(); 17 | } 18 | 19 | #[wasm_bindgen_test] 20 | async fn allow_load_bitcoin() { 21 | set_panic_hook(); 22 | bp_tx_stl(); 23 | } 24 | 25 | #[wasm_bindgen_test] 26 | async fn allow_load_contracts_stl() { 27 | set_panic_hook(); 28 | rgb_contract_stl(); 29 | } 30 | 31 | #[wasm_bindgen_test] 32 | async fn allow_load_rgb20_stl() { 33 | set_panic_hook(); 34 | rgb20_stl(); 35 | } 36 | 37 | #[wasm_bindgen_test] 38 | async fn allow_load_rgb21_stl() { 39 | set_panic_hook(); 40 | rgb21_stl(); 41 | } 42 | -------------------------------------------------------------------------------- /src/rgb/constants.rs: -------------------------------------------------------------------------------- 1 | // Default 2 | pub const LIB_NAME_BITMASK: &str = "bitmask"; 3 | pub const RGB_CHANGE_INDEX: &str = "0"; 4 | pub const RGB_PSBT_TAPRET: &str = "TAPRET"; 5 | pub const RGB_DEFAULT_NAME: &str = "default"; 6 | pub const RGB_OLDEST_VERSION: [u8; 8] = [0; 8]; 7 | pub const RGB_STRICT_TYPE_VERSION: [u8; 8] = *b"rgbst161"; 8 | pub const RGB_DEFAULT_FETCH_LIMIT: u32 = 10; 9 | pub const BITCOIN_DEFAULT_FETCH_LIMIT: u32 = 20; 10 | pub const RGB20_DERIVATION_INDEX: u32 = 20; 11 | pub const RGB21_DERIVATION_INDEX: u32 = 21; 12 | 13 | // General Errors 14 | #[cfg(target_arch = "wasm32")] 15 | pub const CARBONADO_UNAVAILABLE: &str = "carbonado filesystem"; 16 | #[cfg(not(target_arch = "wasm32"))] 17 | pub const CARBONADO_UNAVAILABLE: &str = "carbonado server"; 18 | pub const STOCK_UNAVAILABLE: &str = "Unable to access Stock data"; 19 | pub const WALLET_UNAVAILABLE: &str = "Unable to access Wallet data"; 20 | pub const TRANSFER_UNAVAILABLE: &str = "Unable to access transfer data"; 21 | -------------------------------------------------------------------------------- /lib/web/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@types/node@^20.8.2": 6 | version "20.11.0" 7 | resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.0.tgz#8e0b99e70c0c1ade1a86c4a282f7b7ef87c9552f" 8 | integrity sha512-o9bjXmDNcF7GbM4CNQpmi+TutCgap/K3w1JyKgxAjqx41zp9qlIAVFi0IhCNsJcXolEqLWhbFbEeL0PvYm4pcQ== 9 | dependencies: 10 | undici-types "~5.26.4" 11 | 12 | typescript@^5.2.2: 13 | version "5.3.3" 14 | resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.3.tgz#b3ce6ba258e72e6305ba66f5c9b452aaee3ffe37" 15 | integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw== 16 | 17 | undici-types@~5.26.4: 18 | version "5.26.5" 19 | resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" 20 | integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== 21 | -------------------------------------------------------------------------------- /tests/rgb/integration/issue.rs: -------------------------------------------------------------------------------- 1 | #![cfg(not(target_arch = "wasm32"))] 2 | use crate::rgb::integration::utils::{get_uda_data, issuer_issue_contract}; 3 | 4 | #[tokio::test] 5 | async fn allow_issuer_issue_fungible_contract() -> anyhow::Result<()> { 6 | let issuer_resp = issuer_issue_contract("RGB20", 5, false, true, None).await; 7 | assert!(issuer_resp.is_ok()); 8 | Ok(()) 9 | } 10 | 11 | #[tokio::test] 12 | async fn allow_issuer_issue_uda_contract() -> anyhow::Result<()> { 13 | let single = Some(get_uda_data()); 14 | let issuer_resp = issuer_issue_contract("RGB21", 1, false, true, single).await; 15 | assert!(issuer_resp.is_ok()); 16 | Ok(()) 17 | } 18 | 19 | // TODO: Review after support multi-token transfer 20 | // async fn _allow_issuer_issue_collectible_contract() -> anyhow::Result<()> { 21 | // let collectible = Some(get_collectible_data()); 22 | // let issuer_resp = issuer_issue_contract("RGB21", 1, false, true, collectible).await; 23 | // assert!(issuer_resp.is_ok()); 24 | // Ok(()) 25 | // } 26 | -------------------------------------------------------------------------------- /docker/bitmask/Dockerfile: -------------------------------------------------------------------------------- 1 | # Builder 2 | FROM rust:1.70-slim-buster AS builder 3 | ARG BUILDER_DIR=/srv/bitmask 4 | ARG BUILDER_SRC=/opt/src/bitmask 5 | 6 | RUN apt-get update -y && \ 7 | apt-get install -y pkg-config make g++ libssl-dev 8 | 9 | WORKDIR $BUILDER_DIR 10 | WORKDIR $BUILDER_SRC 11 | COPY . . 12 | 13 | RUN cargo install --locked --features server --path . --root ${BUILDER_DIR} 14 | 15 | # Runtime 16 | FROM rust:1.70-slim-buster AS runtime 17 | 18 | ARG BUILDER_DIR=/srv/bitmask 19 | ARG BIN_DIR=/usr/local/bin 20 | ARG DATA_DIR=/tmp/bitmaskd/carbonado/ 21 | ARG USER=bitmask 22 | 23 | RUN apt-get update -y && apt-get install -y iputils-ping telnet \ 24 | && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* 25 | 26 | RUN adduser --home "${DATA_DIR}" --shell /bin/bash --disabled-login \ 27 | --gecos "${USER} user" ${USER} 28 | 29 | COPY --from=builder --chown=${USER}:${USER} \ 30 | "${BUILDER_DIR}/bin/" "${BIN_DIR}" 31 | 32 | USER ${USER} 33 | VOLUME ${DATA_DIR} 34 | EXPOSE 7070 35 | 36 | WORKDIR ${BIN_DIR} 37 | 38 | ENTRYPOINT ["bitmaskd"] 39 | -------------------------------------------------------------------------------- /tests/scripts/check_headless.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | chromedriver_path=$(command -v chromedriver) 4 | 5 | unameOut="$(uname -s)" 6 | case "${unameOut}" in 7 | Linux*) chrome_path="/usr/bin/google-chrome";; 8 | Darwin*) chrome_path="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";; 9 | esac 10 | chromedriver_version=$("${chromedriver_path}" --version) 11 | chrome_version=$("${chrome_path}" --version) 12 | 13 | chromedriver_major_version=$("${chromedriver_path}" --version | cut -f 2 -d " " | cut -f 1 -d ".") 14 | chrome_major_version=$("${chrome_path}" --version | cut -f 3 -d " " | cut -f 1 -d ".") 15 | 16 | if [ "${chromedriver_major_version}" == "${chrome_major_version}" ]; then 17 | echo "Chromedriver matches chrome version ✓" 18 | exit 0 19 | else 20 | echo "Wasm-Pack often fails with 'invalid session id' if Chromedriver and Chrome have different versions." 21 | echo "Chromedriver version: ${chromedriver_version} (${chromedriver_path})" 22 | echo "Chrome version : ${chrome_version} (${chrome_path})" 23 | exit 1 24 | fi 25 | -------------------------------------------------------------------------------- /tests/rgb/unit/amount.rs: -------------------------------------------------------------------------------- 1 | #![cfg(not(target_arch = "wasm32"))] 2 | use bitmask_core::rgb::structs::ContractAmount; 3 | #[tokio::test] 4 | async fn create_contract_amount() -> anyhow::Result<()> { 5 | let amount = ContractAmount::new(9900, 2); 6 | assert_eq!(amount.int, 99); 7 | assert_eq!(amount.fract, 0); 8 | assert_eq!(amount.to_value(), 9900); 9 | assert_eq!(amount.to_string(), "99.00"); 10 | 11 | let amount = ContractAmount::new(10000, 2); 12 | assert_eq!(amount.int, 100); 13 | assert_eq!(amount.fract, 0); 14 | assert_eq!(amount.to_value(), 10000); 15 | assert_eq!(amount.to_string(), "100.00"); 16 | 17 | let amount = ContractAmount::with(4000, 0, 2); 18 | assert_eq!(amount.int, 4000); 19 | assert_eq!(amount.fract, 0); 20 | assert_eq!(amount.to_value(), 400000); 21 | assert_eq!(amount.to_string(), "4000.00"); 22 | 23 | let amount = ContractAmount::with(1000, 1, 2); 24 | assert_eq!(amount.int, 1000); 25 | assert_eq!(amount.fract, 1); 26 | assert_eq!(amount.to_value(), 100001); 27 | assert_eq!(amount.to_string(), "1000.01"); 28 | 29 | Ok(()) 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022 DIBA Global, Inc. 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /tests/rgb/web/stl_ids.rs: -------------------------------------------------------------------------------- 1 | #![cfg(target_arch = "wasm32")] 2 | use rgbstd::{ 3 | interface::{rgb20_stl, rgb21_stl, rgb25_stl, LIB_ID_RGB20, LIB_ID_RGB21, LIB_ID_RGB25}, 4 | stl::{rgb_contract_stl, rgb_std_stl, LIB_ID_RGB_CONTRACT, LIB_ID_RGB_STD}, 5 | }; 6 | 7 | use wasm_bindgen_test::*; 8 | 9 | wasm_bindgen_test_configure!(run_in_browser); 10 | 11 | #[wasm_bindgen_test] 12 | async fn check_rgb20_stl() { 13 | let shema = rgb20_stl(); 14 | assert_eq!(LIB_ID_RGB20, shema.id().to_string()); 15 | } 16 | 17 | #[wasm_bindgen_test] 18 | async fn check_rgb21_stl() { 19 | let shema = rgb21_stl(); 20 | assert_eq!(LIB_ID_RGB21, shema.id().to_string()); 21 | } 22 | 23 | #[wasm_bindgen_test] 24 | async fn check_rgb25_stl() { 25 | let shema = rgb25_stl(); 26 | assert_eq!(LIB_ID_RGB25, shema.id().to_string()); 27 | } 28 | 29 | #[wasm_bindgen_test] 30 | async fn check_rgbstd_stl() { 31 | let shema = rgb_std_stl(); 32 | assert_eq!(LIB_ID_RGB_STD, shema.id().to_string()); 33 | } 34 | 35 | #[wasm_bindgen_test] 36 | async fn check_rgbcontract_stl() { 37 | let shema = rgb_contract_stl(); 38 | assert_eq!(LIB_ID_RGB_CONTRACT, shema.id().to_string()); 39 | } 40 | -------------------------------------------------------------------------------- /RGB_LIB_IDs.toml: -------------------------------------------------------------------------------- 1 | # Auto-generated semantic IDs for RGB consensus-critical libraries and their corresponding versions of bitmask-core. 2 | 3 | [LIB_ID_RGB] 4 | # Consensus-breaking: If changed, assets must be reissued 5 | "urn:ubideco:stl:4fGZWR5mH5zZzRZ1r7CSRe776zm3hLBUngfXc4s3vm3V#saturn-flash-emerald" = "0.6.0-rc.17" 6 | 7 | [LIB_ID_RGB_CONTRACT] 8 | # Interface-only: If changed, only a new interface implementation is needed. No reiussance or migration necessary. 9 | "urn:ubideco:stl:6vbr9ZrtsD9aBjo5qRQ36QEZPVucqvRRjKCPqE8yPeJr#choice-little-boxer" = "0.6.0-rc.17" 10 | 11 | [LIB_ID_RGB20] 12 | "urn:ubideco:stl:GVz4mvYE94aQ9q2HPtV9VuoppcDdduP54BMKffF7YoFH#prince-scarlet-ringo" = "0.6.0-rc.17" 13 | 14 | [LIB_ID_RGB21] 15 | "urn:ubideco:stl:3miGC5GTW58CeuGJgomApmdjm8N6Yu6YuuURS8N4WVBA#opera-cool-bread" = "0.6.0-rc.17" 16 | 17 | [LIB_ID_RGB25] 18 | "urn:ubideco:stl:4JmGrg7oTgwuCQtyC4ezC38ToHMzgMCVS5kMSDPwo2ee#camera-betty-bank" = "0.6.0-rc.17" 19 | 20 | [LIB_ID_RGB_STD] 21 | # Not consensus-breaking: If changed, only stash and consignments must be updated. No reiussance or migration necessary. 22 | "urn:ubideco:stl:3KXsWZ6hSKRbPjSVwRGbwnwJp3ZNQ2tfe6QUwLJEDG6K#twist-paul-carlo" = "0.6.0-rc.17" 23 | -------------------------------------------------------------------------------- /lib/web/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bitmask-segwit", 3 | "version": "0.7.0-beta.10", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "bitmask-segwit", 9 | "version": "0.7.0-beta.10", 10 | "license": "MIT", 11 | "devDependencies": { 12 | "@types/node": "^20.8.2", 13 | "typescript": "^5.2.2" 14 | } 15 | }, 16 | "node_modules/@types/node": { 17 | "version": "20.8.2", 18 | "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.2.tgz", 19 | "integrity": "sha512-Vvycsc9FQdwhxE3y3DzeIxuEJbWGDsnrxvMADzTDF/lcdR9/K+AQIeAghTQsHtotg/q0j3WEOYS/jQgSdWue3w==", 20 | "dev": true 21 | }, 22 | "node_modules/typescript": { 23 | "version": "5.2.2", 24 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", 25 | "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", 26 | "dev": true, 27 | "bin": { 28 | "tsc": "bin/tsc", 29 | "tsserver": "bin/tsserver" 30 | }, 31 | "engines": { 32 | "node": ">=14.17" 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/bitcoin/assets.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use bdk::{database::AnyDatabase, LocalUtxo, SignOptions, Wallet}; 3 | use bitcoin::psbt::PartiallySignedTransaction; 4 | 5 | use crate::debug; 6 | 7 | pub fn dust_tx( 8 | btc_wallet: &Wallet, 9 | fee_rate: f32, 10 | first_utxo: Option<&LocalUtxo>, 11 | ) -> Result { 12 | let dust_amt = if fee_rate < 3.0 { 13 | 546 14 | } else { 15 | (182.0 * fee_rate).ceil() as u64 16 | }; 17 | 18 | let pubkey = match first_utxo { 19 | Some(utxo) => utxo.txout.script_pubkey.to_owned(), 20 | None => todo!(), 21 | }; 22 | 23 | let mut tx_builder = btc_wallet.build_tx(); 24 | tx_builder.add_recipient(pubkey, dust_amt); 25 | let (mut dust_psbt, tx_details) = tx_builder.finish()?; 26 | 27 | debug!(format!("dust tx details: {tx_details:#?}")); 28 | let finalized = btc_wallet.sign(&mut dust_psbt, SignOptions::default())?; 29 | debug!(format!("PSBT signed. Finalized: {finalized}")); 30 | btc_wallet.finalize_psbt(&mut dust_psbt, SignOptions::default())?; 31 | debug!(format!("PSBT finalized. Finalized: {finalized}")); 32 | 33 | Ok(dust_psbt) 34 | } 35 | -------------------------------------------------------------------------------- /tests/rgb.rs: -------------------------------------------------------------------------------- 1 | mod rgb { 2 | 3 | mod unit { 4 | mod amount; 5 | mod invoice; 6 | mod issue; 7 | mod psbt; 8 | mod stl; 9 | mod stock; 10 | pub mod utils; 11 | } 12 | 13 | mod integration { 14 | // TODO: Review after support multi-token transfer 15 | // mod collectibles; 16 | mod accept; 17 | mod batch; 18 | mod cambria; 19 | mod collectibles; 20 | mod crdt; 21 | mod drain; 22 | mod dustless; 23 | mod fungibles; 24 | mod import; 25 | mod inspect; 26 | mod issue; 27 | mod proxy; 28 | mod rbf; 29 | mod sign_hash; 30 | mod states; 31 | mod swaps; 32 | mod transfers; 33 | mod udas; 34 | pub mod utils; 35 | mod watcher; 36 | } 37 | 38 | mod web { 39 | mod contracts; 40 | mod imports; 41 | mod inspect; 42 | mod proxy; 43 | mod stl_ids; 44 | mod stl_load; 45 | mod swaps; 46 | mod transfers; 47 | mod utils; 48 | } 49 | 50 | mod sre { 51 | mod st160; 52 | mod st160_web; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /tests/rgb/unit/stl.rs: -------------------------------------------------------------------------------- 1 | #![cfg(not(target_arch = "wasm32"))] 2 | use anyhow::Result; 3 | use rgbstd::{ 4 | interface::{rgb20_stl, rgb21_stl, rgb25_stl, LIB_ID_RGB20, LIB_ID_RGB21, LIB_ID_RGB25}, 5 | stl::{rgb_contract_stl, rgb_std_stl, LIB_ID_RGB_CONTRACT, LIB_ID_RGB_STD}, 6 | }; 7 | 8 | #[tokio::test] 9 | async fn check_rgb20_stl() -> Result<()> { 10 | let shema = rgb20_stl(); 11 | assert_eq!(LIB_ID_RGB20, shema.id().to_string()); 12 | Ok(()) 13 | } 14 | 15 | #[tokio::test] 16 | async fn check_rgb21_stl() -> Result<()> { 17 | let shema = rgb21_stl(); 18 | assert_eq!(LIB_ID_RGB21, shema.id().to_string()); 19 | Ok(()) 20 | } 21 | 22 | #[tokio::test] 23 | async fn check_rgb25_stl() -> Result<()> { 24 | let shema = rgb25_stl(); 25 | assert_eq!(LIB_ID_RGB25, shema.id().to_string()); 26 | Ok(()) 27 | } 28 | 29 | #[tokio::test] 30 | async fn check_rgbstd_stl() -> Result<()> { 31 | let shema = rgb_std_stl(); 32 | assert_eq!(LIB_ID_RGB_STD, shema.id().to_string()); 33 | Ok(()) 34 | } 35 | 36 | #[tokio::test] 37 | async fn check_rgbcontract_stl() -> Result<()> { 38 | let shema = rgb_contract_stl(); 39 | assert_eq!(LIB_ID_RGB_CONTRACT, shema.id().to_string()); 40 | Ok(()) 41 | } 42 | -------------------------------------------------------------------------------- /lib/web/carbonado.ts: -------------------------------------------------------------------------------- 1 | // Methods meant to work with Carbonado storage defined within the web::carbonado module from bitmask-core: 2 | // https://github.com/diba-io/bitmask-core/blob/development/src/web.rs 3 | 4 | import * as BMC from "./bitmask_core"; 5 | 6 | export const store = async ( 7 | nostrHexSk: string, 8 | data: Uint8Array, 9 | force: boolean, 10 | name?: string, 11 | meta?: Uint8Array 12 | ): Promise => BMC.store(nostrHexSk, name || "", data, force, meta); 13 | 14 | export const retrieve = ( 15 | nostrHexSk: string, 16 | lookup: string 17 | ): Promise => BMC.retrieve(nostrHexSk, lookup); 18 | 19 | export const retrieveMetadata = ( 20 | nostrHexSk: string, 21 | lookup: string 22 | ): Promise => BMC.retrieve_metadata(nostrHexSk, lookup); 23 | 24 | export const encodeHex = (bytes: Uint8Array): string => BMC.encode_hex(bytes); 25 | export const encodeBase64 = (bytes: Uint8Array): string => 26 | BMC.encode_base64(bytes); 27 | 28 | export const decodeHex = (str: string): Uint8Array => BMC.decode_hex(str); 29 | export const decodeBase64 = (str: string): Uint8Array => BMC.decode_base64(str); 30 | 31 | export interface FileMetadata { 32 | filename: string; 33 | metadata: Uint8Array; 34 | } 35 | -------------------------------------------------------------------------------- /docker/bitmask/Dockerfile.ST120: -------------------------------------------------------------------------------- 1 | # Builder 2 | FROM rust:1.69-slim-buster AS builder 3 | ARG BUILDER_DIR=/srv/bitmask 4 | ARG BUILDER_SRC=/opt/src/bitmask 5 | ARG SOURCE_CODE=https://github.com/diba-io/bitmask-core.git 6 | ARG VERSION=c18cba0375dbc4c2f2a5a4fe56401bf3d6d52ce7 7 | 8 | RUN apt-get update -y && \ 9 | apt-get install -y pkg-config make g++ libssl-dev git 10 | 11 | WORKDIR $BUILDER_DIR 12 | WORKDIR $BUILDER_SRC 13 | RUN git clone $SOURCE_CODE $BUILDER_SRC 14 | RUN git checkout $VERSION 15 | RUN rm rust-toolchain.toml 16 | RUN cargo install --locked --features server --path . --root ${BUILDER_DIR} 17 | 18 | # Runtime 19 | FROM rust:1.69-slim-buster AS runtime 20 | 21 | ARG BUILDER_DIR=/srv/bitmask 22 | ARG BIN_DIR=/usr/local/bin 23 | ARG DATA_DIR=/tmp/bitmaskd/carbonado/ 24 | ARG USER=bitmask 25 | 26 | RUN apt-get update -y && apt-get install -y iputils-ping telnet \ 27 | && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* 28 | 29 | RUN adduser --home "${DATA_DIR}" --shell /bin/bash --disabled-login \ 30 | --gecos "${USER} user" ${USER} 31 | 32 | COPY --from=builder --chown=${USER}:${USER} \ 33 | "${BUILDER_DIR}/bin/" "${BIN_DIR}" 34 | 35 | USER ${USER} 36 | VOLUME ${DATA_DIR} 37 | EXPOSE 7070 38 | 39 | WORKDIR ${BIN_DIR} 40 | ENTRYPOINT ["bitmaskd"] 41 | -------------------------------------------------------------------------------- /tests/web_storage.rs: -------------------------------------------------------------------------------- 1 | #![cfg(target_arch = "wasm32")] 2 | use bitmask_core::{ 3 | info, 4 | structs::FileMetadata, 5 | web::{ 6 | carbonado::{retrieve, retrieve_metadata, store}, 7 | json_parse, resolve, set_panic_hook, 8 | }, 9 | }; 10 | use js_sys::Uint8Array; 11 | use wasm_bindgen::JsValue; 12 | use wasm_bindgen_test::*; 13 | 14 | wasm_bindgen_test_configure!(run_in_browser); 15 | 16 | #[wasm_bindgen_test] 17 | async fn web_storage() { 18 | set_panic_hook(); 19 | 20 | let sk = "76e9a09d5fa501c9048cb7ff48415786f7f6580726f33823010d130b19f61680".to_owned(); 21 | let name = "test-my-file.c15".to_owned(); 22 | let data = b"Hello world!".to_vec(); 23 | 24 | info!("Testing web data store"); 25 | resolve(store(sk.clone(), name.clone(), data.clone(), false, None)).await; 26 | 27 | info!("Testing web data retrieve"); 28 | let result: JsValue = resolve(retrieve(sk.clone(), name.clone())).await; 29 | let array = Uint8Array::new(&result); 30 | let bytes: Vec = array.to_vec(); 31 | assert_eq!(data, bytes, "Data stored and data retrieved match"); 32 | 33 | let metadata: JsValue = resolve(retrieve_metadata(sk, name)).await; 34 | let metadata: FileMetadata = json_parse(&metadata); 35 | assert!(metadata.filename.ends_with(".c15")); 36 | assert_eq!(metadata.metadata, [0; 8]); 37 | } 38 | -------------------------------------------------------------------------------- /src/carbonado/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | #[derive(Error, Debug, Display)] 4 | #[display(doc_comments)] 5 | pub enum CarbonadoError { 6 | /// std io error: {0} 7 | StdIoError(#[from] std::io::Error), 8 | /// std io error: {0} 9 | StdStrUtf8Error(#[from] std::str::Utf8Error), 10 | /// Error decoding hexadecimal-encoded string: {0} 11 | HexDecodeError(#[from] hex::FromHexError), 12 | /// Error decoding base64-encoded string: {0} 13 | Base64DecodeError(#[from] base64::DecodeError), 14 | /// Error creating Nostr private key: {0} 15 | NostrPrivateKey(#[from] nostr_sdk::secp256k1::Error), 16 | /// General Carbonado error: {0} 17 | CarbonadoError(#[from] carbonado::error::CarbonadoError), 18 | /// General Carbonado error: {0} 19 | SerdeJsonError(#[from] serde_json::Error), 20 | /// JS Error: {0} 21 | #[cfg(target_arch = "wasm32")] 22 | JsError(#[from] gloo_utils::errors::JsError), 23 | /// Serde WASM Error: {0} 24 | #[cfg(target_arch = "wasm32")] 25 | SerdeWasm(#[from] serde_wasm_bindgen::Error), 26 | /// All endpoints failed error 27 | AllEndpointsFailed, 28 | /// Wrong Nostr private key 29 | WrongNostrPrivateKey, 30 | /// Wrong Nostr public key 31 | WrongNostrPublicKey, 32 | /// Debug: {0} 33 | Debug(String), 34 | /// Error: {0} 35 | AnyhowError(#[from] anyhow::Error), 36 | } 37 | -------------------------------------------------------------------------------- /src/regtest.rs: -------------------------------------------------------------------------------- 1 | #![cfg(not(target_arch = "wasm32"))] 2 | use std::{ 3 | env, fs, path, 4 | process::{Command, Stdio}, 5 | }; 6 | 7 | use anyhow::Result; 8 | 9 | use crate::carbonado::metrics::MetricsData; 10 | 11 | pub fn init_fs() -> Result<()> { 12 | let dir = env::var("CARBONADO_DIR").unwrap_or("/tmp/bitmaskd/carbonado".to_owned()); 13 | let dir = path::Path::new(&dir); 14 | 15 | fs::create_dir_all(dir)?; 16 | fs::write( 17 | dir.join("metrics.json"), 18 | serde_json::to_string_pretty(&MetricsData::default())?, 19 | )?; 20 | 21 | Ok(()) 22 | } 23 | 24 | pub fn send_coins(address: &str, amount: &str) { 25 | let path = env::current_dir().expect("oh no!"); 26 | let path = path.to_str().expect("oh no!"); 27 | let full_file = format!("{}/regtest/send_coins.sh", path); 28 | Command::new("bash") 29 | .arg(full_file) 30 | .args([address, amount]) 31 | .stdout(Stdio::null()) 32 | .stderr(Stdio::null()) 33 | .spawn() 34 | .unwrap() 35 | .wait() 36 | .expect("oh no!"); 37 | } 38 | 39 | pub fn new_block() { 40 | let path = env::current_dir().expect("oh no!"); 41 | let path = path.to_str().expect("oh no!"); 42 | let full_file = format!("{}/regtest/new_block.sh", path); 43 | Command::new("bash") 44 | .arg(full_file) 45 | .stdout(Stdio::null()) 46 | .stderr(Stdio::null()) 47 | .spawn() 48 | .unwrap() 49 | .wait() 50 | .expect("oh no!"); 51 | } 52 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | 3 | networks: 4 | bmnet: 5 | driver: bridge 6 | 7 | volumes: 8 | node1_data: 9 | bitmaskd_data: 10 | 11 | services: 12 | node1: 13 | container_name: bitcoin1 14 | image: bitmask/node1:latest 15 | platform: linux/amd64 16 | build: 17 | context: ./docker/esplora/ 18 | restart: unless-stopped 19 | command: ["/srv/explorer/run.sh", "bitcoin-regtest", "explorer"] 20 | volumes: 21 | - node1_data:/data 22 | ports: 23 | - 50001:50001 24 | - 3000:80 25 | networks: 26 | bmnet: 27 | # ipv4_address: 172.21.0.4 28 | aliases: 29 | - node1 30 | 31 | carbonado: 32 | container_name: carbonado 33 | image: bitmask/carbonado:latest 34 | platform: linux/amd64 35 | build: 36 | context: ./ 37 | dockerfile: ./docker/bitmask/Dockerfile 38 | restart: unless-stopped 39 | environment: 40 | - BITCOIN_NETWORK=regtest 41 | - BITCOIN_EXPLORER_API_REGTEST=http://node1:80/regtest/api 42 | - RGB_PROXY_ENDPOINT=http://localhost:3001 43 | ports: 44 | - 7070:7070 45 | networks: 46 | bmnet: 47 | aliases: 48 | - bitmaskd 49 | 50 | rgb-proxy: 51 | container_name: proxy 52 | image: bitmask/proxy:latest 53 | platform: linux/amd64 54 | build: 55 | context: ./ 56 | dockerfile: ./docker/rgb-proxy/Dockerfile 57 | restart: unless-stopped 58 | ports: 59 | - 3001:3000 60 | networks: 61 | bmnet: 62 | aliases: 63 | - bitmaskd 64 | -------------------------------------------------------------------------------- /file_hashes.toml: -------------------------------------------------------------------------------- 1 | [ASSETS_STOCK] 2 | "bitcoin-4b1bc93ea7f03c49c4424b56561c9c7437e5f16e5714cece615a48e249264a84.c15" = "urn:ubideco:stl:4fGZWR5mH5zZzRZ1r7CSRe776zm3hLBUngfXc4s3vm3V#saturn-flash-emerald-bitmask-fungible_assets_stock.c15" 3 | 4 | [ASSETS_WALLETS] 5 | "bitcoin-6075e9716c984b37840f76ad2b50b3d1b98ed286884e5ceba5bcc8e6b74988d3.c15" = "urn:ubideco:stl:4fGZWR5mH5zZzRZ1r7CSRe776zm3hLBUngfXc4s3vm3V#saturn-flash-emerald-bitmask-fungible_assets_wallets.c15" 6 | 7 | [ASSETS_TRANSFERS] 8 | "bitcoin-28cc77c6e8e65def696101d839d0f335effa5da22d4a9c6d843bc6caa87e957e.c15" = "urn:ubideco:stl:4fGZWR5mH5zZzRZ1r7CSRe776zm3hLBUngfXc4s3vm3V#saturn-flash-emerald-bitmask_assets_transfers.c15" 9 | 10 | [ASSETS_OFFERS] 11 | "bitcoin-dd9131edcd48ad270192f29706ca614078731640d126dd5ae0210006f41e1b05.c15" = "urn:ubideco:stl:4fGZWR5mH5zZzRZ1r7CSRe776zm3hLBUngfXc4s3vm3V#saturn-flash-emerald-bitmask-asset_offers.c15" 12 | 13 | [ASSETS_BIDS] 14 | "bitcoin-722c348c9b4764133e06fd7f0ae1f63e55237b2998a22b6a80825a7c916116c6.c15" = "urn:ubideco:stl:4fGZWR5mH5zZzRZ1r7CSRe776zm3hLBUngfXc4s3vm3V#saturn-flash-emerald-bitmask-asset_bids.c15" 15 | 16 | [MARKETPLACE_OFFERS] 17 | "bitcoin-1c4976934148eef543fc1926420101a6c50d865088e25e68484e5ad4fab9a9b0.c15" = "urn:ubideco:stl:4fGZWR5mH5zZzRZ1r7CSRe776zm3hLBUngfXc4s3vm3V#saturn-flash-emerald-bitmask-marketplace_public_offers.c15" 18 | 19 | [MARKETPLACE_BIDS] 20 | "bitcoin-b4a36514625daafd29414b889185aff46cb9e60b94176baff6fb2fcb974ed79e.c15" = "urn:ubideco:stl:4fGZWR5mH5zZzRZ1r7CSRe776zm3hLBUngfXc4s3vm3V#saturn-flash-emerald-bitmask-marketplace_public_bids.c15" 21 | -------------------------------------------------------------------------------- /tests/rgb/integration/collectibles.rs: -------------------------------------------------------------------------------- 1 | #![cfg(not(target_arch = "wasm32"))] 2 | // use crate::rgb::integration::utils::{ 3 | // create_new_invoice, create_new_psbt, create_new_transfer, issuer_issue_contract, 4 | // ISSUER_MNEMONIC, 5 | // }; 6 | // use bitmask_core::{ 7 | // bitcoin::{save_mnemonic, sign_and_publish_psbt_file}, 8 | // rgb::accept_transfer, 9 | // structs::{AcceptRequest, SecretString, SignPsbtRequest}, 10 | // }; 11 | 12 | // #[tokio::test] 13 | // async fn _allow_beneficiary_accept_transfer() -> anyhow::Result<()> { 14 | // let collectible = Some(get_collectible_data()); 15 | // let issuer_keys = save_mnemonic( 16 | // &SecretString(ISSUER_MNEMONIC.to_string()), 17 | // &SecretString("".to_string()), 18 | // ) 19 | // .await?; 20 | // let issuer_resp = issuer_issue_contract("RGB21", 1, false, true, collectible).await?; 21 | // let owner_resp = create_new_invoice(issuer_resp.clone(), None).await?; 22 | // let psbt_resp = create_new_psbt(issuer_keys.clone(), issuer_resp.clone()).await?; 23 | // let transfer_resp = create_new_transfer(issuer_keys.clone(), owner_resp, psbt_resp).await?; 24 | 25 | // let sk = issuer_keys.private.nostr_prv.to_string(); 26 | // let request = SignPsbtRequest { 27 | // psbt: transfer_resp.psbt, 28 | // descriptor: SecretString(issuer_keys.private.rgb_udas_descriptor_xprv), 29 | // }; 30 | // let resp = sign_and_publish_psbt_file(request).await; 31 | // assert!(resp.is_ok()); 32 | 33 | // let request = AcceptRequest { 34 | // consignment: transfer_resp.consig, 35 | // force: false, 36 | // }; 37 | 38 | // let resp = accept_transfer(&sk, request).await; 39 | // assert!(resp.is_ok()); 40 | // assert!(resp?.valid); 41 | // Ok(()) 42 | // } 43 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | # :: Explorers :: 2 | # BITCOIN_EXPLORER_API_MAINNET=http://18.217.213.66:3000 3 | # BITCOIN_EXPLORER_API_TESTNET=http://18.217.213.66:3000 4 | # BITCOIN_EXPLORER_API_SIGNET=http://18.217.213.66:3000 5 | BITCOIN_ELECTRUM_API_MAINNET=18.217.213.66:50001 6 | BITCOIN_ELECTRUM_API_TESTNET=18.217.213.66:60001 7 | BITCOIN_ELECTRUM_API_SIGNET=18.217.213.66:60601 8 | BITCOIN_ELECTRUM_API_REGTEST=localhost:50001 9 | BITCOIN_EXPLORER_API_MAINNET=https://diba.mempool.space/api 10 | BITCOIN_EXPLORER_API_TESTNET=https://diba.mempool.space/testnet/api 11 | BITCOIN_EXPLORER_API_SIGNET=https://mutinynet.com/api 12 | BITCOIN_EXPLORER_API_REGTEST=http://localhost:3000/regtest/api 13 | # BITCOIN_EXPLORER_API_MAINNET=http://127.0.0.1:3000 14 | # BITCOIN_EXPLORER_API_TESTNET=http://127.0.0.1:3000 15 | # BITCOIN_EXPLORER_API_SIGNET=http://127.0.0.1:3000 16 | # BITCOIN_ELECTRUM_API_MAINNET=mempool.space:50001 17 | # BITCOIN_ELECTRUM_API_TESTNET=mempool.space:60001 18 | # BITCOIN_ELECTRUM_API_SIGNET=mempool.space:60601 19 | 20 | # :: LN :: 21 | LNDHUB_ENDPOINT=https://lndhubx-prod.bitmask.app 22 | #LNDHUB_ENDPOINT=https://lndhubx.bitmask.app 23 | 24 | # :: Bitmask & Carbonado Server :: 25 | STRESS_TEST=false 26 | BITCOIN_NETWORK=regtest 27 | BITMASK_ENDPOINT=http://localhost:7070 28 | CARBONADO_ENDPOINT=http://localhost:7070/carbonado 29 | # CARBONADO_ENDPOINT=https://qvijq4x0ei.execute-api.us-east-2.amazonaws.com/dev/carbonado 30 | 31 | # :: Marketplace :: 32 | UDAS_UTXO=3b367e1facc3174e97658295961faf6a4ed889129c881b7a73db1f074b49bd8a: 33 | MARKETPLACE_SEED="lion bronze dumb tuna perfect fantasy wall orphan improve business harbor sadness" 34 | MARKETPLACE_NOSTR=cd591c134a0d88991326b1619953d0eae2287d315a7c4a93c1e4883a8c26c464 35 | # 1..100 36 | MARKETPLACE_FEE_PERC= 37 | # xpub.. 38 | MARKETPLACE_FEE_XPUB= 39 | 40 | # :: Coordinator :: 41 | COORDINATOR_NOSTR=9e8294eb38ba77c0fba982da8fbd370b8868c6dbfc9ca414aff4863c15dfbcff 42 | 43 | # :: RGB PROXY :: 44 | RGB_PROXY_ENDPOINT=http://localhost:3001 45 | -------------------------------------------------------------------------------- /tests/payjoin.rs: -------------------------------------------------------------------------------- 1 | #![cfg(not(target_arch = "wasm32"))] 2 | 3 | use std::env; 4 | 5 | use anyhow::Result; 6 | use bitmask_core::{ 7 | bitcoin::{decrypt_wallet, encrypt_wallet, get_wallet_data, hash_password, send_sats}, 8 | constants::switch_network, 9 | structs::SecretString, 10 | util::init_logging, 11 | }; 12 | use log::{debug, info}; 13 | 14 | const ENCRYPTION_PASSWORD: &str = "hunter2"; 15 | const SEED_PASSWORD: &str = ""; 16 | 17 | #[ignore] 18 | #[tokio::test] 19 | async fn payjoin() -> Result<()> { 20 | init_logging("payjoin=warn"); 21 | 22 | switch_network("testnet").await?; 23 | 24 | info!("Import wallets"); 25 | let mnemonic = env::var("TEST_WALLET_SEED")?; 26 | let hash = hash_password(&SecretString(ENCRYPTION_PASSWORD.to_owned())); 27 | let encrypted_descriptors = encrypt_wallet( 28 | &SecretString(mnemonic), 29 | &hash, 30 | &SecretString(SEED_PASSWORD.to_owned()), 31 | ) 32 | .await?; 33 | 34 | let vault = decrypt_wallet(&hash, &encrypted_descriptors)?; 35 | 36 | let wallet = get_wallet_data( 37 | &SecretString(vault.private.btc_descriptor_xprv.clone()), 38 | Some(&SecretString( 39 | vault.private.btc_change_descriptor_xprv.clone(), 40 | )), 41 | ) 42 | .await?; 43 | info!("Address: {}", wallet.address); 44 | 45 | info!("Initiating PayJoin using BIP-21"); 46 | let address = wallet.address; 47 | let destination = format!("bitcoin:{address}?pj=https://testnet.demo.btcpayserver.org/BTC/pj"); 48 | let amount = 1000; 49 | 50 | match send_sats( 51 | &SecretString(vault.private.btc_descriptor_xprv.clone()), 52 | &SecretString(vault.private.btc_change_descriptor_xprv.clone()), 53 | &destination, 54 | amount, 55 | Some(1.1), 56 | ) 57 | .await 58 | { 59 | Ok(_) => { 60 | panic!("Unexpected"); 61 | } 62 | Err(e) => { 63 | debug!("{:#?}", e); 64 | assert!(e.to_string().contains("invoice-not-found")); 65 | } 66 | }; 67 | 68 | Ok(()) 69 | } 70 | -------------------------------------------------------------------------------- /tests/rgb/integration/drain.rs: -------------------------------------------------------------------------------- 1 | #![cfg(not(target_arch = "wasm32"))] 2 | use anyhow::Result; 3 | use bitmask_core::{ 4 | bitcoin::{drain_wallet, get_wallet_data, new_mnemonic, save_mnemonic}, 5 | structs::SecretString, 6 | }; 7 | 8 | use crate::rgb::integration::utils::{send_some_coins, OWNER_MNEMONIC}; 9 | 10 | #[tokio::test] 11 | pub async fn drain() -> Result<()> { 12 | // 1. Initial Setup 13 | let old_keys = new_mnemonic(&SecretString("".to_string())).await?; 14 | let new_keys = save_mnemonic( 15 | &SecretString(OWNER_MNEMONIC.to_string()), 16 | &SecretString("".to_string()), 17 | ) 18 | .await?; 19 | 20 | let old_wallet_data = get_wallet_data( 21 | &SecretString(old_keys.public.btc_descriptor_xpub.clone()), 22 | Some(&SecretString( 23 | old_keys.public.btc_change_descriptor_xpub.clone(), 24 | )), 25 | ) 26 | .await?; 27 | 28 | send_some_coins(&old_wallet_data.address, "0.1").await; 29 | send_some_coins(&old_wallet_data.address, "0.1").await; 30 | send_some_coins(&old_wallet_data.address, "0.1").await; 31 | 32 | let new_wallet_data = get_wallet_data( 33 | &SecretString(new_keys.public.btc_descriptor_xpub.clone()), 34 | Some(&SecretString( 35 | new_keys.public.btc_change_descriptor_xpub.clone(), 36 | )), 37 | ) 38 | .await?; 39 | 40 | // 2. Drain sats from original wallet to new wallet 41 | let drain_wallet_details = drain_wallet( 42 | &new_wallet_data.address, 43 | &SecretString(old_keys.private.btc_descriptor_xprv.clone()), 44 | Some(&SecretString( 45 | old_keys.private.btc_change_descriptor_xprv.clone(), 46 | )), 47 | Some(2.0), 48 | ) 49 | .await?; 50 | 51 | assert_eq!( 52 | drain_wallet_details.details.received, 0, 53 | "received no funds in this transaction" 54 | ); 55 | assert_eq!( 56 | drain_wallet_details.details.sent + drain_wallet_details.details.fee.expect("fee present"), 57 | 30_000_000, 58 | "received 0.3 tBTC" 59 | ); 60 | 61 | Ok(()) 62 | } 63 | -------------------------------------------------------------------------------- /tests/rgb/unit/psbt.rs: -------------------------------------------------------------------------------- 1 | #![cfg(not(target_arch = "wasm32"))] 2 | use crate::rgb::unit::utils::{ 3 | create_fake_contract, create_fake_invoice, create_fake_psbt, DumbResolve, 4 | }; 5 | use bitmask_core::{ 6 | rgb::{ 7 | consignment::NewTransferOptions, 8 | psbt::{create_psbt, extract_output_commit, NewPsbtOptions}, 9 | transfer::pay_invoice, 10 | }, 11 | structs::{PsbtInputRequest, SecretString}, 12 | util::init_logging, 13 | }; 14 | use rgb::persistence::Stock; 15 | 16 | #[tokio::test] 17 | async fn allow_create_psbt_file() -> anyhow::Result<()> { 18 | init_logging("rgb_psbt=warn"); 19 | 20 | let desc = "tr(m=[280a5963]/86h/1h/0h=[tpubDCa3US185mM8yGTXtPWY1wNRMCiX89kzN4dwTMKUJyiJnnq486MTeyYShvHiS8Dd1zR2myy5xyJFDs5YacVHn6JZbVaDAtkrXZE3tTVRHPu]/*/*)#8an50cqp"; 21 | let asset_utxo = "5ca6cd1f54c081c8b3a7b4bcc988e55fe3c420ac87512b53a58c55233e15ba4f:1"; 22 | let asset_utxo_terminal = "/0/0"; 23 | 24 | let fee = 1000; 25 | let tx_resolver = DumbResolve {}; 26 | 27 | let psbt = create_psbt( 28 | vec![PsbtInputRequest { 29 | descriptor: SecretString(desc.to_string()), 30 | utxo: asset_utxo.to_string(), 31 | utxo_terminal: asset_utxo_terminal.to_string(), 32 | sigh_hash: None, 33 | tapret: None, 34 | }], 35 | vec![], 36 | fee, 37 | Some("/0/1".to_string()), 38 | None, 39 | &tx_resolver, 40 | NewPsbtOptions::default(), 41 | ); 42 | assert!(psbt.is_ok()); 43 | 44 | Ok(()) 45 | } 46 | 47 | #[tokio::test] 48 | async fn allow_extract_output_commit_from_psbt() -> anyhow::Result<()> { 49 | let mut stock = Stock::default(); 50 | let psbt = create_fake_psbt(); 51 | 52 | let contract_id = create_fake_contract(&mut stock); 53 | 54 | let seal = "tapret1st:ed823b41d8b9309933826b18e4af530363b359f05919c02bbe72f28cec6dec3e:0"; 55 | let invoice = create_fake_invoice(contract_id, seal, &mut stock); 56 | 57 | let options = NewTransferOptions::default(); 58 | let result = pay_invoice(invoice.to_string(), psbt.to_string(), options, &mut stock); 59 | assert!(result.is_ok()); 60 | 61 | let (psbt, _) = result.unwrap(); 62 | 63 | let commit = extract_output_commit(psbt); 64 | assert!(commit.is_ok()); 65 | Ok(()) 66 | } 67 | -------------------------------------------------------------------------------- /lib/web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bitmask-core", 3 | "collaborators": [ 4 | "Jose Diego Robles ", 5 | "Hunter Trujillo ", 6 | "Francisco Calderón " 7 | ], 8 | "description": "Core functionality for the BitMask wallet", 9 | "version": "0.7.0-beta.10", 10 | "license": "MIT", 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/diba-io/bitmask-core.git" 14 | }, 15 | "files": [ 16 | "index.js", 17 | "bitcoin.js", 18 | "carbonado.js", 19 | "constants.js", 20 | "lightning.js", 21 | "nostr.js", 22 | "rgb.js", 23 | "index.ts", 24 | "bitcoin.ts", 25 | "carbonado.ts", 26 | "constants.ts", 27 | "lightning.ts", 28 | "nostr.ts", 29 | "rgb.ts", 30 | "index.ts", 31 | "index.d.ts", 32 | "index.d.ts.map", 33 | "bitcoin.ts", 34 | "bitcoin.d.ts", 35 | "bitcoin.d.ts.map", 36 | "carbonado.ts", 37 | "carbonado.d.ts", 38 | "carbonado.d.ts.map", 39 | "constants.ts", 40 | "constants.d.ts", 41 | "constants.d.ts.map", 42 | "lightning.ts", 43 | "lightning.d.ts", 44 | "lightning.d.ts.map", 45 | "nostr.ts", 46 | "nostr.d.ts", 47 | "nostr.d.ts.map", 48 | "rgb.ts", 49 | "rgb.d.ts", 50 | "rgb.d.ts.map", 51 | "bitmask_core_bg.wasm", 52 | "bitmask_core_bg.wasm.d.ts", 53 | "bitmask_core.js", 54 | "bitmask_core_bg.js", 55 | "bitmask_core.d.ts", 56 | "LICENSE-APACHE", 57 | "LICENSE-MIT" 58 | ], 59 | "scripts": { 60 | "prepare": "tsc -p ./tsconfig.json" 61 | }, 62 | "module": "index.js", 63 | "homepage": "https://bitmask.app", 64 | "types": "index.d.ts", 65 | "devDependencies": { 66 | "@types/node": "^20.8.2", 67 | "typescript": "^5.2.2" 68 | }, 69 | "sideEffects": [ 70 | "./bitmask_core.js", 71 | "./snippets/*" 72 | ], 73 | "keywords": [ 74 | "bitmask", 75 | "bitcoin", 76 | "wallet", 77 | "crypto", 78 | "web3", 79 | "diba", 80 | "nostr", 81 | "bitmask.js", 82 | "bitmaskjs", 83 | "wasm", 84 | "rust", 85 | "typescript", 86 | "lightning", 87 | "network", 88 | "rgb", 89 | "tokens", 90 | "contracts", 91 | "nft", 92 | "protocol", 93 | "satoshi", 94 | "nakamoto", 95 | "carbonado" 96 | ] 97 | } 98 | -------------------------------------------------------------------------------- /tests/rgb/web/inspect.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | #![allow(unused_imports)] 3 | #![allow(unused_variables)] 4 | #![cfg(target_arch = "wasm32")] 5 | use bitmask_core::{ 6 | info, 7 | structs::{ 8 | ContractResponse, ContractsResponse, DecryptedWalletData, IssueRequest, 9 | NextAddressResponse, NextUtxoResponse, SecretString, WatcherRequest, WatcherResponse, 10 | }, 11 | web::{ 12 | bitcoin::{ 13 | decrypt_wallet, encrypt_wallet, get_assets_vault, get_wallet_data, hash_password, 14 | }, 15 | constants::switch_network, 16 | json_parse, resolve, 17 | rgb::{ 18 | create_watcher, get_contract, import_contract, issue_contract, watcher_next_address, 19 | watcher_next_utxo, 20 | }, 21 | set_panic_hook, 22 | }, 23 | }; 24 | use wasm_bindgen::prelude::*; 25 | use wasm_bindgen_futures::JsFuture; 26 | use wasm_bindgen_test::*; 27 | use web_sys::console; 28 | 29 | wasm_bindgen_test_configure!(run_in_browser); 30 | 31 | const ENCRYPTION_PASSWORD: &str = ""; 32 | const SEED_PASSWORD: &str = ""; 33 | 34 | // #[wasm_bindgen_test] 35 | async fn inspect_contract_states() { 36 | set_panic_hook(); 37 | let mnemonic = ""; 38 | let hash = hash_password(ENCRYPTION_PASSWORD.to_owned()); 39 | 40 | resolve(switch_network("bitcoin".to_string())).await; 41 | 42 | info!("Import Seed"); 43 | let mnemonic_data_str = resolve(encrypt_wallet( 44 | mnemonic.to_owned(), 45 | hash.clone(), 46 | SEED_PASSWORD.to_owned(), 47 | )) 48 | .await; 49 | let mnemonic_data: SecretString = json_parse(&mnemonic_data_str); 50 | 51 | info!("Get Vault"); 52 | let wallet_keys: JsValue = resolve(decrypt_wallet(hash, mnemonic_data.0.clone())).await; 53 | 54 | info!("Get Keys"); 55 | let wallet_keys: DecryptedWalletData = json_parse(&wallet_keys); 56 | let sk = &wallet_keys.private.nostr_prv; 57 | 58 | info!("Get Contract"); 59 | let contract_id = ""; 60 | let get_contract_resp: JsValue = 61 | resolve(get_contract(sk.to_string(), contract_id.to_string())).await; 62 | let get_contract_resp: ContractResponse = json_parse(&get_contract_resp); 63 | info!(format!( 64 | "Contract {} ({}): \n {:#?}", 65 | get_contract_resp.name, get_contract_resp.balance, get_contract_resp.allocations 66 | )); 67 | } 68 | -------------------------------------------------------------------------------- /tests/rgb/unit/issue.rs: -------------------------------------------------------------------------------- 1 | #![cfg(not(target_arch = "wasm32"))] 2 | use anyhow::Result; 3 | use bitmask_core::{ 4 | rgb::issue::issue_contract, structs::IssueRequest, util::init_logging, validators::RGBContext, 5 | }; 6 | use garde::Validate; 7 | use rgbstd::persistence::Stock; 8 | 9 | use crate::rgb::unit::utils::{get_uda_data, DumbResolve}; 10 | 11 | #[tokio::test] 12 | async fn issue_request_params_check() -> Result<()> { 13 | init_logging("rgb_issue=warn"); 14 | 15 | let ticker = "DIBA"; 16 | let name = "DIBA"; 17 | let description = 18 | "1 2 3 testing... 1 2 3 testing... 1 2 3 testing... 1 2 3 testing.... 1 2 3 testing"; 19 | let precision = 8; 20 | let supply = 10; 21 | let iface = "RGB20"; 22 | let seal = "tapret1st:70339a6b27f55105da2d050babc759f046c21c26b7b75e9394bc1d818e50ff52:0"; 23 | 24 | let ctx = &RGBContext::default(); 25 | let rgb20 = IssueRequest { 26 | ticker: ticker.to_string(), 27 | name: name.to_string(), 28 | description: description.to_string(), 29 | supply, 30 | precision, 31 | seal: seal.to_string(), 32 | iface: iface.to_string(), 33 | meta: None, 34 | }; 35 | assert!(rgb20.validate(ctx).is_ok()); 36 | 37 | let rgb21 = IssueRequest { 38 | ticker: ticker.to_string(), 39 | name: name.to_string(), 40 | description: description.to_string(), 41 | supply, 42 | precision, 43 | seal: seal.to_string(), 44 | iface: iface.to_string(), 45 | meta: Some(get_uda_data()), 46 | }; 47 | assert!(rgb21.validate(ctx).is_ok()); 48 | 49 | Ok(()) 50 | } 51 | 52 | #[tokio::test] 53 | async fn issue_contract_test() -> Result<()> { 54 | init_logging("rgb_issue=warn"); 55 | 56 | let ticker = "DIBA"; 57 | let name = "DIBA"; 58 | let description = 59 | "1 2 3 testing... 1 2 3 testing... 1 2 3 testing... 1 2 3 testing.... 1 2 3 testing"; 60 | let precision = 8; 61 | let supply = 10; 62 | let iface = "RGB20"; 63 | let seal = "tapret1st:70339a6b27f55105da2d050babc759f046c21c26b7b75e9394bc1d818e50ff52:0"; 64 | let network = "regtest"; 65 | 66 | let mut stock = Stock::default(); 67 | let mut resolver = DumbResolve {}; 68 | 69 | let contract = issue_contract( 70 | ticker, 71 | name, 72 | description, 73 | precision, 74 | supply, 75 | iface, 76 | seal, 77 | network, 78 | None, 79 | &mut resolver, 80 | &mut stock, 81 | ); 82 | 83 | assert!(contract.is_ok()); 84 | Ok(()) 85 | } 86 | -------------------------------------------------------------------------------- /tests/rgb/integration/fungibles.rs: -------------------------------------------------------------------------------- 1 | #![cfg(not(target_arch = "wasm32"))] 2 | use crate::rgb::integration::utils::{ 3 | create_new_invoice, create_new_psbt, create_new_transfer, issuer_issue_contract_v2, UtxoFilter, 4 | ISSUER_MNEMONIC, OWNER_MNEMONIC, 5 | }; 6 | use bitmask_core::{ 7 | bitcoin::{save_mnemonic, sign_and_publish_psbt_file}, 8 | rgb::{accept_transfer, structs::ContractAmount}, 9 | structs::{AcceptRequest, SecretString, SignPsbtRequest}, 10 | }; 11 | 12 | #[tokio::test] 13 | async fn accept_fungible_transfer() -> anyhow::Result<()> { 14 | let issuer_keys = save_mnemonic( 15 | &SecretString(ISSUER_MNEMONIC.to_string()), 16 | &SecretString("".to_string()), 17 | ) 18 | .await?; 19 | let owner_keys = save_mnemonic( 20 | &SecretString(OWNER_MNEMONIC.to_string()), 21 | &SecretString("".to_string()), 22 | ) 23 | .await?; 24 | let issuer_resp = issuer_issue_contract_v2( 25 | 1, 26 | "RGB20", 27 | ContractAmount::with(5, 0, 2).to_value(), 28 | false, 29 | true, 30 | None, 31 | Some("0.1".to_string()), 32 | Some(UtxoFilter::with_amount_equal_than(10000000)), 33 | None, 34 | ) 35 | .await?; 36 | let issuer_resp = &issuer_resp[0]; 37 | 38 | let owner_resp = &create_new_invoice( 39 | &issuer_resp.contract_id, 40 | &issuer_resp.iface, 41 | ContractAmount::with(1, 0, issuer_resp.precision), 42 | owner_keys.clone(), 43 | None, 44 | Some(issuer_resp.clone().contract.legacy), 45 | ) 46 | .await?; 47 | let psbt_resp = create_new_psbt( 48 | &issuer_resp.contract_id, 49 | &issuer_resp.iface, 50 | vec![issuer_resp.issue_utxo.clone()], 51 | issuer_keys.clone(), 52 | ) 53 | .await?; 54 | let transfer_resp = 55 | &create_new_transfer(issuer_keys.clone(), owner_resp.clone(), psbt_resp).await?; 56 | 57 | let sk = issuer_keys.private.nostr_prv.to_string(); 58 | let request = SignPsbtRequest { 59 | psbt: transfer_resp.psbt.clone(), 60 | descriptors: [SecretString( 61 | issuer_keys.private.rgb_assets_descriptor_xprv.clone(), 62 | )] 63 | .to_vec(), 64 | }; 65 | let resp = sign_and_publish_psbt_file(request).await; 66 | assert!(resp.is_ok()); 67 | 68 | let request = AcceptRequest { 69 | consignment: transfer_resp.consig.clone(), 70 | force: false, 71 | }; 72 | 73 | let resp = accept_transfer(&sk, request).await; 74 | assert!(resp.is_ok()); 75 | assert!(resp?.valid); 76 | Ok(()) 77 | } 78 | -------------------------------------------------------------------------------- /tests/rgb/web/utils.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | #![allow(unused_imports)] 3 | #![allow(unused_variables)] 4 | #![cfg(target_arch = "wasm32")] 5 | use bitmask_core::constants::BITMASK_ENDPOINT; 6 | use gloo_net::http::Request; 7 | use wasm_bindgen::prelude::*; 8 | use wasm_bindgen_futures::JsFuture; 9 | use wasm_bindgen_test::*; 10 | use web_sys::console; 11 | 12 | pub async fn new_block() -> Result { 13 | let bitmask_endpoint = BITMASK_ENDPOINT.read().await.to_string(); 14 | let endpoint = format!("{bitmask_endpoint}/regtest/block"); 15 | 16 | let request = Request::get(&endpoint) 17 | .header("Content-Type", "application/octet-stream") 18 | .header("Cache-Control", "no-cache") 19 | .build(); 20 | 21 | let request = match request { 22 | Ok(request) => request, 23 | Err(e) => return Err(JsValue::from(e.to_string())), 24 | }; 25 | 26 | let response = request.send().await; 27 | match response { 28 | Ok(response) => { 29 | let status_code = response.status(); 30 | if status_code == 200 { 31 | match response.text().await { 32 | Ok(text) => Ok(JsValue::from(&text)), 33 | Err(e) => Err(JsValue::from(e.to_string())), 34 | } 35 | } else { 36 | Err(JsValue::from(status_code)) 37 | } 38 | } 39 | Err(e) => Err(JsValue::from(e.to_string())), 40 | } 41 | } 42 | 43 | pub async fn send_coins(address: &str, amount: &str) -> Result { 44 | let bitmask_endpoint = BITMASK_ENDPOINT.read().await.to_string(); 45 | let endpoint = format!("{bitmask_endpoint}/regtest/send/{address}/{amount}"); 46 | 47 | let request = Request::get(&endpoint) 48 | .header("Content-Type", "application/octet-stream") 49 | .header("Cache-Control", "no-cache") 50 | .build(); 51 | 52 | let request = match request { 53 | Ok(request) => request, 54 | Err(e) => return Err(JsValue::from(e.to_string())), 55 | }; 56 | 57 | let response = request.send().await; 58 | match response { 59 | Ok(response) => { 60 | let status_code = response.status(); 61 | if status_code == 200 { 62 | match response.text().await { 63 | Ok(text) => Ok(JsValue::from(&text)), 64 | Err(e) => Err(JsValue::from(e.to_string())), 65 | } 66 | } else { 67 | Err(JsValue::from(status_code)) 68 | } 69 | } 70 | Err(e) => Err(JsValue::from(e.to_string())), 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /tests/keys.rs: -------------------------------------------------------------------------------- 1 | #![cfg(not(target_arch = "wasm32"))] 2 | 3 | use anyhow::Result; 4 | use bitmask_core::{ 5 | bitcoin::{get_wallet_data, save_mnemonic}, 6 | constants::switch_network, 7 | structs::SecretString, 8 | util::init_logging, 9 | }; 10 | 11 | #[tokio::test] 12 | pub async fn taproot() -> Result<()> { 13 | init_logging("taproot=debug"); 14 | 15 | const MNEMONIC: &str = 16 | "empty faculty salute fortune select asthma attract question violin movie smile erupt half step lion deposit render stumble double mobile fossil height usual topple"; 17 | 18 | switch_network("bitcoin").await?; 19 | 20 | let decrypted_wallet = save_mnemonic( 21 | &SecretString(MNEMONIC.to_owned()), 22 | &SecretString("".to_owned()), 23 | ) 24 | .await?; 25 | 26 | let wallet_data = get_wallet_data( 27 | &SecretString(decrypted_wallet.public.btc_descriptor_xpub.to_owned()), 28 | None, 29 | ) 30 | .await?; 31 | 32 | // Compare to those generated by Sparrow Wallet 33 | assert_eq!( 34 | decrypted_wallet.public.btc_descriptor_xpub, 35 | "tr([496f1ccc/86'/0'/0']xpub6CBkARCPxmbRjaxzHxC38e9sKUVtMTRFqBYUFdXAHFBpeQzJz6mYSaQ1qSvCrNzYUNuvpD9FS6fmK9YowdCxaiCUSpjzNm5hvV2JxEodZ1q/0/*)", 36 | "correct taproot xpub descriptor is derived from mnemonic" 37 | ); 38 | 39 | assert_eq!( 40 | decrypted_wallet.public.xpub, 41 | "xpub6CBkARCPxmbRjaxzHxC38e9sKUVtMTRFqBYUFdXAHFBpeQzJz6mYSaQ1qSvCrNzYUNuvpD9FS6fmK9YowdCxaiCUSpjzNm5hvV2JxEodZ1q", 42 | "correct taproot xpub is derived from mnemonic" 43 | ); 44 | 45 | assert_eq!( 46 | wallet_data.address, "bc1pljwytlvv9n8ug5e7cxrjrfmhudd2w7r0nmdpt7j0387mc0zzpveq6jeqs6", 47 | "correct first address is derived" 48 | ); 49 | 50 | Ok(()) 51 | } 52 | 53 | #[tokio::test] 54 | pub async fn nip06() -> Result<()> { 55 | init_logging("nostr_tests=debug"); 56 | 57 | // Using this tool: 58 | // https://nip06.jaonoct.us 59 | 60 | const MNEMONIC: &str = 61 | "garment castle exhaust confirm wrong timber earth invest output comfort actress slot"; 62 | 63 | let wallet_data = save_mnemonic( 64 | &SecretString(MNEMONIC.to_owned()), 65 | &SecretString("".to_owned()), 66 | ) 67 | .await?; 68 | 69 | assert_eq!( 70 | wallet_data.private.nostr_nsec, 71 | "nsec1t9s9xyu55mezxwkf98uws2m8h6smjvehgngme0346rwm456g3kfs8pt0qw", 72 | "correct nsec is generated" 73 | ); 74 | assert_eq!( 75 | wallet_data.public.nostr_npub, 76 | "npub1v5zwxyd3dtmrvgnamxxlfxj92t52hwkg09dmwjhmkjujkq8kdzms77547c", 77 | "correct npub is generated" 78 | ); 79 | 80 | Ok(()) 81 | } 82 | -------------------------------------------------------------------------------- /tests/rgb/integration/crdt.rs: -------------------------------------------------------------------------------- 1 | #![cfg(not(target_arch = "wasm32"))] 2 | use crate::rgb::integration::utils::get_raw_wallet; 3 | use amplify::confinement::Collection; 4 | use automerge::AutoCommit; 5 | use autosurgeon::{hydrate, reconcile}; 6 | use bitmask_core::rgb::crdt::{RawRgbWallet, RawUtxo}; 7 | use rgb::{RgbWallet, Utxo}; 8 | 9 | #[tokio::test] 10 | async fn allow_fork_with_previous_version() -> anyhow::Result<()> { 11 | let current_raw_wallet = get_raw_wallet(); 12 | let new_raw_utxo = RawUtxo { 13 | outpoint: "9a5d21d4cc15ffa14c6f416396235c082cddb5e227abd863974445709f8e9af0:0".to_string(), 14 | block: 0, 15 | amount: 10000000, 16 | terminal: "20:1".to_string(), 17 | tweak: Some("gu0x5*b6~L+)(kY3 anyhow::Result<()> { 44 | let current_raw_wallet = get_raw_wallet(); 45 | let new_raw_utxo = RawUtxo { 46 | outpoint: "9a5d21d4cc15ffa14c6f416396235c082cddb5e227abd863974445709f8e9af0:0".to_string(), 47 | block: 0, 48 | amount: 10000000, 49 | terminal: "20:1".to_string(), 50 | tweak: Some("gu0x5*b6~L+)(kY3 Result { 13 | // we generate a random string to be used as username and password 14 | let random_number = bip39::rand::random::(); 15 | let s = hex::encode(random_number.to_le_bytes()); 16 | // We put to sleep the test to avoid hit too fast the API 17 | thread::sleep(Duration::from_secs(1)); 18 | let res = create_wallet(&s, &s).await?; 19 | 20 | Ok(res) 21 | } 22 | 23 | #[tokio::test] 24 | pub async fn new_nostr_pubkey_test() -> Result<()> { 25 | init_logging("nostr_tests=debug"); 26 | info!("We create user Alice"); 27 | let res = new_wallet().await?; 28 | let mut alice = String::new(); 29 | if let CreateWalletResponse::Username { username } = res { 30 | alice = username; 31 | } 32 | let alice_response = auth(&alice, &alice).await?; 33 | thread::sleep(Duration::from_secs(1)); 34 | 35 | use nostr_sdk::prelude::*; 36 | 37 | // Generate new nostr keys 38 | let keys: Keys = Keys::generate(); 39 | let pubkey = keys.public_key().to_string(); 40 | 41 | if let AuthResponse::Result { refresh: _, token } = alice_response { 42 | let response = new_nostr_pubkey(&pubkey, &token).await?; 43 | assert_eq!(response.status, "ok".to_string()); 44 | } 45 | 46 | Ok(()) 47 | } 48 | 49 | #[tokio::test] 50 | pub async fn update_nostr_pubkey_test() -> Result<()> { 51 | init_logging("nostr_tests=debug"); 52 | info!("We create user Alice"); 53 | let res = new_wallet().await?; 54 | let mut alice = String::new(); 55 | if let CreateWalletResponse::Username { username } = res { 56 | alice = username; 57 | } 58 | let alice_response = auth(&alice, &alice).await?; 59 | thread::sleep(Duration::from_secs(1)); 60 | 61 | use nostr_sdk::prelude::*; 62 | 63 | // Generate new nostr keys 64 | let keys: Keys = Keys::generate(); 65 | let pubkey = keys.public_key().to_string(); 66 | 67 | if let AuthResponse::Result { refresh: _, token } = alice_response { 68 | let response = new_nostr_pubkey(&pubkey, &token).await?; 69 | assert_eq!(response.status, "ok".to_string()); 70 | // Update the nostr pubkey 71 | // Generate newer nostr keys 72 | let keys: Keys = Keys::generate(); 73 | let pubkey = keys.public_key().to_string(); 74 | let response = update_nostr_pubkey(&pubkey, &token).await?; 75 | assert_eq!(response.status, "ok".to_string()); 76 | } 77 | 78 | Ok(()) 79 | } 80 | -------------------------------------------------------------------------------- /tests/rgb/unit/stock.rs: -------------------------------------------------------------------------------- 1 | #![cfg(not(target_arch = "wasm32"))] 2 | use bitmask_core::util::init_logging; 3 | use rgbstd::persistence::{Inventory, Stash, Stock}; 4 | 5 | use crate::rgb::unit::utils::create_fake_contract; 6 | 7 | #[tokio::test] 8 | async fn allow_list_contracts() -> anyhow::Result<()> { 9 | init_logging("rgb_stock=warn"); 10 | 11 | let mut stock = Stock::default(); 12 | create_fake_contract(&mut stock); 13 | 14 | let mut contracts = vec![]; 15 | for schema_id in stock.schema_ids().expect("invalid schemas state") { 16 | let schema = stock.schema(schema_id).expect("invalid schemas state"); 17 | for (iface_id, _) in schema.clone().iimpls.into_iter() { 18 | for contract_id in stock.contract_ids().expect("invalid contracts state") { 19 | if stock.contract_iface(contract_id, iface_id).is_ok() { 20 | let iface = stock.iface_by_id(iface_id).expect("oh no!"); 21 | let iface_name = iface.name.to_string(); 22 | let contract = contract_id.to_string(); 23 | contracts.push(format!("{iface_name}:{contract}")); 24 | } 25 | } 26 | } 27 | } 28 | assert!(!contracts.is_empty()); 29 | Ok(()) 30 | } 31 | 32 | #[tokio::test] 33 | async fn allow_list_interfaces() -> anyhow::Result<()> { 34 | init_logging("rgb_stock=warn"); 35 | 36 | let mut stock = Stock::default(); 37 | create_fake_contract(&mut stock); 38 | 39 | let mut interfaces = vec![]; 40 | for schema_id in stock.schema_ids().expect("invalid schemas state") { 41 | let schema = stock.schema(schema_id).expect("invalid schemas state"); 42 | for (iface_id, iimpl) in schema.clone().iimpls.into_iter() { 43 | let iface = stock.iface_by_id(iface_id).expect("oh no!"); 44 | let iface_name = iface.name.to_string(); 45 | let iimpl_id = iimpl.impl_id().to_string(); 46 | interfaces.push(format!("{iface_name}:{iimpl_id}")); 47 | } 48 | } 49 | assert!(!interfaces.is_empty()); 50 | Ok(()) 51 | } 52 | 53 | #[tokio::test] 54 | async fn allow_list_schemas() -> anyhow::Result<()> { 55 | init_logging("rgb_stock=warn"); 56 | 57 | let mut stock = Stock::default(); 58 | create_fake_contract(&mut stock); 59 | 60 | let mut schemas = vec![]; 61 | for schema_id in stock.schema_ids().expect("invalid schemas state") { 62 | let schema = stock.schema(schema_id).expect("invalid schemas state"); 63 | for (iface_id, _) in schema.clone().iimpls.into_iter() { 64 | let iface = stock.iface_by_id(iface_id).expect("oh no!"); 65 | let iface_name = iface.name.to_string(); 66 | schemas.push(format!("{schema_id}:{iface_name}")); 67 | } 68 | } 69 | assert!(!schemas.is_empty()); 70 | Ok(()) 71 | } 72 | -------------------------------------------------------------------------------- /tests/rgb/unit/invoice.rs: -------------------------------------------------------------------------------- 1 | #![cfg(not(target_arch = "wasm32"))] 2 | use std::collections::HashMap; 3 | 4 | use amplify::{confinement::U32, hex::ToHex}; 5 | use bitmask_core::{ 6 | rgb::{ 7 | consignment::NewTransferOptions, 8 | transfer::{accept_transfer, create_invoice, pay_invoice}, 9 | }, 10 | util::init_logging, 11 | }; 12 | use rgbstd::persistence::Stock; 13 | use strict_encoding::StrictSerialize; 14 | 15 | use crate::rgb::unit::utils::{ 16 | create_fake_contract, create_fake_invoice, create_fake_psbt, DumbResolve, 17 | }; 18 | 19 | #[tokio::test] 20 | async fn allow_create_invoice() -> anyhow::Result<()> { 21 | init_logging("rgb_invoice=warn"); 22 | 23 | let iface = "RGB20"; 24 | let seal = "tapret1st:ed823b41d8b9309933826b18e4af530363b359f05919c02bbe72f28cec6dec3e:0"; 25 | let amount = 1; 26 | 27 | let mut stock = Stock::default(); 28 | let params = HashMap::new(); 29 | let contract_id = create_fake_contract(&mut stock); 30 | let result = create_invoice( 31 | &contract_id.to_string(), 32 | iface, 33 | amount, 34 | seal, 35 | "regtest", 36 | params, 37 | &mut stock, 38 | ); 39 | 40 | assert!(result.is_ok()); 41 | Ok(()) 42 | } 43 | 44 | #[tokio::test] 45 | async fn allow_pay_invoice() -> anyhow::Result<()> { 46 | init_logging("rgb_invoice=warn"); 47 | 48 | let mut resolver = DumbResolve {}; 49 | let mut stock = Stock::default(); 50 | let psbt = create_fake_psbt(); 51 | 52 | let contract_id = create_fake_contract(&mut stock); 53 | 54 | let seal = "tapret1st:ed823b41d8b9309933826b18e4af530363b359f05919c02bbe72f28cec6dec3e:0"; 55 | let invoice = create_fake_invoice(contract_id, seal, &mut stock); 56 | 57 | let options = NewTransferOptions::default(); 58 | let result = pay_invoice(invoice.to_string(), psbt.to_string(), options, &mut stock); 59 | assert!(result.is_ok()); 60 | 61 | let (_, transfer) = result.unwrap(); 62 | 63 | let transfer = &transfer[0]; 64 | let pay_status = transfer.clone().unbindle().validate(&mut resolver); 65 | assert!(pay_status.is_ok()); 66 | Ok(()) 67 | } 68 | 69 | #[tokio::test] 70 | async fn allow_accept_invoice() -> anyhow::Result<()> { 71 | init_logging("rgb_invoice=warn"); 72 | 73 | let mut resolver = DumbResolve {}; 74 | let mut stock = Stock::default(); 75 | let psbt = create_fake_psbt(); 76 | 77 | let contract_id = create_fake_contract(&mut stock); 78 | 79 | let seal = "tapret1st:ed823b41d8b9309933826b18e4af530363b359f05919c02bbe72f28cec6dec3e:0"; 80 | let invoice = create_fake_invoice(contract_id, seal, &mut stock); 81 | 82 | let options = NewTransferOptions::default(); 83 | let result = pay_invoice(invoice.to_string(), psbt.to_string(), options, &mut stock); 84 | assert!(result.is_ok()); 85 | 86 | let (_, transfer) = result.unwrap(); 87 | let transfer = &transfer[0]; 88 | let transfer_hex = transfer.to_strict_serialized::().unwrap().to_hex(); 89 | 90 | let pay_status = accept_transfer(transfer_hex, true, &mut resolver, &mut stock); 91 | assert!(pay_status.is_ok()); 92 | Ok(()) 93 | } 94 | -------------------------------------------------------------------------------- /src/nostr.rs: -------------------------------------------------------------------------------- 1 | use ::bitcoin::hashes::hex::ToHex; 2 | use anyhow::{anyhow, Result}; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | use crate::{constants::LNDHUB_ENDPOINT, util::post_json_auth}; 6 | 7 | /// Nostr pubkey 8 | #[derive(Debug, Serialize, Deserialize, Clone)] 9 | pub struct Nostr { 10 | pub pubkey: String, 11 | } 12 | 13 | /// Status response 14 | #[derive(Debug, Serialize, Deserialize, Clone)] 15 | pub struct Response { 16 | pub status: String, 17 | } 18 | 19 | fn validate_pubkey(pubkey: &str) -> Result { 20 | let pubkey = hex::decode(pubkey)?; 21 | 22 | if pubkey.len() == 32 { 23 | Ok(hex::encode(pubkey)) 24 | } else if pubkey.len() == 33 && (pubkey[0] == 2 || pubkey[0] == 3) { 25 | Ok(hex::encode(pubkey.get(1..33).unwrap())) 26 | } else { 27 | Err(anyhow!("Hex key is of wrong length or format")) 28 | } 29 | } 30 | 31 | #[test] 32 | fn test_validate_pubkey() -> Result<()> { 33 | let result = 34 | validate_pubkey("03b0635d6a9851d3aed0cd6c495b282167acf761729078d975fc341b22650b07b9")?; 35 | 36 | assert_eq!( 37 | "b0635d6a9851d3aed0cd6c495b282167acf761729078d975fc341b22650b07b9", result, 38 | "strips leading parity byte on 33 byte x-only pubkey" 39 | ); 40 | 41 | Ok(()) 42 | } 43 | 44 | #[test] 45 | fn test_validate_32bytes_pubkey() -> Result<()> { 46 | let result = 47 | validate_pubkey("0000066e0359c33a0bed474853a610f744404f265140ecf5171b38483aaea2bb")?; 48 | 49 | assert_eq!( 50 | "0000066e0359c33a0bed474853a610f744404f265140ecf5171b38483aaea2bb", result, 51 | "32 bytes pubkey" 52 | ); 53 | 54 | Ok(()) 55 | } 56 | 57 | fn parse_npub(pubkey: &str) -> Result { 58 | use nostr_sdk::prelude::*; 59 | 60 | if pubkey.starts_with("npub") { 61 | let key = XOnlyPublicKey::from_bech32(pubkey)?; 62 | Ok(key.to_hex()) 63 | } else { 64 | Ok(pubkey.to_owned()) 65 | } 66 | } 67 | 68 | #[test] 69 | fn test_parse_npub() -> Result<()> { 70 | let result = parse_npub("npub1qqqqqqqx2tj99mng5qgc07cgezv5jm95dj636x4qsq7svwkwmwnse3rfkq")?; 71 | 72 | assert_eq!( 73 | result, 74 | "000000000652e452ee68a01187fb08c899496cb46cb51d1aa0803d063acedba7" 75 | ); 76 | 77 | Ok(()) 78 | } 79 | 80 | /// Add a new nostr pubkey to a user 81 | pub async fn new_nostr_pubkey(pubkey: &str, token: &str) -> Result { 82 | let pubkey = parse_npub(pubkey)?; 83 | let pubkey = validate_pubkey(&pubkey)?; 84 | 85 | let endpoint = LNDHUB_ENDPOINT.read().await; 86 | let pubkey = Nostr { 87 | pubkey: pubkey.to_string(), 88 | }; 89 | let url = format!("{endpoint}/nostr_pubkey"); 90 | let response = post_json_auth(&url, &Some(pubkey), Some(token)).await?; 91 | 92 | let res: Response = serde_json::from_str(&response)?; 93 | 94 | Ok(res) 95 | } 96 | 97 | /// Update the user nostr pubkey 98 | pub async fn update_nostr_pubkey(pubkey: &str, token: &str) -> Result { 99 | let pubkey = parse_npub(pubkey)?; 100 | let pubkey = validate_pubkey(&pubkey)?; 101 | 102 | let endpoint = LNDHUB_ENDPOINT.read().await; 103 | let pubkey = Nostr { 104 | pubkey: pubkey.to_string(), 105 | }; 106 | let url = format!("{endpoint}/update_nostr_pubkey"); 107 | let response = post_json_auth(&url, &Some(pubkey), Some(token)).await?; 108 | 109 | let res: Response = serde_json::from_str(&response)?; 110 | 111 | Ok(res) 112 | } 113 | -------------------------------------------------------------------------------- /tests/rgb/integration/udas.rs: -------------------------------------------------------------------------------- 1 | #![cfg(not(target_arch = "wasm32"))] 2 | use crate::rgb::integration::utils::{ 3 | create_new_invoice, create_new_psbt, create_new_transfer, get_real_uda_data, get_uda_data, 4 | issuer_issue_contract_v2, UtxoFilter, ISSUER_MNEMONIC, OWNER_MNEMONIC, 5 | }; 6 | use bitmask_core::{ 7 | bitcoin::{save_mnemonic, sign_and_publish_psbt_file}, 8 | rgb::{accept_transfer, import_uda_data, structs::ContractAmount}, 9 | structs::{AcceptRequest, IssueMediaRequest, SecretString, SignPsbtRequest}, 10 | }; 11 | 12 | #[tokio::test] 13 | async fn accept_uda_transfer() -> anyhow::Result<()> { 14 | let issuer_keys = &save_mnemonic( 15 | &SecretString(ISSUER_MNEMONIC.to_string()), 16 | &SecretString("".to_string()), 17 | ) 18 | .await?; 19 | let owner_keys = save_mnemonic( 20 | &SecretString(OWNER_MNEMONIC.to_string()), 21 | &SecretString("".to_string()), 22 | ) 23 | .await?; 24 | let meta = Some(get_uda_data()); 25 | let issuer_resp = issuer_issue_contract_v2( 26 | 1, 27 | "RGB21", 28 | ContractAmount::with(1, 0, 0).to_value(), 29 | false, 30 | true, 31 | meta, 32 | Some("0.1".to_string()), 33 | Some(UtxoFilter::with_amount_equal_than(10_000_000)), 34 | None, 35 | ) 36 | .await?; 37 | let issuer_resp = issuer_resp[0].clone(); 38 | let owner_resp = create_new_invoice( 39 | &issuer_resp.contract_id, 40 | &issuer_resp.iface, 41 | ContractAmount::with(1, 0, issuer_resp.precision), 42 | owner_keys, 43 | None, 44 | Some(issuer_resp.clone().contract.legacy), 45 | ) 46 | .await?; 47 | let psbt_resp = create_new_psbt( 48 | &issuer_resp.contract_id, 49 | &issuer_resp.iface, 50 | vec![issuer_resp.issue_utxo.clone()], 51 | issuer_keys.clone(), 52 | ) 53 | .await?; 54 | let transfer_resp = create_new_transfer(issuer_keys.clone(), owner_resp, psbt_resp).await?; 55 | 56 | let sk = issuer_keys.private.nostr_prv.to_string(); 57 | let request = SignPsbtRequest { 58 | psbt: transfer_resp.psbt, 59 | descriptors: [SecretString( 60 | issuer_keys.private.rgb_udas_descriptor_xprv.clone(), 61 | )] 62 | .to_vec(), 63 | }; 64 | let resp = sign_and_publish_psbt_file(request).await; 65 | assert!(resp.is_ok()); 66 | 67 | let request = AcceptRequest { 68 | consignment: transfer_resp.consig, 69 | force: false, 70 | }; 71 | 72 | let resp = accept_transfer(&sk, request).await; 73 | assert!(resp.is_ok()); 74 | assert!(resp?.valid); 75 | 76 | Ok(()) 77 | } 78 | 79 | #[tokio::test] 80 | async fn create_uda_save_medias() -> anyhow::Result<()> { 81 | let issuer_keys = &save_mnemonic( 82 | &SecretString(ISSUER_MNEMONIC.to_string()), 83 | &SecretString("".to_string()), 84 | ) 85 | .await?; 86 | 87 | let metadata = get_real_uda_data(); 88 | let resp = import_uda_data(metadata).await?; 89 | let meta = Some(IssueMediaRequest::from(resp)); 90 | 91 | let _issuer_resp = issuer_issue_contract_v2( 92 | 1, 93 | "RGB21", 94 | ContractAmount::with(1, 0, 0).to_value(), 95 | false, 96 | true, 97 | meta, 98 | Some("0.1".to_string()), 99 | Some(UtxoFilter::with_amount_equal_than(10_000_000)), 100 | Some(issuer_keys.clone()), 101 | ) 102 | .await?; 103 | Ok(()) 104 | } 105 | -------------------------------------------------------------------------------- /lib/web/constants.ts: -------------------------------------------------------------------------------- 1 | // Methods meant to work with bitmask-core constants defined within the web::constants module from bitmask-core: 2 | // https://github.com/diba-io/bitmask-core/blob/development/src/web.rs 3 | 4 | import initBMC, * as BMC from "./bitmask_core"; 5 | 6 | export const getNetwork = async (): Promise => 7 | JSON.parse(await BMC.get_network()); 8 | 9 | export const switchNetwork = async (network: Network): Promise => 10 | BMC.switch_network(network.toString()); 11 | 12 | export const getEnv = async (key: string): Promise => 13 | JSON.parse(await BMC.get_env(key)); 14 | 15 | export const setEnv = async (key: string, value: string): Promise => 16 | BMC.set_env(key, value); 17 | 18 | export enum Network { 19 | bitcoin = "bitcoin", 20 | testnet = "testnet", 21 | signet = "signet", 22 | regtest = "regtest", 23 | } 24 | type NetworkType = typeof Network; 25 | type NetworkKeyType = keyof NetworkType; 26 | 27 | export const DISABLE_LN = 28 | process.env?.DISABLE_LN === "true" ? true : false || ""; 29 | export let LNDHUBX = false; 30 | export let CARBONADO = false; 31 | export let BITMASK = false; 32 | 33 | export const init = async (networkOverride?: string) => { 34 | try { 35 | await initBMC(); 36 | 37 | if (networkOverride) 38 | window.localStorage.setItem("network", networkOverride); 39 | const storedNetwork = 40 | networkOverride || window.localStorage.getItem("network"); 41 | if (storedNetwork) { 42 | await switchNetwork(Network[storedNetwork as NetworkKeyType]); 43 | } else { 44 | window.localStorage.setItem("network", Network.bitcoin); 45 | await switchNetwork(Network.bitcoin); 46 | } 47 | 48 | const network = await getNetwork(); 49 | if (network === "bitcoin" && process.env.PROD_LNDHUB_ENDPOINT) { 50 | await setEnv("LNDHUB_ENDPOINT", process.env.PROD_LNDHUB_ENDPOINT); 51 | } else if (process.env.TEST_LNDHUB_ENDPOINT) { 52 | await setEnv("LNDHUB_ENDPOINT", process.env.TEST_LNDHUB_ENDPOINT); 53 | } 54 | if (process.env.CARBONADO_ENDPOINT) { 55 | await setEnv("CARBONADO_ENDPOINT", process.env.CARBONADO_ENDPOINT); 56 | } 57 | if (process.env.BITMASK_ENDPOINT) { 58 | await setEnv("BITMASK_ENDPOINT", process.env.BITMASK_ENDPOINT); 59 | } 60 | if (process.env.BITCOIN_EXPLORER_API_MAINNET) { 61 | await setEnv( 62 | "BITCOIN_EXPLORER_API_MAINNET", 63 | process.env.BITCOIN_EXPLORER_API_MAINNET 64 | ); 65 | } 66 | } catch (err) { 67 | console.error("Error in setEnv", err); 68 | } 69 | 70 | const lndhubx = await getEnv("LNDHUB_ENDPOINT"); 71 | const carbonado = await getEnv("CARBONADO_ENDPOINT"); 72 | const bitmask = await getEnv("BITMASK_ENDPOINT"); 73 | 74 | try { 75 | await fetch(`${lndhubx}/nodeinfo`); 76 | LNDHUBX = true; 77 | console.debug(`${lndhubx}/nodeinfo successfully reached`); 78 | } catch (e) { 79 | LNDHUBX = false; 80 | console.warn("Could not reach lndhubx", lndhubx, e); 81 | } 82 | try { 83 | await fetch(`${carbonado}/status`); 84 | CARBONADO = true; 85 | console.debug(`${carbonado}/status successfully reached`); 86 | } catch (e) { 87 | CARBONADO = false; 88 | console.warn("Could not reach carbonado", carbonado, e); 89 | } 90 | try { 91 | await fetch(`${bitmask}/carbonado/status`); 92 | BITMASK = true; 93 | console.debug(`${bitmask}/status successfully reached`); 94 | } catch (e) { 95 | BITMASK = false; 96 | console.warn("Could not reach bitmask", bitmask, e); 97 | } 98 | 99 | console.debug("Using LNDHubX endpoint:", lndhubx); 100 | console.debug("Using Carbonado endpoint:", carbonado); 101 | console.debug("Using bitmaskd endpoint:", bitmask); 102 | }; 103 | 104 | init(); 105 | -------------------------------------------------------------------------------- /tests/rgb/integration/proxy.rs: -------------------------------------------------------------------------------- 1 | #![cfg(not(target_arch = "wasm32"))] 2 | 3 | use std::{collections::BTreeMap, str::FromStr}; 4 | 5 | use anyhow::Result; 6 | use bitmask_core::{ 7 | bitcoin::new_mnemonic, 8 | rgb::{ 9 | create_watcher, get_contract, 10 | proxy::{get_consignment, post_consignments}, 11 | structs::ContractAmount, 12 | watcher_next_address, 13 | }, 14 | structs::{RgbTransferResponse, SecretString, WatcherRequest}, 15 | }; 16 | use rgbwallet::RgbInvoice; 17 | 18 | use crate::rgb::integration::utils::{ 19 | create_new_invoice, create_new_psbt_v2, create_new_transfer, issuer_issue_contract_v2, 20 | send_some_coins, UtxoFilter, 21 | }; 22 | 23 | #[tokio::test] 24 | pub async fn store_and_retrieve_transfer_by_proxy() -> Result<()> { 25 | // 1. Initial Setup 26 | let issuer_keys = new_mnemonic(&SecretString("".to_string())).await?; 27 | let owner_keys = new_mnemonic(&SecretString("".to_string())).await?; 28 | 29 | let issuer_sk = &issuer_keys.private.nostr_prv; 30 | let fungibles_resp = issuer_issue_contract_v2( 31 | 1, 32 | "RGB20", 33 | ContractAmount::with(5, 0, 2).to_value(), 34 | false, 35 | true, 36 | None, 37 | Some("0.10000000".to_string()), 38 | Some(UtxoFilter::with_amount_equal_than(10_000_000)), 39 | Some(issuer_keys.clone()), 40 | ) 41 | .await?; 42 | let issuer_resp = &fungibles_resp[0]; 43 | 44 | // 2. Get Allocations 45 | let contract_id = &issuer_resp.contract_id; 46 | let issuer_contract = get_contract(issuer_sk, contract_id).await?; 47 | let new_alloc = issuer_contract 48 | .allocations 49 | .into_iter() 50 | .find(|x| x.is_mine) 51 | .unwrap(); 52 | let allocs = [new_alloc]; 53 | 54 | // 2. Create PSBT (First Transaction) 55 | let psbt_resp = create_new_psbt_v2( 56 | &issuer_resp.iface, 57 | allocs.to_vec(), 58 | issuer_keys.clone(), 59 | vec![], 60 | vec![], 61 | None, 62 | ) 63 | .await?; 64 | 65 | // 3. Generate Invoice 66 | let watcher_name = "default"; 67 | let owner_sk = owner_keys.private.nostr_prv.to_string(); 68 | let create_watch_req = WatcherRequest { 69 | name: watcher_name.to_string(), 70 | xpub: owner_keys.public.watcher_xpub.clone(), 71 | force: false, 72 | }; 73 | create_watcher(&owner_sk, create_watch_req).await?; 74 | let owner_fungible_address = watcher_next_address(&owner_sk, watcher_name, "RGB20").await?; 75 | send_some_coins(&owner_fungible_address.address, "1").await; 76 | 77 | let owner_resp = &create_new_invoice( 78 | &issuer_resp.contract_id, 79 | &issuer_resp.iface, 80 | ContractAmount::with(1, 0, issuer_resp.precision), 81 | owner_keys.clone(), 82 | None, 83 | Some(issuer_resp.clone().contract.strict), 84 | ) 85 | .await?; 86 | 87 | // 4. Generate Transfer 88 | let transfer_resp = 89 | &create_new_transfer(issuer_keys.clone(), owner_resp.clone(), psbt_resp.clone()).await?; 90 | 91 | // 5. Store in RGB Proxy 92 | let RgbTransferResponse { 93 | consig: expected, .. 94 | } = transfer_resp; 95 | let rgb_invoice = RgbInvoice::from_str(&owner_resp.invoice)?; 96 | let consig_or_receipt_id = rgb_invoice.beneficiary.to_string(); 97 | 98 | let mut consigs = BTreeMap::new(); 99 | consigs.insert(consig_or_receipt_id.clone(), expected.clone()); 100 | 101 | post_consignments(consigs).await?; 102 | 103 | // 6. Retrieve in RGB Proxy 104 | let consig = get_consignment(&consig_or_receipt_id) 105 | .await? 106 | .unwrap_or_default(); 107 | assert_eq!(expected.to_string(), consig.to_string()); 108 | 109 | Ok(()) 110 | } 111 | -------------------------------------------------------------------------------- /src/rgb/import.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | 3 | use amplify::{ 4 | confinement::{Confined, U32}, 5 | hex::FromHex, 6 | }; 7 | use bech32::{decode, FromBase32}; 8 | use rgb_schemata::{nia_rgb20, nia_schema, uda_rgb21, uda_schema}; 9 | use rgbstd::{ 10 | containers::{Bindle, Contract}, 11 | contract::Genesis, 12 | interface::{rgb20, rgb21, IfacePair}, 13 | persistence::{Inventory, Stash, Stock}, 14 | resolvers::ResolveHeight, 15 | validation::ResolveTx, 16 | }; 17 | use strict_encoding::StrictDeserialize; 18 | 19 | use crate::structs::AssetType; 20 | 21 | #[derive(Clone, Eq, PartialEq, Debug, Display, Error, From)] 22 | #[display(doc_comments)] 23 | // TODO: Complete errors 24 | pub enum ImportContractError {} 25 | 26 | pub fn import_contract( 27 | contract: &str, 28 | asset_type: AssetType, 29 | stock: &mut Stock, 30 | resolver: &mut R, 31 | ) -> Result 32 | where 33 | R: ResolveHeight + ResolveTx, 34 | R::Error: 'static, 35 | { 36 | let contract = if contract.starts_with("-----BEGIN RGB CONTRACT-----") { 37 | contract_from_armored(contract) 38 | } else { 39 | contract_from_other_formats(contract, Some(asset_type), Some(stock)) 40 | }; 41 | 42 | let contract_id = contract.contract_id(); 43 | let contract = contract.validate(resolver).expect("invalid contract state"); 44 | 45 | if !stock 46 | .contract_ids() 47 | .expect("contract_ids from stock") 48 | .contains(&contract_id) 49 | { 50 | stock 51 | .import_contract(contract.clone(), resolver) 52 | .expect("import contract failed"); 53 | }; 54 | 55 | Ok(contract) 56 | } 57 | 58 | pub fn contract_from_armored(contract: &str) -> Contract { 59 | Bindle::::from_str(contract) 60 | .expect("invalid serialized contract/genesis (base58 format)") 61 | .unbindle() 62 | } 63 | 64 | pub fn contract_from_other_formats( 65 | contract: &str, 66 | asset_type: Option, 67 | stock: Option<&mut Stock>, 68 | ) -> Contract { 69 | let serialized = if contract.starts_with("rgb1") { 70 | let (_, serialized, _) = 71 | decode(contract).expect("invalid serialized contract/genesis (bech32m format)"); 72 | Vec::::from_base32(&serialized) 73 | .expect("invalid hexadecimal contract/genesis (bech32m format)") 74 | } else { 75 | Vec::::from_hex(contract).expect("invalid hexadecimal contract/genesis") 76 | }; 77 | 78 | let confined: Confined, 0, { U32 }> = 79 | Confined::try_from_iter(serialized.iter().copied()) 80 | .expect("invalid strict serialized data"); 81 | 82 | match asset_type { 83 | Some(asset_type) => match Genesis::from_strict_serialized::<{ U32 }>(confined.clone()) { 84 | Ok(genesis) => contract_from_genesis(genesis, asset_type, stock), 85 | Err(_) => Contract::from_strict_serialized::<{ U32 }>(confined) 86 | .expect("invalid strict contract data"), 87 | }, 88 | None => Contract::from_strict_serialized::<{ U32 }>(confined) 89 | .expect("invalid strict contract data"), 90 | } 91 | } 92 | 93 | pub fn contract_from_genesis( 94 | genesis: Genesis, 95 | asset_type: AssetType, 96 | stock: Option<&mut Stock>, 97 | ) -> Contract { 98 | let (schema, iface, iimpl) = match asset_type { 99 | AssetType::RGB20 => (nia_schema(), rgb20(), nia_rgb20()), 100 | AssetType::RGB21 => (uda_schema(), rgb21(), uda_rgb21()), 101 | _ => (nia_schema(), rgb20(), nia_rgb20()), 102 | }; 103 | 104 | if let Some(stock) = stock { 105 | stock 106 | .import_iface(iface.clone()) 107 | .expect("import iface failed"); 108 | } 109 | let mut contract = Contract::new(schema, genesis); 110 | contract 111 | .ifaces 112 | .insert(iface.iface_id(), IfacePair::with(iface, iimpl)) 113 | .expect("import iface pair failed"); 114 | 115 | contract 116 | } 117 | -------------------------------------------------------------------------------- /tests/rgb/web/contracts.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | #![allow(unused_imports)] 3 | #![allow(unused_variables)] 4 | #![cfg(target_arch = "wasm32")] 5 | use bitmask_core::{ 6 | info, 7 | structs::{ 8 | ContractsResponse, DecryptedWalletData, FullIssueRequest, NextAddressResponse, 9 | NextUtxoResponse, SecretString, WatcherRequest, WatcherResponse, 10 | }, 11 | web::{ 12 | bitcoin::{ 13 | decrypt_wallet, encrypt_wallet, get_assets_vault, get_wallet_data, hash_password, 14 | }, 15 | json_parse, resolve, 16 | rgb::{ 17 | create_watcher, full_issue_contract, import_contract, list_contracts, 18 | watcher_next_address, watcher_next_utxo, 19 | }, 20 | set_panic_hook, 21 | }, 22 | }; 23 | use wasm_bindgen::prelude::*; 24 | use wasm_bindgen_futures::JsFuture; 25 | use wasm_bindgen_test::*; 26 | use web_sys::console; 27 | 28 | wasm_bindgen_test_configure!(run_in_browser); 29 | 30 | const ENCRYPTION_PASSWORD: &str = "hunter2"; 31 | const SEED_PASSWORD: &str = ""; 32 | 33 | // #[wasm_bindgen_test] 34 | async fn allow_issue_and_list_contracts() { 35 | set_panic_hook(); 36 | let mnemonic = env!("TEST_WALLET_SEED", "TEST_WALLET_SEED variable not set"); 37 | let hash = hash_password(ENCRYPTION_PASSWORD.to_owned()); 38 | 39 | info!("Import Seed"); 40 | let mnemonic_data_str = resolve(encrypt_wallet( 41 | mnemonic.to_owned(), 42 | hash.clone(), 43 | SEED_PASSWORD.to_owned(), 44 | )) 45 | .await; 46 | let mnemonic_data: SecretString = json_parse(&mnemonic_data_str); 47 | 48 | info!("Get Vault"); 49 | let issuer_keys: JsValue = resolve(decrypt_wallet(hash, mnemonic_data.0.clone())).await; 50 | 51 | info!("Get Keys"); 52 | let issuer_keys: DecryptedWalletData = json_parse(&issuer_keys); 53 | 54 | info!("Issue Contract"); 55 | let sk = &issuer_keys.private.nostr_prv; 56 | 57 | info!("Create Watcher"); 58 | let iface = "RGB20"; 59 | let watcher_name = "default"; 60 | let create_watch_req = WatcherRequest { 61 | name: watcher_name.to_string(), 62 | xpub: issuer_keys.public.watcher_xpub.clone(), 63 | force: true, 64 | }; 65 | 66 | let create_watch_req = serde_wasm_bindgen::to_value(&create_watch_req).expect(""); 67 | let watcher_resp: JsValue = 68 | resolve(create_watcher(sk.to_string(), create_watch_req.clone())).await; 69 | let watcher_resp: WatcherResponse = json_parse(&watcher_resp); 70 | 71 | info!("Get Address"); 72 | let next_address: JsValue = resolve(watcher_next_address( 73 | sk.to_string(), 74 | watcher_name.to_string(), 75 | iface.to_string(), 76 | )) 77 | .await; 78 | let next_address: NextAddressResponse = json_parse(&next_address); 79 | info!(format!("Show Address {}", next_address.address)); 80 | 81 | info!("Get UTXO"); 82 | let next_address: JsValue = resolve(watcher_next_utxo( 83 | sk.to_string(), 84 | watcher_name.to_string(), 85 | iface.to_string(), 86 | )) 87 | .await; 88 | let next_utxo: NextUtxoResponse = json_parse(&next_address); 89 | let next_utxo = next_utxo.utxo.unwrap().outpoint; 90 | info!(format!("Show Utxo {}", next_utxo)); 91 | 92 | info!("Generate Issue"); 93 | let supply = 5; 94 | let issue_utxo = next_utxo; 95 | let issue_seal = format!("tapret1st:{issue_utxo}"); 96 | let issue_req = FullIssueRequest { 97 | ticker: "DIBA".to_string(), 98 | name: "DIBA".to_string(), 99 | description: "DIBA".to_string(), 100 | precision: 2, 101 | supply, 102 | seal: issue_seal.to_owned(), 103 | iface: iface.to_string(), 104 | meta: None, 105 | }; 106 | 107 | let issue_req = serde_wasm_bindgen::to_value(&issue_req).expect(""); 108 | let issue_resp: JsValue = resolve(full_issue_contract(sk.to_string(), issue_req)).await; 109 | 110 | info!("List Contracts"); 111 | let list_contracts_resp: JsValue = resolve(list_contracts(sk.to_string())).await; 112 | let list_contracts_resp: ContractsResponse = json_parse(&list_contracts_resp); 113 | info!(format!("Show Contracts {:?}", list_contracts_resp)); 114 | } 115 | -------------------------------------------------------------------------------- /lib/web/lightning.ts: -------------------------------------------------------------------------------- 1 | // Methods meant to work with LNDHubX defined within the web::lightning module from bitmask-core: 2 | // https://github.com/diba-io/bitmask-core/blob/development/src/web.rs 3 | 4 | import * as BMC from "./bitmask_core"; 5 | 6 | export const createWallet = async ( 7 | username: string, 8 | password: string 9 | ): Promise => 10 | JSON.parse(await BMC.create_wallet(username, password)); 11 | 12 | export const auth = async ( 13 | username: string, 14 | password: string 15 | ): Promise => JSON.parse(await BMC.auth(username, password)); 16 | 17 | export const createInvoice = async ( 18 | description: string, 19 | amount: number, 20 | token: string 21 | ): Promise => 22 | JSON.parse(await BMC.ln_create_invoice(description, amount, token)); 23 | 24 | export const getBalance = async (token: string): Promise => 25 | JSON.parse(await BMC.get_balance(token)); 26 | 27 | export const getTxs = async (token: string): Promise => 28 | JSON.parse(await BMC.get_txs(token)); 29 | 30 | export const payInvoice = async ( 31 | paymentRequest: string, 32 | token: string 33 | ): Promise => 34 | JSON.parse(await BMC.pay_invoice(paymentRequest, token)); 35 | 36 | export const checkPayment = async ( 37 | paymentHash: string 38 | ): Promise => 39 | JSON.parse(await BMC.check_payment(paymentHash)); 40 | 41 | export const swapBtcLn = async (token: string): Promise => 42 | JSON.parse(await BMC.swap_btc_ln(token)); 43 | 44 | export const swapLnBtc = async ( 45 | address: string, 46 | amount: bigint, 47 | token: string 48 | ): Promise => 49 | JSON.parse(await BMC.swap_ln_btc(address, amount, token)); 50 | 51 | // Core type interfaces based on structs defined within the bitmask-core Rust crate: 52 | // https://github.com/diba-io/bitmask-core/blob/development/src/structs.rs 53 | 54 | export interface LnCredentials { 55 | login: string; 56 | password: string; 57 | refreshToken: string; 58 | accessToken: string; 59 | } 60 | 61 | // Lndhubx Create wallet endpoint response 62 | export interface CreateWalletResponse { 63 | username?: string; 64 | error?: string; 65 | } 66 | 67 | // lndhubx Auth response 68 | export type AuthResponse = ErrorResponse | AuthResponseOk; 69 | 70 | export interface AuthResponseOk { 71 | refresh: string; 72 | token: string; 73 | } 74 | 75 | export interface ErrorResponse { 76 | error: string; 77 | } 78 | 79 | // User Account 80 | export interface Account { 81 | account_id: string; 82 | balance: string; 83 | currency: string; 84 | } 85 | 86 | // Amount and currency 87 | export interface Money { 88 | value: string; 89 | currency: string; 90 | } 91 | 92 | // Lndhubx Add invoice endpoint response 93 | export interface AddInvoiceResponse { 94 | req_id: string; 95 | uid: number; 96 | payment_request: string; 97 | meta: string; 98 | metadata: string; 99 | amount: Money; 100 | rate: string; 101 | currency: string; 102 | target_account_currency: string; 103 | account_id: string; 104 | error: string; 105 | fees: string; 106 | } 107 | 108 | // Lndhubx lightning transaction 109 | export interface LnTransaction { 110 | txid: string; 111 | fee_txid: string; 112 | outbound_txid: string; 113 | inbound_txid: string; 114 | created_at: bigint; 115 | date: number; 116 | outbound_amount: string; 117 | inbound_amount: string; 118 | outbound_account_id: string; 119 | inbound_account_id: string; 120 | outbound_uid: number; 121 | inbound_uid: number; 122 | outbound_currency: string; 123 | inbound_currency: string; 124 | exchange_rate: string; 125 | tx_type: string; 126 | fees: string; 127 | reference: string; 128 | } 129 | 130 | export interface LnWalletData { 131 | balance: Account; 132 | transactions: LnTransaction[]; 133 | } 134 | 135 | // Lndhubx Pay invoice response 136 | export interface PayInvoiceResponse { 137 | payment_hash: string; 138 | uid: number; 139 | success: boolean; 140 | currency: string; 141 | payment_request: string; 142 | amount: Money; 143 | fees: Money; 144 | error: string; 145 | payment_preimage: string; 146 | destination: string; 147 | description: string; 148 | } 149 | 150 | // Lndhubx Check payment response 151 | export interface CheckPaymentResponse { 152 | paid: boolean; 153 | } 154 | 155 | export interface SwapBtcLnResponse { 156 | address: string; 157 | commitment: string; 158 | signature: string; 159 | secret_access_key: string; 160 | } 161 | 162 | export interface SwapLnBtcResponse { 163 | bolt11_invoice: string; 164 | fee_sats: number; 165 | } 166 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BitMask Core 2 | Core functionality for the BitMask wallet - 3 | 4 | **BitMask** is a Bitcoin-only web wallet and browser extension for accessing decentralized web applications on the Bitcoin timechain. It is designed to support UTXO-based smart contracting protocols such as [RGB](https://rgb.tech), in addition to Lightning payments. 5 | 6 | [![Build status](https://img.shields.io/github/actions/workflow/status/diba-io/bitmask-core/rust.yaml?branch=development&style=flat-square)](https://github.com/diba-io/bitmask-core/actions/workflows/rust.yaml) 7 | [![Crates.io](https://img.shields.io/crates/v/bitmask-core?style=flat-square)](https://docs.rs/bitmask-core/latest/bitmask-core/) 8 | [![npm: bitmask-core](https://img.shields.io/npm/v/bitmask-core?style=flat-square)](https://www.npmjs.com/package/bitmask-core) 9 | [![License: MIT+APACHE](https://img.shields.io/crates/l/bitmask-core?style=flat-square)](https://mit-license.org) 10 | [![Telegram](https://img.shields.io/badge/telegram-invite-blue?style=flat-square)](https://t.me/+eQk5aQ5--iUxYzVk) 11 | 12 | ## Uses 13 | 14 | - [bdk](https://github.com/bitcoindevkit/bdk) - Bitcoin Dev Kit 15 | - [rgb-wallet](https://github.com/RGB-WG/rgb-wallet) - RGB Wallet 16 | - [wasm-pack](https://github.com/rustwasm/wasm-pack) - WebAssembly 17 | - [lndhubx](https://lndhubx.kollider.xyz) - Custodial Lightning 18 | - [nostr-sdk](https://github.com/rust-nostr/nostr) - Nostr SDK 19 | - [carbonado](https://github.com/diba-io/carbonado) - Carbonado e2ee decentralized storage 20 | 21 | ## Build 22 | 23 | This should work with either wasm-pack, [trunk](https://github.com/thedodd/trunk), or x86-64. 24 | 25 | Some environment variables may be needed in order to compile on macos-aarch64, for more, [see this](https://github.com/sapio-lang/sapio/issues/146#issuecomment-960659800). 26 | 27 | If there are issues compiling, be sure to check you're compiling with the latest Rust version. 28 | 29 | To build this as a NodeJS module, use: `wasm-pack build --release --target bundler` 30 | 31 | ## Test 32 | 33 | 1. Lint against wasm32: `cargo clippy --target wasm32-unknown-unknown` 34 | 2. Run tests in browser: `TEST_WALLET_SEED="replace with a 12 word mnemonic for a wallet containing testnet sats" wasm-pack test --headless --chrome` 35 | 36 | ## Run 37 | 38 | To run the bitmaskd node with REST server, either for testing the web wallet, or simply for increased privacy: 39 | 40 | `cargo install --features=server --path .` 41 | 42 | Then run `bitmaskd`. 43 | 44 | ## Development 45 | 46 | Parts of this application are built with conditional compilation statements for wasm32 support. This is a helpful command for checking linting and correctness while also developing on desktop platforms: 47 | 48 | `cargo clippy --target wasm32-unknown-unknown --no-default-features --release` 49 | 50 | ## Release 51 | 52 | Upon a new release, follow these steps: 53 | 54 | 1. Run `cargo update` to update to latest deps. 55 | 1. Run `cargo +nightly udeps` to see if there are any unused dependencies. 56 | 57 | ## Docker 58 | 59 | For running bitmask-core tests in Regtest Mode, please follow the steps below: 60 | 61 | ### Initial Setup 62 | 63 | 1. Build bitcoin node + electrum: `docker-compose build`. 64 | 2. Up and running Docker containers: `docker-compose up -d node1 bitmaskd`. 65 | 3. Load the command line: `source .commands` 66 | 4. Download and install BDK cli: `cargo install bdk-cli`. We will use BDK to generate the mnemonic. 67 | 5. Generate a new mnemonic: `bdk-cli generate`. 68 | 6. Create an environment variable called **TEST_WALLET_SEED** with mnemonic generated in the **step 5** (only wasm32). 69 | 7. Run the test to get main address for bitcoin and rgb: `cargo test --test wallet -- create_wallet --exact`. 70 | 8. Load your wallet in the bitcoin node: `node1 loadwallet default`. 71 | 9. Generate new first 500 blocks: `node1 -generate 500`. 72 | 10. Send some coins to the main wallet address: `node1 sendtoaddress {MAIN_VAULT_ADDRESS} 10`. Change `{MAIN_VAULT_ADDRESS}` with the address generated in the **step 7**. 73 | 11. Send some coins to the rgb wallet address: `node1 sendtoaddress {RGB_VAULT_ADDRESS} 10`. Change `{RGB_VAULT_ADDRESS}` with the address generated in the **step 7**. 74 | 12. Mine a new block: `node1 -generate 1` 75 | 13. Run the test to check the balance: `cargo test --test wallet -- get_wallet_balance --exact`. 76 | 77 | ### Running the tests 78 | 79 | Running the tests: `cargo test --test-threads 1` 80 | 81 | ### Troubleshooting 82 | 83 | #### **1. After restarting the container** 84 | 85 | **A.The bitcoin node does not work?** 86 | 87 | Check if your wallet is loaded. For that, run the command `node1 loadwallet default`. 88 | 89 | **B.The electrs node does not work?** 90 | 91 | To stop the electrs freeze, run `node1 -generate`. 92 | -------------------------------------------------------------------------------- /src/rgb/proxy.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | 3 | use amplify::confinement::U32; 4 | use postcard::from_bytes; 5 | use strict_encoding::StrictSerialize; 6 | 7 | use crate::proxy::{ 8 | proxy_consig_retrieve, proxy_consig_store, proxy_media_data_store, proxy_media_retrieve, 9 | proxy_metadata_retrieve, 10 | }; 11 | 12 | use crate::proxy::ProxyServerError; 13 | use crate::structs::{MediaEncode, MediaItemRequest}; 14 | 15 | use super::{ 16 | structs::{MediaMetadata, RgbProxyConsigFileReq, RgbProxyConsigUpload}, 17 | transfer::extract_transfer, 18 | }; 19 | 20 | #[derive(Debug, Clone, Eq, PartialEq, Display, From, Error)] 21 | #[display(doc_comments)] 22 | pub enum ProxyError { 23 | /// Proxy Server error. {0} 24 | IO(ProxyServerError), 25 | /// Consignment cannot be decoded. {0} 26 | WrongConsig(String), 27 | /// Retrieve '{0}' serialize causes error. {1} 28 | SerializeRetrieve(String, String), 29 | /// Write '{0}' serialize causes error. {1} 30 | SerializeWrite(String, String), 31 | } 32 | 33 | pub async fn post_consignments(consignments: BTreeMap) -> Result<(), ProxyError> { 34 | for (recipient_id, transfer) in consignments { 35 | let hashed_name = blake3::hash(recipient_id.as_bytes()) 36 | .to_hex() 37 | .to_lowercase(); 38 | 39 | let (txid, transfer) = 40 | extract_transfer(transfer).map_err(|op| ProxyError::WrongConsig(op.to_string()))?; 41 | let bytes = transfer 42 | .unbindle() 43 | .to_strict_serialized::() 44 | .map_err(|op| ProxyError::WrongConsig(op.to_string()))?; 45 | 46 | let consig_rq = RgbProxyConsigFileReq { 47 | params: RgbProxyConsigUpload { 48 | recipient_id, 49 | txid: txid.to_string(), 50 | }, 51 | bytes: bytes.to_vec(), 52 | file_name: hashed_name, 53 | }; 54 | 55 | let _ = proxy_consig_store(consig_rq).await.map_err(ProxyError::IO); 56 | } 57 | 58 | Ok(()) 59 | } 60 | 61 | pub async fn get_consignment(consig_or_receipt_id: &str) -> Result, ProxyError> { 62 | let resp = proxy_consig_retrieve(consig_or_receipt_id) 63 | .await 64 | .map_err(ProxyError::IO)?; 65 | 66 | if resp.is_none() { 67 | return Ok(None); 68 | } 69 | 70 | let bytes = &base64::decode(&resp.unwrap().result.consignment).map_err(|op| { 71 | ProxyError::SerializeRetrieve("consignment.get".to_string(), op.to_string()) 72 | })?; 73 | 74 | if bytes.is_empty() { 75 | Ok(None) 76 | } else { 77 | Ok(Some(hex::encode(bytes))) 78 | } 79 | } 80 | 81 | pub async fn get_media(media_id: &str) -> Result>, ProxyError> { 82 | let resp = proxy_media_retrieve(media_id) 83 | .await 84 | .map_err(ProxyError::IO)?; 85 | 86 | if resp.is_none() { 87 | return Ok(None); 88 | } 89 | 90 | let bytes = base64::decode(&resp.unwrap().result) 91 | .map_err(|op| ProxyError::SerializeRetrieve("media".to_string(), op.to_string()))?; 92 | 93 | if bytes.is_empty() { 94 | Ok(None) 95 | } else { 96 | Ok(Some(bytes)) 97 | } 98 | } 99 | 100 | pub async fn get_media_metadata(media_id: &str) -> Result, ProxyError> { 101 | let resp = proxy_metadata_retrieve(&format!("{media_id}-metadata")) 102 | .await 103 | .map_err(ProxyError::IO)?; 104 | 105 | if resp.is_none() { 106 | return Ok(None); 107 | } 108 | 109 | let bytes = base64::decode(&resp.unwrap().result) 110 | .map_err(|op| ProxyError::SerializeRetrieve("metadata".to_string(), op.to_string()))?; 111 | 112 | if bytes.is_empty() { 113 | Ok(None) 114 | } else { 115 | let metadata: MediaMetadata = from_bytes(&bytes).map_err(|op| { 116 | ProxyError::SerializeRetrieve("metadata.postcard".to_string(), op.to_string()) 117 | })?; 118 | 119 | Ok(Some(metadata)) 120 | } 121 | } 122 | 123 | pub async fn post_media_metadata( 124 | data: MediaItemRequest, 125 | encode: MediaEncode, 126 | ) -> Result { 127 | let data = proxy_media_data_store(data, encode) 128 | .await 129 | .map_err(ProxyError::IO)?; 130 | 131 | Ok(data) 132 | } 133 | 134 | pub async fn post_media_metadata_list( 135 | data: Vec, 136 | encode: MediaEncode, 137 | ) -> Result, ProxyError> { 138 | let mut list = vec![]; 139 | for item in data { 140 | let data: MediaMetadata = proxy_media_data_store(item, encode.clone()) 141 | .await 142 | .map_err(ProxyError::IO)?; 143 | 144 | list.push(data); 145 | } 146 | 147 | Ok(list) 148 | } 149 | -------------------------------------------------------------------------------- /src/validators.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | 3 | use bp::{Chain, Txid}; 4 | use miniscript_crate::Descriptor; 5 | use rgbwallet::RgbInvoice; 6 | use seals::txout::ExplicitSeal; 7 | use wallet::hd::{DerivationAccount, DerivationSubpath, UnhardenedIndex}; 8 | 9 | use crate::structs::{IssueMediaRequest, SecretString}; 10 | 11 | /// Errors happening during checking of requests to RGB operations 12 | #[derive(Clone, PartialEq, Eq, Debug, Display, Error, From)] 13 | #[display(inner)] 14 | pub enum RGBParamsError { 15 | /// wrong or unspecified seal closed method. Only TapRet (tapret1st) 16 | /// is supported 17 | #[display(doc_comments)] 18 | NoClosedMethod, 19 | 20 | /// the {0} need At least {1} media information to create RGB21 contracts 21 | #[display(doc_comments)] 22 | NoMediaType(String, u8), 23 | 24 | /// '{0}' is invalid terminal path (ex: /0/0) 25 | #[display(doc_comments)] 26 | WrongTerminal(String), 27 | 28 | /// '{0}' is invalid descriptor. {1} 29 | #[display(doc_comments)] 30 | WrongDescriptor(String, String), 31 | 32 | /// Rgb Invoice cannot be decoded. {0} 33 | WrongInvoice(String), 34 | } 35 | 36 | #[derive(Debug, Display)] 37 | #[display(doc_comments)] 38 | pub struct RGBContext { 39 | // Close Method supported 40 | closed_method: String, 41 | 42 | // Current Network 43 | current_network: String, 44 | 45 | // Minimum number of the media types (Only RGB21) 46 | min_media_types: u8, 47 | } 48 | 49 | impl Default for RGBContext { 50 | fn default() -> Self { 51 | Self { 52 | closed_method: "tapret1st".to_string(), 53 | current_network: String::new(), 54 | min_media_types: 1, 55 | } 56 | } 57 | } 58 | 59 | impl RGBContext { 60 | pub fn with(network: &str) -> Self { 61 | Self { 62 | current_network: network.to_string(), 63 | ..Default::default() 64 | } 65 | } 66 | } 67 | 68 | pub fn verify_tapret_seal(value: &str, context: &RGBContext) -> garde::Result { 69 | if !value.contains(&context.closed_method) { 70 | return Err(garde::Error::new( 71 | RGBParamsError::NoClosedMethod.to_string(), 72 | )); 73 | } 74 | ExplicitSeal::::from_str(value).map_err(|op| garde::Error::new(op.to_string()))?; 75 | Ok(()) 76 | } 77 | 78 | pub fn verify_terminal_path(value: &str, _context: &RGBContext) -> garde::Result { 79 | let resp = value 80 | .parse::>() 81 | .map_err(|op| RGBParamsError::WrongTerminal(op.to_string())); 82 | 83 | if resp.is_err() { 84 | return Err(garde::Error::new(resp.err().unwrap().to_string())); 85 | } 86 | 87 | Ok(()) 88 | } 89 | 90 | pub fn verify_descriptor(value: &SecretString, _context: &RGBContext) -> garde::Result { 91 | let resp: Result, _> = Descriptor::from_str(&value.to_string()) 92 | .map_err(|op| RGBParamsError::WrongDescriptor(value.to_string(), op.to_string())); 93 | 94 | if resp.is_err() { 95 | return Err(garde::Error::new(resp.err().unwrap().to_string())); 96 | } 97 | 98 | Ok(()) 99 | } 100 | 101 | pub fn verify_media_request( 102 | value: &Option, 103 | context: &RGBContext, 104 | ) -> garde::Result { 105 | if let Some(request) = value { 106 | let mut media_type = 0; 107 | media_type += request.preview.is_some() as u8; 108 | media_type += request.media.is_some() as u8; 109 | media_type += request.attachments.len() as u8; 110 | 111 | if media_type < context.min_media_types { 112 | return Err(garde::Error::new( 113 | RGBParamsError::NoMediaType("UDA".to_string(), context.min_media_types).to_string(), 114 | )); 115 | }; 116 | } 117 | Ok(()) 118 | } 119 | 120 | pub fn verify_rgb_invoice(value: &str, context: &RGBContext) -> garde::Result { 121 | let rgb_invoice = 122 | RgbInvoice::from_str(value).map_err(|err| RGBParamsError::WrongInvoice(err.to_string())); 123 | 124 | if rgb_invoice.is_err() { 125 | return Err(garde::Error::new(rgb_invoice.err().unwrap().to_string())); 126 | } 127 | 128 | if let Some(chain) = rgb_invoice.unwrap().chain { 129 | let network = &context.current_network; 130 | let current_chain = 131 | Chain::from_str(network).map_err(|op| RGBParamsError::WrongInvoice(op.to_string())); 132 | 133 | if current_chain.is_err() { 134 | return Err(garde::Error::new(current_chain.err().unwrap().to_string())); 135 | } 136 | 137 | if current_chain.unwrap() != chain { 138 | return Err(garde::Error::new( 139 | RGBParamsError::WrongInvoice("Network mismatch".to_string()).to_string(), 140 | )); 141 | } 142 | } 143 | 144 | Ok(()) 145 | } 146 | -------------------------------------------------------------------------------- /tests/rgb/integration/inspect.rs: -------------------------------------------------------------------------------- 1 | #![cfg(not(target_arch = "wasm32"))] 2 | use amplify::confinement::{Confined, U32}; 3 | use amplify::hex::ToHex; 4 | use anyhow::Result; 5 | use bitcoin::secp256k1::{PublicKey, SecretKey}; 6 | use bitmask_core::constants::storage_keys::{ASSETS_STOCK, ASSETS_WALLETS}; 7 | use bitmask_core::rgb::cambria::{ModelVersion, RgbAccountVersions}; 8 | use bitmask_core::rgb::inspect_contract; 9 | use bitmask_core::rgb::structs::RgbAccountV1; 10 | use bitmask_core::{ 11 | bitcoin::save_mnemonic, 12 | constants::{switch_network, CARBONADO_ENDPOINT, NETWORK}, 13 | structs::SecretString, 14 | }; 15 | use rgbstd::persistence::Stock; 16 | use rgbstd::stl::LIB_ID_RGB; 17 | use strict_encoding::StrictDeserialize; 18 | 19 | #[ignore = "Only for troubleshooting"] 20 | #[tokio::test] 21 | pub async fn inspect_contract_states() -> Result<()> { 22 | // 0. Switc network 23 | switch_network("bitcoin").await?; 24 | 25 | // 1. Retrieve all keys 26 | let wallet_a_keys = 27 | save_mnemonic(&SecretString("".to_string()), &SecretString("".to_string())).await?; 28 | let wallet_b_keys = 29 | save_mnemonic(&SecretString("".to_string()), &SecretString("".to_string())).await?; 30 | 31 | let contract_id = ""; 32 | 33 | // 2. Extract Stock and RgbAccount (wallet A) 34 | let wallet_a_sk = &wallet_a_keys.private.nostr_prv; 35 | let mut stock = retrieve_stock(wallet_a_sk, ASSETS_STOCK).await?; 36 | let rgb_account = retrieve_account(wallet_a_sk, ASSETS_WALLETS).await?; 37 | 38 | println!("Wallet A"); 39 | let contract = inspect_contract(&mut stock, rgb_account, contract_id).await?; 40 | println!( 41 | "Contract {} ({}): \n {:#?}", 42 | contract.name, contract.contract_id, contract.allocations 43 | ); 44 | 45 | // 3. Extract Stock and RgbAccount (wallet B) 46 | let wallet_b_sk = &wallet_b_keys.private.nostr_prv; 47 | let mut stock = retrieve_stock(wallet_b_sk, ASSETS_STOCK).await?; 48 | let rgb_account = retrieve_account(wallet_b_sk, ASSETS_WALLETS).await?; 49 | 50 | println!("Wallet B"); 51 | let contract = inspect_contract(&mut stock, rgb_account, contract_id).await?; 52 | println!( 53 | "Contract {} ({}): \n {:#?}", 54 | contract.name, contract.balance, contract.allocations 55 | ); 56 | 57 | Ok(()) 58 | } 59 | 60 | async fn retrieve_stock(sk_str: &str, name: &str) -> Result { 61 | let sk = hex::decode(sk_str)?; 62 | let secret_key = SecretKey::from_slice(&sk)?; 63 | let public_key = PublicKey::from_secret_key_global(&secret_key); 64 | let pk = public_key.to_hex(); 65 | 66 | let hashed_name = blake3::hash(format!("{LIB_ID_RGB}-{name}").as_bytes()) 67 | .to_hex() 68 | .to_lowercase(); 69 | 70 | let final_name = format!("{hashed_name}.c15"); 71 | let network = NETWORK.read().await.to_string(); 72 | 73 | let endpoint = CARBONADO_ENDPOINT.read().await.to_string(); 74 | let url = format!("{endpoint}/{pk}/{network}-{final_name}"); 75 | 76 | let (data, _) = fetch(sk_str, url).await?; 77 | 78 | if data.is_empty() { 79 | Ok(Stock::default()) 80 | } else { 81 | let confined = Confined::try_from_iter(data)?; 82 | let stock = Stock::from_strict_serialized::(confined)?; 83 | 84 | Ok(stock) 85 | } 86 | } 87 | 88 | async fn retrieve_account(sk_str: &str, name: &str) -> Result { 89 | let sk = hex::decode(sk_str)?; 90 | let secret_key = SecretKey::from_slice(&sk)?; 91 | let public_key = PublicKey::from_secret_key_global(&secret_key); 92 | let pk = public_key.to_hex(); 93 | 94 | let hashed_name = blake3::hash(format!("{LIB_ID_RGB}-{name}").as_bytes()) 95 | .to_hex() 96 | .to_lowercase(); 97 | 98 | let final_name = format!("{hashed_name}.c15"); 99 | let network = NETWORK.read().await.to_string(); 100 | 101 | let endpoint = CARBONADO_ENDPOINT.read().await.to_string(); 102 | let url = format!("{endpoint}/{pk}/{network}-{final_name}"); 103 | 104 | let (data, metadata) = fetch(sk_str, url).await?; 105 | 106 | if data.is_empty() { 107 | Ok(RgbAccountV1::default()) 108 | } else { 109 | let mut version: [u8; 8] = [0, 0, 0, 0, 0, 0, 0, 0]; 110 | if let Some(metadata) = metadata { 111 | version.copy_from_slice(&metadata); 112 | } 113 | 114 | let rgb_wallets = RgbAccountVersions::from_bytes(data, version)?; 115 | Ok(rgb_wallets) 116 | } 117 | } 118 | 119 | async fn fetch(sk: &str, url: String) -> Result<(Vec, Option<[u8; 8]>)> { 120 | let sk = hex::decode(sk)?; 121 | let client = reqwest::Client::new(); 122 | let resp = client 123 | .get(url) 124 | .header("Content-Type", "application/octet-stream") 125 | .header("Cache-Control", "no-cache") 126 | .send() 127 | .await?; 128 | 129 | let bytes = resp.bytes().await?.to_vec(); 130 | let (header, decoded) = carbonado::file::decode(&sk, &bytes)?; 131 | 132 | Ok((decoded, header.metadata)) 133 | } 134 | -------------------------------------------------------------------------------- /.github/workflows/rust.yaml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | - push 5 | 6 | env: 7 | CARGO_TERM_COLOR: always 8 | 9 | jobs: 10 | format: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v3 15 | 16 | - uses: dtolnay/rust-toolchain@stable 17 | with: 18 | toolchain: stable 19 | targets: x86_64-unknown-linux-gnu 20 | components: rustfmt 21 | 22 | - uses: Swatinem/rust-cache@v2 23 | 24 | - name: Check Formatting 25 | run: cargo fmt --all -- --check 26 | 27 | lint: 28 | runs-on: ubuntu-latest 29 | 30 | steps: 31 | - uses: actions/checkout@v3 32 | 33 | - uses: dtolnay/rust-toolchain@stable 34 | with: 35 | toolchain: stable 36 | targets: x86_64-unknown-linux-gnu 37 | components: clippy 38 | 39 | - uses: olix0r/cargo-action-fmt/setup@v2 40 | - uses: Swatinem/rust-cache@v2 41 | 42 | - name: Lint src 43 | run: cargo clippy --locked --all-features --lib --message-format=json -- -D warnings | cargo-action-fmt 44 | 45 | - name: Lint tests 46 | run: cargo clippy --locked --all-features --tests --message-format=json -- -D warnings | cargo-action-fmt 47 | env: 48 | TEST_WALLET_SEED: "" 49 | 50 | lint-wasm: 51 | runs-on: ubuntu-latest 52 | 53 | steps: 54 | - uses: actions/checkout@v3 55 | 56 | - uses: dtolnay/rust-toolchain@stable 57 | with: 58 | toolchain: stable 59 | targets: wasm32-unknown-unknown 60 | components: clippy 61 | 62 | - uses: olix0r/cargo-action-fmt/setup@v2 63 | - uses: jetli/wasm-pack-action@v0.4.0 64 | - uses: Swatinem/rust-cache@v2 65 | 66 | - name: Add wasm32 target 67 | run: rustup target add wasm32-unknown-unknown 68 | 69 | - name: Lint src (wasm32) 70 | run: cargo clippy --locked --target wasm32-unknown-unknown --lib --message-format=json -- -D warnings | cargo-action-fmt 71 | 72 | - name: Lint tests (wasm32) 73 | run: cargo clippy --locked --target wasm32-unknown-unknown --tests --message-format=json -- -D warnings | cargo-action-fmt 74 | env: 75 | TEST_WALLET_SEED: "" 76 | 77 | test: 78 | runs-on: ubuntu-latest 79 | needs: lint 80 | 81 | steps: 82 | - uses: actions/checkout@v3 83 | 84 | - uses: dtolnay/rust-toolchain@stable 85 | with: 86 | toolchain: stable 87 | targets: x86_64-unknown-linux-gnu 88 | components: clippy, rustfmt 89 | 90 | # - uses: Swatinem/rust-cache@v2 91 | 92 | - name: Up Bitcoin Node & RGB Proxy 93 | run: | 94 | docker compose up -d node1 rgb-proxy 95 | 96 | - name: Setup Bitcoin Node 97 | run: | 98 | sleep 5 99 | docker-compose exec -T node1 cli loadwallet default 100 | docker-compose exec -T node1 cli -generate 500 101 | docker-compose exec -T node1 cli sendtoaddress $MAIN_VAULT_ADDRESS 10 102 | docker-compose exec -T node1 cli sendtoaddress $MAIN_VAULT_ADDRESS 10 103 | docker-compose exec -T node1 cli sendtoaddress $MAIN_VAULT_ADDRESS 10 104 | docker-compose exec -T node1 cli -generate 1 105 | env: 106 | MAIN_VAULT_ADDRESS: ${{ secrets.MAIN_VAULT_ADDRESS }} 107 | RUST_BACKTRACE: 1 108 | 109 | - name: RGB Test Init 110 | run: cargo test --locked --features server --test _init -- _init --nocapture --test-threads 1 111 | 112 | - name: RGB Tests 113 | run: cargo test --locked --features server --test rgb -- rgb --nocapture --test-threads 1 114 | env: 115 | TEST_WALLET_SEED: ${{ secrets.TEST_WALLET_SEED }} 116 | RUST_BACKTRACE: 1 117 | 118 | - name: Wallet, LN & Payjoin Tests 119 | run: cargo test --locked -- --skip rgb --test-threads 1 --nocapture 120 | env: 121 | TEST_WALLET_SEED: ${{ secrets.TEST_WALLET_SEED }} 122 | RUST_BACKTRACE: 1 123 | 124 | 125 | test-wasm: 126 | runs-on: ubuntu-latest 127 | needs: lint-wasm 128 | 129 | steps: 130 | - uses: actions/checkout@v3 131 | 132 | - uses: dtolnay/rust-toolchain@stable 133 | with: 134 | toolchain: stable 135 | targets: wasm32-unknown-unknown 136 | 137 | # - uses: Swatinem/rust-cache@v2 138 | - uses: jetli/wasm-pack-action@v0.4.0 139 | 140 | - name: Up Bitcoin Node & RGB Proxy 141 | run: | 142 | docker compose up -d node1 rgb-proxy 143 | 144 | - name: Setup Bitcoin Node 145 | run: | 146 | sleep 5 147 | docker-compose exec -T node1 cli loadwallet default 148 | docker-compose exec -T node1 cli -generate 500 149 | docker-compose exec -T node1 cli sendtoaddress $MAIN_VAULT_ADDRESS 10 150 | docker-compose exec -T node1 cli sendtoaddress $RGB_VAULT_ADDRESS 10 151 | docker-compose exec -T node1 cli -generate 1 152 | env: 153 | MAIN_VAULT_ADDRESS: ${{ secrets.MAIN_VAULT_ADDRESS }} 154 | RGB_VAULT_ADDRESS: ${{ secrets.RGB_VAULT_ADDRESS }} 155 | RUST_BACKTRACE: 1 156 | 157 | - name: Run bitmaskd node 158 | run: cargo run --locked --features server & sleep 1 159 | 160 | - name: Test WASM 161 | run: wasm-pack test --headless --chrome 162 | env: 163 | TEST_WALLET_SEED: ${{ secrets.TEST_WALLET_SEED }} 164 | WASM_BINDGEN_TEST_TIMEOUT: 240 165 | RUST_BACKTRACE: 1 166 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bitmask-core" 3 | version = "0.7.0-beta.11" 4 | authors = [ 5 | "Jose Diego Robles ", 6 | "Hunter Trujillo ", 7 | "Francisco Calderón ", 8 | "Armando Dutra ", 9 | ] 10 | description = "Core functionality for the BitMask wallet" 11 | edition = "2021" 12 | license = "MIT" 13 | homepage = "https://bitmask.app" 14 | repository = "https://github.com/diba-io/bitmask-core" 15 | 16 | [lib] 17 | crate-type = ["cdylib", "rlib"] 18 | 19 | [[bin]] 20 | name = "bitmaskd" 21 | required-features = ["server"] 22 | 23 | [features] 24 | all = [] 25 | default = [] 26 | web = [] 27 | segwit = [] 28 | server = ["tokio/full", "tower-http/cors"] 29 | 30 | [dependencies] 31 | anyhow = "1.0.75" 32 | amplify = "4.5.0" 33 | argon2 = "0.5.2" 34 | automerge = "0.5.2" 35 | autosurgeon = "0.8" 36 | baid58 = "0.4.4" 37 | base64 = { package = "base64-compat", version = "1.0.0" } 38 | base85 = "2.0.0" 39 | bech32 = "0.9.1" 40 | bip39 = { version = "2.0.0", features = ["rand"] } 41 | bitcoin_30 = { package = "bitcoin", version = "0.30", features = ["base64"] } 42 | bitcoin = { version = "0.29.2", features = ["base64"] } 43 | bitcoin_hashes = "0.13.0" 44 | bitcoin_scripts = { version = "0.10.0", default-features = false } 45 | bitcoin_blockchain = { version = "0.10.0", default-features = false } 46 | blake3 = "1.5.0" 47 | bp-core = { version = "0.10.11", features = ["stl"] } 48 | bp-seals = "0.10.11" 49 | carbonado = "0.3.6" 50 | chrono = "0.4" 51 | commit_verify = { version = "0.10.6", features = ["stl"] } 52 | console_error_panic_hook = "0.1.7" 53 | descriptor-wallet = { version = "0.10.1", features = [ 54 | "serde", 55 | "miniscript", 56 | ], default-features = false } 57 | futures = { version = "0.3.28", features = [ 58 | "executor", 59 | ], default-features = true } 60 | garde = { version = "0.16", features = ["derive"], default-features = false } 61 | rand = "0.8.5" 62 | getrandom = { version = "0.2.10", features = ["js"] } 63 | hex = "0.4.3" 64 | indexmap = "2.0.2" 65 | lightning-invoice = "0.26.0" 66 | log = "0.4.20" 67 | miniscript_crate = { package = "miniscript", version = "9.0.2", features = [ 68 | "compiler", 69 | ] } 70 | nostr-sdk = "0.25.0" 71 | once_cell = "1.19.0" 72 | payjoin = { version = "0.8.0", features = ["send"] } 73 | postcard = { version = "1.0.7", features = ["alloc"] } 74 | pretty_env_logger = "0.5.0" 75 | psbt = { version = "0.10.0-alpha.2", features = [ 76 | "sign", 77 | "miniscript", 78 | "serde", 79 | "construct", 80 | ] } 81 | regex = "1.7.0" 82 | reqwest = { version = "0.11.22", features = ["json", "multipart"] } 83 | rgb-std = { version = "0.10.9" } 84 | rgb-wallet = { version = "0.10.9" } 85 | rgb-schemata = { version = "0.10.1" } 86 | rgb-contracts = { version = "0.10.2", default-features = false } 87 | serde = "1.0.189" 88 | serde_json = "1.0.107" 89 | serde-encrypt = "0.7.0" 90 | strict_encoding = "2.6.1" 91 | strict_types = "1.6.3" 92 | thiserror = "1.0" 93 | tokio = { version = "1.33.0", features = ["macros", "sync"] } 94 | zeroize = "1.6.0" 95 | walkdir = "2.4.0" 96 | 97 | [target.'cfg(target_arch = "wasm32")'.dependencies] 98 | bdk = { version = "0.28.2", features = [ 99 | "use-esplora-async", 100 | "async-interface", 101 | ], default-features = false } 102 | gloo-console = "0.3.0" 103 | gloo-net = { version = "0.4.0", features = ["http"] } 104 | gloo-utils = "0.2.0" 105 | js-sys = "0.3.64" 106 | serde-wasm-bindgen = "0.6.0" 107 | wasm-bindgen = { version = "0.2.87", features = ["serde-serialize"] } 108 | wasm-bindgen-futures = "0.4.37" 109 | web-sys = "0.3.64" 110 | 111 | [target.'cfg(not(target_arch = "wasm32"))'.dependencies] 112 | bdk = { version = "0.28.2", features = [ 113 | "use-esplora-async", 114 | "async-interface", 115 | "reqwest-default-tls", 116 | ], default-features = false } 117 | axum = { version = "0.6.20", features = ["headers"] } 118 | axum-macros = "0.3.8" 119 | deflate = "1.0.0" 120 | esplora_block = { version = "0.5.0", package = "esplora-client", default-features = false, features = [ 121 | "blocking", 122 | ] } 123 | inflate = "0.4.5" 124 | sled = "0.34.7" 125 | tower-http = { version = "0.4.4", features = ["cors"], optional = true } 126 | 127 | [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] 128 | tokio = { version = "1.33.0", features = ["full"] } 129 | 130 | [dev-dependencies] 131 | wasm-bindgen-test = "0.3.37" 132 | 133 | 134 | [build-dependencies] 135 | anyhow = "1.0.71" 136 | blake3 = "1.4.1" 137 | rgb-std = { version = "0.10.2" } 138 | serde = "1.0.189" 139 | serde_json = "1.0.107" 140 | toml = { version = "0.8.0", features = ["preserve_order"] } 141 | 142 | [patch.crates-io] 143 | # Remove after merge and release https://github.com/BP-WG/bitcoin_foundation/pull/20 144 | bitcoin_scripts = { git = "https://github.com/crisdut/bp-foundation", branch = "feat/bump-amplify-4" } 145 | bitcoin_blockchain = { git = "https://github.com/crisdut/bp-foundation", branch = "feat/bump-amplify-4" } 146 | 147 | # Remove after merge and release https://github.com/BP-WG/descriptor-wallet/pull/75 148 | psbt = { git = "https://github.com/crisdut/descriptor-wallet", branch = "feat/bump-amplify-4" } 149 | descriptor-wallet = { git = "https://github.com/crisdut/descriptor-wallet", branch = "feat/bump-amplify-4" } 150 | 151 | # Remove after merge and release https://github.com/RGB-WG/rgb-wallet/pull/108 152 | rgb-wallet = { git = "https://github.com/crisdut/rgb-wallet", branch = "feat/improviments" } 153 | rgb-std = { git = "https://github.com/crisdut/rgb-wallet", branch = "feat/improviments" } 154 | 155 | # Remove after merge and release https://github.com/RGB-WG/rgb/pull/32 156 | rgb-contracts = { git = "https://github.com/crisdut/rgb", branch = "feat/improviments" } 157 | -------------------------------------------------------------------------------- /tests/wallet.rs: -------------------------------------------------------------------------------- 1 | #![cfg(not(target_arch = "wasm32"))] 2 | use std::env; 3 | 4 | use anyhow::Result; 5 | use bitmask_core::{ 6 | bitcoin::{ 7 | decrypt_wallet, encrypt_wallet, get_wallet_data, hash_password, new_wallet, send_sats, 8 | BitcoinError, 9 | }, 10 | constants::{get_network, switch_network}, 11 | structs::SecretString, 12 | util::init_logging, 13 | warn, 14 | }; 15 | use log::info; 16 | 17 | const ENCRYPTION_PASSWORD: &str = "hunter2"; 18 | const SEED_PASSWORD: &str = ""; 19 | 20 | #[tokio::test] 21 | async fn error_for_bad_mnemonic() -> Result<()> { 22 | init_logging("wallet=info"); 23 | 24 | let network = get_network().await; 25 | info!("Wallet test on {network}"); 26 | 27 | info!("Import wallets"); 28 | let mnemonic = "this is a bad mnemonic that is meant to break"; 29 | let hash = hash_password(&SecretString(ENCRYPTION_PASSWORD.to_owned())); 30 | let mnemonic_data_result = encrypt_wallet( 31 | &SecretString(mnemonic.to_owned()), 32 | &hash, 33 | &SecretString(SEED_PASSWORD.to_owned()), 34 | ) 35 | .await; 36 | 37 | assert!(mnemonic_data_result.is_err()); 38 | 39 | Ok(()) 40 | } 41 | 42 | #[tokio::test] 43 | async fn create_wallet() -> Result<()> { 44 | init_logging("wallet=info"); 45 | 46 | switch_network("bitcoin").await?; 47 | let network = get_network().await; 48 | info!("Asset test on {network}"); 49 | 50 | info!("Create wallet"); 51 | let hash = hash_password(&SecretString(ENCRYPTION_PASSWORD.to_owned())); 52 | let encrypted_descriptors = new_wallet(&hash, &SecretString(SEED_PASSWORD.to_owned())).await?; 53 | let decrypted_wallet = decrypt_wallet(&hash, &encrypted_descriptors)?; 54 | 55 | let main_btc_wallet = get_wallet_data( 56 | &SecretString(decrypted_wallet.private.btc_descriptor_xprv.clone()), 57 | None, 58 | ) 59 | .await?; 60 | // let main_rgb_wallet = 61 | // get_wallet_data(&decrypted_wallet.private.rgb_assets_descriptor_xprv, None).await?; 62 | 63 | println!( 64 | "Descriptor: {}", 65 | decrypted_wallet.private.btc_descriptor_xprv 66 | ); 67 | println!("Address (Bitcoin): {}", main_btc_wallet.address); 68 | 69 | Ok(()) 70 | } 71 | 72 | #[tokio::test] 73 | async fn import_wallet() -> Result<()> { 74 | init_logging("wallet=info"); 75 | 76 | let network = get_network().await; 77 | info!("Asset test on {network}"); 78 | 79 | info!("Import wallets"); 80 | let seed_password = SecretString(SEED_PASSWORD.to_owned()); 81 | let main_mnemonic = SecretString(env::var("TEST_WALLET_SEED")?); 82 | let hash0 = hash_password(&SecretString(ENCRYPTION_PASSWORD.to_owned())); 83 | let encrypted_descriptors = encrypt_wallet(&main_mnemonic, &hash0, &seed_password).await?; 84 | let _main_vault = decrypt_wallet(&hash0, &encrypted_descriptors)?; 85 | 86 | info!("Try once more"); 87 | let hash1 = hash_password(&SecretString(ENCRYPTION_PASSWORD.to_owned())); 88 | assert_eq!(hash0.0, hash1.0, "hashes match"); 89 | 90 | let encrypted_descriptors = encrypt_wallet(&main_mnemonic, &hash1, &seed_password).await?; 91 | let main_vault = decrypt_wallet(&hash1, &encrypted_descriptors)?; 92 | 93 | let main_btc_wallet = get_wallet_data( 94 | &SecretString(main_vault.private.btc_descriptor_xprv.clone()), 95 | None, 96 | ) 97 | .await?; 98 | let main_rgb_wallet = get_wallet_data( 99 | &SecretString(main_vault.private.rgb_assets_descriptor_xprv.clone()), 100 | None, 101 | ) 102 | .await?; 103 | 104 | println!("Descriptor: {}", main_vault.private.btc_descriptor_xprv); 105 | println!("Address (Bitcoin): {}", main_btc_wallet.address); 106 | println!("Address (RGB): {}", main_rgb_wallet.address); 107 | 108 | Ok(()) 109 | } 110 | 111 | #[tokio::test] 112 | async fn get_wallet_balance() -> Result<()> { 113 | init_logging("wallet=info"); 114 | 115 | let main_mnemonic = SecretString(env::var("TEST_WALLET_SEED")?); 116 | let seed_password = SecretString(SEED_PASSWORD.to_owned()); 117 | let hash = hash_password(&SecretString(ENCRYPTION_PASSWORD.to_owned())); 118 | let encrypted_descriptors = encrypt_wallet(&main_mnemonic, &hash, &seed_password).await?; 119 | let main_vault = decrypt_wallet(&hash, &encrypted_descriptors)?; 120 | 121 | let btc_wallet = get_wallet_data( 122 | &SecretString(main_vault.private.btc_descriptor_xprv.clone()), 123 | None, 124 | ) 125 | .await?; 126 | 127 | warn!("Descriptor:", main_vault.private.btc_descriptor_xprv); 128 | warn!("Address:", btc_wallet.address); 129 | warn!("Wallet Balance:", btc_wallet.balance.confirmed.to_string()); 130 | 131 | Ok(()) 132 | } 133 | 134 | #[tokio::test] 135 | async fn wrong_network() -> Result<()> { 136 | init_logging("wallet=info"); 137 | 138 | switch_network("testnet").await?; 139 | let network = get_network().await; 140 | info!("Asset test on {network}"); 141 | 142 | let main_mnemonic = SecretString(env::var("TEST_WALLET_SEED")?); 143 | let seed_password = SecretString(SEED_PASSWORD.to_owned()); 144 | let hash = hash_password(&SecretString(ENCRYPTION_PASSWORD.to_owned())); 145 | let encrypted_descriptors = encrypt_wallet(&main_mnemonic, &hash, &seed_password).await?; 146 | 147 | let main_vault = decrypt_wallet(&hash, &encrypted_descriptors)?; 148 | 149 | let result = send_sats( 150 | &SecretString(main_vault.private.btc_descriptor_xprv.to_owned()), 151 | &SecretString(main_vault.private.btc_change_descriptor_xprv.to_owned()), 152 | "bc1pgxpvg7cz0s3akgl9vhv687rzya7frskenukgx3gwuh6q3un5wqgq7xmnhe", 153 | 1000, 154 | Some(1.0), 155 | ) 156 | .await; 157 | 158 | assert!(matches!(result, Err(BitcoinError::WrongNetwork))); 159 | 160 | Ok(()) 161 | } 162 | -------------------------------------------------------------------------------- /src/rgb/cambria.rs: -------------------------------------------------------------------------------- 1 | use crate::rgb::structs::{ 2 | RgbAccountV0, RgbAccountV1, RgbTransferV0, RgbTransferV1, RgbTransfersV0, RgbTransfersV1, 3 | }; 4 | use postcard::from_bytes; 5 | 6 | #[derive(Debug, Clone, Eq, PartialEq, Display, From, Error)] 7 | #[display(doc_comments)] 8 | pub enum ModelVersionError { 9 | /// Version unknown. {0:?} 10 | Unknown(String), 11 | /// Decode Error. {0} 12 | WrongDecode(postcard::Error), 13 | } 14 | 15 | pub trait ModelVersion { 16 | fn from_bytes(bytes: Vec, version: [u8; 8]) -> Result; 17 | } 18 | 19 | #[derive(Debug, Eq, PartialEq, Clone, Display, From, Error)] 20 | #[display(doc_comments)] 21 | pub enum RgbAccountVersions { 22 | Unknown, 23 | V0(RgbAccountV0), 24 | V1(RgbAccountV1), 25 | } 26 | 27 | impl From for RgbAccountVersions { 28 | fn from(value: u8) -> Self { 29 | match value { 30 | 0 => RgbAccountVersions::V0(RgbAccountV0::default()), 31 | 1 => RgbAccountVersions::V1(RgbAccountV1::default()), 32 | _ => RgbAccountVersions::Unknown, 33 | } 34 | } 35 | } 36 | 37 | impl From for RgbAccountVersions { 38 | fn from(value: String) -> Self { 39 | match value.to_lowercase().as_str() { 40 | "v0" | "0" | "rgbst161" | "" => RgbAccountVersions::V0(RgbAccountV0::default()), 41 | "v10" | "v1" | "1" => RgbAccountVersions::V1(RgbAccountV1::default()), 42 | _ => RgbAccountVersions::Unknown, 43 | } 44 | } 45 | } 46 | 47 | impl From<[u8; 8]> for RgbAccountVersions { 48 | fn from(value: [u8; 8]) -> Self { 49 | let value = String::from_utf8(value.to_vec()).expect("invalid rgb account metadata value"); 50 | let value = value.trim_matches(char::from(0)); 51 | RgbAccountVersions::from(value.to_string()) 52 | } 53 | } 54 | 55 | impl ModelVersion for RgbAccountVersions { 56 | fn from_bytes(bytes: Vec, version: [u8; 8]) -> Result { 57 | let target_version = RgbAccountVersions::from(version); 58 | let latest_version = match target_version { 59 | RgbAccountVersions::Unknown => { 60 | return Err(ModelVersionError::Unknown( 61 | String::from_utf8(version.to_vec()).unwrap(), 62 | )) 63 | } 64 | RgbAccountVersions::V0(mut previous_version) => { 65 | previous_version = from_bytes(&bytes).map_err(ModelVersionError::WrongDecode)?; 66 | RgbAccountV1::from(previous_version) 67 | } 68 | RgbAccountVersions::V1(mut current_version) => { 69 | current_version = from_bytes(&bytes).map_err(ModelVersionError::WrongDecode)?; 70 | current_version 71 | } 72 | }; 73 | 74 | Ok(latest_version) 75 | } 76 | } 77 | 78 | impl From for RgbAccountV1 { 79 | fn from(value: RgbAccountV0) -> Self { 80 | Self { 81 | wallets: value.wallets, 82 | ..Default::default() 83 | } 84 | } 85 | } 86 | 87 | #[derive(Debug, Eq, PartialEq, Clone, Display, From, Error)] 88 | #[display(doc_comments)] 89 | pub enum RgbtransferVersions { 90 | Unknown, 91 | V0(RgbTransfersV0), 92 | V1(RgbTransfersV1), 93 | } 94 | 95 | impl From for RgbtransferVersions { 96 | fn from(value: String) -> Self { 97 | match value.to_lowercase().as_str() { 98 | "v0" | "0" | "rgbst161" | "" => RgbtransferVersions::V0(RgbTransfersV0::default()), 99 | "v10" | "v1" | "1" => RgbtransferVersions::V1(RgbTransfersV1::default()), 100 | _ => RgbtransferVersions::Unknown, 101 | } 102 | } 103 | } 104 | 105 | impl From<[u8; 8]> for RgbtransferVersions { 106 | fn from(value: [u8; 8]) -> Self { 107 | let value = String::from_utf8(value.to_vec()).expect("invalid rgb account metadata value"); 108 | let value = value.trim_matches(char::from(0)); 109 | RgbtransferVersions::from(value.to_string()) 110 | } 111 | } 112 | 113 | impl ModelVersion for RgbtransferVersions { 114 | fn from_bytes(bytes: Vec, version: [u8; 8]) -> Result { 115 | let target_version = RgbtransferVersions::from(version); 116 | let latest_version = match target_version { 117 | RgbtransferVersions::Unknown => { 118 | return Err(ModelVersionError::Unknown( 119 | String::from_utf8(version.to_vec()).unwrap(), 120 | )) 121 | } 122 | RgbtransferVersions::V0(mut previous_version) => { 123 | previous_version = from_bytes(&bytes).map_err(ModelVersionError::WrongDecode)?; 124 | RgbTransfersV1::from(previous_version) 125 | } 126 | RgbtransferVersions::V1(mut current_version) => { 127 | current_version = from_bytes(&bytes).map_err(ModelVersionError::WrongDecode)?; 128 | current_version 129 | } 130 | }; 131 | 132 | Ok(latest_version) 133 | } 134 | } 135 | 136 | impl From for RgbTransfersV1 { 137 | fn from(value: RgbTransfersV0) -> Self { 138 | let mut transfers = RgbTransfersV1::default(); 139 | for (k, v) in value.transfers { 140 | let map = v.into_iter().map(RgbTransferV1::from).collect(); 141 | transfers.transfers.insert(k, map); 142 | } 143 | transfers 144 | } 145 | } 146 | 147 | impl From for RgbTransferV1 { 148 | fn from(value: RgbTransferV0) -> Self { 149 | let RgbTransferV0 { 150 | consig_id, 151 | iface, 152 | consig, 153 | tx: tx_id, 154 | is_send: sender, 155 | } = value; 156 | 157 | Self { 158 | consig_id, 159 | tx_id, 160 | iface, 161 | consig, 162 | sender, 163 | rbf: false, 164 | utxos: vec![], 165 | beneficiaries: vec![], 166 | } 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /lib/web/bitcoin.ts: -------------------------------------------------------------------------------- 1 | // Methods meant to work with BDK defined within the web::bitcoin module from bitmask-core: 2 | // https://github.com/diba-io/bitmask-core/blob/development/src/web.rs 3 | 4 | import * as BMC from "./bitmask_core"; 5 | 6 | export const hashPassword = (password: string) => BMC.hash_password(password); 7 | 8 | export const decryptWallet = async ( 9 | hash: string, 10 | encryptedDescriptors: string 11 | ): Promise => 12 | JSON.parse(await BMC.decrypt_wallet(hash, encryptedDescriptors)); 13 | 14 | export const upgradeWallet = async ( 15 | hash: string, 16 | encryptedDescriptors: string, 17 | seedPassword = "" 18 | ): Promise => 19 | JSON.parse( 20 | await BMC.upgrade_wallet(hash, encryptedDescriptors, seedPassword) 21 | ); 22 | 23 | export const syncWallets = async (): Promise => BMC.sync_wallets(); 24 | 25 | export const newWallet = async ( 26 | hash: string, 27 | seedPassword: string 28 | ): Promise => JSON.parse(await BMC.new_wallet(hash, seedPassword)); 29 | 30 | export const encryptWallet = async ( 31 | mnemonic: string, 32 | hash: string, 33 | seedPassword: string 34 | ): Promise => 35 | JSON.parse(await BMC.encrypt_wallet(mnemonic, hash, seedPassword)); 36 | 37 | export const getWalletData = async ( 38 | descriptor: string, 39 | changeDescriptor?: string 40 | ): Promise => 41 | JSON.parse(await BMC.get_wallet_data(descriptor, changeDescriptor)); 42 | 43 | export const getNewAddress = async ( 44 | descriptor: string, 45 | changeDescriptor?: string 46 | ): Promise => 47 | JSON.parse(await BMC.get_new_address(descriptor, changeDescriptor)); 48 | 49 | export const sendSats = async ( 50 | descriptor: string, 51 | changeDescriptor: string, 52 | address: string, 53 | amount: bigint, 54 | feeRate: number 55 | ): Promise => 56 | JSON.parse( 57 | await BMC.send_sats(descriptor, changeDescriptor, address, amount, feeRate) 58 | ); 59 | 60 | export const fundVault = async ( 61 | descriptor: string, 62 | changeDescriptor: string, 63 | assetAddress1: string, 64 | udaAddress1: string, 65 | feeRate: number 66 | ): Promise => 67 | JSON.parse( 68 | await BMC.fund_vault( 69 | descriptor, 70 | changeDescriptor, 71 | assetAddress1, 72 | udaAddress1, 73 | feeRate 74 | ) 75 | ); 76 | 77 | export const getAssetsVault = async ( 78 | rgbAssetsDescriptorXpub: string, 79 | rgbUdasDescriptorXpub: string 80 | ): Promise => 81 | JSON.parse( 82 | await BMC.get_assets_vault(rgbAssetsDescriptorXpub, rgbUdasDescriptorXpub) 83 | ); 84 | 85 | export const drainWallet = async ( 86 | destination: string, 87 | descriptor: string, 88 | changeDescriptor?: string, 89 | feeRate?: number 90 | ): Promise => 91 | JSON.parse( 92 | await BMC.drain_wallet(destination, descriptor, changeDescriptor, feeRate) 93 | ); 94 | 95 | export const bumpFee = async ( 96 | txid: string, 97 | feeRate: number, 98 | broadcast: boolean, 99 | descriptor: string, 100 | changeDescriptor?: string, 101 | ): Promise => 102 | JSON.parse( 103 | await BMC.bump_fee(txid, feeRate, descriptor, changeDescriptor, broadcast) 104 | ); 105 | 106 | // Core type interfaces based on structs defined within the bitmask-core Rust crate: 107 | // https://github.com/diba-io/bitmask-core/blob/development/src/structs.rs 108 | 109 | export interface PrivateWalletData { 110 | xprvkh: string; 111 | btcDescriptorXprv: string; 112 | btcChangeDescriptorXprv: string; 113 | rgbAssetsDescriptorXprv: string; 114 | rgbUdasDescriptorXprv: string; 115 | nostrPrv: string; 116 | nostrNsec: string; 117 | } 118 | 119 | export interface PublicWalletData { 120 | xpub: string; 121 | xpubkh: string; 122 | watcherXpub: string; 123 | btcDescriptorXpub: string; 124 | btcChangeDescriptorXpub: string; 125 | rgbAssetsDescriptorXpub: string; 126 | rgbUdasDescriptorXpub: string; 127 | nostrPub: string; 128 | nostrNpub: string; 129 | } 130 | 131 | export interface Vault { 132 | mnemonic: string; 133 | private: PrivateWalletData; 134 | public: PublicWalletData; 135 | } 136 | 137 | export interface Transaction extends WalletTransaction { 138 | amount: number; 139 | asset?: string; 140 | assetType: string; 141 | fee: number; 142 | message?: string; 143 | note?: string; 144 | } 145 | 146 | export interface Activity extends Transaction { 147 | id: string; 148 | date: number; 149 | action: string; 150 | status: string; 151 | lightning?: boolean; 152 | sender?: { 153 | name: string; 154 | address: string; 155 | }; 156 | recipient?: { 157 | name: string; 158 | address: string; 159 | invoice: string; 160 | }; 161 | } 162 | 163 | export interface TransactionDetails extends Transaction { 164 | sender: { 165 | name: string; 166 | address: string; 167 | }; 168 | recipient: { 169 | name: string; 170 | address: string; 171 | invoice: string; 172 | }; 173 | } 174 | 175 | export interface TransactionData { 176 | details: TransactionDataDetails; 177 | vsize: number; 178 | feeRate: number; 179 | } 180 | 181 | export interface TransactionDataDetails { 182 | transaction?: Transaction; 183 | txid: string; 184 | received: number; 185 | sent: number; 186 | fee: number; 187 | confirmationTime?: ConfirmationTime; 188 | confirmed?: boolean; 189 | } 190 | 191 | export interface ConfirmationTime { 192 | height: number; 193 | timestamp: number; 194 | } 195 | 196 | export interface WalletTransaction { 197 | txid: string; 198 | received: number; 199 | sent: number; 200 | fee: number; 201 | confirmed: boolean; 202 | confirmationTime: ConfirmationTime; 203 | vsize: number; 204 | feeRate: number; 205 | } 206 | 207 | export interface WalletBalance { 208 | immature: number; 209 | trustedPending: number; 210 | untrustedPending: number; 211 | confirmed: number; 212 | } 213 | 214 | export interface WalletData { 215 | wallet?: string; 216 | name: string; 217 | address: string; 218 | balance: WalletBalance; 219 | transactions: WalletTransaction[]; 220 | utxos: string[]; 221 | } 222 | 223 | export interface FundVaultDetails { 224 | assetsOutput?: string; 225 | udasOutput?: string; 226 | isFunded: boolean; 227 | fundTxid: string; 228 | } 229 | -------------------------------------------------------------------------------- /tests/rgb/integration/watcher.rs: -------------------------------------------------------------------------------- 1 | #![cfg(not(target_arch = "wasm32"))] 2 | use bitmask_core::{ 3 | bitcoin::{get_wallet, save_mnemonic, sync_wallet}, 4 | rgb::{create_watcher, watcher_address, watcher_next_address, watcher_next_utxo, watcher_utxo}, 5 | structs::{SecretString, WatcherRequest}, 6 | }; 7 | 8 | use crate::rgb::integration::utils::{send_some_coins, ISSUER_MNEMONIC, OWNER_MNEMONIC}; 9 | 10 | #[tokio::test] 11 | async fn allow_monitoring_address() -> anyhow::Result<()> { 12 | let issuer_keys = &save_mnemonic( 13 | &SecretString(ISSUER_MNEMONIC.to_string()), 14 | &SecretString("".to_string()), 15 | ) 16 | .await?; 17 | 18 | // Create Watcher 19 | let watcher_name = "default"; 20 | let sk = issuer_keys.private.nostr_prv.clone(); 21 | let create_watch_req = WatcherRequest { 22 | name: watcher_name.to_string(), 23 | xpub: issuer_keys.public.watcher_xpub.clone(), 24 | force: true, 25 | }; 26 | 27 | create_watcher(&sk, create_watch_req.clone()).await?; 28 | 29 | // Get Address 30 | let issuer_wallet = get_wallet( 31 | &SecretString(issuer_keys.private.rgb_assets_descriptor_xprv.clone()), 32 | None, 33 | ) 34 | .await?; 35 | sync_wallet(&issuer_wallet).await?; 36 | 37 | let address = issuer_wallet 38 | .lock() 39 | .await 40 | .get_address(bdk::wallet::AddressIndex::LastUnused)?; 41 | 42 | // Register Address (Watcher) 43 | let resp = watcher_address(&sk, watcher_name, &address.address.to_string()).await; 44 | assert!(resp.is_ok()); 45 | assert!(resp?.utxos.is_empty()); 46 | Ok(()) 47 | } 48 | 49 | #[tokio::test] 50 | async fn allow_monitoring_address_with_coins() -> anyhow::Result<()> { 51 | let issuer_keys = &save_mnemonic( 52 | &SecretString(ISSUER_MNEMONIC.to_string()), 53 | &SecretString("".to_string()), 54 | ) 55 | .await?; 56 | 57 | // Create Watcher 58 | let watcher_name = "default"; 59 | let sk = issuer_keys.private.nostr_prv.clone(); 60 | let create_watch_req = WatcherRequest { 61 | name: watcher_name.to_string(), 62 | xpub: issuer_keys.public.watcher_xpub.clone(), 63 | force: true, 64 | }; 65 | 66 | create_watcher(&sk, create_watch_req.clone()).await?; 67 | 68 | // Get Address 69 | let issuer_wallet = get_wallet( 70 | &SecretString(issuer_keys.private.rgb_assets_descriptor_xprv.clone()), 71 | None, 72 | ) 73 | .await?; 74 | sync_wallet(&issuer_wallet).await?; 75 | 76 | let address = issuer_wallet 77 | .lock() 78 | .await 79 | .get_address(bdk::wallet::AddressIndex::LastUnused)?; 80 | let address = address.address.to_string(); 81 | 82 | // Send some coins 83 | send_some_coins(&address, "0.01").await; 84 | 85 | // Register Address (Watcher) 86 | let resp = watcher_address(&sk, watcher_name, &address).await; 87 | assert!(resp.is_ok()); 88 | assert!(!resp?.utxos.is_empty()); 89 | Ok(()) 90 | } 91 | 92 | #[tokio::test] 93 | async fn allow_monitoring_invalid_utxo() -> anyhow::Result<()> { 94 | let issuer_keys = &save_mnemonic( 95 | &SecretString(ISSUER_MNEMONIC.to_string()), 96 | &SecretString("".to_string()), 97 | ) 98 | .await?; 99 | 100 | // Create Watcher 101 | let watcher_name = "default"; 102 | let sk = issuer_keys.private.nostr_prv.clone(); 103 | let create_watch_req = WatcherRequest { 104 | name: watcher_name.to_string(), 105 | xpub: issuer_keys.public.watcher_xpub.clone(), 106 | force: true, 107 | }; 108 | create_watcher(&sk, create_watch_req.clone()).await?; 109 | 110 | // Get UTXO 111 | let next_utxo = "a6bbd6839ed4ad9ce53cf8bb56a01792031bfee6eed20877311408f2187bc239:0"; 112 | 113 | // Force Watcher (Recreate) 114 | create_watcher(&sk, create_watch_req.clone()).await?; 115 | 116 | // Register Utxo (Watcher) 117 | let resp = watcher_utxo(&sk, watcher_name, next_utxo).await; 118 | assert!(resp.is_ok()); 119 | assert!(resp?.utxos.is_empty()); 120 | Ok(()) 121 | } 122 | 123 | #[tokio::test] 124 | async fn allow_monitoring_valid_utxo() -> anyhow::Result<()> { 125 | let issuer_keys = &save_mnemonic( 126 | &SecretString(ISSUER_MNEMONIC.to_string()), 127 | &SecretString("".to_string()), 128 | ) 129 | .await?; 130 | 131 | // Create Watcher 132 | let watcher_name = "default"; 133 | let sk = issuer_keys.private.nostr_prv.clone(); 134 | let create_watch_req = WatcherRequest { 135 | name: watcher_name.to_string(), 136 | xpub: issuer_keys.public.watcher_xpub.clone(), 137 | force: true, 138 | }; 139 | create_watcher(&sk, create_watch_req.clone()).await?; 140 | 141 | // Get Address 142 | let next_addr = watcher_next_address(&sk, watcher_name, "RGB20").await?; 143 | 144 | // Send some coins 145 | send_some_coins(&next_addr.address, "0.01").await; 146 | 147 | // Get UTXO 148 | let next_utxo = watcher_next_utxo(&sk, watcher_name, "RGB20").await?; 149 | 150 | // Force Watcher (Recreate) 151 | create_watcher(&sk, create_watch_req.clone()).await?; 152 | 153 | // Register Utxo (Watcher) 154 | let resp = watcher_utxo(&sk, watcher_name, &next_utxo.utxo.unwrap().outpoint).await; 155 | assert!(resp.is_ok()); 156 | assert!(!resp?.utxos.is_empty()); 157 | Ok(()) 158 | } 159 | 160 | #[tokio::test] 161 | async fn allow_migrate_watcher() -> anyhow::Result<()> { 162 | let issuer_keys = &save_mnemonic( 163 | &SecretString(ISSUER_MNEMONIC.to_string()), 164 | &SecretString("".to_string()), 165 | ) 166 | .await?; 167 | 168 | let owner_keys = &save_mnemonic( 169 | &SecretString(OWNER_MNEMONIC.to_string()), 170 | &SecretString("".to_string()), 171 | ) 172 | .await?; 173 | 174 | // Create Watcher (Wrong Key) 175 | let watcher_name = "default"; 176 | let sk = issuer_keys.private.nostr_prv.clone(); 177 | let create_watch_req = WatcherRequest { 178 | name: watcher_name.to_string(), 179 | xpub: owner_keys.public.watcher_xpub.clone(), 180 | force: false, 181 | }; 182 | 183 | create_watcher(&sk, create_watch_req.clone()).await?; 184 | 185 | // Create Watcher (Correct Key) 186 | let create_watch_req = WatcherRequest { 187 | name: watcher_name.to_string(), 188 | xpub: issuer_keys.public.watcher_xpub.clone(), 189 | force: false, 190 | }; 191 | 192 | let resp = create_watcher(&sk, create_watch_req.clone()).await?; 193 | assert!(resp.migrate); 194 | Ok(()) 195 | } 196 | -------------------------------------------------------------------------------- /src/bitcoin/wallet.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::BTreeMap, sync::Arc}; 2 | 3 | use bdk::{blockchain::esplora::EsploraBlockchain, database::MemoryDatabase, SyncOptions, Wallet}; 4 | use bitcoin::Network; 5 | use bitcoin_hashes::{sha256, Hash}; 6 | use futures::Future; 7 | use once_cell::sync::Lazy; 8 | use thiserror::Error; 9 | use tokio::sync::{Mutex, RwLock}; 10 | 11 | use crate::{ 12 | constants::{BITCOIN_EXPLORER_API, NETWORK}, 13 | debug, 14 | structs::SecretString, 15 | }; 16 | 17 | #[derive(Error, Debug)] 18 | pub enum BitcoinWalletError { 19 | /// Unexpected key variant in get_descriptor 20 | #[error("Unexpected key variant in get_descriptor")] 21 | UnexpectedKey, 22 | /// BDK error 23 | #[error(transparent)] 24 | BdkError(#[from] bdk::Error), 25 | } 26 | 27 | pub type MemoryWallet = Arc>>; 28 | type Wallets = BTreeMap<(String, Option), MemoryWallet>; 29 | type NetworkWallet = Arc>; 30 | 31 | #[derive(Default)] 32 | struct Networks { 33 | bitcoin: NetworkWallet, 34 | testnet: NetworkWallet, 35 | signet: NetworkWallet, 36 | regtest: NetworkWallet, 37 | } 38 | 39 | static BDK: Lazy = Lazy::new(Networks::default); 40 | 41 | async fn access_network_wallets( 42 | network: Network, 43 | mut f: F, 44 | ) -> Result<(), BitcoinWalletError> 45 | where 46 | U: 'static + Send, 47 | F: 'static + FnMut(NetworkWallet) -> Fut + Send, 48 | Fut: 'static + Future> + Send, 49 | { 50 | match network { 51 | Network::Bitcoin => { 52 | f(BDK.bitcoin.clone()).await?; 53 | } 54 | Network::Testnet => { 55 | f(BDK.testnet.clone()).await?; 56 | } 57 | Network::Signet => { 58 | f(BDK.signet.clone()).await?; 59 | } 60 | Network::Regtest => { 61 | f(BDK.regtest.clone()).await?; 62 | } 63 | }; 64 | 65 | Ok(()) 66 | } 67 | 68 | pub async fn get_wallet( 69 | descriptor: &SecretString, 70 | change_descriptor: Option<&SecretString>, 71 | ) -> Result>>, BitcoinWalletError> { 72 | let descriptor_key = format!("{descriptor:?}{change_descriptor:?}"); 73 | let key: String = sha256::Hash::hash(descriptor_key.as_bytes()).to_string(); 74 | 75 | let network_lock = NETWORK.read().await; 76 | let network = network_lock.to_owned(); 77 | drop(network_lock); 78 | 79 | let wallets = match network { 80 | Network::Bitcoin => BDK.bitcoin.clone(), 81 | Network::Testnet => BDK.testnet.clone(), 82 | Network::Signet => BDK.signet.clone(), 83 | Network::Regtest => BDK.regtest.clone(), 84 | }; 85 | 86 | let wallets = wallets.clone(); 87 | let wallets_lock = wallets.read().await; 88 | let wallets_ref = wallets_lock.get(&(key.clone(), None)); 89 | if let Some(wallets) = wallets_ref { 90 | return Ok(wallets.clone()); 91 | } 92 | drop(wallets_lock); 93 | 94 | let new_wallet = Arc::new(Mutex::new(Wallet::new( 95 | &descriptor.0, 96 | change_descriptor.map(|desc| &desc.0), 97 | network, 98 | MemoryDatabase::default(), 99 | )?)); 100 | 101 | let key_outer = key; 102 | let new_wallet_outer = new_wallet.clone(); 103 | 104 | access_network_wallets(network, move |wallets| { 105 | let key_inner = key_outer.clone(); 106 | let new_wallet_inner = new_wallet_outer.clone(); 107 | 108 | async move { 109 | wallets 110 | .write() 111 | .await 112 | .insert((key_inner, None), new_wallet_inner); 113 | Ok(()) 114 | } 115 | }) 116 | .await?; 117 | 118 | Ok(new_wallet) 119 | } 120 | 121 | pub async fn get_blockchain() -> EsploraBlockchain { 122 | debug!("Getting blockchain"); 123 | EsploraBlockchain::new(&BITCOIN_EXPLORER_API.read().await, 1) 124 | } 125 | 126 | pub async fn sync_wallet(wallet: &MemoryWallet) -> Result<(), BitcoinWalletError> { 127 | let blockchain = get_blockchain().await; 128 | wallet 129 | .lock() 130 | .await 131 | .sync(&blockchain, SyncOptions::default()) 132 | .await?; 133 | 134 | debug!("Wallet synced"); 135 | Ok(()) 136 | } 137 | 138 | pub async fn sync_wallets() -> Result<(), BitcoinWalletError> { 139 | let network_lock = NETWORK.read().await; 140 | let network = network_lock.to_owned(); 141 | drop(network_lock); 142 | 143 | /* // BDK RefCell prevents this from working: 144 | access_network_wallets(network, move |wallets| async move { 145 | for (key, &mut wallet) in wallets.write().await.iter_mut() { 146 | let blockchain = get_blockchain().await; 147 | let wallet = wallet.lock().await; 148 | let wallet_sync_fut = wallet.sync(&blockchain, SyncOptions::default()); 149 | wallet_sync_fut.await?; 150 | } 151 | 152 | Ok(()) 153 | }); 154 | */ 155 | 156 | match network { 157 | Network::Bitcoin => { 158 | let wallets = BDK.bitcoin.clone(); 159 | for (_key, wallet) in wallets.write().await.iter_mut() { 160 | let blockchain = get_blockchain().await; 161 | let wallet = wallet.lock().await; 162 | let wallet_sync_fut = wallet.sync(&blockchain, SyncOptions::default()); 163 | wallet_sync_fut.await?; 164 | } 165 | } 166 | Network::Testnet => { 167 | let wallets = BDK.testnet.clone(); 168 | for (_key, wallet) in wallets.write().await.iter_mut() { 169 | let blockchain = get_blockchain().await; 170 | let wallet = wallet.lock().await; 171 | let wallet_sync_fut = wallet.sync(&blockchain, SyncOptions::default()); 172 | wallet_sync_fut.await?; 173 | } 174 | } 175 | Network::Signet => { 176 | let wallets = BDK.signet.clone(); 177 | for (_key, wallet) in wallets.write().await.iter_mut() { 178 | let blockchain = get_blockchain().await; 179 | let wallet = wallet.lock().await; 180 | let wallet_sync_fut = wallet.sync(&blockchain, SyncOptions::default()); 181 | wallet_sync_fut.await?; 182 | } 183 | } 184 | Network::Regtest => { 185 | let wallets = BDK.regtest.clone(); 186 | for (_key, wallet) in wallets.write().await.iter_mut() { 187 | let blockchain = get_blockchain().await; 188 | let wallet = wallet.lock().await; 189 | let wallet_sync_fut = wallet.sync(&blockchain, SyncOptions::default()); 190 | wallet_sync_fut.await?; 191 | } 192 | } 193 | }; 194 | 195 | debug!("All wallets synced"); 196 | Ok(()) 197 | } 198 | -------------------------------------------------------------------------------- /tests/rgb/unit/utils.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, convert::Infallible, str::FromStr}; 2 | 3 | use amplify::hex::{FromHex, ToHex}; 4 | use bitcoin::Transaction; 5 | use bitmask_core::{ 6 | rgb::issue::issue_contract, 7 | rgb::transfer::create_invoice, 8 | structs::{IssueMediaRequest, MediaInfo}, 9 | }; 10 | use bp::{ 11 | LockTime, Outpoint, Sats, ScriptPubkey, SeqNo, Tx, TxIn, TxOut, TxVer, Txid, VarIntArray, 12 | Witness, 13 | }; 14 | use psbt::{serialize::Deserialize, Psbt}; 15 | use rgbstd::{ 16 | containers::BindleContent, 17 | contract::{ContractId, WitnessOrd}, 18 | persistence::Stock, 19 | resolvers::ResolveHeight, 20 | validation::ResolveTx as RgbResolveTx, 21 | }; 22 | use rgbwallet::RgbInvoice; 23 | use wallet::onchain::ResolveTx; 24 | 25 | // Resolvers 26 | pub struct DumbResolve {} 27 | 28 | impl ResolveHeight for DumbResolve { 29 | type Error = Infallible; 30 | fn resolve_height(&mut self, _txid: Txid) -> std::result::Result { 31 | Ok(WitnessOrd::OffChain) 32 | } 33 | } 34 | impl ResolveTx for DumbResolve { 35 | fn resolve_tx( 36 | &self, 37 | _txid: bitcoin::Txid, 38 | ) -> Result { 39 | let hex = "020000000001019d8420cc5666b02f260bbaea43326c50a2c2eb99292fcf4c42a6179e132344de0000000000fdffffff02db9a8b44000000002251205d853a4a3da1dc163d2a2d9e8a76ae63db83f9310a25caa5d216a0fd962923a900e1f505000000002251206a61bf8aea7388b8541f16d773b77f897110eaa6bc17ada61c50bc70a93e5d610247304402202814bbcab5708f17d3e8ad42100ea1c156bbce287260d3394587339142767451022079d1c3bbe495fa57a0fab035c09a255502264d8bc3249f3ac5cd4c8878b91e0e012102c7c433670742289165c540c733d3473a7f458126e2a85c1b86b6b975a4ef5739f4010000"; 40 | let transaction = Transaction::deserialize(&Vec::from_hex(hex).unwrap()).unwrap(); 41 | Ok(transaction) 42 | } 43 | } 44 | 45 | impl RgbResolveTx for DumbResolve { 46 | fn resolve_tx(&self, _txid: Txid) -> Result { 47 | let hex = "020000000001014fba153e23558ca5532b5187ac20c4e35fe588c9bcb4a7b3c881c0541fcda65c0100000000ffffffff0118ddf50500000000225120d9b9957aa15bb91d856ed862cd04183555c9b9ea04ec3763c3b1e388adebe8e601417b5df1ce9c9c56c914203d8b2827000c72a15733e85f18c6a35f1fafa9c5068a8c73169dc3d98113112d7309114ca449fe3f740e949dbc6712ff945115d666c10100000000"; 48 | let transaction = Transaction::deserialize(&Vec::from_hex(hex).unwrap()).unwrap(); 49 | 50 | let mut ti = VarIntArray::new(); 51 | let tx_input = &transaction.input[0]; 52 | let input = TxIn { 53 | prev_output: Outpoint::new( 54 | Txid::from_str(&tx_input.previous_output.txid.to_hex()).expect("oh no!"), 55 | tx_input.previous_output.vout, 56 | ), 57 | sig_script: tx_input.script_sig.to_bytes().into(), 58 | sequence: SeqNo::from_consensus_u32(tx_input.sequence.to_consensus_u32()), 59 | witness: Witness::from_consensus_stack(tx_input.witness.to_vec()), 60 | }; 61 | ti.push(input).expect("fail"); 62 | 63 | let mut to = VarIntArray::new(); 64 | let tx_output = &transaction.output[0]; 65 | let output = TxOut { 66 | value: Sats::from(tx_output.value), 67 | script_pubkey: ScriptPubkey::from(tx_output.script_pubkey.to_bytes()), 68 | }; 69 | to.push(output).expect("fail"); 70 | 71 | let tx = Tx { 72 | version: TxVer::V2, 73 | inputs: ti, 74 | outputs: to, 75 | lock_time: LockTime::from_consensus_u32(422), 76 | }; 77 | Ok(tx) 78 | } 79 | } 80 | 81 | // Helpers 82 | #[allow(dead_code)] 83 | pub fn create_fake_psbt() -> Psbt { 84 | let psbt_hex = "70736274ff01005e02000000014fba153e23558ca5532b5187ac20c4e35fe588c9bcb4a7b3c881c0541fcda65c0100000000ffffffff0118ddf505000000002251202aa594ee4dc05d289387c77a44ee3d5401a7edc269e355f2345c2792d9f8d014000000004f01043587cf034a3acf0b80000000fe80c9c11d65f2a2bfbf8e582c49b829e0f453e2a7138ec303ddd724aa295ebf02008b0bc2899bf59a892479c553d7c7e6901a0fc8db3e5570529101bc783743bf10280a59635600008001000080000000800001008902000000019d8420cc5666b02f260bbaea43326c50a2c2eb99292fcf4c42a6179e132344de0000000000fdffffff02db9a8b44000000002251205d853a4a3da1dc163d2a2d9e8a76ae63db83f9310a25caa5d216a0fd962923a900e1f505000000002251206a61bf8aea7388b8541f16d773b77f897110eaa6bc17ada61c50bc70a93e5d61f4010000010304010000002116e7e50584e394cb1b467f440e8760bf3806835d55378f78cbacb8c651d2e11d0f1900280a59635600008001000080000000800000000000000000011720e7e50584e394cb1b467f440e8760bf3806835d55378f78cbacb8c651d2e11d0f0022020269c3a787c625331a17fd8a5cf7094d4672fb0385b5fd8fa2813181de3a1cef3e18280a5963560000800100008000000080010000000000000001052069c3a787c625331a17fd8a5cf7094d4672fb0385b5fd8fa2813181de3a1cef3e09fc06544150524554000000"; 85 | Psbt::from_str(psbt_hex).expect("invalid dumb psbt") 86 | } 87 | 88 | #[allow(dead_code)] 89 | pub fn create_fake_contract(stock: &mut Stock) -> ContractId { 90 | let ticker = "DIBA"; 91 | let name = "DIBA"; 92 | let description = 93 | "1 2 3 testing... 1 2 3 testing... 1 2 3 testing... 1 2 3 testing.... 1 2 3 testing"; 94 | let precision = 8; 95 | let supply = 10; 96 | let seal = "tapret1st:5ca6cd1f54c081c8b3a7b4bcc988e55fe3c420ac87512b53a58c55233e15ba4f:1"; 97 | let network = "regtest"; 98 | let iface = "RGB20"; 99 | let mut resolver = DumbResolve {}; 100 | 101 | let contract = issue_contract( 102 | ticker, 103 | name, 104 | description, 105 | precision, 106 | supply, 107 | iface, 108 | seal, 109 | network, 110 | None, 111 | &mut resolver, 112 | stock, 113 | ) 114 | .expect("test issue_contract failed"); 115 | 116 | let mut dumb = DumbResolve {}; 117 | 118 | let bindle = contract.bindle(); 119 | let contract = bindle 120 | .unbindle() 121 | .validate(&mut dumb) 122 | .map_err(|c| c.validation_status().expect("just validated").to_string()) 123 | .expect("invalid contract"); 124 | 125 | contract.contract_id() 126 | } 127 | 128 | #[allow(dead_code)] 129 | pub fn create_fake_invoice(contract_id: ContractId, seal: &str, stock: &mut Stock) -> RgbInvoice { 130 | let amount = 1; 131 | let iface = "RGB20"; 132 | let params = HashMap::new(); 133 | create_invoice( 134 | &contract_id.to_string(), 135 | iface, 136 | amount, 137 | seal, 138 | "regtest", 139 | params, 140 | stock, 141 | ) 142 | .expect("create_invoice failed") 143 | } 144 | 145 | #[allow(dead_code)] 146 | pub fn get_uda_data() -> IssueMediaRequest { 147 | IssueMediaRequest { 148 | media: Some(MediaInfo { 149 | ty: "image/png".to_string(), 150 | source: "b9bf23a30bedcc4c7e5b7e83412f8962ed33012dbb822a15fb536e5f2fbf9d28".to_string(), 151 | }), 152 | ..Default::default() 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/bitcoin/psbt.rs: -------------------------------------------------------------------------------- 1 | use bdk::{blockchain::Blockchain, psbt::PsbtUtils, SignOptions, TransactionDetails}; 2 | use bitcoin::{consensus::serialize, hashes::hex::ToHex, util::psbt::PartiallySignedTransaction}; 3 | use thiserror::Error; 4 | 5 | use crate::{ 6 | bitcoin::{get_blockchain, MemoryWallet}, 7 | debug, 8 | }; 9 | 10 | #[derive(Error, Debug)] 11 | pub enum BitcoinPsbtError { 12 | /// Could not broadcast PSBT 13 | #[error("Could not broadcast PSBT")] 14 | CouldNotBroadcastPsbt(String), 15 | /// Could not finalize when signing PSBT 16 | #[error("Could not finalize when signing PSBT")] 17 | CouldNotFinalizePsbt, 18 | /// BDK error 19 | #[error(transparent)] 20 | BdkError(#[from] bdk::Error), 21 | /// BDK esplora error 22 | #[error(transparent)] 23 | BdkEsploraError(#[from] bdk::esplora_client::Error), 24 | } 25 | 26 | // Only signs an original psbt. 27 | pub async fn sign_psbt( 28 | wallet: &MemoryWallet, 29 | mut psbt: PartiallySignedTransaction, 30 | ) -> Result { 31 | debug!("Funding PSBT..."); 32 | let opts = SignOptions { 33 | allow_all_sighashes: true, 34 | remove_partial_sigs: false, 35 | ..Default::default() 36 | }; 37 | wallet.lock().await.sign(&mut psbt, opts)?; 38 | Ok(psbt) 39 | } 40 | 41 | pub async fn multi_sign_psbt( 42 | wallets: Vec, 43 | mut psbt: PartiallySignedTransaction, 44 | ) -> Result { 45 | let total_wallets = wallets.len(); 46 | debug!(format!( 47 | "Signing PSBT ({total_wallets}/{total_wallets}) ..." 48 | )); 49 | 50 | let mut sign_count = 0; 51 | for wallet in wallets { 52 | let sign = wallet.lock().await.sign( 53 | &mut psbt, 54 | SignOptions { 55 | allow_all_sighashes: true, 56 | remove_partial_sigs: false, 57 | ..Default::default() 58 | }, 59 | )?; 60 | 61 | if sign { 62 | sign_count += 1; 63 | } 64 | 65 | debug!(format!("PSBT Sign: ({sign_count}/{total_wallets})")); 66 | } 67 | 68 | Ok(psbt) 69 | } 70 | 71 | pub async fn publish_psbt( 72 | psbt: PartiallySignedTransaction, 73 | ) -> Result { 74 | debug!("Signed PSBT:", base64::encode(&serialize(&psbt))); 75 | let fee_amount = psbt.fee_amount().expect("fee amount on PSBT is known"); 76 | let tx = psbt.extract_tx(); 77 | debug!("tx:", &serialize(&tx.clone()).to_hex()); 78 | let blockchain = get_blockchain().await; 79 | blockchain 80 | .broadcast(&tx) 81 | .await 82 | .map_err(|op| BitcoinPsbtError::CouldNotBroadcastPsbt(op.to_string()))?; 83 | 84 | let txid = tx.txid(); 85 | let tx = blockchain.get_tx(&txid).await?; 86 | 87 | let mut sent = 0; 88 | let mut received = 0; 89 | 90 | if let Some(tx) = tx.clone() { 91 | sent = tx.output.iter().fold(0, |sum, output| output.value + sum); 92 | received = sent - fee_amount; 93 | } 94 | 95 | let details = TransactionDetails { 96 | transaction: tx, 97 | txid, 98 | received, 99 | sent, 100 | fee: Some(fee_amount), 101 | confirmation_time: None, 102 | }; 103 | 104 | Ok(details) 105 | } 106 | 107 | /// Signs and broadcasts a transaction given a Psbt 108 | pub async fn sign_and_publish_psbt( 109 | wallet: &MemoryWallet, 110 | mut psbt: PartiallySignedTransaction, 111 | ) -> Result { 112 | debug!("Signing PSBT..."); 113 | let finalized = wallet 114 | .lock() 115 | .await 116 | .sign(&mut psbt, SignOptions::default())?; 117 | debug!(format!("Finalized: {finalized}")); 118 | if finalized { 119 | debug!("Signed PSBT:", base64::encode(&serialize(&psbt))); 120 | let fee_amount = psbt.fee_amount().expect("fee amount on PSBT is known"); 121 | let tx = psbt.extract_tx(); 122 | debug!("tx:", &serialize(&tx.clone()).to_hex()); 123 | let blockchain = get_blockchain().await; 124 | blockchain.broadcast(&tx).await?; 125 | 126 | let txid = tx.txid(); 127 | let tx = blockchain.get_tx(&txid).await?; 128 | 129 | let mut sent = 0; 130 | let mut received = 0; 131 | 132 | if let Some(tx) = tx.clone() { 133 | sent = tx.output.iter().fold(0, |sum, output| output.value + sum); 134 | received = sent - fee_amount; 135 | } 136 | 137 | let details = TransactionDetails { 138 | transaction: tx, 139 | txid, 140 | received, 141 | sent, 142 | fee: Some(fee_amount), 143 | confirmation_time: None, 144 | }; 145 | 146 | Ok(details) 147 | } else { 148 | Err(BitcoinPsbtError::CouldNotFinalizePsbt) 149 | } 150 | } 151 | 152 | /// Signs and broadcasts a transaction given a Psbt 153 | pub async fn multi_sign_and_publish_psbt( 154 | wallets: Vec, 155 | mut psbt: PartiallySignedTransaction, 156 | ) -> Result { 157 | let total_wallets = wallets.len(); 158 | debug!(format!( 159 | "Signing PSBT ({total_wallets}/{total_wallets}) ..." 160 | )); 161 | 162 | let mut sign_count = 0; 163 | let mut finalized = false; 164 | for wallet in wallets { 165 | finalized = wallet.lock().await.sign( 166 | &mut psbt, 167 | SignOptions { 168 | allow_all_sighashes: true, 169 | remove_partial_sigs: false, 170 | ..Default::default() 171 | }, 172 | )?; 173 | 174 | sign_count += 1; 175 | debug!(format!("PSBT Sign: ({sign_count}/{total_wallets})")); 176 | } 177 | 178 | debug!(format!("Finalized: {finalized}")); 179 | if finalized { 180 | debug!("Signed PSBT:", base64::encode(&serialize(&psbt))); 181 | let fee_amount = psbt.fee_amount().expect("fee amount on PSBT is known"); 182 | let tx = psbt.extract_tx(); 183 | debug!("tx:", &serialize(&tx.clone()).to_hex()); 184 | let blockchain = get_blockchain().await; 185 | blockchain.broadcast(&tx).await?; 186 | 187 | let txid = tx.txid(); 188 | let tx = blockchain.get_tx(&txid).await?; 189 | 190 | let mut sent = 0; 191 | let mut received = 0; 192 | 193 | if let Some(tx) = tx.clone() { 194 | sent = tx.output.iter().fold(0, |sum, output| output.value + sum); 195 | received = sent - fee_amount; 196 | } 197 | 198 | let details = TransactionDetails { 199 | transaction: tx, 200 | txid, 201 | received, 202 | sent, 203 | fee: Some(fee_amount), 204 | confirmation_time: None, 205 | }; 206 | 207 | Ok(details) 208 | } else { 209 | Err(BitcoinPsbtError::CouldNotFinalizePsbt) 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /src/bitcoin/payment.rs: -------------------------------------------------------------------------------- 1 | use bdk::{wallet::tx_builder::TxOrdering, FeeRate, TransactionDetails}; 2 | 3 | use bitcoin::{ 4 | consensus::serialize, 5 | psbt::{Input, Psbt}, 6 | Amount, TxIn, 7 | }; 8 | use payjoin::{send::Configuration, PjUri, PjUriExt}; 9 | use thiserror::Error; 10 | 11 | use crate::{ 12 | bitcoin::{ 13 | psbt::{sign_and_publish_psbt, sign_psbt, BitcoinPsbtError}, 14 | wallet::MemoryWallet, 15 | }, 16 | debug, info, 17 | structs::SatsInvoice, 18 | }; 19 | 20 | #[derive(Error, Debug)] 21 | pub enum BitcoinPaymentError { 22 | /// Payjoin error response 23 | #[error("Error performing payjoin: {0}")] 24 | PayjoinError(String), 25 | /// BitMask Core Bitcoin Psbt error 26 | #[error(transparent)] 27 | BitcoinPsbtError(#[from] BitcoinPsbtError), 28 | /// BDK error 29 | #[error(transparent)] 30 | BdkError(#[from] bdk::Error), 31 | /// Payjoin Request error 32 | #[error(transparent)] 33 | PayjoinGetRequestError(#[from] payjoin::send::CreateRequestError), 34 | /// Payjoin Send error 35 | #[error(transparent)] 36 | PayjoinSendError(#[from] payjoin::send::ValidationError), 37 | /// Reqwest error 38 | #[error(transparent)] 39 | ReqwestError(#[from] reqwest::Error), 40 | } 41 | 42 | pub async fn create_transaction( 43 | invoices: Vec, 44 | wallet: &MemoryWallet, 45 | fee_rate: Option, 46 | ) -> Result { 47 | let (psbt, details) = { 48 | let locked_wallet = wallet.lock().await; 49 | let mut builder = locked_wallet.build_tx(); 50 | for invoice in invoices { 51 | builder.add_recipient(invoice.address.script_pubkey(), invoice.amount); 52 | } 53 | 54 | builder.ordering(TxOrdering::Untouched); // TODO: Remove after implementing wallet persistence 55 | builder.enable_rbf().fee_rate(fee_rate.unwrap_or_default()); 56 | builder.finish()? 57 | }; 58 | 59 | debug!(format!("Create transaction: {details:#?}")); 60 | debug!("Unsigned PSBT:", base64::encode(&serialize(&psbt))); 61 | let details = sign_and_publish_psbt(wallet, psbt).await?; 62 | info!("PSBT successfully signed"); 63 | 64 | Ok(details) 65 | } 66 | 67 | pub async fn create_payjoin( 68 | invoices: Vec, 69 | wallet: &MemoryWallet, 70 | fee_rate: Option, 71 | pj_uri: PjUri<'_>, // TODO specify Uri 72 | ) -> Result { 73 | let enacted_fee_rate = fee_rate.unwrap_or_default(); 74 | let (psbt, details) = { 75 | let locked_wallet = wallet.lock().await; 76 | let mut builder = locked_wallet.build_tx(); 77 | for invoice in &invoices { 78 | builder.add_recipient(invoice.address.script_pubkey(), invoice.amount); 79 | } 80 | builder.enable_rbf().fee_rate(enacted_fee_rate); 81 | builder.finish()? 82 | }; 83 | 84 | debug!(format!("Request PayJoin transaction: {details:#?}")); 85 | debug!("Unsigned Original PSBT:", base64::encode(&serialize(&psbt))); 86 | let original_psbt = sign_psbt(wallet, psbt.clone()).await?; 87 | info!("Original PSBT successfully signed"); 88 | 89 | let additional_fee_index = psbt 90 | .unsigned_tx 91 | .output 92 | .clone() 93 | .into_iter() 94 | .enumerate() 95 | .find(|(_, txo)| { 96 | invoices 97 | .iter() 98 | .all(|invoice| txo.script_pubkey != invoice.address.script_pubkey()) 99 | }) 100 | .map(|(i, _)| i); 101 | 102 | let pj_params = match additional_fee_index { 103 | Some(index) => { 104 | let amount_available = psbt 105 | .clone() 106 | .unsigned_tx 107 | .output 108 | .get(index) 109 | .map(|o| Amount::from_sat(o.value)) 110 | .unwrap_or_default(); 111 | const P2TR_INPUT_WEIGHT: usize = 58; // bitmask is taproot only 112 | let recommended_fee = Amount::from_sat(enacted_fee_rate.fee_wu(P2TR_INPUT_WEIGHT)); 113 | let max_additional_fee = std::cmp::min( 114 | recommended_fee, 115 | amount_available, // "clamp" to amount available if recommendation is not 116 | ); 117 | 118 | Configuration::with_fee_contribution(max_additional_fee, Some(index)) 119 | .clamp_fee_contribution(true) 120 | } 121 | None => Configuration::non_incentivizing(), 122 | }; 123 | 124 | let (req, ctx) = pj_uri.create_pj_request(original_psbt.clone(), pj_params)?; 125 | info!("Built PayJoin request"); 126 | let response = reqwest::Client::new() 127 | .post(req.url) 128 | .header("Content-Type", "text/plain") 129 | .body(reqwest::Body::from(req.body)) 130 | .send() 131 | .await?; 132 | info!("Got PayJoin response"); 133 | 134 | let res = response.text().await?; 135 | info!(format!("Response: {res}")); 136 | 137 | if res.contains("errorCode") { 138 | return Err(BitcoinPaymentError::PayjoinError(format!("{res:?}"))); 139 | } 140 | 141 | let payjoin_psbt = ctx.process_response(&mut res.as_bytes())?; 142 | let payjoin_psbt = add_back_original_input(&original_psbt, payjoin_psbt); 143 | 144 | debug!( 145 | "Proposed PayJoin PSBT:", 146 | base64::encode(&serialize(&payjoin_psbt)) 147 | ); 148 | // sign_psbt also broadcasts; 149 | let tx = sign_and_publish_psbt(wallet, payjoin_psbt).await?; 150 | 151 | Ok(tx) 152 | } 153 | 154 | /// Unlike Bitcoin Core's walletprocesspsbt RPC, BDK's finalize_psbt only checks 155 | /// if the script in the PSBT input map matches the descriptor and does not 156 | /// check whether it has control of the OutPoint specified in the unsigned_tx's 157 | /// TxIn. So the original_psbt input data needs to be added back into 158 | /// payjoin_psbt without overwriting receiver input. 159 | fn add_back_original_input(original_psbt: &Psbt, payjoin_psbt: Psbt) -> Psbt { 160 | // input_pairs is only used here. It may be added to payjoin, rust-bitcoin, or BDK in time. 161 | fn input_pairs(psbt: &Psbt) -> Box + '_> { 162 | Box::new( 163 | psbt.unsigned_tx 164 | .input 165 | .iter() 166 | .cloned() // Clone each TxIn for better ergonomics than &muts 167 | .zip(psbt.inputs.iter().cloned()), // Clone each Input too 168 | ) 169 | } 170 | 171 | let mut original_inputs = input_pairs(original_psbt).peekable(); 172 | 173 | for (proposed_txin, mut proposed_psbtin) in input_pairs(&payjoin_psbt) { 174 | if let Some((original_txin, original_psbtin)) = original_inputs.peek() { 175 | if proposed_txin.previous_output == original_txin.previous_output { 176 | proposed_psbtin.witness_utxo = original_psbtin.witness_utxo.clone(); 177 | proposed_psbtin.non_witness_utxo = original_psbtin.non_witness_utxo.clone(); 178 | original_inputs.next(); 179 | } 180 | } 181 | } 182 | payjoin_psbt 183 | } 184 | -------------------------------------------------------------------------------- /tests/web_wallet.rs: -------------------------------------------------------------------------------- 1 | #![cfg(target_arch = "wasm32")] 2 | use bitmask_core::{ 3 | debug, info, 4 | structs::{DecryptedWalletData, SecretString, TransactionData, WalletData}, 5 | web::{ 6 | bitcoin::{ 7 | decrypt_wallet, encrypt_wallet, get_wallet_data, hash_password, new_wallet, send_sats, 8 | sync_wallets, 9 | }, 10 | json_parse, resolve, set_panic_hook, 11 | }, 12 | }; 13 | use wasm_bindgen::prelude::*; 14 | use wasm_bindgen_test::*; 15 | 16 | wasm_bindgen_test_configure!(run_in_browser); 17 | 18 | const MNEMONIC: &str = 19 | "outdoor nation key manual street net kidney insect ranch dial follow furnace"; 20 | const ENCRYPTION_PASSWORD: &str = "hunter2"; 21 | const SEED_PASSWORD: &str = ""; 22 | 23 | const DESCRIPTOR: &str = "tr([41e7fa8b/86'/1'/0']tprv8fddZuQpcukmaC4nND6cQTbPsim88ArLnVT6K2Vcnzi37gDFh7EhtKaEdDqyUGc1mRwVyPzkbNe2ZWd8Ryj5CWMRpLDn3ppKtgozUvp17rv/0/*)"; 24 | const CHANGE_DESCRIPTOR: &str = "tr([41e7fa8b/86'/1'/0']tprv8fddZuQpcukmaC4nND6cQTbPsim88ArLnVT6K2Vcnzi37gDFh7EhtKaEdDqyUGc1mRwVyPzkbNe2ZWd8Ryj5CWMRpLDn3ppKtgozUvp17rv/1/*)"; 25 | const PUBKEY_HASH: &str = "41e7fa8bc772add75092e31f0a15c10675163e82"; 26 | 27 | /// Tests for Wallet Creation Workflow 28 | 29 | /// Create wallet 30 | #[wasm_bindgen_test] 31 | async fn create_wallet() { 32 | set_panic_hook(); 33 | 34 | info!("Mnemonic string is 24 words long"); 35 | let hash = hash_password(ENCRYPTION_PASSWORD.to_owned()); 36 | let mnemonic: JsValue = resolve(new_wallet(hash.clone(), SEED_PASSWORD.to_owned())).await; 37 | 38 | assert!(!mnemonic.is_undefined()); 39 | assert!(mnemonic.is_string()); 40 | 41 | let mnemonic_data: SecretString = json_parse(&mnemonic); 42 | 43 | let encrypted_wallet_str: JsValue = 44 | resolve(decrypt_wallet(hash, mnemonic_data.0.clone())).await; 45 | let encrypted_wallet_data: DecryptedWalletData = json_parse(&encrypted_wallet_str); 46 | 47 | assert_eq!(encrypted_wallet_data.mnemonic.split(' ').count(), 24); 48 | } 49 | 50 | /// Can import a hardcoded mnemonic 51 | /// Can open a wallet and view address and balance 52 | #[wasm_bindgen_test] 53 | async fn import_and_open_wallet() { 54 | set_panic_hook(); 55 | 56 | info!("Import wallet"); 57 | let hash = hash_password(ENCRYPTION_PASSWORD.to_owned()); 58 | let mnemonic_data_str = resolve(encrypt_wallet( 59 | MNEMONIC.to_owned(), 60 | hash.clone(), 61 | SEED_PASSWORD.to_owned(), 62 | )) 63 | .await; 64 | 65 | let mnemonic_data: SecretString = json_parse(&mnemonic_data_str); 66 | 67 | info!("Get encrypted wallet properties"); 68 | let encrypted_wallet_str: JsValue = 69 | resolve(decrypt_wallet(hash, mnemonic_data.0.clone())).await; 70 | let encrypted_wallet_data: DecryptedWalletData = json_parse(&encrypted_wallet_str); 71 | 72 | assert_eq!( 73 | encrypted_wallet_data.private.btc_descriptor_xprv, DESCRIPTOR, 74 | "expected receive descriptor matches loaded wallet" 75 | ); 76 | assert_eq!( 77 | encrypted_wallet_data.private.btc_change_descriptor_xprv, CHANGE_DESCRIPTOR, 78 | "expected change descriptor matches loaded wallet" 79 | ); 80 | assert_eq!( 81 | encrypted_wallet_data.public.xpubkh, PUBKEY_HASH, 82 | "expected xpubkh matches loaded wallet" 83 | ); 84 | 85 | info!("Get wallet data"); 86 | let wallet_str: JsValue = resolve(get_wallet_data( 87 | DESCRIPTOR.to_owned(), 88 | Some(CHANGE_DESCRIPTOR.to_owned()), 89 | )) 90 | .await; 91 | 92 | info!("Parse wallet data"); 93 | let wallet_data: WalletData = json_parse(&wallet_str); 94 | 95 | assert_eq!(wallet_data.balance.confirmed, 0, "wallet has no sats"); 96 | assert!(wallet_data.transactions.is_empty(), "wallet has no txs"); 97 | } 98 | 99 | /// Can import the testing mnemonic 100 | /// Can open a wallet and view address and balance 101 | #[wasm_bindgen_test] 102 | async fn import_test_wallet() { 103 | set_panic_hook(); 104 | 105 | let mnemonic = env!("TEST_WALLET_SEED", "TEST_WALLET_SEED variable not set"); 106 | 107 | info!("Import wallet"); 108 | let hash0 = hash_password(ENCRYPTION_PASSWORD.to_owned()); 109 | let mnemonic_data_str = resolve(encrypt_wallet( 110 | mnemonic.to_owned(), 111 | hash0.clone(), 112 | SEED_PASSWORD.to_owned(), 113 | )) 114 | .await; 115 | let mnemonic_data: SecretString = json_parse(&mnemonic_data_str); 116 | 117 | info!("Get vault properties"); 118 | let vault_str: JsValue = resolve(decrypt_wallet(hash0.clone(), mnemonic_data.0.clone())).await; 119 | let _encrypted_wallet_data: DecryptedWalletData = json_parse(&vault_str); 120 | 121 | info!("Import wallet once more"); 122 | let hash1 = hash_password(ENCRYPTION_PASSWORD.to_owned()); 123 | assert_eq!(&hash0, &hash1, "hashes match"); 124 | 125 | let mnemonic_data_str = resolve(encrypt_wallet( 126 | mnemonic.to_owned(), 127 | hash1.clone(), 128 | SEED_PASSWORD.to_owned(), 129 | )) 130 | .await; 131 | let mnemonic_data: SecretString = json_parse(&mnemonic_data_str); 132 | 133 | info!("Get vault properties"); 134 | let vault_str: JsValue = resolve(decrypt_wallet(hash1, mnemonic_data.0.clone())).await; 135 | let encrypted_wallet_data: DecryptedWalletData = json_parse(&vault_str); 136 | 137 | info!("Get wallet data"); 138 | let wallet_str: JsValue = resolve(get_wallet_data( 139 | encrypted_wallet_data.private.btc_descriptor_xprv.clone(), 140 | Some( 141 | encrypted_wallet_data 142 | .private 143 | .btc_change_descriptor_xprv 144 | .clone(), 145 | ), 146 | )) 147 | .await; 148 | let wallet_data: WalletData = json_parse(&wallet_str); 149 | resolve(sync_wallets()).await; 150 | 151 | debug!(format!("Wallet address: {}", wallet_data.address)); 152 | 153 | assert!( 154 | wallet_data.balance.confirmed > 0, 155 | "test wallet balance is greater than zero" 156 | ); 157 | assert!( 158 | wallet_data 159 | .transactions 160 | .first() 161 | .expect("transactions already in wallet") 162 | .confirmation_time 163 | .is_some(), 164 | "first transaction is confirmed" 165 | ); 166 | assert!( 167 | wallet_data 168 | .transactions 169 | .first() 170 | .expect("transactions already in wallet") 171 | .confirmed, 172 | "first transaction has the confirmed property and is true" 173 | ); 174 | 175 | info!("Test sending a transaction back to itself for a thousand sats"); 176 | let tx_details = resolve(send_sats( 177 | encrypted_wallet_data.private.btc_descriptor_xprv.clone(), 178 | encrypted_wallet_data 179 | .private 180 | .btc_change_descriptor_xprv 181 | .clone(), 182 | wallet_data.address, 183 | 1_000, 184 | Some(1.1), 185 | )) 186 | .await; 187 | 188 | info!("Parse tx_details"); 189 | let tx_data: TransactionData = json_parse(&tx_details); 190 | 191 | assert!( 192 | tx_data.details.confirmation_time.is_none(), 193 | "latest transaction hasn't been confirmed yet" 194 | ); 195 | } 196 | -------------------------------------------------------------------------------- /tests/rgb/integration/rbf.rs: -------------------------------------------------------------------------------- 1 | #![cfg(not(target_arch = "wasm32"))] 2 | use anyhow::Result; 3 | use bdk::{ 4 | database::MemoryDatabase, 5 | descriptor::IntoWalletDescriptor, 6 | wallet::{tx_builder::TxOrdering, AddressIndex}, 7 | SignOptions, SyncOptions, 8 | }; 9 | use bitcoin::{secp256k1::Secp256k1, Network, Txid}; 10 | use bitmask_core::{ 11 | bitcoin::{bump_fee, get_blockchain, new_mnemonic, sign_and_publish_psbt_file}, 12 | rgb::{get_contract, structs::ContractAmount}, 13 | structs::{PsbtFeeRequest, PsbtResponse, SecretString, SignPsbtRequest}, 14 | }; 15 | use std::str::FromStr; 16 | 17 | use crate::rgb::integration::utils::{ 18 | create_new_psbt_v2, issuer_issue_contract_v2, send_some_coins, UtxoFilter, 19 | }; 20 | 21 | #[tokio::test] 22 | pub async fn create_simple_rbf_bitcoin_transfer() -> Result<()> { 23 | // 1. Initial Setup 24 | let whatever_address = "bcrt1p76gtucrxhmn8s5622r859dpnmkj0kgfcel9xy0sz6yj84x6ppz2qk5hpsw"; 25 | let issuer_keys = new_mnemonic(&SecretString("".to_string())).await?; 26 | 27 | let issuer_resp = issuer_issue_contract_v2( 28 | 1, 29 | "RGB20", 30 | ContractAmount::with(5, 0, 2).to_value(), 31 | false, 32 | true, 33 | None, 34 | Some("0.10000000".to_string()), 35 | Some(UtxoFilter::with_amount_equal_than(10_000_000)), 36 | Some(issuer_keys.clone()), 37 | ) 38 | .await?; 39 | let issuer_resp = &issuer_resp[0]; 40 | send_some_coins(whatever_address, "0.1").await; 41 | 42 | // 2. Get Allocations 43 | let issuer_sk = &issuer_keys.private.nostr_prv; 44 | let contract_id = &issuer_resp.contract_id; 45 | let issuer_contract = get_contract(issuer_sk, contract_id).await?; 46 | let new_alloc = issuer_contract 47 | .allocations 48 | .into_iter() 49 | .find(|x| x.is_mine) 50 | .unwrap(); 51 | let allocs = [new_alloc]; 52 | 53 | // 2. Create PSBT (First Transaction) 54 | let psbt_resp = create_new_psbt_v2( 55 | &issuer_resp.iface, 56 | allocs.to_vec(), 57 | issuer_keys.clone(), 58 | vec![], 59 | vec![format!("{whatever_address}:1000")], 60 | None, 61 | ) 62 | .await?; 63 | 64 | // 3. Sign and Broadcast 65 | let PsbtResponse { psbt, .. } = psbt_resp; 66 | let psbt_req = SignPsbtRequest { 67 | psbt, 68 | descriptors: vec![SecretString( 69 | issuer_keys.private.rgb_assets_descriptor_xprv.clone(), 70 | )], 71 | }; 72 | let psbt_resp = sign_and_publish_psbt_file(psbt_req).await; 73 | assert!(psbt_resp.is_ok()); 74 | 75 | // 4. Check TX 76 | let txid1 = Txid::from_str(&psbt_resp?.txid)?; 77 | let explorer = get_blockchain().await; 78 | let transaction = explorer.get_tx(&txid1).await; 79 | assert!(transaction.is_ok()); 80 | assert!(transaction?.is_some()); 81 | 82 | // 5. Create PSBT (Second Transaction) 83 | let psbt_resp = create_new_psbt_v2( 84 | &issuer_resp.iface, 85 | allocs.to_vec(), 86 | issuer_keys.clone(), 87 | vec![], 88 | vec![format!("{whatever_address}:1000")], 89 | Some(PsbtFeeRequest::Value(2000)), 90 | ) 91 | .await?; 92 | 93 | // 6. Sign and Broadcast 94 | let PsbtResponse { psbt, .. } = psbt_resp; 95 | let psbt_req = SignPsbtRequest { 96 | psbt, 97 | descriptors: vec![SecretString( 98 | issuer_keys.private.rgb_assets_descriptor_xprv.clone(), 99 | )], 100 | }; 101 | let psbt_resp = sign_and_publish_psbt_file(psbt_req).await; 102 | assert!(psbt_resp.is_ok()); 103 | 104 | // 7. Check Both TX 105 | let txid2 = Txid::from_str(&psbt_resp?.txid)?; 106 | let explorer = get_blockchain().await; 107 | let transaction2 = explorer.get_tx(&txid2).await; 108 | assert!(transaction2.is_ok()); 109 | 110 | let transaction1 = explorer.get_tx(&txid1).await; 111 | assert!(transaction1.is_ok()); 112 | 113 | let tx_1 = transaction1?.unwrap(); 114 | let tx_2 = transaction2?.unwrap(); 115 | 116 | // 8. Get Wallet 117 | let secp = Secp256k1::new(); 118 | let db = MemoryDatabase::new(); 119 | let descriptor = issuer_keys 120 | .private 121 | .rgb_assets_descriptor_xprv 122 | .into_wallet_descriptor(&secp, Network::Regtest)?; 123 | let issuer_vault = bdk::Wallet::new(descriptor, None, Network::Regtest, db)?; 124 | issuer_vault.sync(&explorer, SyncOptions::default()).await?; 125 | 126 | let list_transactions = &issuer_vault.list_transactions(false)?; 127 | assert!(!list_transactions.iter().any(|x| x.txid == tx_1.txid())); 128 | assert!(list_transactions.iter().any(|x| x.txid == tx_2.txid())); 129 | 130 | Ok(()) 131 | } 132 | 133 | #[tokio::test] 134 | pub async fn create_bdk_rbf_transaction() -> Result<()> { 135 | // 1. Initial Setup 136 | let user_keys = new_mnemonic(&SecretString("".to_string())).await?; 137 | 138 | let blockchain = get_blockchain().await; 139 | let secp = Secp256k1::new(); 140 | let db = MemoryDatabase::new(); 141 | let descriptor = user_keys 142 | .private 143 | .btc_descriptor_xprv 144 | .into_wallet_descriptor(&secp, Network::Regtest)?; 145 | let user_wallet_data = bdk::Wallet::new(descriptor, None, Network::Regtest, db)?; 146 | 147 | let user_address = user_wallet_data.get_address(AddressIndex::New)?; 148 | send_some_coins(&user_address.address.to_string(), "1").await; 149 | 150 | // 2. Send sats 151 | user_wallet_data 152 | .sync(&blockchain, SyncOptions::default()) 153 | .await?; 154 | let mut builder = user_wallet_data.build_tx(); 155 | 156 | let address = user_wallet_data.get_address(AddressIndex::New)?; 157 | builder.add_recipient(address.address.script_pubkey(), 100000); 158 | 159 | builder.ordering(TxOrdering::Bip69Lexicographic); 160 | builder.fee_rate(bdk::FeeRate::from_sat_per_vb(1.0)); 161 | builder.enable_rbf(); 162 | 163 | let (mut psbt, _) = builder.finish()?; 164 | 165 | let _ = user_wallet_data.sign(&mut psbt, SignOptions::default())?; 166 | 167 | let tx = psbt.extract_tx(); 168 | blockchain.broadcast(&tx).await?; 169 | 170 | user_wallet_data 171 | .sync(&blockchain, SyncOptions::default()) 172 | .await?; 173 | 174 | let txs = user_wallet_data.list_transactions(false)?; 175 | assert_eq!(2, txs.len()); 176 | 177 | let tx_1_utxos: Vec = tx 178 | .input 179 | .clone() 180 | .into_iter() 181 | .map(|u| u.previous_output.to_string()) 182 | .collect(); 183 | 184 | bump_fee( 185 | tx.txid().to_string(), 186 | 5.0, 187 | &SecretString(user_keys.private.btc_descriptor_xprv.to_owned()), 188 | None, 189 | true, 190 | ) 191 | .await?; 192 | 193 | user_wallet_data 194 | .sync(&blockchain, SyncOptions::default()) 195 | .await?; 196 | 197 | let txs = user_wallet_data.list_transactions(false)?; 198 | assert_eq!(2, txs.len()); 199 | 200 | let tx_2_utxos: Vec = tx 201 | .input 202 | .into_iter() 203 | .map(|u| u.previous_output.to_string()) 204 | .collect(); 205 | 206 | // println!("tx 1 utxos: {:#?}", tx_1_utxos); 207 | // println!("tx 2 utxos: {:#?}", tx_2_utxos); 208 | assert_eq!(tx_1_utxos, tx_2_utxos); 209 | 210 | Ok(()) 211 | } 212 | -------------------------------------------------------------------------------- /tests/rgb/integration/states.rs: -------------------------------------------------------------------------------- 1 | #![cfg(not(target_arch = "wasm32"))] 2 | use bitmask_core::{ 3 | bitcoin::{new_mnemonic, sign_and_publish_psbt_file}, 4 | rgb::{accept_transfer, create_watcher, get_contract, structs::ContractAmount}, 5 | structs::{AcceptRequest, SecretString, SignPsbtRequest, WatcherRequest}, 6 | }; 7 | 8 | use crate::rgb::integration::utils::{ 9 | create_new_invoice, create_new_psbt, create_new_transfer, get_uda_data, import_new_contract, 10 | issuer_issue_contract, issuer_issue_contract_v2, send_some_coins, UtxoFilter, 11 | }; 12 | 13 | #[tokio::test] 14 | async fn allow_import_fungible_contract() -> anyhow::Result<()> { 15 | let issuer_resp = issuer_issue_contract("RGB20", 5, false, true, None).await; 16 | assert!(issuer_resp.is_ok()); 17 | 18 | let import_resp = import_new_contract(issuer_resp?).await; 19 | assert!(import_resp.is_ok()); 20 | Ok(()) 21 | } 22 | 23 | #[tokio::test] 24 | async fn allow_import_uda_contract() -> anyhow::Result<()> { 25 | let single = Some(get_uda_data()); 26 | let issuer_resp = issuer_issue_contract("RGB21", 1, false, true, single).await; 27 | assert!(issuer_resp.is_ok()); 28 | 29 | let import_resp = import_new_contract(issuer_resp?).await; 30 | assert!(import_resp.is_ok()); 31 | Ok(()) 32 | } 33 | 34 | #[tokio::test] 35 | async fn check_fungible_allocations() -> anyhow::Result<()> { 36 | // 1. Issue and Generate Transfer (Issuer side) 37 | let whatever_address = "bcrt1p76gtucrxhmn8s5622r859dpnmkj0kgfcel9xy0sz6yj84x6ppz2qk5hpsw"; 38 | let issuer_keys = new_mnemonic(&SecretString("".to_string())).await?; 39 | let owner_keys = new_mnemonic(&SecretString("".to_string())).await?; 40 | 41 | let issuer_sk = issuer_keys.private.nostr_prv.to_string(); 42 | let owner_sk = owner_keys.private.nostr_prv.to_string(); 43 | 44 | let issuer_resp = issuer_issue_contract_v2( 45 | 1, 46 | "RGB20", 47 | ContractAmount::with(5, 0, 2).to_value(), 48 | false, 49 | true, 50 | None, 51 | Some("0.1".to_string()), 52 | Some(UtxoFilter::with_amount_equal_than(10000000)), 53 | Some(issuer_keys.clone()), 54 | ) 55 | .await?; 56 | let issuer_resp = &issuer_resp[0]; 57 | 58 | let owner_resp = &create_new_invoice( 59 | &issuer_resp.contract_id, 60 | &issuer_resp.iface, 61 | ContractAmount::with(1, 0, issuer_resp.precision), 62 | owner_keys.clone(), 63 | None, 64 | Some(issuer_resp.clone().contract.legacy), 65 | ) 66 | .await?; 67 | let psbt_resp = create_new_psbt( 68 | &issuer_resp.contract_id, 69 | &issuer_resp.iface, 70 | vec![issuer_resp.issue_utxo.clone()], 71 | issuer_keys.clone(), 72 | ) 73 | .await?; 74 | let transfer_resp = 75 | &create_new_transfer(issuer_keys.clone(), owner_resp.clone(), psbt_resp).await?; 76 | 77 | // 2. Sign and Publish TX (Issuer side) 78 | let request = SignPsbtRequest { 79 | psbt: transfer_resp.psbt.clone(), 80 | descriptors: [SecretString( 81 | issuer_keys.private.rgb_assets_descriptor_xprv.clone(), 82 | )] 83 | .to_vec(), 84 | }; 85 | let resp = sign_and_publish_psbt_file(request).await; 86 | assert!(resp.is_ok()); 87 | send_some_coins(whatever_address, "0.001").await; 88 | 89 | // 3. Accept Consig (Issuer Side) 90 | let request = AcceptRequest { 91 | consignment: transfer_resp.clone().consig, 92 | force: true, 93 | }; 94 | let resp = accept_transfer(&issuer_sk, request).await; 95 | assert!(resp.is_ok()); 96 | assert!(resp?.valid); 97 | 98 | // 4. Accept Consig (Owner Side) 99 | let request = AcceptRequest { 100 | consignment: transfer_resp.consig.clone(), 101 | force: false, 102 | }; 103 | let resp = accept_transfer(&owner_sk, request).await; 104 | assert!(resp.is_ok()); 105 | assert!(resp?.valid); 106 | 107 | // 5. Retrieve Contract (Issuer Side) 108 | let contract_id = &issuer_resp.contract_id; 109 | let resp = get_contract(&issuer_sk, contract_id).await; 110 | assert!(resp.is_ok()); 111 | assert_eq!(4.0, resp?.balance_normalized); 112 | 113 | // 6. Create Watcher (Owner Side) 114 | let watcher_name = "default"; 115 | let create_watch_req = WatcherRequest { 116 | name: watcher_name.to_string(), 117 | xpub: owner_keys.clone().public.watcher_xpub.clone(), 118 | force: true, 119 | }; 120 | create_watcher(&owner_sk, create_watch_req).await?; 121 | 122 | // 7. Retrieve Contract (Owner Side) 123 | let contract_id = &issuer_resp.contract_id; 124 | let resp = get_contract(&owner_sk, contract_id).await; 125 | assert!(resp.is_ok()); 126 | assert_eq!(1., resp?.balance_normalized); 127 | 128 | Ok(()) 129 | } 130 | 131 | #[tokio::test] 132 | async fn check_uda_allocations() -> anyhow::Result<()> { 133 | // 1. Issue and Generate Transfer (Issuer side) 134 | let whatever_address = "bcrt1p76gtucrxhmn8s5622r859dpnmkj0kgfcel9xy0sz6yj84x6ppz2qk5hpsw"; 135 | let issuer_keys = new_mnemonic(&SecretString("".to_string())).await?; 136 | let owner_keys = new_mnemonic(&SecretString("".to_string())).await?; 137 | 138 | let meta = Some(get_uda_data()); 139 | let issuer_resp = issuer_issue_contract_v2( 140 | 1, 141 | "RGB21", 142 | ContractAmount::with(1, 0, 0).to_value(), 143 | false, 144 | true, 145 | meta, 146 | Some("0.1".to_string()), 147 | Some(UtxoFilter::with_amount_equal_than(10000000)), 148 | Some(issuer_keys.clone()), 149 | ) 150 | .await?; 151 | let issuer_resp = issuer_resp[0].clone(); 152 | let owner_resp = &create_new_invoice( 153 | &issuer_resp.contract_id, 154 | &issuer_resp.iface, 155 | ContractAmount::with(1, 0, issuer_resp.precision), 156 | owner_keys.clone(), 157 | None, 158 | Some(issuer_resp.clone().contract.legacy), 159 | ) 160 | .await?; 161 | let psbt_resp = create_new_psbt( 162 | &issuer_resp.contract_id, 163 | &issuer_resp.iface, 164 | vec![issuer_resp.issue_utxo.clone()], 165 | issuer_keys.clone(), 166 | ) 167 | .await?; 168 | let transfer_resp = 169 | &create_new_transfer(issuer_keys.clone(), owner_resp.clone(), psbt_resp).await?; 170 | 171 | // 2. Sign and Publish TX (Issuer side) 172 | let issuer_sk = issuer_keys.private.nostr_prv.to_string(); 173 | let owner_sk = owner_keys.clone().private.nostr_prv.to_string(); 174 | let request = SignPsbtRequest { 175 | psbt: transfer_resp.psbt.clone(), 176 | descriptors: [SecretString( 177 | issuer_keys.private.rgb_udas_descriptor_xprv.clone(), 178 | )] 179 | .to_vec(), 180 | }; 181 | let resp = sign_and_publish_psbt_file(request).await; 182 | assert!(resp.is_ok()); 183 | send_some_coins(whatever_address, "0.001").await; 184 | 185 | // 3. Accept Consig (Issuer Side) 186 | let request = AcceptRequest { 187 | consignment: transfer_resp.consig.clone(), 188 | force: false, 189 | }; 190 | let resp = accept_transfer(&issuer_sk, request).await; 191 | assert!(resp.is_ok()); 192 | assert!(resp?.valid); 193 | 194 | // 4. Accept Consig (Owner Side) 195 | let request = AcceptRequest { 196 | consignment: transfer_resp.consig.clone(), 197 | force: false, 198 | }; 199 | let resp = accept_transfer(&owner_sk, request).await; 200 | assert!(resp.is_ok()); 201 | assert!(resp?.valid); 202 | 203 | // 5. Retrieve Contract (Issuer Side) 204 | let contract_id = &issuer_resp.contract_id; 205 | let resp = get_contract(&issuer_sk, contract_id).await; 206 | assert!(resp.is_ok()); 207 | assert_eq!(0.0, resp?.balance_normalized); 208 | 209 | // 6. Create Watcher (Owner Side) 210 | let watcher_name = "default"; 211 | let create_watch_req = WatcherRequest { 212 | name: watcher_name.to_string(), 213 | xpub: owner_keys.clone().public.watcher_xpub.clone(), 214 | force: true, 215 | }; 216 | create_watcher(&owner_sk, create_watch_req).await?; 217 | 218 | // 7. Retrieve Contract (Owner Side) 219 | let contract_id = &issuer_resp.contract_id; 220 | let resp = get_contract(&owner_sk, contract_id).await; 221 | assert!(resp.is_ok()); 222 | assert_eq!(1., resp?.balance_normalized); 223 | 224 | Ok(()) 225 | } 226 | -------------------------------------------------------------------------------- /tests/migration.rs: -------------------------------------------------------------------------------- 1 | #![cfg(not(target_arch = "wasm32"))] 2 | 3 | use anyhow::Result; 4 | use bitmask_core::{ 5 | bitcoin::{decrypt_wallet, upgrade_wallet}, 6 | constants::switch_network, 7 | structs::SecretString, 8 | util::init_logging, 9 | }; 10 | use log::{debug, info}; 11 | 12 | const ENCRYPTION_PASSWORD: &str = "asdfasdf"; 13 | const SEED_PASSWORD: &str = ""; 14 | const ENCRYPTED_DESCRIPTOR_04: &str = "d80b2c6af514802c5e7b7a91e8c84f93edbe705f0849cf57abf5bfd465a0c4b2792e8fb16a1b76d7f65b8d68a65c3c001565318ddcd9905536391ca0abb68789da28bb3ccbc923760b36474876070fff4c67e2c23c79f9d5272513fa17dfa4ea47101c7fb0487a678eb40b37ccda73805ab821b63feb9abcc7c60bfa7aac2ff076d906fd542400fb81d8a4bf8905932f3db10a252a1cd9661515f996724545c7e732db17899e6210e80af14be0610b0db90143513586bf8670deaa14e05f66b556936c7cc6f82a3363ee7e77f8081205cb6a1a5ad6d627d4dae6c174cd1f2158384daa276a37d8ef6b51b8ae7c8351fa07b606beb2f083e8a2f64f8148e834f326c256cc274adee7e5d05a946e23e9a76c165fbc25ff5618e2936b5222f394066fa071f954ff6ee3446d67375ec5d1caa5d01d7722576ecdf6f67aff833dccb7ade77a1988959cb250a8723a1dff2d3b6b39877f0291c5a34fb2ccc01ba0b6f9bdf6b1d30a3309870ee5d4f828e5d45edc7c5c08ff94ca8fd572e1038ff6975031a5a48733b968cf28eaf90a5895b393ef1230b1632c800f96b95cd0e77e8c971d20311a38da90aa49cf9570be8ef9e97534f4364d1b5840a5214fc534fbe63e76e3d8aeb159cd446fbaabf2502c0efce9a899650344df9fdd499862c92749fd26ea5070a51d3768515d0716f12312e29ca450c231ccf8714e43aa548f7a9a8b883e62b33b290f723bbf1469094ed589a7d0fdb12fc76e6267825924fe616eebb24202c387d0326a9d3dab7245cca9abe28003c7f574e9d3e63854732e5fdc6edda4180384a3424ad6ab1109c6a49acc8eb99af347f0ce72637723a9b3377124324dc9e4ec5a3f3c4673eebb1e7e4d5b7fd7d2568987edb71853fdb1bbd922eff16cf5cac008e43a90ff281ceec9f4213a0d6c2a3df4d579aa1ab20003a4792421cbc7a7822faec1430018861c39380878993f75b6051642af06857f57ba9ad067b9537f19dc35f69fa72935fd4690935168a812e20874f586d63af04b5a4955e1734d2a5d3e7d69b8f9f136a5bff94de0f5a932a89fe00d535145e971510ac16d3ddcd3053d7727a0164d5f560c372d98f13d98e67a108753b5df4abe6bbc5536ee551bbdf28cbf311afa41f2826d338cc8a3f87411e3fa1d178ec21da27b9382b9480cb974aa588f7c7ba09e08fd5428019ac017164e10a2a585ca063c518db1514fe3081f4f393fc06fb6d0d1719d33e85dec3a17a506fdf860ec07dde0bcf3d77d345bd893f0f79cda14d55577a1f6b768ef0bf1e2c69d8c201348f6fa2748cc63bc397aad7afe629122188b0b806546237e60be07063d6f1372d36da554d95e741f6bca9c7ffdc48cdf367e9d5b893fd710e74f24c3f0d8194aa389d0ff9f6f9a5f93cf07c1ce5b67589d99a77aa5fb122c1b88d38f5d6e0d18ed66c9637a73fb9085fc4c43e73e7e0124e383d91407c9ddfa285450ef09889b03e6b1550da033e1edbf0cfcf346c9d1929c07d6853999b7f5eea341dee10547d543463434dacde09926d2db1a5788d70711437e530d93c3bfaf58d5500e4bff89a680951976089832b3e1382cb943cbe7be40c5e0770363d30887d5634681cd903927003334bdd10364c39c0b9457fbc9b7a3f23808971d091e20363934555c449840a763eaa39d6407db0550693b2649517ee0696f10244b3814c37e0473f80d1acf26fbe2a129d35809b22cc6d047612ca9344c1e1c6cbeeab3907fb331b1ad90232b3f6984a90d8e1b3e47397f43ca0c9ab5b1273cdf0c368bc537e31a63b278aa76dc282cdf2550549e694afc45f32be88b436392d2b4637ea81b74447fc6892c9608722845cd8d0518459804fc1eefa624bd12a24b8bef12a5bb857906a213e2d4bcb31b6983b004946798c38ece5e2d7d82c04034deedee709f94210cabc0a8916c9b465fdd49fe708b7c2862474bb43368ae42fd4f8e0e45e28b1d1619069d25d3368e53244943952bfbc36004bf88b8e19e3d"; 15 | const ENCRYPTED_DESCRIPTOR_05: &str = "0b079114f43a0e3b16a9d68e37a316fc3ddd91ebbcb32466c5080d5ba4e29722565f757b97f376fcd995d3152b270d51ac6424622471f3d62cf42a453d2d3165072c35545294f63197fc8ad51457b4631d1966e45021c3f31dc556a9297a511cf1f16ebd02acc9dd94175a6e5fd3dc33325f1342c01774f6c6c2b4e75cd45b5e9b45295bde0214c2d4db1569bcb14784bf1fd1e975c347685b5e8d9df179722c94847d095a813245822b2d800814dd93466b6eb8892a4ebf42b6e1ea37e415a7a579c8f40c36092f348c54f24784fcf74ace9028e47645b09ae89f1a30190baec96e5be0c9a6399151684cb0a76dff72f7a71f1f28341f095a00106d209a47b4c52ec7f033fac235fb7b88b8668fb572c5a7bffa9ced677b92afcd2567170d4068f9b35a9aa47e90a6ee0ee8850d8bda0721b501308336676a0b85acd6a443510aa02ebe7b91a96d481a75edd5bb7dd9a91574b6e51a5d7dc383798ccbef7e71e64cd40a5721e756f3a8a3733db61ec897890261c9cc6942154ac37ebe66e3640f83ec4eb91a0cdf15fdcba70c96c66742af3d7342e2293a043bcc4943732325051f3cf4bfae8ac84e598742ad826f58330283a459f4207f7940f58ac64c10fd0ad5fc7f99199a9cc10862decfe9d16a0cf3071b120d3138ef5fa0f6b04028dc0fc6e95d5caf3428298e36c5a1a5872b2411bad08bbc0ee74427a247c641e89a54f8dcc1c68ad0a0e1ca6f0501847beb5b8f1ae689130e55279eefd34bde2b00fbc2260fc12bda257ff541e93fb55fb779fdab45805f5f5c96d9a643ad01786504cda7afb49896b0fe2ec843a36401d55fec20d517bca89c55008352587613b0aedd1f7f44abfdf09310a7f09d5f68a158d4e3f5c9bb10ea5bbcf0ecaa562b5fa4e7f320c9b601e22342e2c46c9b16de4c409ddef2f1386e23f78b6695075333230588344575478b2e587db4ee8bef0af8d185b66a0b300730b9ce8c69d3b88fd5b83d500776d5b6420276d34b411eb6f5539882a74e5f4d5910f5e01c84a1c69fb70f6607b0e8071b96285d530fefbee0c45c38f4a101ebc9bf45c3b2cca04131a767ecb2fe346b4617af3e4424774906ca249f33226f3156532a57a219a3de5809f2dd7e1dff0e1a1b67a42d717f800eff2c7579049a2eeda09eda55b6706a41ac287d4cd35fb80062a2c63b5ddd5370a1768ba986695188be379aa9ef5d471538d6a301e4d3acf9dc20b56defe5293c0918b78c00b6d76b05f0d5360f5877f296a9ba09a43ad591d2c31d192de0854938f1990e42b5930232e6fb055fcc2a7bd0143b8b70ce7d2eaa52866b52753e4b86c1e0c994c00c6f36dd9aa7702e833c4699f8855f0925a944e2a51a22c7a813195acde571e8275efa1ef253cf54ec2f5dcba04a1faaf79c52d79dd501da1e7df9951d8e297d3669ded78779462e61ab8d8a6100aac12ec8a444e319508e1533a80878e60aea1527b77abeb536d05754a1b47e69bba6133df33b219c6d959d7b97cdab2eb0fb736635c8d7ab57417188ac7733c263b1cb771d93bbb66c1dc3c6b6240a28374704542aae695732de726749c681867a61de12a69a47a7507891f103bbdd33be49c8c30a575aa890c8a1f38896e62512e56d7f8fcd51ec8633cea9d27fe668b7ba12f52b9814bb5bdfceada55de1ad9796b6699f0789b530faec43eb1a2c8f8788cf4cf1ed07a6ba9c73d8da1b43c75b10a82b17cb8be0ec4046d79f804058c20649b76f2a8a3fae1b77fff657916f2aae5e3d6523036b2d62a0cf6d264afc6a29f6d4e7863ee043d67d3df9c1040845ff3ee52a51dc6f475dbb6babc7d634ce0303bf27894ed6a94644040fbe5a65f4e0ef96588c876a3d5577210a56edaa3b1b5a90e70050a56dce7232bfff9eebfc54cbd0511ee5e4a8f4cbbc12a9ecb5ed6d2b1ac843985ca90008695b944e6d721398dce471e1e8f9d76b4cd27c545fd10638505086ff73f9e38a329f3e84cc9f32a67eb3e1dc71cd39366cfa0701a210c4ecab"; 16 | 17 | #[ignore = "No longer necessary due to password breaking change in bitmask-core 0.6"] 18 | #[tokio::test] 19 | async fn migration_v4() -> Result<()> { 20 | init_logging("migration=debug"); 21 | 22 | switch_network("testnet").await?; 23 | 24 | info!("Import bitmask-core 0.4 encrypted descriptor"); 25 | let wallet = decrypt_wallet( 26 | &SecretString(ENCRYPTION_PASSWORD.to_owned()), 27 | &SecretString(ENCRYPTED_DESCRIPTOR_04.to_owned()), 28 | ); 29 | 30 | assert!(wallet.is_err(), "Importing an old descriptor should error"); 31 | 32 | let upgraded_descriptor = upgrade_wallet( 33 | &SecretString(ENCRYPTION_PASSWORD.to_owned()), 34 | &SecretString(ENCRYPTED_DESCRIPTOR_04.to_owned()), 35 | &SecretString(SEED_PASSWORD.to_owned()), 36 | ) 37 | .await?; 38 | 39 | debug!( 40 | "Upgraded descriptor: {}", 41 | serde_json::to_string_pretty(&upgraded_descriptor)? 42 | ); 43 | 44 | let wallet = decrypt_wallet( 45 | &SecretString(ENCRYPTION_PASSWORD.to_owned()), 46 | &upgraded_descriptor, 47 | )?; 48 | 49 | assert_eq!( 50 | wallet.public.xpub, "tpubD6NzVbkrYhZ4Xxrh54Ew5kjkagEfUhS3aCNqRJmUuNfnTXhK4LGXyUzZ5kxgn8f2txjnFtypnoYfRQ9Y8P2nhSNXffxVKutJgxNPxgmwpUR", 51 | "Upgraded wallet should upgrade the descriptor" 52 | ); 53 | 54 | Ok(()) 55 | } 56 | 57 | #[ignore = "No longer necessary due to password breaking change in bitmask-core 0.6"] 58 | #[tokio::test] 59 | async fn migration_v5() -> Result<()> { 60 | init_logging("migration=debug"); 61 | 62 | switch_network("testnet").await?; 63 | 64 | info!("Import bitmask-core 0.5 encrypted descriptor"); 65 | let wallet = decrypt_wallet( 66 | &SecretString(ENCRYPTION_PASSWORD.to_owned()), 67 | &SecretString(ENCRYPTED_DESCRIPTOR_05.to_owned()), 68 | ); 69 | 70 | assert!(wallet.is_err(), "Importing an old descriptor should error"); 71 | 72 | let upgraded_descriptor = upgrade_wallet( 73 | &SecretString(ENCRYPTION_PASSWORD.to_owned()), 74 | &SecretString(ENCRYPTED_DESCRIPTOR_05.to_owned()), 75 | &SecretString(SEED_PASSWORD.to_owned()), 76 | ) 77 | .await?; 78 | 79 | println!( 80 | "Upgraded descriptor: {}", 81 | serde_json::to_string_pretty(&upgraded_descriptor)? 82 | ); 83 | 84 | let wallet = decrypt_wallet( 85 | &SecretString(ENCRYPTION_PASSWORD.to_owned()), 86 | &upgraded_descriptor, 87 | )?; 88 | 89 | assert_eq!( 90 | wallet.public.xpub, "tpubD6NzVbkrYhZ4XJmEMNjxuARFrP5kME8ndqpk9M2QeqtuTv2kTrm87a93Td47bHRRCrSSVvVEu3trvwthVswtPNwK2Kyc9PpudxC1MZrPuNL", 91 | "Upgraded wallet should upgrade the descriptor" 92 | ); 93 | 94 | Ok(()) 95 | } 96 | -------------------------------------------------------------------------------- /tests/rgb/integration/dustless.rs: -------------------------------------------------------------------------------- 1 | #![cfg(not(target_arch = "wasm32"))] 2 | use crate::rgb::integration::utils::{ 3 | create_new_invoice, create_new_psbt_v2, create_new_transfer, issuer_issue_contract_v2, 4 | send_some_coins, UtxoFilter, 5 | }; 6 | use bdk::wallet::AddressIndex; 7 | use bitmask_core::{ 8 | bitcoin::{ 9 | fund_vault, get_new_address, get_wallet, new_mnemonic, sign_and_publish_psbt_file, 10 | sync_wallet, 11 | }, 12 | rgb::{ 13 | accept_transfer, create_watcher, full_transfer_asset, get_contract, structs::ContractAmount, 14 | }, 15 | structs::{ 16 | AcceptRequest, FullRgbTransferRequest, PsbtFeeRequest, PsbtInputRequest, SecretString, 17 | SignPsbtRequest, WatcherRequest, 18 | }, 19 | }; 20 | 21 | #[tokio::test] 22 | async fn create_dustless_transfer_with_fee_value() -> anyhow::Result<()> { 23 | // 1. Initial Setup 24 | let issuer_keys = new_mnemonic(&SecretString("".to_string())).await?; 25 | let owner_keys = new_mnemonic(&SecretString("".to_string())).await?; 26 | 27 | let issuer_resp = issuer_issue_contract_v2( 28 | 1, 29 | "RGB20", 30 | ContractAmount::with(5, 0, 2).to_value(), 31 | false, 32 | true, 33 | None, 34 | Some("0.00001".to_string()), 35 | Some(UtxoFilter::with_amount_equal_than(1000)), 36 | Some(issuer_keys.clone()), 37 | ) 38 | .await?; 39 | let issuer_resp = issuer_resp[0].clone(); 40 | let owner_resp = &create_new_invoice( 41 | &issuer_resp.contract_id, 42 | &issuer_resp.iface, 43 | ContractAmount::with(1, 0, issuer_resp.precision), 44 | owner_keys.clone(), 45 | None, 46 | Some(issuer_resp.clone().contract.strict), 47 | ) 48 | .await?; 49 | 50 | // 2. Get UTXO RGB with insufficient stats 51 | let issuer_sk = &issuer_keys.private.nostr_prv; 52 | let contract_id = &issuer_resp.contract_id; 53 | let issuer_contract = get_contract(issuer_sk, contract_id).await?; 54 | let new_alloc = issuer_contract 55 | .allocations 56 | .into_iter() 57 | .find(|x| x.is_mine) 58 | .unwrap(); 59 | let allocs = [new_alloc]; 60 | 61 | // 3. Get Bitcoin UTXO 62 | let issuer_btc_desc = &issuer_keys.public.btc_descriptor_xpub; 63 | let issuer_vault = get_wallet(&SecretString(issuer_btc_desc.to_string()), None).await?; 64 | let issuer_address = &issuer_vault 65 | .lock() 66 | .await 67 | .get_address(AddressIndex::LastUnused)? 68 | .address 69 | .to_string(); 70 | 71 | send_some_coins(issuer_address, "0.1").await; 72 | sync_wallet(&issuer_vault).await?; 73 | 74 | let btc_utxo = issuer_vault.lock().await.list_unspent()?; 75 | let btc_utxo = btc_utxo.first().unwrap(); 76 | let bitcoin_inputs = [PsbtInputRequest { 77 | descriptor: SecretString(issuer_btc_desc.to_owned()), 78 | utxo: btc_utxo.outpoint.to_string(), 79 | utxo_terminal: "/0/0".to_string(), 80 | ..Default::default() 81 | }]; 82 | 83 | // 2. Create PSBT 84 | let psbt_resp = create_new_psbt_v2( 85 | &issuer_resp.iface, 86 | allocs.to_vec(), 87 | issuer_keys.clone(), 88 | bitcoin_inputs.to_vec(), 89 | vec![], 90 | None, 91 | ) 92 | .await?; 93 | let transfer_resp = 94 | &create_new_transfer(issuer_keys.clone(), owner_resp.clone(), psbt_resp).await?; 95 | 96 | let sk = issuer_keys.private.nostr_prv.to_string(); 97 | let request = SignPsbtRequest { 98 | psbt: transfer_resp.psbt.clone(), 99 | descriptors: [ 100 | SecretString(issuer_keys.private.rgb_assets_descriptor_xprv.clone()), 101 | SecretString(issuer_keys.private.btc_descriptor_xprv.clone()), 102 | ] 103 | .to_vec(), 104 | }; 105 | let resp = sign_and_publish_psbt_file(request).await; 106 | assert!(resp.is_ok()); 107 | 108 | let whatever_address = "bcrt1p76gtucrxhmn8s5622r859dpnmkj0kgfcel9xy0sz6yj84x6ppz2qk5hpsw"; 109 | send_some_coins(whatever_address, "0.1").await; 110 | 111 | let request = AcceptRequest { 112 | consignment: transfer_resp.consig.clone(), 113 | force: false, 114 | }; 115 | 116 | let resp = accept_transfer(&sk, request).await; 117 | assert!(resp.is_ok()); 118 | assert!(resp?.valid); 119 | Ok(()) 120 | } 121 | 122 | #[tokio::test] 123 | async fn create_dustless_transfer_with_fee_rate() -> anyhow::Result<()> { 124 | // 1. Initial Setup 125 | let issuer_keys = new_mnemonic(&SecretString("".to_string())).await?; 126 | let owner_keys = new_mnemonic(&SecretString("".to_string())).await?; 127 | 128 | // Create Watcher 129 | let watcher_name = "default"; 130 | let issuer_sk = &issuer_keys.private.nostr_prv; 131 | let create_watch_req = WatcherRequest { 132 | name: watcher_name.to_string(), 133 | xpub: issuer_keys.public.watcher_xpub.clone(), 134 | force: true, 135 | }; 136 | 137 | create_watcher(issuer_sk, create_watch_req.clone()).await?; 138 | 139 | let btc_address_1 = get_new_address( 140 | &SecretString(issuer_keys.public.btc_descriptor_xpub.clone()), 141 | None, 142 | ) 143 | .await?; 144 | 145 | // Min amount of satoshis 146 | let default_coins = "0.00010000"; 147 | send_some_coins(&btc_address_1, default_coins).await; 148 | 149 | let btc_descriptor_xprv = SecretString(issuer_keys.private.btc_descriptor_xprv.clone()); 150 | let btc_change_descriptor_xprv = 151 | SecretString(issuer_keys.private.btc_change_descriptor_xprv.clone()); 152 | 153 | let assets_address_1 = get_new_address( 154 | &SecretString(issuer_keys.public.rgb_assets_descriptor_xpub.clone()), 155 | None, 156 | ) 157 | .await?; 158 | 159 | let uda_address_1 = get_new_address( 160 | &SecretString(issuer_keys.public.rgb_udas_descriptor_xpub.clone()), 161 | None, 162 | ) 163 | .await?; 164 | 165 | let btc_wallet = get_wallet(&btc_descriptor_xprv, Some(&btc_change_descriptor_xprv)).await?; 166 | sync_wallet(&btc_wallet).await?; 167 | 168 | let fund_vault = fund_vault( 169 | &btc_descriptor_xprv, 170 | &btc_change_descriptor_xprv, 171 | &assets_address_1, 172 | &uda_address_1, 173 | Some(1.1), 174 | ) 175 | .await?; 176 | 177 | let whatever_address = "bcrt1p76gtucrxhmn8s5622r859dpnmkj0kgfcel9xy0sz6yj84x6ppz2qk5hpsw"; 178 | send_some_coins(whatever_address, default_coins).await; 179 | 180 | let issuer_resp = issuer_issue_contract_v2( 181 | 1, 182 | "RGB20", 183 | ContractAmount::with(5, 0, 2).to_value(), 184 | false, 185 | false, 186 | None, 187 | None, 188 | Some(UtxoFilter::with_outpoint( 189 | fund_vault.assets_output.unwrap_or_default(), 190 | )), 191 | Some(issuer_keys.clone()), 192 | ) 193 | .await?; 194 | let issuer_resp = issuer_resp[0].clone(); 195 | let owner_resp = &create_new_invoice( 196 | &issuer_resp.contract_id, 197 | &issuer_resp.iface, 198 | ContractAmount::with(1, 0, issuer_resp.precision), 199 | owner_keys.clone(), 200 | None, 201 | Some(issuer_resp.clone().contract.strict), 202 | ) 203 | .await?; 204 | 205 | let sk = issuer_keys.private.nostr_prv.to_string(); 206 | let request = FullRgbTransferRequest { 207 | contract_id: issuer_resp.contract_id, 208 | iface: issuer_resp.iface, 209 | rgb_invoice: owner_resp.invoice.to_string(), 210 | descriptor: SecretString(issuer_keys.public.rgb_assets_descriptor_xpub.to_string()), 211 | change_terminal: "/20/1".to_string(), 212 | fee: PsbtFeeRequest::FeeRate(1.1), 213 | bitcoin_changes: vec![], 214 | }; 215 | 216 | let transfer_resp = full_transfer_asset(&sk, request).await?; 217 | let request = SignPsbtRequest { 218 | psbt: transfer_resp.psbt.clone(), 219 | descriptors: [ 220 | SecretString(issuer_keys.private.rgb_assets_descriptor_xprv.clone()), 221 | SecretString(issuer_keys.private.btc_descriptor_xprv.clone()), 222 | SecretString(issuer_keys.private.btc_change_descriptor_xprv.clone()), 223 | ] 224 | .to_vec(), 225 | }; 226 | let resp = sign_and_publish_psbt_file(request).await; 227 | assert!(resp.is_ok()); 228 | 229 | let request = AcceptRequest { 230 | consignment: transfer_resp.consig.clone(), 231 | force: false, 232 | }; 233 | 234 | let resp = accept_transfer(&sk, request).await; 235 | assert!(resp.is_ok()); 236 | assert!(resp?.valid); 237 | Ok(()) 238 | } 239 | --------------------------------------------------------------------------------