Wallet API
The Wallet API exposes Hydra App's WalletService — deposit address management, on-chain and off-chain balances, and transaction history per network.
JSON-RPC namespace: wallet
Endpoints
- Get Deposit Address
- Get Unique Deposit Address
- Release Deposit Address
- Get Balances
- Get Balance
- Get Transactions
- Get Transaction
Get Deposit Address
Returns a deposit address for the specified network. The address can be shared with others to receive on-chain funds. Each call may return the same or a new address depending on the protocol.
Method: GetDepositAddress
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
network | Network | YES | Target network |
Response:
| Field | Type | Description |
|---|---|---|
address | string | Deposit address for this wallet |
Example Request:
import { WalletServiceClient } from './proto/WalletServiceClientPb'
import { GetDepositAddressRequest } from './proto/wallet_pb'
const client = new WalletServiceClient('http://localhost:5001')
const request = new GetDepositAddressRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
const response = await client.getDepositAddress(request, {})
console.log('Deposit address:', response.getAddress())
Example Response:
{ "address": "bc1qxyz..." }
Get Unique Deposit Address
Returns a unique, never-before-used deposit address. Only supported on UTXO-based protocols (e.g., Bitcoin). Each call returns a fresh address from the HD derivation chain. If there are previously released addresses in the free pool, the lowest available index is reused; otherwise a new derivation index is revealed.
Method: GetUniqueDepositAddress
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
network | Network | YES | Target network (must be UTXO-based) |
Response:
| Field | Type | Description |
|---|---|---|
address | string | A fresh address guaranteed to be unique |
Example Request:
import { GetUniqueDepositAddressRequest } from './proto/wallet_pb'
const request = new GetUniqueDepositAddressRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
const response = await client.getUniqueDepositAddress(request, {})
console.log('Fresh address:', response.getAddress())
Example Response:
{ "address": "bc1qfreshaddress..." }
Release Deposit Address
Releases a previously acquired unique deposit address back to the pool. Must only be called when the associated invoice has expired without receiving any on-chain payment. The server verifies that no funds have been received at the address before reinserting it into the free pool.
Returns an error if the address has received funds or was not previously acquired via
GetUniqueDepositAddress.
Method: ReleaseDepositAddress
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
network | Network | YES | Target network |
address | string | YES | The address to release back to the free pool |
Response: Empty.
Example Request:
import { ReleaseDepositAddressRequest } from './proto/wallet_pb'
const request = new ReleaseDepositAddressRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setAddress('bc1qfreshaddress...')
await client.releaseDepositAddress(request, {})
console.log('Address released')
Example Response:
{}
Get Balances
Get all asset balances for a specific network.
Method: GetBalances
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
network | Network | YES | Network to query balances for |
Network Object:
| Field | Type | Description |
|---|---|---|
protocol | Protocol enum | PROTOCOL_BITCOIN = 1, PROTOCOL_EVM = 2 |
id | string | Magic bytes (hex) for Bitcoin, decimal chain ID for EVM |
Response:
| Field | Type | Description |
|---|---|---|
balances | map<string, Balance> | Map of asset_id → Balance |
Balance Object:
| Field | Type | Description |
|---|---|---|
onchain | OnchainBalance | Onchain balance details |
offchain | OffchainBalance | Offchain (Lightning) balance details |
OnchainBalance:
| Field | Type | Description |
|---|---|---|
confirmed | DecimalString | Confirmed balance |
unconfirmed | DecimalString | Unconfirmed balance |
reserved | DecimalString | Reserved for channel operations |
OffchainBalance:
| Field | Type | Description |
|---|---|---|
local | DecimalString | Balance you can send |
remote | DecimalString | Balance you can receive |
Example Request:
TypeScript
import { WalletServiceClient } from './proto/WalletServiceClientPb'
import { GetBalancesRequest } from './proto/wallet_pb'
const client = new WalletServiceClient('http://localhost:5001')
const request = new GetBalancesRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
const response = await client.getBalances(request, {})
const balances = response.getBalancesMap()
Go
import (
pb "github.com/hydra/hydra-go/proto"
)
client := pb.NewWalletServiceClient(conn)
req := &pb.GetBalancesRequest{
Network: &pb.Network{
Protocol: pb.Protocol_PROTOCOL_BITCOIN,
Id: "0a03cf40",
},
}
resp, err := client.GetBalances(context.Background(), req)
if err != nil {
log.Fatal(err)
}
balances := resp.Balances
Rust
use hydra_app::wallet_service_client::WalletServiceClient;
use hydra_app::{GetBalancesRequest, Network};
let mut client = WalletServiceClient::new(channel);
let request = tonic::Request::new(GetBalancesRequest {
network: Some(Network {
protocol: Protocol::Bitcoin as i32,
id: "0a03cf40".to_string(),
}),
});
let response = client.get_balances(request).await?;
let balances = response.into_inner().balances;
Example Response:
{
"balances": {
"BTC": {
"onchain": {
"confirmed": "50000000",
"unconfirmed": "0",
"reserved": "5000000"
},
"offchain": {
"local": "10000000",
"remote": "5000000"
}
},
"RGB:abc123": {
"onchain": {
"confirmed": "1000",
"unconfirmed": "0",
"reserved": "0"
},
"offchain": {
"local": "500",
"remote": "500"
}
}
}
}
Get Balance
Get the balance of a specific asset on a network.
Method: GetBalance
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
network | Network | YES | Network to query |
asset_id | string | YES | Asset identifier (e.g., "BTC", "0x...") |
Response:
| Field | Type | Description |
|---|---|---|
balance | Balance | Balance details for the asset |
Example Request:
TypeScript
const request = new GetBalanceRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setAssetId('BTC')
const response = await client.getBalance(request, {})
const balance = response.getBalance()
Go
req := &pb.GetBalanceRequest{
Network: &pb.Network{
Protocol: pb.Protocol_PROTOCOL_BITCOIN,
Id: "0a03cf40",
},
AssetId: "BTC",
}
resp, err := client.GetBalance(context.Background(), req)
if err != nil {
log.Fatal(err)
}
balance := resp.Balance
Rust
let request = tonic::Request::new(GetBalanceRequest {
network: Some(Network {
protocol: Protocol::Bitcoin as i32,
id: "0a03cf40".to_string(),
}),
asset_id: "BTC".to_string(),
});
let response = client.get_balance(request).await?;
let balance = response.into_inner().balance;
Example Response:
{
"balance": {
"onchain": {
"confirmed": "50000000",
"unconfirmed": "0",
"reserved": "5000000"
},
"offchain": {
"local": "10000000",
"remote": "5000000"
}
}
}
Get Transactions
Get all transactions for a network.
Method: GetTransactions
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
network | Network | YES | Network to query transactions for |
Response:
| Field | Type | Description |
|---|---|---|
transactions | Transaction[] | Array of transactions |
Transaction Object:
| Field | Type | Description |
|---|---|---|
txid | string | Transaction ID |
timestamp | Timestamp | Transaction timestamp |
confirmations | uint32 | Number of confirmations |
asset_transfers | AssetTransfer[] | Asset movements in this transaction |
Example Request:
TypeScript
const request = new GetTransactionsRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
const response = await client.getTransactions(request, {})
const transactions = response.getTransactionsList()
Go
req := &pb.GetTransactionsRequest{
Network: &pb.Network{
Protocol: pb.Protocol_PROTOCOL_BITCOIN,
Id: "0a03cf40",
},
}
resp, err := client.GetTransactions(context.Background(), req)
if err != nil {
log.Fatal(err)
}
transactions := resp.Transactions
Rust
let request = tonic::Request::new(GetTransactionsRequest {
network: Some(Network {
protocol: Protocol::Bitcoin as i32,
id: "0a03cf40".to_string(),
}),
});
let response = client.get_transactions(request).await?;
let transactions = response.into_inner().transactions;
Example Response:
{
"transactions": [
{
"txid": "abc123...",
"timestamp": "2025-10-03T10:30:00Z",
"confirmations": 6,
"asset_transfers": [
{
"asset_id": "BTC",
"amount": "1000000",
"direction": "OUTGOING"
}
]
},
{
"txid": "def456...",
"timestamp": "2025-10-02T14:20:00Z",
"confirmations": 144,
"asset_transfers": [
{
"asset_id": "BTC",
"amount": "5000000",
"direction": "INCOMING"
}
]
}
]
}
Get Transaction
Get details of a specific transaction by its ID.
Method: GetTransaction
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
network | Network | YES | Network where transaction occurred |
txid | string | YES | Transaction ID |
Response:
| Field | Type | Description |
|---|---|---|
transaction | Transaction | Transaction details |
Example Request:
TypeScript
const request = new GetTransactionRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setTxid('abc123...')
const response = await client.getTransaction(request, {})
const transaction = response.getTransaction()
Go
req := &pb.GetTransactionRequest{
Network: &pb.Network{
Protocol: pb.Protocol_PROTOCOL_BITCOIN,
Id: "0a03cf40",
},
Txid: "abc123...",
}
resp, err := client.GetTransaction(context.Background(), req)
if err != nil {
log.Fatal(err)
}
transaction := resp.Transaction
Rust
let request = tonic::Request::new(GetTransactionRequest {
network: Some(Network {
protocol: Protocol::Bitcoin as i32,
id: "0a03cf40".to_string(),
}),
txid: "abc123...".to_string(),
});
let response = client.get_transaction(request).await?;
let transaction = response.into_inner().transaction;
Example Response:
{
"transaction": {
"txid": "abc123...",
"timestamp": "2025-10-03T10:30:00Z",
"confirmations": 6,
"fee": "2500",
"asset_transfers": [
{
"asset_id": "BTC",
"amount": "1000000",
"direction": "OUTGOING"
}
]
}
}
Asset ID Formats
Different networks use different asset ID formats:
Bitcoin
- Native:
"BTC" - RGB Assets:
"RGB:contract_id"
Ethereum/EVM
- Native:
"0x0000000000000000000000000000000000000000"(42-character zero address) - ERC20 Tokens:
"ERC20:0x..."(contract address)
Common Patterns
Check if user has sufficient balance
TypeScript
async function hasSufficientBalance(
client: WalletServiceClient,
network: Network,
assetId: string,
requiredAmount: string,
balanceType: 'onchain' | 'offchain'
): Promise<boolean> {
const request = new GetBalanceRequest()
request.setNetwork(network)
request.setAssetId(assetId)
const response = await client.getBalance(request, {})
const balance = response.getBalance()
const available = balanceType === 'onchain'
? balance.getOnchain()?.getConfirmed()
: balance.getOffchain()?.getLocal()
return BigInt(available || '0') >= BigInt(requiredAmount)
}
Go
func hasSufficientBalance(
client pb.WalletServiceClient,
network *pb.Network,
assetId string,
requiredAmount string,
balanceType string,
) (bool, error) {
req := &pb.GetBalanceRequest{
Network: network,
AssetId: assetId,
}
resp, err := client.GetBalance(context.Background(), req)
if err != nil {
return false, err
}
balance := resp.Balance
var available string
if balanceType == "onchain" {
available = balance.Onchain.Confirmed
} else {
available = balance.Offchain.Local
}
requiredBig := new(big.Int)
requiredBig.SetString(requiredAmount, 10)
availableBig := new(big.Int)
availableBig.SetString(available, 10)
return availableBig.Cmp(requiredBig) >= 0, nil
}
Rust
async fn has_sufficient_balance(
client: &mut WalletServiceClient<Channel>,
network: Network,
asset_id: String,
required_amount: String,
balance_type: &str,
) -> Result<bool, Box<dyn std::error::Error>> {
let request = tonic::Request::new(GetBalanceRequest {
network: Some(network),
asset_id,
});
let response = client.get_balance(request).await?;
let balance = response.into_inner().balance.unwrap();
let available = if balance_type == "onchain" {
balance.onchain.unwrap().confirmed
} else {
balance.offchain.unwrap().local
};
let required: u64 = required_amount.parse()?;
let available_amount: u64 = available.parse()?;
Ok(available_amount >= required)
}
Monitor transaction confirmations
TypeScript
async function waitForConfirmations(
client: WalletServiceClient,
network: Network,
txid: string,
requiredConfirmations: number
): Promise<void> {
while (true) {
const request = new GetTransactionRequest()
request.setNetwork(network)
request.setTxid(txid)
const response = await client.getTransaction(request, {})
const tx = response.getTransaction()
if (tx.getConfirmations() >= requiredConfirmations) {
return
}
await new Promise(resolve => setTimeout(resolve, 30000)) // Wait 30s
}
}
Go
func waitForConfirmations(
client pb.WalletServiceClient,
network *pb.Network,
txid string,
requiredConfirmations uint32,
) error {
for {
req := &pb.GetTransactionRequest{
Network: network,
Txid: txid,
}
resp, err := client.GetTransaction(context.Background(), req)
if err != nil {
return err
}
tx := resp.Transaction
if tx.Confirmations >= requiredConfirmations {
return nil
}
time.Sleep(30 * time.Second) // Wait 30s
}
}
Rust
async fn wait_for_confirmations(
client: &mut WalletServiceClient<Channel>,
network: Network,
txid: String,
required_confirmations: u32,
) -> Result<(), Box<dyn std::error::Error>> {
loop {
let request = tonic::Request::new(GetTransactionRequest {
network: Some(network.clone()),
txid: txid.clone(),
});
let response = client.get_transaction(request).await?;
let tx = response.into_inner().transaction.unwrap();
if tx.confirmations >= required_confirmations {
return Ok(());
}
tokio::time::sleep(tokio::time::Duration::from_secs(30)).await; // Wait 30s
}
}
Error Handling
| Error Code | Description | Solution |
|---|---|---|
INVALID_ARGUMENT | Invalid network or asset_id | Check asset ID format for the network |
NOT_FOUND | Transaction not found | Verify txid is correct |
UNAVAILABLE | Service temporarily unavailable | Retry with exponential backoff |
Best Practices
- Cache balance queries - Balances don't change instantly, cache for 10-30 seconds
- Use GetBalances for multiple assets - More efficient than multiple GetBalance calls
- Check both onchain and offchain - Lightning payments require offchain balance
- Monitor reserved balance - Reserved funds cannot be spent until channel operations complete
- Handle unconfirmed balance - Don't rely on unconfirmed funds for critical operations