Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

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.