Skip to content

Commit 68c9a0a

Browse files
committed
refactor(examples,sdk): simplify lifecycle example to single-wallet flow and enhance error handling with custom error decoding
- Refactor lifecycle.rs to use one private key for all roles (client/provider/evaluator) with inline documentation explaining contract validates msg.sender, not role distinctness - Add IERC20 approve step before funding to show complete USDC interaction pattern - Replace separate connect() helper with inline provider setup using network.rpc_url() - Add contract_address() getter
1 parent 38daada commit 68c9a0a

7 files changed

Lines changed: 183 additions & 122 deletions

File tree

erc8183/examples/lifecycle.rs

Lines changed: 63 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,97 +1,90 @@
11
//! Complete lifecycle on Monad Mainnet: `Open → Funded → Submitted → Completed`.
22
//!
3+
//! A single private key plays all three roles (client, provider, evaluator).
4+
//! This is valid because the contract checks `msg.sender == role_address`,
5+
//! not that the roles are distinct addresses.
6+
//!
37
//! ```sh
4-
//! export RPC_URL=https://rpc.monad.xyz
5-
//! export CLIENT_KEY=0xac0974...
6-
//! export PROVIDER_KEY=0x59c699...
7-
//! export EVALUATOR_KEY=0x5de411...
8+
//! export PRIVATE_KEY=0xac0974...
89
//! cargo run --example lifecycle
910
//! ```
1011
11-
use std::env;
12+
use std::{env, time::SystemTime};
1213

1314
use alloy::{
1415
network::EthereumWallet,
15-
primitives::{Address, FixedBytes, U256},
16-
providers::{Provider, ProviderBuilder},
17-
signers::local::PrivateKeySigner,
16+
primitives::{FixedBytes, U256},
17+
providers::ProviderBuilder,
18+
sol,
1819
};
19-
use erc8183::{Erc8183, Network, job::JobHandle, types::CreateJobParams};
20-
21-
fn connect(
22-
rpc: &str,
23-
key_env: &str,
24-
) -> Result<(Erc8183<impl Provider>, Address), Box<dyn std::error::Error>> {
25-
let signer: PrivateKeySigner = env::var(key_env)?.parse()?;
26-
let addr = signer.address();
27-
let sdk = Erc8183::new(
28-
ProviderBuilder::new()
29-
.wallet(EthereumWallet::from(signer))
30-
.connect_http(rpc.parse()?),
31-
)
32-
.with_network(Network::MonadMainnet);
33-
Ok((sdk, addr))
34-
}
35-
36-
/// Client: create job → set budget → fund escrow.
37-
async fn client_create_and_fund<P: Provider>(
38-
job: &JobHandle<P>,
39-
provider: Address,
40-
evaluator: Address,
41-
) -> erc8183::Result<U256> {
42-
let params = CreateJobParams::new(
43-
provider,
44-
evaluator,
45-
U256::from(u64::MAX),
46-
"Analyze market data",
47-
);
48-
let id = job.create_job(&params).await?;
49-
let budget = U256::from(1_000_000u64);
50-
job.set_budget(id, budget, None).await?;
51-
job.fund(id, budget, None).await?;
52-
Ok(id)
53-
}
20+
use erc8183::{Erc8183, Network, types::CreateJobParams};
5421

55-
/// Provider: submit work deliverable.
56-
async fn provider_submit<P: Provider>(job: &JobHandle<P>, id: U256) -> erc8183::Result<()> {
57-
job.submit(id, FixedBytes::from([0xAB; 32]), None).await
22+
sol! {
23+
#[sol(rpc)]
24+
interface IERC20 {
25+
function approve(address spender, uint256 amount) external returns (bool);
26+
}
5827
}
5928

60-
/// Evaluator: attest completion → release escrow.
61-
async fn evaluator_complete<P: Provider>(job: &JobHandle<P>, id: U256) -> erc8183::Result<()> {
62-
job.complete(id, FixedBytes::ZERO, None).await
29+
fn now_secs() -> u64 {
30+
SystemTime::now()
31+
.duration_since(SystemTime::UNIX_EPOCH)
32+
.expect("system clock before epoch")
33+
.as_secs()
6334
}
6435

6536
#[allow(clippy::print_stdout)]
6637
#[tokio::main]
6738
async fn main() -> Result<(), Box<dyn std::error::Error>> {
68-
let rpc = env::var("RPC_URL")?;
39+
let network = Network::MonadMainnet;
40+
let signer: alloy::signers::local::PrivateKeySigner = env::var("PRIVATE_KEY")?.parse()?;
41+
let addr = signer.address();
42+
43+
let provider = ProviderBuilder::new()
44+
.wallet(EthereumWallet::from(signer))
45+
.connect_http(network.rpc_url().parse()?);
6946

70-
let (client_sdk, _) = connect(&rpc, "CLIENT_KEY")?;
71-
let (provider_sdk, provider_addr) = connect(&rpc, "PROVIDER_KEY")?;
72-
let (evaluator_sdk, evaluator_addr) = connect(&rpc, "EVALUATOR_KEY")?;
47+
let sdk = Erc8183::new(&provider).with_network(network);
48+
let job = sdk.job()?;
7349

74-
let client = client_sdk.job()?;
75-
let provider = provider_sdk.job()?;
76-
let evaluator = evaluator_sdk.job()?;
50+
println!("Network: {network}");
51+
println!("Contract: {}", job.contract_address());
52+
println!("Wallet: {addr}\n");
53+
54+
// 1. Create job — one wallet is client, provider, and evaluator
55+
let expires_at = U256::from(now_secs() + 3600);
56+
let params = CreateJobParams::new(addr, addr, expires_at, "Analyze market data");
57+
let id = job.create_job(&params).await?;
58+
println!("[1/5] Created job #{id}");
7759

78-
// Open → Funded → Submitted → Completed
79-
let id = client_create_and_fund(&client, provider_addr, evaluator_addr).await?;
80-
println!("[Client] Created & funded job #{id}");
60+
// 2. Set budget (1 USDC = 1_000_000 units with 6 decimals)
61+
let budget = U256::from(1_000_000u64);
62+
job.set_budget(id, budget, None).await?;
63+
println!("[2/5] Budget set: {budget} (1 USDC)");
64+
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?;
74+
println!("[3/5] Funded escrow");
8175

82-
provider_submit(&provider, id).await?;
83-
println!("[Provider] Submitted deliverable");
76+
// 4. Submit work deliverable
77+
job.submit(id, FixedBytes::from([0xAB; 32]), None).await?;
78+
println!("[4/5] Submitted deliverable");
8479

85-
evaluator_complete(&evaluator, id).await?;
86-
println!("[Evaluator] Completed — payment released");
80+
// 5. Evaluator completes — releases payment
81+
job.complete(id, FixedBytes::ZERO, None).await?;
82+
println!("[5/5] Completed — payment released");
8783

88-
let job = client.get_job(id).await?;
89-
println!(
90-
"\nJob #{}: {} (terminal: {})",
91-
job.id,
92-
job.status,
93-
job.status.is_terminal()
94-
);
84+
// Query final state
85+
let data = job.get_job(id).await?;
86+
println!("\n{data}");
87+
println!("Terminal: {}", data.status.is_terminal());
9588

9689
Ok(())
9790
}

erc8183/src/client.rs

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,14 @@
88
//! use erc8183::{Erc8183, Network};
99
//!
1010
//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
11+
//! let network = Network::MonadMainnet;
1112
//! let provider = ProviderBuilder::new()
12-
//! .connect_http("https://monad-rpc.example.com".parse()?);
13+
//! .connect_http(network.rpc_url().parse()?);
1314
//!
14-
//! // Built-in network (Monad Mainnet)
15-
//! let client = Erc8183::new(provider)
16-
//! .with_network(Network::MonadMainnet);
17-
//!
18-
//! let job = client.job()?.get_job(U256::from(1)).await?;
15+
//! let job = Erc8183::new(provider)
16+
//! .with_network(network)
17+
//! .job()?
18+
//! .get_job(U256::from(1)).await?;
1919
//! # Ok(())
2020
//! # }
2121
//! ```
@@ -44,11 +44,12 @@ use crate::{
4444
/// use erc8183::{Erc8183, Network};
4545
///
4646
/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
47+
/// let network = Network::MonadMainnet;
4748
/// let client = Erc8183::new(
4849
/// ProviderBuilder::new()
49-
/// .connect_http("https://monad-rpc.example.com".parse()?),
50+
/// .connect_http(network.rpc_url().parse()?),
5051
/// )
51-
/// .with_network(Network::MonadMainnet);
52+
/// .with_network(network);
5253
/// # Ok(())
5354
/// # }
5455
/// ```

erc8183/src/error.rs

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
//! Typed error definitions for the ERC-8183 SDK.
22
3-
use alloy::primitives::U256;
3+
use alloy::sol_types::SolError;
4+
5+
use crate::contracts::AgenticCommerce;
46

57
/// The primary error type for all ERC-8183 SDK operations.
68
#[derive(Debug, thiserror::Error)]
@@ -17,11 +19,11 @@ pub enum Error {
1719
#[error("contract not configured")]
1820
ContractNotConfigured,
1921

20-
/// The requested job does not exist on-chain.
21-
#[error("job {job_id} does not exist")]
22-
JobNotFound {
23-
/// The queried job ID.
24-
job_id: U256,
22+
/// Expected event was not found in a transaction receipt.
23+
#[error("event not found in receipt: {context}")]
24+
EventNotFound {
25+
/// Description of which event was expected.
26+
context: &'static str,
2527
},
2628

2729
/// A pending transaction was dropped or failed to confirm.
@@ -42,3 +44,41 @@ pub enum Error {
4244

4345
/// A convenience type alias used throughout the SDK.
4446
pub type Result<T> = core::result::Result<T, Error>;
47+
48+
/// Decode a 4-byte revert selector into a human-readable error name.
49+
///
50+
/// Returns the error name if it matches a known `AgenticCommerce` custom
51+
/// error, or `None` for unrecognized selectors.
52+
///
53+
/// # Examples
54+
///
55+
/// ```
56+
/// assert_eq!(
57+
/// erc8183::error::decode_revert_reason(&[0xd3, 0x6c, 0x85, 0x00]),
58+
/// Some("InvalidExpiry"),
59+
/// );
60+
/// ```
61+
#[must_use]
62+
pub fn decode_revert_reason(data: &[u8]) -> Option<&'static str> {
63+
let sel: [u8; 4] = data.get(..4)?.try_into().ok()?;
64+
match sel {
65+
AgenticCommerce::ZeroAddress::SELECTOR => Some("ZeroAddress"),
66+
AgenticCommerce::InvalidExpiry::SELECTOR => Some("InvalidExpiry"),
67+
AgenticCommerce::InvalidStatus::SELECTOR => Some("InvalidStatus"),
68+
AgenticCommerce::Unauthorized::SELECTOR => Some("Unauthorized"),
69+
AgenticCommerce::ProviderAlreadySet::SELECTOR => Some("ProviderAlreadySet"),
70+
AgenticCommerce::ProviderNotSet::SELECTOR => Some("ProviderNotSet"),
71+
AgenticCommerce::BudgetMismatch::SELECTOR => Some("BudgetMismatch"),
72+
AgenticCommerce::ZeroBudget::SELECTOR => Some("ZeroBudget"),
73+
AgenticCommerce::JobAlreadyExpired::SELECTOR => Some("JobAlreadyExpired"),
74+
AgenticCommerce::JobNotExpired::SELECTOR => Some("JobNotExpired"),
75+
AgenticCommerce::FeeTooHigh::SELECTOR => Some("FeeTooHigh"),
76+
AgenticCommerce::JobDoesNotExist::SELECTOR => Some("JobDoesNotExist"),
77+
AgenticCommerce::HookNotWhitelisted::SELECTOR => Some("HookNotWhitelisted"),
78+
AgenticCommerce::HookInterfaceNotSupported::SELECTOR => Some("HookInterfaceNotSupported"),
79+
AgenticCommerce::DescriptionTooLong::SELECTOR => Some("DescriptionTooLong"),
80+
AgenticCommerce::OwnableUnauthorizedAccount::SELECTOR => Some("OwnableUnauthorizedAccount"),
81+
AgenticCommerce::OwnableInvalidOwner::SELECTOR => Some("OwnableInvalidOwner"),
82+
_ => None,
83+
}
84+
}

erc8183/src/job.rs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,12 @@ impl<P: Provider> JobHandle<P> {
4848
Self { address, provider }
4949
}
5050

51+
/// Returns the contract address this handle points to.
52+
#[must_use]
53+
pub const fn contract_address(&self) -> Address {
54+
self.address
55+
}
56+
5157
/// Standard ERC-8183 interface — portable across any compliant contract.
5258
const fn standard(&self) -> IERC8183::IERC8183Instance<&P> {
5359
IERC8183::new(self.address, &self.provider)
@@ -476,8 +482,8 @@ impl<P: Provider> JobHandle<P> {
476482
.ok()
477483
.map(|e| e.inner.data.jobId)
478484
})
479-
.ok_or(Error::Contract(alloy::contract::Error::UnknownFunction(
480-
"createJob: no JobCreated event found".to_owned(),
481-
)))
485+
.ok_or(Error::EventNotFound {
486+
context: "JobCreated event not found in createJob receipt",
487+
})
482488
}
483489
}

erc8183/src/lib.rs

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,30 +17,28 @@
1717
//! use erc8183::{Erc8183, Network, types::CreateJobParams};
1818
//!
1919
//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
20-
//! // 1. Create an alloy provider (any transport works: HTTP, WS, IPC)
20+
//! // 1. Connect using the built-in RPC URL
21+
//! let network = Network::MonadMainnet;
2122
//! let provider = ProviderBuilder::new()
22-
//! .connect_http("https://monad-rpc.example.com".parse()?);
23+
//! .connect_http(network.rpc_url().parse()?);
2324
//!
24-
//! // 2. Wrap it with the ERC-8183 client
25-
//! let client = Erc8183::new(provider)
26-
//! .with_network(Network::MonadMainnet);
25+
//! // 2. Create client and get a job handle
26+
//! let job = Erc8183::new(provider)
27+
//! .with_network(network)
28+
//! .job()?;
2729
//!
28-
//! // 3. Get a job handle for contract interactions
29-
//! let job = client.job()?;
30-
//!
31-
//! // 4. Create a job (requires signer-enabled provider)
30+
//! // 3. Create a job (requires signer-enabled provider)
3231
//! let params = CreateJobParams::new(
3332
//! Address::ZERO, // deferred provider
3433
//! "0xEvaluator...".parse()?, // evaluator
3534
//! U256::from(1_700_000_000u64), // expiredAt
3635
//! "Build a REST API", // description
3736
//! );
3837
//! let job_id = job.create_job(&params).await?;
39-
//! println!("Created job: {job_id}");
4038
//!
41-
//! // 5. Query job data
39+
//! // 4. Query job data
4240
//! let data = job.get_job(job_id).await?;
43-
//! println!("Job status: {}", data.status);
41+
//! println!("{data}"); // "Job #1 [Open] budget=0 client=0x... provider=0x..."
4442
//! # Ok(())
4543
//! # }
4644
//! ```

erc8183/src/networks.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
1010
use alloy::primitives::{Address, address};
1111

12+
const MONAD_MAINNET_RPC: &str = "https://rpc.monad.xyz";
13+
const MONAD_MAINNET_EXPLORER: &str = "https://monad.socialscan.io";
14+
1215
/// The Monad Mainnet `AgenticCommerce` deployment.
1316
const MONAD_MAINNET: Address = address!("E8c4FFb4A6F7B8040a7AE39F6651290E06A40725");
1417

@@ -40,6 +43,28 @@ impl Network {
4043
}
4144
}
4245

46+
/// Returns the public RPC endpoint URL for this network.
47+
#[must_use]
48+
pub const fn rpc_url(self) -> &'static str {
49+
match self {
50+
Self::MonadMainnet => MONAD_MAINNET_RPC,
51+
}
52+
}
53+
54+
/// Returns the block explorer base URL for this network.
55+
#[must_use]
56+
pub const fn explorer_base_url(self) -> &'static str {
57+
match self {
58+
Self::MonadMainnet => MONAD_MAINNET_EXPLORER,
59+
}
60+
}
61+
62+
/// Returns a block explorer URL for a given contract address.
63+
#[must_use]
64+
pub fn explorer_url(self, address: Address) -> String {
65+
format!("{}/address/{address}", self.explorer_base_url())
66+
}
67+
4368
/// All known ERC-8183 network variants.
4469
pub const ALL: &[Self] = &[Self::MonadMainnet];
4570

@@ -51,3 +76,11 @@ impl Network {
5176
Self::ALL.iter().find(|n| n.chain_id() == chain_id).copied()
5277
}
5378
}
79+
80+
impl std::fmt::Display for Network {
81+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82+
match self {
83+
Self::MonadMainnet => write!(f, "Monad Mainnet"),
84+
}
85+
}
86+
}

0 commit comments

Comments
 (0)