Skip to content

Commit 1c54955

Browse files
committed
feat(sdk): add approve_and_fund convenience method and payment token helpers
- Add IERC20 interface to contracts.rs with approve/allowance/balanceOf functions - Implement approve_payment_token() for explicit token approval before funding - Add approve_and_fund() convenience method combining approval + funding in single call - Add payment_token_allowance() and payment_token_balance() query helpers - Simplify lifecycle.rs example by replacing manual IERC20 approval with approve_and_fund() - Remove
1 parent 68c9a0a commit 1c54955

3 files changed

Lines changed: 82 additions & 18 deletions

File tree

erc8183/examples/lifecycle.rs

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,9 @@ use alloy::{
1515
network::EthereumWallet,
1616
primitives::{FixedBytes, U256},
1717
providers::ProviderBuilder,
18-
sol,
1918
};
2019
use erc8183::{Erc8183, Network, types::CreateJobParams};
2120

22-
sol! {
23-
#[sol(rpc)]
24-
interface IERC20 {
25-
function approve(address spender, uint256 amount) external returns (bool);
26-
}
27-
}
28-
2921
fn now_secs() -> u64 {
3022
SystemTime::now()
3123
.duration_since(SystemTime::UNIX_EPOCH)
@@ -62,15 +54,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
6254
job.set_budget(id, budget, None).await?;
6355
println!("[2/5] Budget set: {budget} (1 USDC)");
6456

65-
// 3. Approve USDC spend, then fund escrow
66-
let usdc = job.payment_token().await?;
67-
IERC20::new(usdc, &provider)
68-
.approve(job.contract_address(), budget)
69-
.send()
70-
.await?
71-
.get_receipt()
72-
.await?;
73-
job.fund(id, budget, None).await?;
57+
// 3. Approve USDC + fund escrow (single convenience call)
58+
job.approve_and_fund(id, budget, None).await?;
7459
println!("[3/5] Funded escrow");
7560

7661
// 4. Submit work deliverable

erc8183/src/contracts.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,20 @@
1818
1919
use alloy::sol;
2020

21+
sol! {
22+
/// Minimal ERC-20 interface for token approval operations.
23+
///
24+
/// Only includes `approve` and `allowance` — the two functions needed
25+
/// to interact with the payment token before funding a job.
26+
#[allow(missing_docs)]
27+
#[sol(rpc)]
28+
interface IERC20 {
29+
function approve(address spender, uint256 amount) external returns (bool);
30+
function allowance(address owner, address spender) external view returns (uint256);
31+
function balanceOf(address account) external view returns (uint256);
32+
}
33+
}
34+
2135
sol! {
2236
/// IERC8183 — Standard interface for the Agentic Commerce Protocol.
2337
///

erc8183/src/job.rs

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ use alloy::{
2121
};
2222

2323
use crate::{
24-
contracts::{AgenticCommerce, IERC8183},
24+
contracts::{AgenticCommerce, IERC20, IERC8183},
2525
error::{Error, Result},
2626
types::{CreateJobParams, Job, JobStatus},
2727
};
@@ -254,6 +254,71 @@ impl<P: Provider> JobHandle<P> {
254254
Ok(())
255255
}
256256

257+
/// Approve the payment token for the contract to spend.
258+
///
259+
/// This is a prerequisite for [`fund`](Self::fund). The contract must be
260+
/// approved to transfer at least `amount` tokens from the caller.
261+
///
262+
/// # Errors
263+
///
264+
/// Returns an error if the transaction fails.
265+
pub async fn approve_payment_token(&self, amount: U256) -> Result<()> {
266+
let token = self.contract().PAYMENT_TOKEN().call().await?;
267+
IERC20::new(token, &self.provider)
268+
.approve(self.address, amount)
269+
.send()
270+
.await?
271+
.get_receipt()
272+
.await?;
273+
Ok(())
274+
}
275+
276+
/// Approve and fund in one call: approve the payment token, then fund
277+
/// the escrow.
278+
///
279+
/// Convenience method that combines
280+
/// [`approve_payment_token`](Self::approve_payment_token) +
281+
/// [`fund`](Self::fund).
282+
///
283+
/// # Errors
284+
///
285+
/// Returns an error if either transaction fails.
286+
pub async fn approve_and_fund(
287+
&self,
288+
job_id: U256,
289+
expected_budget: U256,
290+
opt_params: Option<Bytes>,
291+
) -> Result<()> {
292+
self.approve_payment_token(expected_budget).await?;
293+
self.fund(job_id, expected_budget, opt_params).await
294+
}
295+
296+
/// Get the caller's current allowance for the payment token.
297+
///
298+
/// # Errors
299+
///
300+
/// Returns an error if the RPC call fails.
301+
pub async fn payment_token_allowance(&self, owner: Address) -> Result<U256> {
302+
let token = self.contract().PAYMENT_TOKEN().call().await?;
303+
Ok(IERC20::new(token, &self.provider)
304+
.allowance(owner, self.address)
305+
.call()
306+
.await?)
307+
}
308+
309+
/// Get the caller's payment token balance.
310+
///
311+
/// # Errors
312+
///
313+
/// Returns an error if the RPC call fails.
314+
pub async fn payment_token_balance(&self, account: Address) -> Result<U256> {
315+
let token = self.contract().PAYMENT_TOKEN().call().await?;
316+
Ok(IERC20::new(token, &self.provider)
317+
.balanceOf(account)
318+
.call()
319+
.await?)
320+
}
321+
257322
/// Claim a refund after job expiry.
258323
///
259324
/// Anyone may call this when `block.timestamp >= job.expiredAt` and the

0 commit comments

Comments
 (0)