use crate::tools::ClosureTool;
use super::bounty::bounty_signer;
use super::guild::format_lh;
async fn resolve_member_token_id(member: &str) -> Result<u64, crate::error::Error> {
let trimmed = member.trim().trim_start_matches('#');
if !trimmed.is_empty() && trimmed.chars().all(|c| c.is_ascii_digit()) {
return trimmed.parse::<u64>().map_err(|_| {
crate::error::Error::bad_args("form_party", format!("invalid member id \"{member}\""))
});
}
match crate::app::registry::id_of_name(&member.trim().to_ascii_lowercase()).await {
Ok(0) => Err(crate::error::Error::other(format!(
"\"{member}\" is not registered"
))),
Ok(id) => Ok(id),
Err(e) => Err(crate::error::Error::other(format!(
"RPC error resolving \"{member}\": {e}"
))),
}
}
pub(crate) fn form_party_tool() -> std::sync::Arc<dyn crate::tools::Tool> {
let schema = crate::tool_params::FormPartyParams::schema();
ClosureTool::new(
"form_party",
"Form an on-chain party: an ad-hoc squad of agent identities around one goal, \
with a fixed bps split. Each member consents (join_party), anyone funds the pot \
(fund_party), then you (the creator) complete_party to split the pot to the \
members' TBAs by their shares. Use this to coordinate a paid collaboration. \
Returns { party_id, members, shares, ttl_hours, tx_hash }.",
schema,
|args: serde_json::Value, _ctx| async move {
let p = crate::tool_params::FormPartyParams::lenient(&args);
let members_arg: Vec<String> = p
.members
.iter()
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
if members_arg.is_empty() {
return Err(crate::error::Error::bad_args(
"form_party",
"members cannot be empty — pass at least one name or token id",
));
}
let mut member_ids: Vec<u64> = Vec::with_capacity(members_arg.len());
for m in &members_arg {
member_ids.push(resolve_member_token_id(m).await?);
}
let shares: Vec<u16> = match args.get("shares").and_then(|v| v.as_array()) {
Some(arr) if !arr.is_empty() => {
if arr.len() != member_ids.len() {
return Err(crate::error::Error::bad_args("form_party", format!(
"shares has {} entries but members has {} — give EVERY member \
a share or NONE (equal split)",
arr.len(),
member_ids.len()
)));
}
let mut out: Vec<u16> = Vec::with_capacity(arr.len());
for v in arr {
let bps = v.as_u64().filter(|&b| b > 0 && b <= 10_000).ok_or_else(|| {
crate::error::Error::bad_args("form_party", "each share must be 1..10000 bps")
})?;
out.push(bps as u16);
}
let sum: u32 = out.iter().map(|&b| b as u32).sum();
if sum != 10_000 {
return Err(crate::error::Error::bad_args("form_party", format!(
"shares must sum to 10000 bps, got {sum}"
)));
}
out
}
_ => {
let n = member_ids.len() as u16;
let base = 10_000 / n;
let remainder = 10_000 - base * n;
(0..n)
.map(|i| if i == 0 { base + remainder } else { base })
.collect()
}
};
let ttl_hours: f64 = match p.ttl_hours.as_deref() {
Some(s) if !s.trim().is_empty() => s
.trim()
.parse::<f64>()
.map_err(|_| crate::error::Error::bad_args("form_party", "ttl_hours must be a number"))?,
_ => 168.0,
};
if ttl_hours <= 0.0 {
return Err(crate::error::Error::bad_args("form_party", "ttl_hours must be greater than 0"));
}
let ttl_secs = (ttl_hours * 3600.0) as u64;
let signer = bounty_signer().await?;
let tx_hash = crate::app::registry::form_party_sponsored(&signer, &member_ids, &shares, ttl_secs)
.await
.map_err(|e| crate::error::Error::other(format!("form_party failed: {e}")))?;
let from_hex = crate::encoding::bytes_to_hex_str(&crate::wallet::address(&signer));
let party_id = crate::app::registry::parties_of(&from_hex)
.await
.ok()
.and_then(|ids| ids.last().copied());
let mut result = serde_json::json!({
"members": member_ids,
"shares": shares,
"ttl_hours": ttl_hours,
"tx_hash": tx_hash,
});
if let Some(id) = party_id {
result["party_id"] = serde_json::json!(id);
}
Ok(result)
},
)
}
pub(crate) fn join_party_tool() -> std::sync::Arc<dyn crate::tools::Tool> {
let schema = crate::tool_params::JoinPartyParams::schema();
ClosureTool::new(
"join_party",
"Consent to a party you've been added to as a member. This marks consented every \
seat your owner holds; the LAST member's consent flips the party Active so it can \
be funded and completed. Returns { party_id, tx_hash }.",
schema,
|args: serde_json::Value, _ctx| async move {
let party_id = crate::tool_params::JoinPartyParams::lenient(&args).party_id()?;
let signer = bounty_signer().await?;
let tx_hash = crate::app::registry::join_party_sponsored(&signer, party_id)
.await
.map_err(|e| crate::error::Error::other(format!("join_party failed: {e}")))?;
Ok(serde_json::json!({
"party_id": party_id,
"tx_hash": tx_hash,
}))
},
)
}
pub(crate) fn fund_party_tool() -> std::sync::Arc<dyn crate::tools::Tool> {
let schema = crate::tool_params::FundPartyParams::schema();
ClosureTool::new(
"fund_party",
"Contribute $LH from your wallet into a party's pooled pot. Anyone can fund; the \
creator completes the party to split the pot to the members' TBAs by their shares. \
Moves value: confirm the amount with the owner before calling. Returns \
{ party_id, amount_lh, tx_hash }.",
schema,
|args: serde_json::Value, _ctx| async move {
let p = crate::tool_params::FundPartyParams::lenient(&args);
let party_id = p.party_id()?;
let amount_arg = p.amount_lh.trim().to_string();
let amount_wei = crate::encoding::parse_token_amount(&amount_arg).ok_or_else(|| {
crate::error::Error::bad_args("fund_party", 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::bad_args("fund_party", "amount_lh must be greater than 0"));
}
let signer = bounty_signer().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_party_sponsored_bridged(&signer, party_id, amount_wei, bridge_wei)
.await
.map_err(|e| crate::error::Error::other(format!("fund_party failed: {e}")))?;
Ok(serde_json::json!({
"party_id": party_id,
"amount_lh": amount_arg,
"tx_hash": tx_hash,
}))
},
)
}
pub(crate) fn complete_party_tool() -> std::sync::Arc<dyn crate::tools::Tool> {
let schema = crate::tool_params::CompletePartyParams::schema();
ClosureTool::new(
"complete_party",
"Complete a party you formed — this RELEASES the pooled $LH to the members' TBAs \
by their agreed shares and dissolves the party. Call it only once the party is \
Active (every member has consented via join_party) and funded. Moves value. \
Returns { party_id, tx_hash }.",
schema,
|args: serde_json::Value, _ctx| async move {
let party_id = crate::tool_params::CompletePartyParams::lenient(&args).party_id()?;
let signer = bounty_signer().await?;
let tx_hash = crate::app::registry::complete_party_sponsored(&signer, party_id)
.await
.map_err(|e| crate::error::Error::other(format!("complete_party failed: {e}")))?;
Ok(serde_json::json!({
"party_id": party_id,
"tx_hash": tx_hash,
}))
},
)
}
pub(crate) fn disband_party_tool() -> std::sync::Arc<dyn crate::tools::Tool> {
let schema = crate::tool_params::DisbandPartyParams::schema();
ClosureTool::new(
"disband_party",
"Disband a party — dissolve it and refund every funder their exact contribution. \
The creator may disband a live party any time; anyone may once its ttl has \
expired. The refund always goes to the FUNDERS, never the caller. Returns \
{ party_id, tx_hash }.",
schema,
|args: serde_json::Value, _ctx| async move {
let party_id = crate::tool_params::DisbandPartyParams::lenient(&args).party_id()?;
let signer = bounty_signer().await?;
let tx_hash = crate::app::registry::disband_party_sponsored(&signer, party_id)
.await
.map_err(|e| crate::error::Error::other(format!("disband_party failed: {e}")))?;
Ok(serde_json::json!({
"party_id": party_id,
"tx_hash": tx_hash,
}))
},
)
}
pub(crate) fn discover_parties_tool() -> std::sync::Arc<dyn crate::tools::Tool> {
ClosureTool::new(
"discover_parties",
"Find live parties (forming or active, unexpired) on-chain. Read-only registry \
scan: returns each live party with its id, status, consent tally, and pot. Use \
this to find parties you can join or fund. Returns { parties: [ { party_id, \
status, accepted_count, member_count, pot_lh } ], count }.",
serde_json::json!({ "type": "object", "properties": {}, "required": [] }),
|_args: serde_json::Value, _ctx| async move {
let ids = crate::app::registry::live_parties(0, 100)
.await
.map_err(crate::error::Error::other)?;
let mut parties = Vec::with_capacity(ids.len());
for id in ids {
if let Ok(p) = crate::app::registry::get_party(id).await {
parties.push(serde_json::json!({
"party_id": id,
"status": p.status_label(),
"accepted_count": p.accepted_count,
"member_count": p.member_count,
"pot_lh": format_lh(p.escrow_wei),
}));
}
}
Ok(serde_json::json!({
"count": parties.len(),
"parties": parties,
}))
},
)
}
pub(crate) fn get_party_tool() -> std::sync::Arc<dyn crate::tools::Tool> {
let schema = crate::tool_params::GetPartyParams::schema();
ClosureTool::new(
"get_party",
"Read full detail for one party: creator, status, members with their shares and \
consent state, the pooled pot, and expiry. Read-only. Use this before \
join_party / fund_party / complete_party. Returns { party_id, creator, status, \
pot_lh, expiry, members: [ { token_id, bps, consented } ] }.",
schema,
|args: serde_json::Value, _ctx| async move {
let party_id = crate::tool_params::GetPartyParams::lenient(&args).party_id()?;
let p = crate::app::registry::get_party(party_id)
.await
.map_err(crate::error::Error::other)?;
if p.creator.trim_start_matches("0x").chars().all(|c| c == '0') {
return Err(crate::error::Error::other(format!(
"party #{party_id} doesn't exist"
)));
}
let member_ids =
crate::app::registry::party_members_of(party_id).await.unwrap_or_default();
let shares =
crate::app::registry::party_shares_of(party_id).await.unwrap_or_default();
let mut members = Vec::with_capacity(member_ids.len());
for (i, token_id) in member_ids.iter().enumerate() {
let consented = crate::app::registry::party_consent_of(party_id, *token_id)
.await
.unwrap_or(false);
members.push(serde_json::json!({
"token_id": token_id,
"bps": shares.get(i).copied().unwrap_or(0),
"consented": consented,
}));
}
Ok(serde_json::json!({
"party_id": party_id,
"creator": p.creator,
"status": p.status_label(),
"pot_lh": format_lh(p.escrow_wei),
"expiry": p.expiry,
"members": members,
}))
},
)
}