use anyhow::Context;
use chrono::Utc;
use ed25519_dalek::SigningKey;
use serde::Deserialize;
use serde_json::json;
use sigil_core::transaction::{TransferData, TransactionType};
use sigil_core::Transaction;
use sigil_sdk::{sign_transaction, SigilClientConfig};
use std::env;
use std::time::Duration;
use tokio::time::sleep;
#[derive(Deserialize)]
struct BalanceResult {
nonce: u64,
}
async fn rpc<T: for<'de> Deserialize<'de>>(
cfg: &SigilClientConfig,
method: &str,
params: serde_json::Value,
) -> anyhow::Result<T> {
let body = json!({ "jsonrpc": "2.0", "id": 1, "method": method, "params": params });
let res: serde_json::Value = reqwest::Client::new()
.post(&cfg.rpc_url)
.json(&body)
.send()
.await?
.json()
.await?;
if let Some(err) = res.get("error") {
anyhow::bail!("RPC {method} failed: {err}");
}
Ok(serde_json::from_value(res["result"].clone())?)
}
async fn send_payment_with_retry(
cfg: &SigilClientConfig,
signing_key: &SigningKey,
sender_did: &str,
recipient_did: &str,
amount_base_units: u64,
) -> anyhow::Result<String> {
let acct: BalanceResult = rpc(
cfg,
"sigil_getBalance",
json!({ "did": sender_did }),
)
.await
.context("failed to fetch nonce")?;
let tx = Transaction {
chain_id: cfg.chain_id.clone(),
sender_did: sender_did.to_string(),
nonce: acct.nonce,
gas_limit: 200_000,
gas_price: 1,
tx_type: TransactionType::Transfer(TransferData {
recipient_did: recipient_did.to_string(),
amount: amount_base_units,
memo: None,
}),
timestamp: Utc::now(),
};
let signed = sign_transaction(tx, signing_key)?;
let mut delay_ms = 250u64;
for attempt in 0..5 {
let body = json!({
"jsonrpc": "2.0",
"id": 1,
"method": "sigil_sendTransaction",
"params": &signed,
});
let res: serde_json::Value = reqwest::Client::new()
.post(&cfg.rpc_url)
.json(&body)
.send()
.await?
.json()
.await?;
if let Some(tx_hash) = res["result"]["tx_hash"].as_str() {
return Ok(tx_hash.to_string());
}
let code = res["error"]["code"].as_i64().unwrap_or(0);
// -32603 internal, -32002 mempool full — both retryable.
if !matches!(code, -32603 | -32002) || attempt == 4 {
anyhow::bail!("send failed: {}", res["error"]);
}
sleep(Duration::from_millis(delay_ms)).await;
delay_ms *= 2;
}
unreachable!()
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let cfg = SigilClientConfig::testnet(
&env::var("SIGIL_RPC_URL")
.unwrap_or_else(|_| "https://testnet-rpc.sigil.ml/rpc".into()),
);
let key_hex = env::var("SIGIL_SIGNING_KEY_HEX")
.context("set SIGIL_SIGNING_KEY_HEX")?;
let key_bytes: [u8; 32] = hex::decode(&key_hex)?
.try_into()
.map_err(|_| anyhow::anyhow!("signing key must be 32 bytes"))?;
let signing_key = SigningKey::from_bytes(&key_bytes);
let sender_did = env::var("SIGIL_SENDER_DID")?;
let recipient_did = env::var("RECIPIENT_DID")?;
let amount: u64 = env::var("AMOUNT_BASE_UNITS")?.parse()?;
let tx_hash = send_payment_with_retry(
&cfg, &signing_key, &sender_did, &recipient_did, amount,
)
.await?;
println!("submitted: {tx_hash}");
Ok(())
}