diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..d5ab96c --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,54 @@ +# Changelog + +All notable changes to `bitsafe-token` are recorded here. The format follows +[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project +follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [0.1.0] - 2026-10-06 + +The first release. `bitsafe-token` replaces `cbtc-lib` and adds BETH. + +### Added + +- `tokens::cbtc` and `tokens::beth`: the mint and burn flows of both assets, + with one API shape. Each module maps a `Network` to its own registrar. +- `tokens::beth::mint::deposit_call`: builds the `depositETH` transaction + for a deposit account. It checks the network, the account id, the amount, + the wei unit and the account limits. It sends nothing. +- `check_dars`: compares one asset's DAR set under `dars/` with a + participant. +- `dars/`: the DAR packages for CBTC and BETH, and `dars/upload_dars.sh + `, which uploads them and stops at the first failed upload. +- Examples for both assets, a localnet integration suite and a contract test + of the live BitSafe API. + +### Changed from `cbtc-lib` + +- The crate is `bitsafe-token` (`bitsafe_token` in code), not `cbtc`. +- The CBTC flows move from `mint_redeem` to `tokens::cbtc::{mint, redeem}`, + and `CBTC_TICKER` is now `tokens::cbtc::TICKER`. +- The DAR folder `cbtc-dars/` is now `dars/`. `upload_dars.sh` takes the + asset as its argument, and reads `jwt_token` and `canton_admin_api_url` + from the environment. +- The crate uses `canton-lib` 0.10.0 instead of 0.9.0. Its active-contract + queries and update streams send `eventFormat` and `updateFormat`, which + Canton 3.6.1 requires. The `ledger` re-export no longer has + `submit::wait_for_transaction_tree` or `common::TransactionFilter`. +- The dependency DAR set changed: `splice-amulet` 0.1.16 instead of 0.1.17, + `splice-util` 0.1.4 instead of 0.1.5, and newer Canton Network Utility + versions. +- The `check_dars` example reads `ASSET`, as every Token Standard example + does. +- The `cbtc_redeem_flow` example requires `DESTINATION_BTC_ADDRESS`, and + reuses only a withdraw account with that destination. + +### Removed from `cbtc-lib` + +- The template-id constants of `mint_redeem::constants`, such as + `DEPOSIT_ACCOUNT_TEMPLATE_ID`. +- `mint_redeem::attestor::get_token_standard_contracts`. +- The deprecated `issuer_credential` of the BitSafe API. The burn sends an + empty `issuer-credentials` list. +- `cbtc-tui`, the terminal UI. diff --git a/Cargo.toml b/Cargo.toml index 4eebed7..227321c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ name = "bitsafe-token" version = "0.1.0" edition = "2024" description = "Client library for BitSafe bridged assets on Canton: CBTC, BETH" -license = "Apache-2.0" +license = "MIT" repository = "https://github.com/DLC-link/bitsafe-token-lib" publish = false autoexamples = false diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..37fdffd --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Bitsafe + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 805a91f..2df3766 100644 --- a/README.md +++ b/README.md @@ -1,35 +1,288 @@ # bitsafe-token -Rust client library for BitSafe bridged assets on Canton Network. It serves -CBTC and BETH with one API shape, and re-exports the Canton Token Standard -operations of [canton-lib](https://github.com/DLC-link/canton-lib) under the -same names [cbtc-lib](https://github.com/DLC-link/cbtc-lib) uses. +A Rust client library for BitSafe's bridged assets on Canton Network: CBTC +(Bitcoin) and BETH (Ether). One API shape serves both assets. The library +also re-exports the Canton Token Standard operations of +[canton-lib](https://github.com/DLC-link/canton-lib), so you can hold, send +and receive both assets. -Status: under construction. CBTC is under `tokens::cbtc`, and BETH is under `tokens::beth`. +**Status: 0.1.0.** The API can change in any 0.x release. The crate is not on +crates.io; you add it as a git dependency. It needs Rust 1.94 or newer +(edition 2024). + +## Install + +Add the crate to your `Cargo.toml`: + +```toml +[dependencies] +bitsafe-token = { git = "https://github.com/DLC-link/bitsafe-token-lib", tag = "v0.1.0" } +# Login. Or use your own OpenID Connect client and pass its access token. +keycloak = { git = "https://github.com/DLC-link/canton-lib", tag = "v0.10.0" } +tokio = { version = "1", features = ["macros", "rt-multi-thread"] } +``` + +If you add other `canton-lib` crates, pin the same tag, `v0.10.0`. Two +different pins make Cargo build two copies of the shared types, and then the +types do not match. + +## Before you start + +You need these, for the network you use: + +1. **A Canton participant node** with the JSON ledger API, such as the + participant of your Canton Network validator. +2. **A party** on that participant. A party is your identity on the ledger, + such as `your-party::1220...`. +3. **A Keycloak (or other OpenID Connect) user** that can act as your party. +4. **The Digital Asset Registry Utility** on your validator, for holdings and + transfers. +5. **The DAR packages** on your participant. See + [`dars/README.md`](dars/README.md); `check_dars` tells you what is + missing. +6. **A Minter credential,** to mint or burn only. The asset's registrar + offers it to your party; ask BitSafe for it. You accept the offer once. + Holdings and transfers need no credential. + +## Concepts + +| Term | Meaning | +|---|---| +| Registrar | The party that issues an asset on one network. Each asset module knows its registrar for each network | +| Minter credential | A credential from the registrar with the claim `hasCBTCRole` or `hasBETHRole` = `Minter`. The mint and burn calls need it | +| Deposit account | A contract that ties your party to a deposit: a BTC address for CBTC, a deposit id for BETH | +| Withdraw account | A contract that holds your payout address and your pending (burned) balance | +| Withdraw request | The payout record. The attestors create it after a burn and archive it after the payout | +| Holding | One unit of an asset that your party owns, like a UTXO | + +## Quick start: CBTC + +Mint: create a deposit account and get the BTC address to fund. Burn: burn +CBTC into a withdraw account. The attestors then pay out BTC. + +```rust +use bitsafe_token::{ + DamlDecimal, Network, + credentials::{ListCredentialsParams, list_credentials}, + tokens::cbtc::{self, mint, redeem}, +}; +use keycloak::login::{PasswordParams, password, token_url}; + +#[tokio::main] +async fn main() -> Result<(), String> { + let network = Network::Devnet; + let ledger_host = "https://participant.example.com/api/json-api".to_string(); + let party = "your-party::1220...".to_string(); + + // Log in as a user that can act as `party`. + let access_token = password(PasswordParams { + client_id: "your-client-id".into(), + username: "your-username".into(), + password: "your-password".into(), + url: token_url("https://keycloak.example.com", "your-realm"), + }) + .await? + .access_token; + + // The registrar's Minter credential, which you accepted once. + let credentials = list_credentials(ListCredentialsParams { + ledger_host: ledger_host.clone(), + party: party.clone(), + access_token: access_token.clone(), + }) + .await?; + let credential_cids = cbtc::minter_credential_cids(network, &credentials); + + // Mint: create a deposit account and get the BTC address to fund. + let rules = mint::get_account_contract_rules(network).await?; + let account = mint::create_deposit_account(mint::CreateDepositAccountParams { + ledger_host: ledger_host.clone(), + party: party.clone(), + access_token: access_token.clone(), + account_rules: rules.clone(), + credential_cids: credential_cids.clone(), + }) + .await?; + let btc_address = mint::get_bitcoin_address(network, &account).await?; + println!("Send BTC to {btc_address}"); + + // Burn: create a withdraw account, then burn 0.001 CBTC into it. + let withdraw_account = redeem::create_withdraw_account(redeem::CreateWithdrawAccountParams { + ledger_host: ledger_host.clone(), + party: party.clone(), + access_token: access_token.clone(), + account_rules: rules, + destination_address: "your-btc-address".into(), + credential_cids: credential_cids.clone(), + }) + .await?; + let holdings = redeem::list_holdings(redeem::ListHoldingsParams { + ledger_host: ledger_host.clone(), + party: party.clone(), + access_token: access_token.clone(), + instrument_id: cbtc::instrument(network), + }) + .await?; + let withdraw_account = redeem::submit_withdraw( + network, + redeem::SubmitWithdrawParams { + ledger_host: ledger_host.clone(), + party: party.clone(), + access_token: access_token.clone(), + account: &withdraw_account, + amount: DamlDecimal::parse("0.001").map_err(|e| e.to_string())?, + holdings: &holdings, + credential_cids, + }, + ) + .await?; + println!("Pending: {}", withdraw_account.pending_balance); + + // Later: the attestors create a withdraw request and pay out. + let requests = redeem::list_withdraw_requests(redeem::ListWithdrawRequestsParams { + ledger_host, + party, + access_token, + }) + .await?; + println!("{} withdraw request(s)", requests.len()); + Ok(()) +} +``` + +## Quick start: BETH + +BETH uses the same calls under `tokens::beth`. Only the deposit step +differs: `deposit_call` builds the Ethereum transaction that mints BETH. The +library holds no keys and sends nothing. You sign and send the transaction +with your own wallet, on the chain in `call.chain_id`. + +```rust +use bitsafe_token::{ + Network, + tokens::beth::mint::{self, U256}, +}; + +// Log in and find the Minter credential as in the CBTC example, with +// `beth::minter_credential_cids`. +async fn mint_beth( + network: Network, + ledger_host: String, + party: String, + access_token: String, + credential_cids: Vec, +) -> Result<(), String> { + let rules = mint::get_account_contract_rules(network).await?; + let account = mint::create_deposit_account(mint::CreateDepositAccountParams { + ledger_host, + party, + access_token, + account_rules: rules, + credential_cids, + }) + .await?; + + // 0.01 ETH, in wei. + let call = mint::deposit_call(network, &account, U256::from(10_000_000_000_000_000_u64))?; + println!("chain {} to {} value {} data {}", call.chain_id, call.to, call.value, call.data); + Ok(()) +} +``` + +**Units.** The two directions use different units: + +- `deposit_call` takes **wei** as a `U256`. 1 ETH is 10^18 wei, and the + amount must be a whole multiple of 100000000 wei. +- `redeem::submit_withdraw` takes **ETH** as a `DamlDecimal`, such as `0.01`, + the same as CBTC takes BTC. +- `beth::mint::wei_to_daml_decimal` and `daml_decimal_to_wei` convert. + +**Chains.** Devnet and testnet BETH run on Sepolia (chain 11155111), so you +need Sepolia ETH. Mainnet BETH runs on Ethereum mainnet (chain 1). The +library does not read the bridge's live `depositLimits()` or `paused()`; +check both with your Ethereum provider before you send. + +## Hold and send + +The re-exported `TokenClient` reads balances and sends either asset. The +asset module gives it the right configuration for each network: + +```rust +use bitsafe_token::{ + DamlDecimal, KeycloakConfig, Network, SendParams, TokenClient, TokenStandardVersion, + tokens::cbtc, +}; +use keycloak::login::token_url; + +#[tokio::main] +async fn main() -> Result<(), String> { + let mut client = TokenClient::connect(cbtc::client_config( + Network::Mainnet, + "https://participant.example.com/api/json-api".into(), + "your-party::1220...".into(), + KeycloakConfig { + client_id: "your-client-id".into(), + username: "your-username".into(), + password: "your-password".into(), + url: token_url("https://keycloak.example.com", "your-realm"), + }, + TokenStandardVersion::V2, + )) + .await?; + + println!("balance {} CBTC", client.balance().await?); + client + .send(SendParams { + receiver: "receiver-party::1220...".into(), + amount: DamlDecimal::parse("0.01").map_err(|e| e.to_string())?, + reference: None, + execute_before: None, + input_holding_cids: None, + }) + .await?; + Ok(()) +} +``` + +On mainnet this moves real funds. Use `token_url` for the Keycloak URL; the +older `password_url` builds a path that current Keycloak servers do not +serve. The Token Standard has two entry points: V1 names a party as a string, +and V2 names an `Account`. Both move the same holdings, and the examples use +V2 where both exist. ## Networks -`Network` is a plain enum: `Devnet`, `Testnet`, `Mainnet`. It carries the -values that do not depend on the asset. Each asset module maps a `Network` -to its own registrar party. +`Network` is a plain enum: `Devnet`, `Testnet`, `Mainnet`. Each asset module +maps a `Network` to its own registrar, so you pass only the network. + +| Network | Utility registry | BitSafe API | BETH chain | +| --- | --- | --- | --- | +| devnet | https://api.utilities.digitalasset-dev.com | https://api.devnet.bitsafe.finance | Sepolia, 11155111 | +| testnet | https://api.utilities.digitalasset-staging.com | https://api.testnet.bitsafe.finance | Sepolia, 11155111 | +| mainnet | https://api.utilities.digitalasset.com | https://api.mainnet.bitsafe.finance | Ethereum, 1 | + +## What the library does not do -| Network | Utility registry | BitSafe API | -| --- | --- | --- | -| devnet | https://api.utilities.digitalasset-dev.com | https://api.devnet.bitsafe.finance | -| testnet | https://api.utilities.digitalasset-staging.com | https://api.testnet.bitsafe.finance | -| mainnet | https://api.utilities.digitalasset.com | https://api.mainnet.bitsafe.finance | +- **It returns errors as text.** Every function returns `Result<_, String>`. +- **It does not wait for a mint.** Poll your holdings to see the minted + amount. +- **It lists only active withdraw requests.** The attestors archive a request + after the payout, so a paid request no longer appears. +- **It does not check the network of a BTC address.** Use an address of the + Bitcoin network that your Canton network pays out on. +- **It reads no Ethereum state.** It reads no bridge limits, no pause state + and no deposit events. -## Layout +## Examples -- `tokens::`: the whole asset-facing API. One module per asset. -- `kits`, `flows`: crate-private building blocks. They never appear in a - public path. -- The crate root: the `canton-lib` re-exports, `Network`, `AssetInfo`, - `check_dars`. -- `dars/`: the DAR packages a participant needs for each asset, and the - script that uploads them. See [`dars/README.md`](dars/README.md). +[`examples/README.md`](examples/README.md) lists 30 runnable examples, for +both assets: credentials, mint, burn, transfers, allocations and batch +distribution. Copy [`.env.example`](.env.example) to `.env`, fill it in, and +run `cargo run --example `. -## Development +## Contributing + +Run the gates before you open a pull request: ```sh cargo fmt -- --check @@ -37,32 +290,23 @@ cargo clippy --all-targets --all-features -- -D warnings cargo test ``` -The dependency rule: `tokens` may use `flows` and `kits`, `flows` may use -`kits`, and nothing imports upward. Review checks it. - -## Localnet suite +`tokens::` is the whole public, asset-facing API. `kits` and `flows` +are crate-private building blocks. `tokens` may use `flows` and `kits`, +`flows` may use `kits`, and nothing imports upward; review checks this. -The localnet suite checks that a local Canton sandbox holds each asset's DAR -packages. It then runs the CBTC and then the BETH credentials, mint and -redeem flows against the sandbox. It needs Docker. Two commands start the -sandbox and run the suite: +The localnet suite runs the CBTC and BETH flows against a local Canton +sandbox. It needs Docker: ```sh docker compose -f localnet/docker-compose.yml up -d --wait cargo test --lib localnet -- --ignored --nocapture --test-threads=1 -``` - -The first pull of the sandbox image takes several minutes. The sandbox then -needs a few more minutes to bootstrap. Each run allocates fresh parties, so a -rerun against a running sandbox works. The suite uses the JSON ledger API at -`http://localhost:7575`; set `LOCALNET_LEDGER_HOST` to use another one. - -One command stops the sandbox and deletes its data: - -```sh docker compose -f localnet/docker-compose.yml down -v ``` +The first start pulls the image and bootstraps the sandbox, which takes +several minutes. Set `LOCALNET_LEDGER_HOST` to use a ledger other than +`http://localhost:7575`. + ## License -Apache-2.0 +MIT. See [`LICENSE`](LICENSE). diff --git a/src/flows/canton_bridge_v1/bind.rs b/src/flows/canton_bridge_v1/bind.rs index d8f7f36..d72c974 100644 --- a/src/flows/canton_bridge_v1/bind.rs +++ b/src/flows/canton_bridge_v1/bind.rs @@ -118,6 +118,8 @@ macro_rules! bind_canton_bridge_v1 { /// Burns the holdings into the withdraw account, after the /// local checks. Returns the account with its new pending balance. + /// `params.amount` is a Daml decimal in the asset's own unit, + /// BTC or ETH, not satoshi or wei. pub async fn submit_withdraw( network: Network, params: SubmitWithdrawParams<'_>, diff --git a/src/flows/canton_bridge_v1/withdraw.rs b/src/flows/canton_bridge_v1/withdraw.rs index e7d1ea1..c0f4363 100644 --- a/src/flows/canton_bridge_v1/withdraw.rs +++ b/src/flows/canton_bridge_v1/withdraw.rs @@ -51,6 +51,9 @@ pub struct SubmitWithdrawParams<'a, A> { pub access_token: String, /// The withdraw account to burn into. Its pending balance must be zero. pub account: &'a WithdrawAccount, + /// The amount to burn, as a Daml decimal in the asset's own unit: BTC + /// for CBTC and ETH for BETH, such as `0.01`. It is not in satoshi or + /// wei. For BETH, `beth::mint::wei_to_daml_decimal` converts wei. pub amount: DamlDecimal, /// The holdings to burn, from `list_holdings`. Their sum must cover `amount`. pub holdings: &'a [Holding], diff --git a/src/tokens/beth/mod.rs b/src/tokens/beth/mod.rs index 1b6cb58..872f0e2 100644 --- a/src/tokens/beth/mod.rs +++ b/src/tokens/beth/mod.rs @@ -211,6 +211,13 @@ pub mod mint { /// every keystroke. It does not read the bridge's live /// `depositLimits()` or `paused()`; check both with your provider. /// + /// `amount_wei` is in wei, as a `U256`: 1 ETH is 10^18 wei, and the + /// amount must be a whole multiple of [`DEPOSIT_UNIT_WEI`]. The burn + /// takes the other unit: `redeem::submit_withdraw` reads its amount as + /// a [`DamlDecimal`](crate::DamlDecimal) in ETH, such as `0.01`. + /// [`wei_to_daml_decimal`] and [`daml_decimal_to_wei`] convert between + /// the two. + /// /// ``` /// use bitsafe_token::{Network, tokens::beth::mint::{self, DepositAccount, DepositCall, U256}}; ///