# Rust Amp Client (amp-rust)
> **⚠️ DEVELOPMENT STATUS**: This package is currently in development and undergoing early integration testing. The API may change and some endpoints are not yet implemented. Don't use in production environments.
A Rust client for the Blockstream AMP API.
## Usage
Add the following to your `Cargo.toml`:
```toml
[dependencies]
amp-rust = "0.0.3"
```
## Examples
For more detailed examples, please refer to the [crate documentation](https://docs.rs/amp-client).
### Running Examples
You can run the included examples using cargo:
```bash
# Show a summary of all assets issued by your credentials
cargo run --example asset_summary
# Display beautiful TUI dashboard for a specific asset (requires ratatui)
cargo run --example asset_display_tui
# View the API changelog
cargo run --example changelog
# Validate a GAID (Global Asset ID)
cargo run --example validate_gaid GAbYScu6jkWUND2jo3L4KJxyvo55d
# Get information about a specific distribution
cargo run --example get_distribution_info asset-uuid-123 distribution-uuid-456
# Create, issue, and authorize a new asset for distribution tests (requires live API)
AMP_TESTS=live cargo run --example create_issue_authorize_asset
# Run end-to-end asset distribution workflow with specific asset and user (requires live API)
cargo run --example end_to_end_distribution_example
```
Make sure to set up your `.env` file with the required credentials first.
### Get a registered user
```rust
use amp_rs::ApiClient;
#[tokio::main]
async fn main() {
let client = ApiClient::new().unwrap();
let users = client.get_registered_users().await.unwrap();
let user_id = users.first().unwrap().id;
let user = client.get_registered_user(user_id).await.unwrap();
println!("{:?}", user);
}
```
### Get an asset
```rust
use amp_rs::ApiClient;
#[tokio::main]
async fn main() {
let client = ApiClient::new().unwrap();
let assets = client.get_assets().await.unwrap();
let asset_uuid = assets.first().unwrap().asset_uuid.clone();
let asset = client.get_asset(&asset_uuid).await.unwrap();
println!("{:?}", asset);
}
```
### Create a category
```rust
use amp_rs::{ApiClient, model::CategoryAdd};
#[tokio::main]
async fn main() {
let client = ApiClient::new().unwrap();
let new_category = CategoryAdd {
name: "Test Category".to_string(),
description: Some("A test category".to_string()),
};
let category = client.add_category(&new_category).await.unwrap();
println!("{:?}", category);
}
```
### Register an asset with the Blockstream Asset Registry
Registering an asset with the Blockstream Asset Registry makes the asset name and ticker visible in user wallets when the asset is issued and distributed. This improves the user experience by displaying human-readable asset information instead of just the asset ID.
```rust
use amp_rs::ApiClient;
#[tokio::main]
async fn main() {
let client = ApiClient::new().unwrap();
let asset_uuid = "your_asset_uuid";
// Register the asset with the public registry
let response = client.register_asset(asset_uuid).await.unwrap();
if response.success {
println!("Asset registered successfully!");
if let Some(message) = response.message {
println!("Message: {}", message);
}
if let Some(asset) = response.asset_data {
println!("Asset name: {}", asset.name);
println!("Asset ticker: {:?}", asset.ticker);
println!("Is registered: {}", asset.is_registered);
}
}
}
```
You can also use the included example:
```bash
cargo run --example register_asset -- <ASSET_UUID>
```
### Manage asset assignments
```rust
use amp_rs::ApiClient;
#[tokio::main]
async fn main() {
let client = ApiClient::new().unwrap();
let asset_uuid = "your_asset_uuid";
let assignment_id = "assignment_id";
// Lock an assignment
let locked_assignment = client
.lock_asset_assignment(asset_uuid, assignment_id)
.await
.unwrap();
println!("Locked assignment: {:?}", locked_assignment);
// Unlock an assignment
let unlocked_assignment = client
.unlock_asset_assignment(asset_uuid, assignment_id)
.await
.unwrap();
println!("Unlocked assignment: {:?}", unlocked_assignment);
// Delete an assignment (destructive operation)
client
.delete_asset_assignment(asset_uuid, assignment_id)
.await
.unwrap();
println!("Assignment deleted");
}
```
### Create and execute asset distributions
```rust
use amp_rs::{ApiClient, ElementsRpc, signer::LwkSoftwareSigner, model::AssetDistributionAssignment};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Setup API client and Elements RPC connection
let client = ApiClient::new().await?;
let elements_rpc = ElementsRpc::from_env()?;
// Create a signer for transaction signing
let (mnemonic, signer) = LwkSoftwareSigner::generate_new_indexed(100)?;
println!("Using signer with mnemonic: {}...", &mnemonic[..50]);
let asset_uuid = "your_asset_uuid";
let wallet_name = "distribution_wallet";
// Define distribution assignments
let assignments = vec![
AssetDistributionAssignment {
user_id: "user123".to_string(),
address: "tlq1qq2xvpcvfup5j8zscjq05u2wxxjcyewk7979f9lq".to_string(),
amount: 0.00000100, // 100 satoshis in BTC units
},
AssetDistributionAssignment {
user_id: "user456".to_string(),
address: "tlq1qq9xvpcvfup5j8zscjq05u2wxxjcyewk7979f9lq".to_string(),
amount: 0.00000050, // 50 satoshis in BTC units
},
];
// Execute the distribution
let result = client.distribute_asset(
asset_uuid,
assignments,
&elements_rpc,
wallet_name,
&signer,
).await?;
println!("Distribution created successfully!");
println!("Distribution UUID: {}", result.distribution_uuid);
println!("Transaction ID: {}", result.txid);
Ok(())
}
```
## Missing Endpoints
The following AMP API endpoints are not yet implemented in this client library. This list may not be exhaustive:
### Asset Operations
- `POST /api/assets/{assetUuid}/reissue-request` - Request asset reissuance
- `POST /api/assets/{assetUuid}/reissue-confirm` - Confirm asset reissuance
- `POST /api/assets/{assetUuid}/burn-request` - Request asset burn
- `POST /api/assets/{assetUuid}/burn-confirm` - Confirm asset burn
- `GET /api/assets/{assetUuid}/reissuances` - Get asset reissuances
- `GET /api/assets/{assetUuid}/txs` - Get asset transactions
- `GET /api/assets/{assetUuid}/txs/{txid}` - Get specific asset transaction
- `GET /api/assets/{assetUuid}/lost-outputs` - Get asset lost outputs
- `POST /api/assets/{assetUuid}/update-blinders` - Update asset blinders
### Manager Operations
- `POST /api/managers/{managerId}/change-password` - Change manager password
These and potentially other endpoints will be added in future releases. If you need any of these endpoints urgently, please open an issue on the project repository.
## Token Management
The AMP client includes sophisticated token management with automatic persistence and refresh capabilities.
### Features
- **Automatic Token Persistence**: Tokens are automatically saved to `token.json` and loaded on subsequent runs
- **Proactive Refresh**: Tokens are automatically refreshed 5 minutes before expiry
- **Thread-Safe Operations**: All token operations are thread-safe and prevent race conditions
- **Retry Logic**: Built-in retry logic with exponential backoff for token operations
- **Secure Storage**: Tokens are stored securely using the `secrecy` crate
### Token Lifecycle
1. **First Run**: Client obtains a new token from the API and persists it to disk
2. **Subsequent Runs**: Client loads the existing token from disk if still valid
3. **Automatic Refresh**: Token is automatically refreshed when it expires soon (within 5 minutes)
4. **Fallback**: If refresh fails, client automatically obtains a new token
### Usage Examples
```rust
use amp_rs::ApiClient;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = ApiClient::new()?;
// Get token (automatically handles persistence and refresh)
let token = client.get_token().await?;
println!("Token: {}...", &token[..20]);
// Get token information
if let Some(info) = client.get_token_info().await? {
println!("Token expires at: {}", info.expires_at);
println!("Token age: {:?}", info.age);
println!("Expires in: {:?}", info.expires_in);
}
// Force refresh token
let refreshed_token = client.force_refresh().await?;
println!("Refreshed token: {}...", &refreshed_token[..20]);
// Clear token (useful for testing)
client.clear_token().await?;
Ok(())
}
```
### Token Persistence Configuration
Token persistence is automatically enabled in the following scenarios:
- When `AMP_TESTS=live` (for live API testing)
- When `AMP_TOKEN_PERSISTENCE=true` is set
- In test environments (`cfg!(test)`)
The token file (`token.json`) contains:
```json
{
"token": "your_jwt_token_here",
"expires_at": "2024-01-01T12:00:00Z",
"obtained_at": "2024-01-01T11:00:00Z"
}
```
## Configuration
### Environment Variables
The client can be configured using the following environment variables:
#### Authentication (Required for live tests)
- `AMP_USERNAME`: Username for AMP API authentication
- `AMP_PASSWORD`: Password for AMP API authentication
- `AMP_API_BASE_URL`: Base URL for the AMP API (default: `https://amp-test.blockstream.com/api`)
#### Retry Configuration (Optional)
- `API_RETRY_MAX_ATTEMPTS`: Maximum number of retry attempts (default: 3)
- `API_RETRY_BASE_DELAY_MS`: Base delay between retries in milliseconds (default: 1000)
- `API_RETRY_MAX_DELAY_MS`: Maximum delay between retries in milliseconds (default: 30000)
- `API_REQUEST_TIMEOUT_SECONDS`: Request timeout in seconds (default: 10)
#### Test Configuration
- `AMP_TESTS`: Set to `live` to run tests against the actual API
#### Token Persistence (Optional)
- `AMP_TOKEN_PERSISTENCE`: Set to `true` to enable token persistence to disk (default: enabled for live tests)
### Example Configuration
```bash
# Authentication
export AMP_USERNAME=your_username
export AMP_PASSWORD=your_password
export AMP_API_BASE_URL=https://amp-test.blockstream.com/api
# Retry configuration (optional)
export API_RETRY_MAX_ATTEMPTS=5
export API_RETRY_BASE_DELAY_MS=2000
export API_RETRY_MAX_DELAY_MS=60000
export API_REQUEST_TIMEOUT_SECONDS=30
# Enable live tests
export AMP_TESTS=live
# Enable token persistence (optional)
export AMP_TOKEN_PERSISTENCE=true
```
## Signer Setup and Usage
The AMP client includes a comprehensive signer implementation for handling asset operations like distribution, reissuance, and burning. The `LwkSoftwareSigner` provides testnet-focused transaction signing using Blockstream's Liquid Wallet Kit (LWK).
### ⚠️ Security Warning
**TESTNET/REGTEST ONLY**: The `LwkSoftwareSigner` is designed exclusively for testnet and regtest environments. It stores mnemonic phrases in plain text and should NEVER be used in production or with real funds.
### Basic Signer Setup
#### Creating a Signer from Existing Mnemonic
```rust
use amp_rs::signer::{LwkSoftwareSigner, Signer};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create signer from existing mnemonic
let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
let signer = LwkSoftwareSigner::new(mnemonic)?;
// Verify testnet configuration
assert!(signer.is_testnet());
println!("Signer ready for testnet operations");
Ok(())
}
```
#### Generating a New Signer
```rust
use amp_rs::signer::LwkSoftwareSigner;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Generate new signer with automatic mnemonic management
let (mnemonic, signer) = LwkSoftwareSigner::generate_new()?;
println!("Generated mnemonic: {}...", &mnemonic[..50]);
println!("Mnemonic saved to mnemonic.local.json");
// Signer is ready for use
assert!(signer.is_testnet());
Ok(())
}
```
#### Indexed Mnemonic Access for Testing
For test isolation and consistent test environments, use indexed mnemonic access:
```rust
use amp_rs::signer::LwkSoftwareSigner;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Generate signers with specific indices for different test roles
let (_, issuer_signer) = LwkSoftwareSigner::generate_new_indexed(100)?;
let (_, distributor_signer) = LwkSoftwareSigner::generate_new_indexed(101)?;
let (_, user_signer) = LwkSoftwareSigner::generate_new_indexed(102)?;
// Each signer uses a different mnemonic for test isolation
println!("Created role-based signers for testing");
Ok(())
}
```
### Generating Addresses for Asset Issuance
Before issuing assets, you need to generate addresses that can receive the issued assets:
```rust
use amp_rs::signer::LwkSoftwareSigner;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create or load signer
let (mnemonic, signer) = LwkSoftwareSigner::generate_new()?;
// Generate a receiving address for asset issuance
let treasury_address = signer.derive_address(0, 0)?; // First receiving address
println!("Treasury address: {}", treasury_address);
// This address can be used as the treasury address for asset operations
// and should be added to your asset's treasury addresses via the API
Ok(())
}
```
### Using Signer with Asset Distribution
The signer integrates seamlessly with the `distribute_asset` method and will be essential for future burn and reissuance operations:
```rust
use amp_rs::{ApiClient, ElementsRpc, signer::LwkSoftwareSigner, model::AssetDistributionAssignment};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Setup API client and Elements RPC
let api_client = ApiClient::new().await?;
let elements_rpc = ElementsRpc::from_env()?;
// Create signer for signing transactions
let (mnemonic, signer) = LwkSoftwareSigner::generate_new_indexed(300)?;
println!("Using signer with mnemonic: {}...", &mnemonic[..50]);
// Setup wallet and distribution assignments
let wallet_name = "amp_distribution_wallet".to_string();
let asset_uuid = "your-asset-uuid";
let assignments = vec![AssetDistributionAssignment {
user_id: "user123".to_string(),
address: "tlq1qq...".to_string(), // User's receiving address
amount: 0.00000001, // Amount in BTC units
}];
// Execute distribution with signer
api_client.distribute_asset(
asset_uuid,
assignments,
&elements_rpc,
&wallet_name,
&signer, // Signer handles transaction signing
).await?;
println!("Asset distribution completed successfully");
Ok(())
}
```
### Wallet Integration
For Elements wallet integration, you can generate descriptors from the signer:
```rust
use amp_rs::signer::LwkSoftwareSigner;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let (_, signer) = LwkSoftwareSigner::generate_new()?;
// Generate descriptor for wallet import
let descriptor = signer.get_wpkh_slip77_descriptor()?;
println!("Descriptor for wallet import: {}", descriptor);
// This descriptor can be imported into Elements using importdescriptors RPC
// to enable the wallet to recognize addresses and UTXOs from this signer
Ok(())
}
```
### Future Operations
The same signer setup and passing pattern will be used for upcoming operations:
- **Asset Reissuance**: `reissue_asset(asset_uuid, amount, &signer)`
- **Asset Burning**: `burn_asset(asset_uuid, amount, &signer)`
- **Advanced Distribution**: Enhanced distribution workflows with complex signing requirements
The signer abstraction ensures consistent transaction signing across all asset operations while maintaining security best practices for testnet development.
## Testing
To run the tests, you will need to set the `AMP_USERNAME` and `AMP_PASSWORD` environment variables.
```
AMP_USERNAME=... AMP_PASSWORD=... cargo test
```
To run the live tests, you will also need to set the `AMP_TESTS` environment variable to `live`.
```
AMP_USERNAME=... AMP_PASSWORD=... AMP_TESTS=live cargo test
```
Some tests that perform state-changing operations are ignored by default. To run them, use the `--ignored` flag.
```
AMP_USERNAME=... AMP_PASSWORD=... AMP_TESTS=live cargo test -- --ignored
```