SingleSigner
A typestate builder for signing transactions against Dango's single-signature accounts.
Setup
use {
anyhow::Result,
dango_sdk::{HttpClient, Secp256k1, Secret, SingleSigner},
grug::Addr,
std::str::FromStr,
};
#[tokio::main]
async fn main() -> Result<()> {
let http = HttpClient::new("https://api-mainnet.dango.zone")?;
let secret = Secp256k1::new_random();
let addr = Addr::from_str("0x0000000000000000000000000000000000000000")?;
let signer = SingleSigner::new(addr, secret)
.with_query_user_index(&http).await?
.with_query_nonce(&http).await?;
let _ = signer;
Ok(())
}pub struct SingleSigner<S, I = Defined<UserIndex>, N = Defined<Nonce>>
where
S: Secret,
I: MaybeDefined<UserIndex>,
N: MaybeDefined<Nonce>,
{
pub address: Addr,
pub secret: S,
pub user_index: I,
pub nonce: N,
}
pub const DEFAULT_DERIVATION_PATH: &str = "m/44'/60'/0'/0/0";The typestate
SingleSigner carries phantom-typed flags for whether user_index and nonce have been filled in:
| State | I | N | Capabilities |
|---|---|---|---|
| Bare | Undefined<UserIndex> | Undefined<Nonce> | new, with_user_index, with_query_user_index, with_nonce, with_query_nonce. |
| Index-only | Defined<UserIndex> | Undefined<Nonce> | new_first_address_available, user_index(), with_nonce, with_query_nonce. |
| Nonce-only | Undefined<UserIndex> | Defined<Nonce> | nonce(), with_user_index, with_query_user_index. |
| Ready | Defined<UserIndex> | Defined<Nonce> | Signer, SequencedSigner, user_index(), nonce(). |
Defined<T> and Undefined<T> are typestate helpers. The compiler enforces both fields are Defined before sign_transaction is callable.
This is a common Rust idiom for builders that must reach a complete state before being usable, but it is unusual in TypeScript/Python where missing fields would be a runtime check.
Configuration
SingleSigner has no static configuration. The signing scheme is dictated by the Secret implementation (Secp256k1 for raw secp256k1+SHA-256, Eip712 for Ethereum typed-data).
Methods
Constructors
| Method | Description |
|---|---|
new | Bare constructor — both phantom slots Undefined. |
new_first_address_available | Discover the first account associated with secret.key_hash() via the account factory. |
State transitions
| Method | Description |
|---|---|
with_user_index | Set user_index from a value. |
with_query_user_index | Set user_index by querying the account factory. |
with_nonce | Set nonce from a value. |
with_query_nonce | Set nonce by querying the account. |
Queries (all states)
| Method | Description |
|---|---|
query_user_index | Fetch this address's UserIndex from the account factory. |
query_next_nonce | Fetch the next unused Nonce. |
Accessors
| Method | Description |
|---|---|
user_index() -> UserIndex | Only on I = Defined<UserIndex>. |
nonce() -> Nonce | Only on N = Defined<Nonce>. |
Trait impls
| Trait | Where | Methods |
|---|---|---|
Addressable | all states | address() |
Signer | Defined<UserIndex> + Defined<Nonce> | unsigned_transaction, sign_transaction |
SequencedSigner (dango_types::signer) | Defined<Nonce> | query_nonce, update_nonce |
End-to-end example
use {
anyhow::Result,
dango_sdk::{HttpClient, Secp256k1, Secret, SingleSigner},
grug::{Addr, BroadcastClient, Coins, Message, NonEmpty, QueryClient, Signer},
std::str::FromStr,
};
#[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 messages = NonEmpty::new(vec![
Message::transfer(
Addr::from_str("0x1111111111111111111111111111111111111111")?,
Coins::one("bridge/usdc", 100_000_000_u128)?,
)?,
])?;
let unsigned = signer.unsigned_transaction(messages.clone(), "dango-1")?;
let gas = http.simulate(unsigned).await?.gas_used as u64;
let tx = signer.sign_transaction(messages, "dango-1", gas * 3 / 2)?;
let outcome = http.broadcast_tx(tx).await?;
println!("hash: {}", outcome.tx_hash);
Ok(())
}Notes
sign_transactionmutatesself.nonce(*self.nonce.inner_mut() += 1) on every success. A single signer instance can produce a stream of consecutive transactions without re-querying. After a broadcast failure, callSequencedSigner::update_nonceto resync.new_first_address_availablecallsQueryForgotUsernameRequestwithlimit: Some(1). It returns the first user-index associated withsecret.key_hash(), regardless of forgotten-username state. Errors with"no user index found for key hash …"when nothing matches.DEFAULT_DERIVATION_PATHism/44'/60'/0'/0/0(Ethereum coin type). Use it when constructingSecret::from_mnemonicwithcoin_type = 60.
See also
Secret— the trait that supplies private keys.- Concepts: Signers and authentication — the full mental model.
- Concepts: Transactions — end-to-end signing and broadcast.