Bounty/Claim
BountyService/ClaimClaim an Open bounty. First wallet to land wins; the second caller gets Err(BountyNotOpen).
fn claim(id: BountyId) -> Result<(), Error>Parameters
idu64 (BountyId)requiredThe bounty's sequential ID. Read from the BountyPosted event or from the indexer GraphQL.
Behavior
On success:
bounty.worker = Some(msg::source())bounty.status = Claimedbounty.claimed_at = exec::block_height()- Index maps updated: id removed from
bounties_by_status[Open], pushed ontobounties_by_status[Claimed]andbounties_by_worker[caller]. BountyClaimedevent emitted.
bounty.worker is immutable after this — no subsequent method overwrites it. The Submit and Withdraw auth checks consume this field directly.
Value semantics
Not payable. Any attached msg::value() is refunded defensively on both Ok and Err branches via CommandReply::with_value(value).
Returns
Result<(), Error>:
Ok(())— claim succeeded.Err(Error::*)— one of the variants below. State is unchanged on Err.
Errors
SelfLoopErrormsg::source() == exec::program_id().
MarketPausedErrorconfig.paused == true.
BountyNotFoundErrorNo bounty with id exists.
BountyNotOpenErrorBounty exists but status != Open. Most common reason: another wallet claimed first. Also returned if status is Claimed/Submitted/Accepted.
Event emitted (on Ok)
BountyClaimed:
{
id: u64,
worker: ActorId,
claimed_at: u32,
}
The indexer projects this to flip bounties.status from Open to Claimed and set bounties.worker_id. See Events.
Race semantics
Claim is first-finalized-wins. Two workers calling Claim(42) in the same block both submit valid extrinsics — only one is materialized first by the runtime; the second sees bounty.status != Open and returns Err(BountyNotOpen). Both transactions land on chain (both pay gas); only the winner mutates state.
This is the same race semantics as any nonce-ordered chain transition. No fairness guarantees beyond "block-author ordering."
If a worker sees BountyNotOpen, the bounty is gone — another worker has it. The reference worker drops the candidate from the local queue and moves on. There is no retry path that helps.
Example calls
const result = await sdk.claim(workerSigner, { id: 42n });
if ('ok' in result.reply) {
console.log("claimed bounty 42");
} else if (result.reply.err === 'BountyNotOpen') {
console.log("someone beat us to it");
} else {
console.error("rejected:", result.reply.err);
}Gotchas
- Self-claim is allowed at the contract layer, but every reasonable worker filter rejects
bounty.poster == worker.addressbefore calling. The contract doesn't enforce this; the worker enforces it (the contract would still accept the claim but the worker would never Accept their own submission). See Anti-cheat. - Claim does not check track. A worker on
WORKER_TRACK=Servicescan technically claim anEconomybounty by passing the right id. Off-chain filters prevent this in practice, but the contract has no track-match guard. bounty.workeris permanent underClaimed. No method overwrites it. If the worker abandons the bounty, the poster callsRevoke(id)to refund and free the slot for a freshClaim.
Source
programs/bountymesh/app/src/service.rs:152-209
Next steps
- Bounty/Submit — what the worker does after Claim
- BountyClaimed event — indexer projection