Transactions
What this teaches: the full path from Message to confirmed transaction, using only dango-sdk primitives.
Mental model
The Rust SDK does not ship Dango action helpers — there is no client.transfer(...) or client.submit_order(...). Transactions are built by hand:
Compose
Build a NonEmpty<Vec<Message>>. This is where the Dango contract knowledge lives — encode the contract message yourself (Message::execute, Message::transfer, …).
Sign
Call SingleSigner::sign_transaction to produce a Tx.
Broadcast
Submit via BroadcastClient::broadcast_tx on HttpClient.
Poll
Wait for inclusion with SearchTxClient::search_tx.
Compose
Most flows use Message::transfer (bank send) or Message::execute (smart contract call).
use {
grug::{Addr, Coins, Message, NonEmpty},
std::str::FromStr,
};
let recipient = Addr::from_str("0x1111111111111111111111111111111111111111")?;
let coins = Coins::one("bridge/usdc", 100_000_000_u128)?; // 100 USDC in base units
let messages = NonEmpty::new(vec![
Message::transfer(recipient, coins)?,
])?;
// Ok::<(), anyhow::Error>(())For a smart-contract call, encode the contract's typed ExecuteMsg:
use {
dango_types::dex,
grug::{Coins, Message},
};
let msg = Message::execute(
/* contract: */ Addr::from_str("0x...")?,
/* payload: */ &dex::ExecuteMsg::SubmitOrders { /* ... */ },
/* funds: */ Coins::new(),
)?;
// Ok::<(), anyhow::Error>(())Estimate gas
HttpClient implements QueryClient::simulate, which runs the transaction against the latest committed state and reports gas used:
use {
dango_sdk::{HttpClient, SingleSigner},
grug::{QueryClient, Signer},
};
let unsigned = signer.unsigned_transaction(messages.clone(), "dango-1")?;
let outcome = http.simulate(unsigned).await?;
let gas_limit = (outcome.gas_used as f64 * 1.5) as u64; // 50% buffer
// Ok::<(), anyhow::Error>(())Apply your own buffer — simulate reports actual usage, not a recommended limit.
Sign
use grug::Signer;
let tx = signer.sign_transaction(messages, "dango-1", gas_limit)?;
// Ok::<(), anyhow::Error>(())sign_transaction consumes the current nonce and increments the in-memory value, so successive calls produce successive transactions without an extra round-trip.
Broadcast
use grug::BroadcastClient;
let result = http.broadcast_tx(tx).await?;
println!("hash={}", result.tx_hash);
// Ok::<(), anyhow::Error>(())Broadcast is tx-sync: the node accepts or rejects the transaction (mempool decision) but does not wait for inclusion. Poll for the receipt next.
Wait for inclusion
use {
grug::SearchTxClient,
std::time::Duration,
tokio::time::sleep,
};
let outcome = loop {
match http.search_tx(result.tx_hash).await {
Ok(o) => break o,
Err(_) => sleep(Duration::from_millis(500)).await,
}
};
println!("included at height {}, gas {}", outcome.height, outcome.outcome.gas_used);
// Ok::<(), anyhow::Error>(())Wrap this loop in a bounded retry helper (tokio-retry, backon, …) — the SDK does not ship one.
Putting it together
use {
anyhow::Result,
dango_sdk::{HttpClient, Secp256k1, Secret, SingleSigner},
grug::{Addr, BroadcastClient, Coins, Message, NonEmpty, QueryClient, SearchTxClient, Signer},
std::{str::FromStr, time::Duration},
tokio::time::sleep,
};
#[tokio::main]
async fn main() -> Result<()> {
let http = HttpClient::new("https://api-mainnet.dango.zone")?;
let secret = Secp256k1::from_bytes([0u8; 32])?;
let addr = Addr::from_str("0x0000000000000000000000000000000000000000")?;
let mut signer = SingleSigner::new(addr, secret)
.with_query_user_index(&http).await?
.with_query_nonce(&http).await?;
let recipient = Addr::from_str("0x1111111111111111111111111111111111111111")?;
let messages = NonEmpty::new(vec![
Message::transfer(recipient, Coins::one("bridge/usdc", 100_000_000_u128)?)?,
])?;
let unsigned = signer.unsigned_transaction(messages.clone(), "dango-1")?;
let estimated = http.simulate(unsigned).await?.gas_used as u64;
let tx = signer.sign_transaction(messages, "dango-1", estimated * 3 / 2)?;
let result = http.broadcast_tx(tx).await?;
loop {
match http.search_tx(result.tx_hash).await {
Ok(o) => { println!("included at {}", o.height); break; }
Err(_) => sleep(Duration::from_millis(500)).await,
}
}
Ok(())
}Next
- Subscriptions — react to chain events instead of polling.
- Error handling — distinguish broadcast-rejection, execution failure, and transport error.