use k256::ecdsa::SigningKey;
use super::*;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GuildRole {
None,
Member,
Officer,
Admin,
}
impl GuildRole {
pub fn from_u8(v: u8) -> GuildRole {
match v {
1 => GuildRole::Member,
2 => GuildRole::Officer,
3 => GuildRole::Admin,
_ => GuildRole::None,
}
}
pub fn as_u8(self) -> u8 {
match self {
GuildRole::None => 0,
GuildRole::Member => 1,
GuildRole::Officer => 2,
GuildRole::Admin => 3,
}
}
pub fn label(self) -> &'static str {
match self {
GuildRole::None => "none",
GuildRole::Member => "member",
GuildRole::Officer => "officer",
GuildRole::Admin => "admin",
}
}
pub fn parse(raw: &str) -> Result<GuildRole, String> {
match raw.trim().to_ascii_lowercase().as_str() {
"member" => Ok(GuildRole::Member),
"officer" => Ok(GuildRole::Officer),
"admin" => Ok(GuildRole::Admin),
other => Err(format!(
"invalid role '{other}' — expected member, officer, or admin"
)),
}
}
}
pub(crate) fn encode_create_guild(name: &str) -> Vec<u8> {
let bytes = name.as_bytes();
let padded_len = bytes.len().div_ceil(32) * 32;
let mut out = Vec::with_capacity(4 + 32 + 32 + padded_len);
out.extend_from_slice(&selector("createGuild(string)"));
out.extend_from_slice(&u256_be(0x20)); out.extend_from_slice(&u256_be(bytes.len() as u128)); out.extend_from_slice(bytes);
out.resize(out.len() + (padded_len - bytes.len()), 0); out
}
pub(crate) fn encode_invite_to_guild(guild_id: u64, member: &[u8; 20]) -> Vec<u8> {
let mut out = Vec::with_capacity(4 + 64);
out.extend_from_slice(&selector("inviteToGuild(uint256,address)"));
out.extend_from_slice(&u256_be(guild_id as u128));
out.extend_from_slice(&addr_word(member));
out
}
pub(crate) fn encode_set_role(guild_id: u64, member: &[u8; 20], role: u8) -> Vec<u8> {
let mut out = Vec::with_capacity(4 + 96);
out.extend_from_slice(&selector("setRole(uint256,address,uint8)"));
out.extend_from_slice(&u256_be(guild_id as u128));
out.extend_from_slice(&addr_word(member));
out.extend_from_slice(&u256_be(role as u128));
out
}
pub(crate) fn encode_fund_guild(guild_id: u64, amount_wei: u128) -> Vec<u8> {
let mut out = Vec::with_capacity(4 + 64);
out.extend_from_slice(&selector("fundGuild(uint256,uint256)"));
out.extend_from_slice(&u256_be(guild_id as u128));
out.extend_from_slice(&u256_be(amount_wei));
out
}
pub(crate) fn encode_spend_treasury(guild_id: u64, to: &[u8; 20], amount_wei: u128, memo: &[u8]) -> Vec<u8> {
let padded_len = memo.len().div_ceil(32) * 32;
let mut out = Vec::with_capacity(4 + 4 * 32 + 32 + padded_len);
out.extend_from_slice(&selector("spendTreasury(uint256,address,uint256,bytes)"));
out.extend_from_slice(&u256_be(guild_id as u128)); out.extend_from_slice(&addr_word(to)); out.extend_from_slice(&u256_be(amount_wei)); out.extend_from_slice(&u256_be(4 * 32)); out.extend_from_slice(&u256_be(memo.len() as u128)); out.extend_from_slice(memo); out.resize(out.len() + (padded_len - memo.len()), 0); out
}
pub async fn create_guild_sponsored(
sender: &SigningKey,
fee_payer: &SigningKey,
name: &str,
fee_token: &str,
) -> Result<String, String> {
let gas = 3_500_000 + (name.len() as u128) * 9_000;
sponsored_diamond_call(sender, fee_payer, encode_create_guild(name), fee_token, gas).await
}
pub async fn invite_to_guild_sponsored(
sender: &SigningKey,
fee_payer: &SigningKey,
guild_id: u64,
member_hex: &str,
fee_token: &str,
) -> Result<String, String> {
let member = parse_eth_address(member_hex)?;
sponsored_diamond_call(
sender,
fee_payer,
encode_invite_to_guild(guild_id, &member),
fee_token,
400_000,
)
.await
}
pub async fn accept_guild_invite_sponsored(
sender: &SigningKey,
fee_payer: &SigningKey,
guild_id: u64,
fee_token: &str,
) -> Result<String, String> {
sponsored_diamond_call(
sender,
fee_payer,
call_uint_bytes("acceptGuildInvite(uint256)", guild_id),
fee_token,
2_000_000,
)
.await
}
pub async fn leave_guild_sponsored(
sender: &SigningKey,
fee_payer: &SigningKey,
guild_id: u64,
fee_token: &str,
) -> Result<String, String> {
sponsored_diamond_call(
sender,
fee_payer,
call_uint_bytes("leaveGuild(uint256)", guild_id),
fee_token,
1_500_000,
)
.await
}
pub async fn set_role_sponsored(
sender: &SigningKey,
fee_payer: &SigningKey,
guild_id: u64,
member_hex: &str,
role: u8,
fee_token: &str,
) -> Result<String, String> {
let member = parse_eth_address(member_hex)?;
sponsored_diamond_call(
sender,
fee_payer,
encode_set_role(guild_id, &member, role),
fee_token,
400_000,
)
.await
}
pub async fn fund_guild_sponsored(
sender: &SigningKey,
fee_payer: &SigningKey,
guild_id: u64,
amount_wei: u128,
fee_token: &str,
) -> Result<String, String> {
sponsored_escrow_diamond_call(
sender,
fee_payer,
amount_wei,
encode_fund_guild(guild_id, amount_wei),
fee_token,
2_000_000,
)
.await
}
pub async fn spend_treasury_sponsored(
sender: &SigningKey,
fee_payer: &SigningKey,
guild_id: u64,
to_hex: &str,
amount_wei: u128,
memo: &[u8],
fee_token: &str,
) -> Result<String, String> {
let to = parse_eth_address(to_hex)?;
let gas = 2_000_000 + (memo.len() as u128) * 9_000;
sponsored_diamond_call(
sender,
fee_payer,
encode_spend_treasury(guild_id, &to, amount_wei, memo),
fee_token,
gas,
)
.await
}
pub async fn members_of_guild(guild_id: u64) -> Result<Vec<String>, String> {
let result = read_view(
selector("guildMembersOf(uint256)"),
&[u256_be(guild_id as u128)],
)
.await?;
let bytes = hex_to_bytes(&result)?;
Ok(decode_address_array(&bytes))
}
pub async fn role_of_guild(guild_id: u64, addr_hex: &str) -> Result<GuildRole, String> {
let addr = parse_eth_address(addr_hex)?;
let result = read_view(
selector("roleOf(uint256,address)"),
&[u256_be(guild_id as u128), addr_word(&addr)],
)
.await?;
let v = decode_u256_as_u64(&result)?;
Ok(GuildRole::from_u8(v as u8))
}
pub async fn is_guild_member(guild_id: u64, addr_hex: &str) -> Result<bool, String> {
let addr = parse_eth_address(addr_hex)?;
let result = read_view(
selector("isGuildMember(uint256,address)"),
&[u256_be(guild_id as u128), addr_word(&addr)],
)
.await?;
decode_u256_as_u64(&result).map(|v| v != 0)
}
pub async fn treasury_balance_of(guild_id: u64) -> Result<u128, String> {
let result = read_view(
selector("treasuryBalanceOf(uint256)"),
&[u256_be(guild_id as u128)],
)
.await?;
decode_u256_as_u128(&result)
}
pub async fn guild_address(guild_id: u64) -> Result<String, String> {
let result = read_view(
selector("guildAddress(uint256)"),
&[u256_be(guild_id as u128)],
)
.await?;
Ok(decode_address(&result).unwrap_or_else(|| zero_address().to_string()))
}
pub async fn guild_name(guild_id: u64) -> Result<String, String> {
let result = read_view(selector("guildName(uint256)"), &[u256_be(guild_id as u128)]).await?;
Ok(decode_string(&result).unwrap_or_default())
}
pub async fn guilds_of(addr_hex: &str) -> Result<Vec<u64>, String> {
let account = parse_eth_address(addr_hex)?;
let result = read_view(selector("guildsOf(address)"), &[addr_word(&account)]).await?;
let bytes = hex_to_bytes(&result)?;
Ok(decode_u64_array(&bytes))
}
pub async fn guild_count() -> Result<u64, String> {
let result = read_view(selector("guildCount()"), &[]).await?;
decode_u256_as_u64(&result)
}
#[cfg(test)]
mod guild_tests {
use super::*;
#[test]
fn create_guild_calldata_layout() {
let cd = encode_create_guild("builders");
assert_eq!(&cd[0..4], &selector("createGuild(string)"));
assert_eq!(u64::from_be_bytes(cd[4 + 24..4 + 32].try_into().unwrap()), 0x20);
assert_eq!(u64::from_be_bytes(cd[36 + 24..36 + 32].try_into().unwrap()), 8);
assert_eq!(&cd[68..68 + 8], b"builders");
assert_eq!(cd.len(), 4 + 32 + 32 + 32); }
#[test]
fn create_guild_pads_long_name() {
let name = "a-very-long-guild-name-over-32-bytes!!"; let cd = encode_create_guild(name);
assert_eq!(u64::from_be_bytes(cd[36 + 24..36 + 32].try_into().unwrap()), name.len() as u64);
assert_eq!(cd.len(), 4 + 32 + 32 + 64);
assert_eq!(&cd[68..68 + name.len()], name.as_bytes());
}
#[test]
fn invite_to_guild_calldata_layout() {
let member = [0xFFu8; 20];
let cd = encode_invite_to_guild(0x2A, &member);
assert_eq!(&cd[0..4], &selector("inviteToGuild(uint256,address)"));
assert_eq!(cd.len(), 4 + 64);
assert_eq!(u64::from_be_bytes(cd[4 + 24..4 + 32].try_into().unwrap()), 0x2A); assert!(cd[36..36 + 12].iter().all(|&b| b == 0));
assert_eq!(&cd[36 + 12..36 + 32], &member);
}
#[test]
fn set_role_calldata_layout() {
let member = [0xABu8; 20];
let cd = encode_set_role(7, &member, GuildRole::Officer.as_u8());
assert_eq!(&cd[0..4], &selector("setRole(uint256,address,uint8)"));
assert_eq!(cd.len(), 4 + 96);
assert_eq!(u64::from_be_bytes(cd[4 + 24..4 + 32].try_into().unwrap()), 7); assert_eq!(&cd[36 + 12..36 + 32], &member); assert!(cd[68..68 + 31].iter().all(|&b| b == 0));
assert_eq!(cd[68 + 31], 2);
}
#[test]
fn fund_guild_calldata_layout() {
let amount = 1_500_000_000_000_000_000u128; let cd = encode_fund_guild(9, amount);
assert_eq!(&cd[0..4], &selector("fundGuild(uint256,uint256)"));
assert_eq!(cd.len(), 4 + 64);
assert_eq!(u64::from_be_bytes(cd[4 + 24..4 + 32].try_into().unwrap()), 9); assert_eq!(u128::from_be_bytes(cd[36 + 16..36 + 32].try_into().unwrap()), amount);
}
#[test]
fn spend_treasury_calldata_layout() {
let to = [0xCDu8; 20];
let amount = 2_000_000_000_000_000_000u128; let memo = b"q3 grant"; let cd = encode_spend_treasury(0x10, &to, amount, memo);
assert_eq!(&cd[0..4], &selector("spendTreasury(uint256,address,uint256,bytes)"));
assert_eq!(u64::from_be_bytes(cd[4 + 24..4 + 32].try_into().unwrap()), 0x10);
assert_eq!(&cd[36 + 12..36 + 32], &to);
assert_eq!(u128::from_be_bytes(cd[68 + 16..68 + 32].try_into().unwrap()), amount);
assert_eq!(u64::from_be_bytes(cd[100 + 24..100 + 32].try_into().unwrap()), 0x80);
assert_eq!(u64::from_be_bytes(cd[132 + 24..132 + 32].try_into().unwrap()), memo.len() as u64);
assert_eq!(&cd[164..164 + memo.len()], memo);
assert_eq!(cd.len(), 4 + 4 * 32 + 32 + 32); }
#[test]
fn spend_treasury_empty_memo() {
let to = [0x01u8; 20];
let cd = encode_spend_treasury(1, &to, 1, b"");
assert_eq!(u64::from_be_bytes(cd[100 + 24..100 + 32].try_into().unwrap()), 0x80); assert_eq!(u64::from_be_bytes(cd[132 + 24..132 + 32].try_into().unwrap()), 0); assert_eq!(cd.len(), 4 + 4 * 32 + 32); }
#[test]
fn accept_and_leave_calldata_layout() {
let accept = call_uint_bytes("acceptGuildInvite(uint256)", 5);
assert_eq!(&accept[0..4], &selector("acceptGuildInvite(uint256)"));
assert_eq!(accept.len(), 36);
assert_eq!(u64::from_be_bytes(accept[28..36].try_into().unwrap()), 5);
let leave = call_uint_bytes("leaveGuild(uint256)", 5);
assert_eq!(&leave[0..4], &selector("leaveGuild(uint256)"));
assert_eq!(leave.len(), 36);
assert_eq!(u64::from_be_bytes(leave[28..36].try_into().unwrap()), 5);
}
#[test]
fn guild_role_from_to_u8_and_parse() {
assert_eq!(GuildRole::from_u8(0), GuildRole::None);
assert_eq!(GuildRole::from_u8(1), GuildRole::Member);
assert_eq!(GuildRole::from_u8(2), GuildRole::Officer);
assert_eq!(GuildRole::from_u8(3), GuildRole::Admin);
assert_eq!(GuildRole::from_u8(99), GuildRole::None); for r in [GuildRole::None, GuildRole::Member, GuildRole::Officer, GuildRole::Admin] {
assert_eq!(GuildRole::from_u8(r.as_u8()), r);
}
assert_eq!(GuildRole::parse("member").unwrap(), GuildRole::Member);
assert_eq!(GuildRole::parse(" OFFICER ").unwrap(), GuildRole::Officer);
assert_eq!(GuildRole::parse("Admin").unwrap(), GuildRole::Admin);
assert!(GuildRole::parse("none").is_err()); assert!(GuildRole::parse("boss").is_err());
}
}