use k256::ecdsa::SigningKey;
use super::*;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Bounty {
pub poster: String,
pub reward_wei: u128,
pub expiry: u64,
pub status: u8,
pub claimant_token_id: u64,
}
impl Bounty {
pub fn status_label(&self) -> &'static str {
match self.status {
0 => "open",
1 => "claimed",
2 => "submitted",
3 => "paid",
4 => "cancelled",
5 => "reclaimed",
_ => "unknown",
}
}
}
pub(crate) fn encode_post_bounty(task: &[u8], reward_wei: u128, ttl_secs: u64) -> Vec<u8> {
let padded_len = task.len().div_ceil(32) * 32;
let mut out = Vec::with_capacity(4 + 3 * 32 + 32 + padded_len);
out.extend_from_slice(&selector("postBounty(bytes,uint128,uint64)"));
out.extend_from_slice(&u256_be(3 * 32));
out.extend_from_slice(&u256_be(reward_wei));
out.extend_from_slice(&u256_be(ttl_secs as u128));
out.extend_from_slice(&u256_be(task.len() as u128));
out.extend_from_slice(task);
out.resize(out.len() + (padded_len - task.len()), 0);
out
}
pub(crate) fn encode_claim_bounty(bounty_id: u64, claimant_token_id: u64) -> Vec<u8> {
let mut out = Vec::with_capacity(4 + 64);
out.extend_from_slice(&selector("claimBounty(uint256,uint256)"));
out.extend_from_slice(&u256_be(bounty_id as u128));
out.extend_from_slice(&u256_be(claimant_token_id as u128));
out
}
pub(crate) fn encode_submit_result(bounty_id: u64, result: &[u8]) -> Vec<u8> {
let padded_len = result.len().div_ceil(32) * 32;
let mut out = Vec::with_capacity(4 + 2 * 32 + 32 + padded_len);
out.extend_from_slice(&selector("submitResult(uint256,bytes)"));
out.extend_from_slice(&u256_be(bounty_id as u128));
out.extend_from_slice(&u256_be(2 * 32)); out.extend_from_slice(&u256_be(result.len() as u128));
out.extend_from_slice(result);
out.resize(out.len() + (padded_len - result.len()), 0);
out
}
#[allow(clippy::too_many_arguments)]
pub async fn post_bounty_sponsored(
sender: &SigningKey,
fee_payer: &SigningKey,
task: &[u8],
reward_wei: u128,
ttl_secs: u64,
fee_token: &str,
) -> Result<String, String> {
let gas = 3_500_000 + (task.len() as u128) * 9_000;
sponsored_escrow_diamond_call(
sender,
fee_payer,
reward_wei,
encode_post_bounty(task, reward_wei, ttl_secs),
fee_token,
gas,
)
.await
}
pub async fn claim_bounty_sponsored(
sender: &SigningKey,
fee_payer: &SigningKey,
bounty_id: u64,
claimant_token_id: u64,
fee_token: &str,
) -> Result<String, String> {
sponsored_diamond_call(
sender,
fee_payer,
encode_claim_bounty(bounty_id, claimant_token_id),
fee_token,
400_000,
)
.await
}
pub async fn submit_result_sponsored(
sender: &SigningKey,
fee_payer: &SigningKey,
bounty_id: u64,
result: &[u8],
fee_token: &str,
) -> Result<String, String> {
let gas = 1_200_000 + (result.len() as u128) * 9_000;
sponsored_diamond_call(
sender,
fee_payer,
encode_submit_result(bounty_id, result),
fee_token,
gas,
)
.await
}
pub async fn accept_result_sponsored(
sender: &SigningKey,
fee_payer: &SigningKey,
bounty_id: u64,
fee_token: &str,
) -> Result<String, String> {
sponsored_diamond_call(
sender,
fee_payer,
call_uint_bytes("acceptResult(uint256)", bounty_id),
fee_token,
2_000_000,
)
.await
}
pub async fn cancel_bounty_sponsored(
sender: &SigningKey,
fee_payer: &SigningKey,
bounty_id: u64,
fee_token: &str,
) -> Result<String, String> {
sponsored_diamond_call(
sender,
fee_payer,
call_uint_bytes("cancelBounty(uint256)", bounty_id),
fee_token,
600_000,
)
.await
}
pub async fn reclaim_expired_sponsored(
sender: &SigningKey,
fee_payer: &SigningKey,
bounty_id: u64,
fee_token: &str,
) -> Result<String, String> {
sponsored_diamond_call(
sender,
fee_payer,
call_uint_bytes("reclaimExpired(uint256)", bounty_id),
fee_token,
600_000,
)
.await
}
pub async fn open_bounties(start_after: u64, limit: u64) -> Result<Vec<u64>, String> {
let result = read_view(
selector("openBounties(uint256,uint256)"),
&[u256_be(start_after as u128), u256_be(limit as u128)],
)
.await?;
let bytes = hex_to_bytes(&result)?;
decode_uint_array_with_cursor(&bytes)
}
pub(crate) fn decode_uint_array_with_cursor(bytes: &[u8]) -> Result<Vec<u64>, String> {
if bytes.len() < 64 {
return Ok(Vec::new());
}
let mut off_buf = [0u8; 8];
off_buf.copy_from_slice(&bytes[24..32]);
let arr_off = u64::from_be_bytes(off_buf) as usize;
let len_start = match arr_off.checked_add(32) {
Some(s) if s <= bytes.len() => arr_off,
_ => return Ok(Vec::new()),
};
let mut len_buf = [0u8; 8];
len_buf.copy_from_slice(&bytes[len_start + 24..len_start + 32]);
let len = u64::from_be_bytes(len_buf) as usize;
let body = len_start + 32; let mut out = Vec::new();
for i in 0..len {
let start = match i.checked_mul(32).and_then(|o| o.checked_add(body)) {
Some(s) => s,
None => break,
};
let Some(word) = start.checked_add(32).and_then(|end| bytes.get(start + 24..end)) else {
break;
};
let mut id_buf = [0u8; 8];
id_buf.copy_from_slice(word);
out.push(u64::from_be_bytes(id_buf));
}
Ok(out)
}
pub async fn bounties_of(account_hex: &str) -> Result<Vec<u64>, String> {
let account = parse_eth_address(account_hex)?;
let result = read_view(selector("bountiesOf(address)"), &[addr_word(&account)]).await?;
let bytes = hex_to_bytes(&result)?;
Ok(decode_u64_array(&bytes))
}
pub async fn get_bounty(bounty_id: u64) -> Result<Bounty, String> {
let result = read_view(selector("getBounty(uint256)"), &[u256_be(bounty_id as u128)]).await?;
let bytes = hex_to_bytes(&result)?;
if bytes.len() < 5 * 32 {
return Err(format!("getBounty: short response {} bytes", bytes.len()));
}
let word = |i: usize| &bytes[i * 32..(i + 1) * 32];
let poster = format!("0x{}", bytes_to_hex(&word(0)[12..32])); Ok(Bounty {
poster,
reward_wei: u128_low(word(1)), expiry: u64_low(word(2)),
status: bytes[3 * 32 + 31], claimant_token_id: u64_low(word(4)),
})
}
pub async fn task_of_bounty(bounty_id: u64) -> Result<String, String> {
decode_bytes_string_call("bountyTaskOf(uint256)", bounty_id, "bountyTaskOf").await
}
pub async fn result_of_bounty(bounty_id: u64) -> Result<String, String> {
decode_bytes_string_call("resultOf(uint256)", bounty_id, "resultOf").await
}
pub(crate) async fn decode_bytes_string_call(sig: &str, id: u64, what: &str) -> Result<String, String> {
let result = read_view(selector(sig), &[u256_be(id as u128)]).await?;
let raw = hex_to_bytes(&result)?;
if raw.len() < 64 {
return Err(format!("{what}: short response {} bytes", raw.len()));
}
let len = u64::from_be_bytes(
raw[56..64].try_into().map_err(|e: std::array::TryFromSliceError| e.to_string())?,
) as usize;
let end = len
.checked_add(64)
.filter(|&end| end <= raw.len())
.ok_or_else(|| format!("{what}: truncated body (len {}, have {})", len, raw.len()))?;
String::from_utf8(raw[64..end].to_vec()).map_err(|e| e.to_string())
}
pub(crate) fn call_uint_bytes(sig: &str, id: u64) -> Vec<u8> {
let mut data = Vec::with_capacity(4 + 32);
data.extend_from_slice(&selector(sig));
data.extend_from_slice(&u256_be(id as u128));
data
}
pub async fn discover_bounties(query: &str, scan: u64) -> Result<Vec<(u64, String, u128)>, String> {
let ids = open_bounties(0, scan).await?;
if ids.is_empty() {
return Ok(Vec::new());
}
let mut entries: Vec<(u64, String, u128)> = Vec::with_capacity(ids.len());
let mut pairs: Vec<(String, String)> = Vec::with_capacity(ids.len());
for id in ids {
let task = task_of_bounty(id).await.unwrap_or_default();
let reward = get_bounty(id).await.map(|b| b.reward_wei).unwrap_or(0);
pairs.push((id.to_string(), task.clone()));
entries.push((id, task, reward));
}
let ranked = rank_agent_matches(&pairs, query);
let mut out: Vec<(u64, String, u128)> = Vec::with_capacity(ranked.len());
for (id_str, _task) in ranked {
if let Some(entry) = entries.iter().find(|(id, _, _)| id.to_string() == id_str) {
out.push(entry.clone());
}
}
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn post_bounty_calldata_layout() {
let task = b"audit my solidity contract"; let reward = 5_000_000_000_000_000_000u128; let cd = encode_post_bounty(task, reward, 86_400);
assert_eq!(&cd[0..4], &selector("postBounty(bytes,uint128,uint64)"));
assert_eq!(cd.len(), 4 + 3 * 32 + 32 + 32);
assert_eq!(u64::from_be_bytes(cd[4 + 24..4 + 32].try_into().unwrap()), 3 * 32);
assert_eq!(
u128::from_be_bytes(cd[4 + 32 + 16..4 + 2 * 32].try_into().unwrap()),
reward
);
assert_eq!(u64::from_be_bytes(cd[4 + 2 * 32 + 24..4 + 3 * 32].try_into().unwrap()), 86_400);
assert_eq!(
u64::from_be_bytes(cd[4 + 3 * 32 + 24..4 + 4 * 32].try_into().unwrap()),
task.len() as u64
);
assert_eq!(&cd[4 + 4 * 32..4 + 4 * 32 + task.len()], task);
assert_eq!(&cd[4 + 4 * 32 + task.len()..], &[0u8; 32 - 26]);
}
#[test]
fn post_bounty_task_exact_multiple_no_extra_pad() {
let task = [0xCDu8; 64];
let cd = encode_post_bounty(&task, 1, 60);
assert_eq!(cd.len(), 4 + 3 * 32 + 32 + 64);
assert_eq!(&cd[4 + 4 * 32..], &task);
}
#[test]
fn claim_bounty_calldata_layout() {
let cd = encode_claim_bounty(7, 42);
assert_eq!(&cd[0..4], &selector("claimBounty(uint256,uint256)"));
assert_eq!(cd.len(), 4 + 64);
assert_eq!(u64::from_be_bytes(cd[4 + 24..4 + 32].try_into().unwrap()), 7); assert_eq!(u64::from_be_bytes(cd[4 + 32 + 24..4 + 2 * 32].try_into().unwrap()), 42); }
#[test]
fn submit_result_calldata_layout() {
let result = b"done: see ipfs://Qm..."; let cd = encode_submit_result(3, result);
assert_eq!(&cd[0..4], &selector("submitResult(uint256,bytes)"));
assert_eq!(cd.len(), 4 + 2 * 32 + 32 + 32);
assert_eq!(u64::from_be_bytes(cd[4 + 24..4 + 32].try_into().unwrap()), 3);
assert_eq!(u64::from_be_bytes(cd[4 + 32 + 24..4 + 2 * 32].try_into().unwrap()), 2 * 32);
assert_eq!(
u64::from_be_bytes(cd[4 + 2 * 32 + 24..4 + 3 * 32].try_into().unwrap()),
result.len() as u64
);
assert_eq!(&cd[4 + 3 * 32..4 + 3 * 32 + result.len()], result);
assert_eq!(&cd[4 + 3 * 32 + result.len()..], &[0u8; 32 - 22]);
}
#[test]
fn single_arg_bounty_calldata_layouts() {
for sig in [
"acceptResult(uint256)",
"cancelBounty(uint256)",
"reclaimExpired(uint256)",
] {
let cd = call_uint_bytes(sig, 11);
assert_eq!(&cd[0..4], &selector(sig));
assert_eq!(cd.len(), 36);
assert_eq!(u64::from_be_bytes(cd[28..36].try_into().unwrap()), 11);
}
}
#[test]
fn open_bounties_cursor_decode() {
let mut bytes = Vec::new();
bytes.extend_from_slice(&u256_be(64));
bytes.extend_from_slice(&u256_be(99)); bytes.extend_from_slice(&u256_be(3));
bytes.extend_from_slice(&u256_be(5));
bytes.extend_from_slice(&u256_be(8));
bytes.extend_from_slice(&u256_be(13));
let ids = decode_uint_array_with_cursor(&bytes).unwrap();
assert_eq!(ids, vec![5, 8, 13]);
}
#[test]
fn open_bounties_cursor_decode_hostile() {
assert!(decode_uint_array_with_cursor(&[]).unwrap().is_empty());
assert!(decode_uint_array_with_cursor(&[0u8; 32]).unwrap().is_empty());
let mut bytes = Vec::new();
bytes.extend_from_slice(&u256_be(64));
bytes.extend_from_slice(&u256_be(0));
bytes.extend_from_slice(&u256_be(1000)); bytes.extend_from_slice(&u256_be(7)); let ids = decode_uint_array_with_cursor(&bytes).unwrap();
assert_eq!(ids, vec![7]); }
#[test]
fn bounty_status_label_maps_enum() {
let mut b = Bounty {
poster: "0x00".into(),
reward_wei: 0,
expiry: 0,
status: 0,
claimant_token_id: 0,
};
for (s, label) in [
(0u8, "open"),
(1, "claimed"),
(2, "submitted"),
(3, "paid"),
(4, "cancelled"),
(5, "reclaimed"),
(9, "unknown"),
] {
b.status = s;
assert_eq!(b.status_label(), label);
}
}
#[test]
fn bounty_rank_over_task_text() {
let pairs = vec![
("1".to_string(), "audit a solidity contract".to_string()),
("2".to_string(), "write a poem".to_string()),
("3".to_string(), "SOLIDITY gas review".to_string()),
];
let hits = rank_agent_matches(&pairs, "solidity");
assert_eq!(hits.len(), 2);
assert_eq!(hits[0].0, "1");
assert_eq!(hits[1].0, "3");
assert_eq!(rank_agent_matches(&pairs, "").len(), 3);
}
}