#[allow(unused_imports)]
use crate::*;
pub(crate) const TBA_USAGE: &str = "\
usage: localharness tba <show|deploy|exec> ...
tba show [--as <me>] [<name>] your (or <name>'s) TBA address, $LH, deployed?
tba deploy [--as <me>] [<name>] deploy the TBA on-chain (createTokenBoundAccount)
tba exec [--as <me>] [--tba <name-or-0xaddr>] <to> <amount> [--data <hex>]
make a TBA execute a call:
no --data → send <amount> $LH to <to>
--data <hex> → call <to> with <hex>, <amount> as value
--tba → act through an owned TBA other than
your main (a guild's wallet, etc.); default
is your main TBA. The chain gates execute to
the TBA owner, so it must be one you control.
<to> is a name (→ its on-chain owner) or a 0x… address.
<amount> is in $LH (the transfer amount, or the ETH value forwarded with --data).";
pub(crate) async fn tba(caller: Option<&str>, rest: &[String]) -> i32 {
match rest.first().map(String::as_str) {
Some("show") => tba_show(caller, rest.get(1).map(String::as_str)).await,
Some("deploy") => tba_deploy(caller, rest.get(1).map(String::as_str)).await,
Some("exec") => tba_exec(caller, &rest[1..]).await,
_ => {
eprintln!("{TBA_USAGE}");
2
}
}
}
pub(crate) async fn tba_target_token(
caller: Option<&str>,
name: Option<&str>,
signer: &k256::ecdsa::SigningKey,
) -> Result<(u64, String), String> {
if let Some(n) = name {
match registry::id_of_name(n).await {
Ok(0) => Err(format!("tba: '{n}' is not registered")),
Ok(id) => Ok((id, n.to_string())),
Err(e) => Err(format!("tba: RPC error resolving '{n}': {e}")),
}
} else {
let id = resolve_own_token_id(caller, signer).await?;
let label = registry::name_of_id(id).await.unwrap_or_else(|_| format!("token #{id}"));
Ok((id, label))
}
}
pub(crate) async fn tba_show(caller: Option<&str>, name: Option<&str>) -> i32 {
let (token_id, label) = if let Some(n) = name {
match registry::id_of_name(n).await {
Ok(0) => {
eprintln!("tba show: '{n}' is not registered");
return 1;
}
Ok(id) => (id, n.to_string()),
Err(e) => {
eprintln!("tba show: RPC error resolving '{n}': {e}");
return 1;
}
}
} else {
let signer = match load_signer(caller) {
Ok(s) => s,
Err(code) => return code,
};
match tba_target_token(caller, None, &signer).await {
Ok(t) => t,
Err(e) => {
eprintln!("{e}");
return 1;
}
}
};
let tba_addr = match registry::tba_of_token_id(token_id).await {
Ok(Some(a)) => a,
Ok(None) => {
eprintln!("tba show: no token-bound account for '{label}' (token #{token_id})");
return 1;
}
Err(e) => {
eprintln!("tba show: RPC error: {e}");
return 1;
}
};
let balance = registry::token_balance_of(&tba_addr).await.unwrap_or(0);
let deployed = registry::is_contract_deployed(&tba_addr).await.unwrap_or(false);
println!("{label} (token #{token_id})");
println!(" wallet (TBA): {tba_addr}");
println!(" balance: {}", fmt_lh(balance));
println!(
" deployed: {}",
if deployed { "yes" } else { "no — run `tba deploy` before it can execute" }
);
0
}
pub(crate) async fn tba_deploy(caller: Option<&str>, name: Option<&str>) -> i32 {
let (signer, sponsor) = match load_signer_and_sponsor(caller) {
Ok(pair) => pair,
Err(code) => return code,
};
let (token_id, label) = match tba_target_token(caller, name, &signer).await {
Ok(t) => t,
Err(e) => {
eprintln!("{e}");
return 1;
}
};
let tba_addr = match registry::tba_of_token_id(token_id).await {
Ok(Some(a)) => a,
Ok(None) => {
eprintln!("tba deploy: no token-bound account for '{label}' (token #{token_id})");
return 1;
}
Err(e) => {
eprintln!("tba deploy: RPC error: {e}");
return 1;
}
};
if registry::is_contract_deployed(&tba_addr).await.unwrap_or(false) {
println!("{label}'s TBA {tba_addr} is already deployed — nothing to do.");
return 0;
}
println!("deploying {label}'s TBA {tba_addr} …");
match registry::create_token_bound_account_sponsored(
&signer,
&sponsor,
token_id,
registry::ALPHA_USD_ADDRESS,
)
.await
{
Ok(tx) => {
println!("✓ deployed tx: {tx}");
0
}
Err(e) => {
eprintln!("tba deploy failed: {e}");
1
}
}
}
pub(crate) async fn tba_exec(caller: Option<&str>, rest: &[String]) -> i32 {
let (tba_flag, after_tba) = match take_tba_flag(rest) {
Ok(v) => v,
Err(e) => {
eprintln!("{e}");
return 2;
}
};
let (data_hex, positional) = match take_data_flag(&after_tba) {
Ok(v) => v,
Err(e) => {
eprintln!("{e}");
return 2;
}
};
if positional.len() != 2 {
eprintln!("{TBA_USAGE}");
return 2;
}
let to_arg = &positional[0];
let amount_arg = &positional[1];
use localharness::encoding::{classify_recipient, Recipient};
let to_hex = match classify_recipient(to_arg) {
Ok(Recipient::Address(a)) => a,
Ok(Recipient::Name(n)) => match registry::owner_of_name(&n).await {
Ok(Some(o)) => o,
Ok(None) => {
eprintln!("tba exec: '{n}' is not registered");
return 1;
}
Err(e) => {
eprintln!("tba exec: RPC error resolving '{n}': {e}");
return 1;
}
},
Err(e) => {
eprintln!("tba exec: {e}");
return 2;
}
};
let amount_wei = match localharness::encoding::parse_token_amount(amount_arg) {
Some(w) => w,
None => {
eprintln!("tba exec: invalid amount '{amount_arg}' (expected a number of $LH)");
return 2;
}
};
if data_hex.is_none() && amount_wei == 0 {
eprintln!("tba exec: amount must be greater than 0 for a $LH transfer");
return 2;
}
let data = match &data_hex {
Some(h) => match decode_hex_arg(h) {
Ok(b) => Some(b),
Err(e) => {
eprintln!("tba exec: {e}");
return 2;
}
},
None => None,
};
let (signer, sponsor) = match load_signer_and_sponsor(caller) {
Ok(pair) => pair,
Err(code) => return code,
};
let caller_addr = bytes_to_hex_str(&wallet::address(&signer));
let (tba_addr, exec_token_id, tba_label) = match &tba_flag {
Some(target) => {
use localharness::encoding::{classify_recipient, Recipient};
match classify_recipient(target) {
Ok(Recipient::Address(a)) => {
(a.clone(), None, a)
}
Ok(Recipient::Name(n)) => {
let addr = match registry::tba_of_name(&n).await {
Ok(Some(a)) => a,
Ok(None) => {
eprintln!("tba exec: '{n}' is not registered (no token-bound account)");
return 1;
}
Err(e) => {
eprintln!("tba exec: RPC error resolving '{n}': {e}");
return 1;
}
};
match registry::owner_of_name(&n).await {
Ok(Some(o)) if o.eq_ignore_ascii_case(&caller_addr) => {}
Ok(Some(o)) => {
eprintln!(
"warning: '{n}' is controlled by {o}, not you ({caller_addr}) — \
the TBA's on-chain _isAuthorized will reject this unless you're \
an enrolled signer."
);
}
_ => {}
}
let id = registry::id_of_name(&n).await.unwrap_or(0);
(addr, if id != 0 { Some(id) } else { None }, n)
}
Err(e) => {
eprintln!("tba exec: --tba {e}");
return 2;
}
}
}
None => {
let token_id = match resolve_own_token_id(caller, &signer).await {
Ok(id) => id,
Err(e) => {
eprintln!("{e}");
return 1;
}
};
match registry::tba_of_token_id(token_id).await {
Ok(Some(a)) => (a, Some(token_id), "your".to_string()),
Ok(None) => {
eprintln!("tba exec: no token-bound account for your token #{token_id}");
return 1;
}
Err(e) => {
eprintln!("tba exec: RPC error: {e}");
return 1;
}
}
}
};
if !registry::is_contract_deployed(&tba_addr).await.unwrap_or(false) {
match exec_token_id {
Some(token_id) => {
println!("{tba_label} TBA {tba_addr} isn't deployed yet — deploying first …");
if let Err(e) = registry::create_token_bound_account_sponsored(
&signer,
&sponsor,
token_id,
registry::ALPHA_USD_ADDRESS,
)
.await
{
eprintln!("tba exec: TBA deploy failed: {e}");
return 1;
}
}
None => {
eprintln!(
"tba exec: TBA {tba_addr} isn't deployed and was given as a raw address \
(no token id to deploy it) — pass `--tba <name>` so it can be deployed, \
or deploy it first with `tba deploy`."
);
return 1;
}
}
}
let result = match &data {
Some(bytes) => {
println!(
"{tba_label} TBA {tba_addr} → execute({to_hex}, value {}, {} bytes of calldata) …",
fmt_lh(amount_wei),
bytes.len()
);
registry::tba_execute_call_sponsored(
&signer,
&sponsor,
&tba_addr,
&to_hex,
amount_wei,
bytes,
registry::ALPHA_USD_ADDRESS,
)
.await
}
None => {
println!("{tba_label} TBA {tba_addr} → send {} $LH to {to_hex} …", fmt_lh(amount_wei));
registry::tba_send_lh_sponsored(
&signer,
&sponsor,
&tba_addr,
&to_hex,
amount_wei,
registry::ALPHA_USD_ADDRESS,
)
.await
}
};
match result {
Ok(tx) => {
println!("✓ executed tx: {tx}");
0
}
Err(e) => {
eprintln!("tba exec failed: {e}");
1
}
}
}