use crate::app::chat::access::credit_address_existing;
use crate::tools::ClosureTool;
use super::bounty::bounty_signers;
pub(crate) fn create_guild_tool() -> std::sync::Arc<dyn crate::tools::Tool> {
let schema = serde_json::json!({
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Display name for the guild (a short label for the org)."
}
},
"required": ["name"]
});
ClosureTool::new(
"create_guild",
"Found an on-chain GUILD: a durable org with members, roles, and a pooled $LH \
treasury. You become its founding Admin. Use this to organize a standing team \
of agents (as opposed to a one-off bounty). Returns { guild_id, name, treasury, \
tx_hash }.",
schema,
|args: serde_json::Value, _ctx| async move {
let name = args.get("name").and_then(|v| v.as_str()).unwrap_or("").trim();
if name.is_empty() {
return Err(crate::error::Error::other("name cannot be empty"));
}
let (signer, fee_payer) = bounty_signers().await?;
let tx_hash = crate::app::registry::create_guild_sponsored(
&signer,
&fee_payer,
name,
crate::app::registry::ALPHA_USD_ADDRESS,
)
.await
.map_err(|e| crate::error::Error::other(format!("create_guild failed: {e}")))?;
let (guild_id, treasury) = match credit_address_existing().await {
Some(addr) => match crate::app::registry::guilds_of(&addr).await.ok().and_then(|ids| {
ids.last().copied()
}) {
Some(id) => {
let t = crate::app::registry::guild_address(id).await.unwrap_or_default();
(Some(id), t)
}
None => (None, String::new()),
},
None => (None, String::new()),
};
let mut result = serde_json::json!({
"name": name,
"tx_hash": tx_hash,
});
if let Some(id) = guild_id {
result["guild_id"] = serde_json::json!(id);
}
if !treasury.is_empty() {
result["treasury"] = serde_json::json!(treasury);
}
Ok(result)
},
)
}
pub(crate) fn invite_to_guild_tool() -> std::sync::Arc<dyn crate::tools::Tool> {
let schema = serde_json::json!({
"type": "object",
"properties": {
"guild_id": {
"type": "integer",
"minimum": 0,
"description": "The id of the guild you administer."
},
"member": {
"type": "string",
"description": "Who to invite — a raw 0x… address OR a subdomain name \
(resolved to that name's on-chain owner)."
}
},
"required": ["guild_id", "member"]
});
ClosureTool::new(
"invite_to_guild",
"Invite an address or subdomain name (its on-chain owner) into a guild you \
administer; they join by accepting. Admin-gated on-chain (only a guild Admin \
can invite). Returns { guild_id, member, resolved_member, tx_hash }.",
schema,
|args: serde_json::Value, _ctx| async move {
let guild_id = args
.get("guild_id")
.and_then(|v| v.as_u64())
.ok_or_else(|| crate::error::Error::other("guild_id is required"))?;
let member_arg = args
.get("member")
.and_then(|v| v.as_str())
.unwrap_or("")
.trim()
.to_string();
let member_hex = resolve_account(&member_arg).await?;
let (signer, fee_payer) = bounty_signers().await?;
let tx_hash = crate::app::registry::invite_to_guild_sponsored(
&signer,
&fee_payer,
guild_id,
&member_hex,
crate::app::registry::ALPHA_USD_ADDRESS,
)
.await
.map_err(|e| crate::error::Error::other(format!("invite_to_guild failed: {e}")))?;
Ok(serde_json::json!({
"guild_id": guild_id,
"member": member_arg,
"resolved_member": member_hex,
"tx_hash": tx_hash,
}))
},
)
}
pub(crate) fn fund_guild_tool() -> std::sync::Arc<dyn crate::tools::Tool> {
let schema = serde_json::json!({
"type": "object",
"properties": {
"guild_id": {
"type": "integer",
"minimum": 0,
"description": "The id of the guild to fund."
},
"amount_lh": {
"type": "string",
"description": "Amount of $LH to contribute, as a decimal string \
(\"5\", \"1.5\"). Pulled from YOUR wallet into the shared treasury. \
Must be > 0."
}
},
"required": ["guild_id", "amount_lh"]
});
ClosureTool::new(
"fund_guild",
"Contribute $LH from your wallet into a guild's pooled treasury. Anyone can \
fund; spending the treasury is Admin-gated. Moves value: confirm the amount \
with the owner before calling. Returns { guild_id, amount_lh, tx_hash }.",
schema,
|args: serde_json::Value, _ctx| async move {
let guild_id = args
.get("guild_id")
.and_then(|v| v.as_u64())
.ok_or_else(|| crate::error::Error::other("guild_id is required"))?;
let amount_arg = args
.get("amount_lh")
.and_then(|v| v.as_str())
.unwrap_or("")
.trim()
.to_string();
let amount_wei = crate::encoding::parse_token_amount(&amount_arg).ok_or_else(|| {
crate::error::Error::other(format!(
"could not parse amount_lh \"{amount_arg}\" — pass a decimal $LH \
figure like \"5\" or \"1.5\""
))
})?;
if amount_wei == 0 {
return Err(crate::error::Error::other("amount_lh must be greater than 0"));
}
let (signer, fee_payer) = bounty_signers().await?;
let from_hex =
crate::encoding::bytes_to_hex_str(&crate::wallet::address(&signer));
let bridge_wei = crate::app::chat::escrow_bridge_wei(&from_hex, amount_wei)
.await
.map_err(crate::error::Error::other)?;
let tx_hash = crate::app::registry::fund_guild_sponsored_bridged(
&signer,
&fee_payer,
guild_id,
amount_wei,
crate::app::registry::ALPHA_USD_ADDRESS,
bridge_wei,
)
.await
.map_err(|e| crate::error::Error::other(format!("fund_guild failed: {e}")))?;
Ok(serde_json::json!({
"guild_id": guild_id,
"amount_lh": amount_arg,
"tx_hash": tx_hash,
}))
},
)
}
pub(crate) fn spend_treasury_tool() -> std::sync::Arc<dyn crate::tools::Tool> {
let schema = serde_json::json!({
"type": "object",
"properties": {
"guild_id": {
"type": "integer",
"minimum": 0,
"description": "The id of the guild whose treasury to spend from."
},
"to": {
"type": "string",
"description": "Recipient — a raw 0x… address OR a subdomain name \
(resolved to that name's on-chain owner)."
},
"amount_lh": {
"type": "string",
"description": "Amount of $LH to pay out, as a decimal string. Must be > 0."
},
"memo": {
"type": "string",
"description": "OPTIONAL note recorded with the payment (what it's for)."
}
},
"required": ["guild_id", "to", "amount_lh"]
});
ClosureTool::new(
"spend_treasury",
"Pay $LH OUT of a guild's pooled treasury to an address or subdomain name, with \
an optional memo. Admin-gated ON-CHAIN: only a guild Admin can spend (the call \
reverts otherwise). Moves value: confirm the recipient + amount with the owner \
before calling. Returns { guild_id, to, resolved_to, amount_lh, tx_hash }.",
schema,
|args: serde_json::Value, _ctx| async move {
let guild_id = args
.get("guild_id")
.and_then(|v| v.as_u64())
.ok_or_else(|| crate::error::Error::other("guild_id is required"))?;
let to_arg = args
.get("to")
.and_then(|v| v.as_str())
.unwrap_or("")
.trim()
.to_string();
let amount_arg = args
.get("amount_lh")
.and_then(|v| v.as_str())
.unwrap_or("")
.trim()
.to_string();
let memo = args.get("memo").and_then(|v| v.as_str()).unwrap_or("").trim();
let amount_wei = crate::encoding::parse_token_amount(&amount_arg).ok_or_else(|| {
crate::error::Error::other(format!(
"could not parse amount_lh \"{amount_arg}\" — pass a decimal $LH \
figure like \"5\" or \"1.5\""
))
})?;
if amount_wei == 0 {
return Err(crate::error::Error::other("amount_lh must be greater than 0"));
}
let to_hex = resolve_account(&to_arg).await?;
let (signer, fee_payer) = bounty_signers().await?;
let tx_hash = crate::app::registry::spend_treasury_sponsored(
&signer,
&fee_payer,
guild_id,
&to_hex,
amount_wei,
memo.as_bytes(),
crate::app::registry::ALPHA_USD_ADDRESS,
)
.await
.map_err(|e| crate::error::Error::other(format!("spend_treasury failed: {e}")))?;
Ok(serde_json::json!({
"guild_id": guild_id,
"to": to_arg,
"resolved_to": to_hex,
"amount_lh": amount_arg,
"tx_hash": tx_hash,
}))
},
)
}
pub(crate) fn list_my_guilds_tool() -> std::sync::Arc<dyn crate::tools::Tool> {
ClosureTool::new(
"list_my_guilds",
"List every guild you belong to — each with its id, name, and pooled $LH \
treasury balance. Read-only. Use when asked about your guilds/orgs. Returns \
{ guilds: [ { guild_id, name, treasury_lh } ], count }.",
serde_json::json!({ "type": "object", "properties": {}, "required": [] }),
|_args: serde_json::Value, _ctx| async move {
let addr = credit_address_existing()
.await
.ok_or_else(|| crate::error::Error::other("no identity — claim a subdomain first"))?;
let ids = crate::app::registry::guilds_of(&addr)
.await
.map_err(crate::error::Error::other)?;
let mut guilds = Vec::new();
for id in ids {
let name = crate::app::registry::guild_name(id).await.unwrap_or_default();
let treasury_wei = crate::app::registry::treasury_balance_of(id).await.unwrap_or(0);
guilds.push(serde_json::json!({
"guild_id": id,
"name": name,
"treasury_lh": format_lh(treasury_wei),
}));
}
Ok(serde_json::json!({
"count": guilds.len(),
"guilds": guilds,
}))
},
)
}
pub(crate) async fn resolve_account(arg: &str) -> Result<String, crate::error::Error> {
use crate::encoding::Recipient;
let kind = crate::encoding::classify_recipient(arg).map_err(crate::error::Error::other)?;
match kind {
Recipient::Address(addr) => Ok(addr),
Recipient::Name(name) => crate::app::registry::owner_of_name(&name)
.await
.map_err(crate::error::Error::other)?
.ok_or_else(|| {
crate::error::Error::other(format!(
"no on-chain owner for subdomain \"{name}\" — is it registered?"
))
}),
}
}
pub(crate) async fn own_token_id() -> Result<u64, crate::error::Error> {
let tenant = crate::app::tenant::current_name().ok_or_else(|| {
crate::error::Error::other("not running on a subdomain — no agent identity to claim as")
})?;
match crate::app::registry::id_of_name(&tenant).await {
Ok(id) if id != 0 => Ok(id),
Ok(_) => Err(crate::error::Error::other(
"this subdomain isn't registered on-chain yet — claim it first",
)),
Err(e) => Err(crate::error::Error::other(format!("id_of_name: {e}"))),
}
}
pub(crate) fn format_lh(wei: u128) -> String {
let whole = wei / 1_000_000_000_000_000_000u128;
let cents = (wei % 1_000_000_000_000_000_000u128) / 10_000_000_000_000_000u128;
format!("{whole}.{cents:02}")
}