1#![forbid(unsafe_code)]
9#![cfg(feature = "web3")]
10
11mod automation;
12mod ccip;
13pub mod channel;
14mod config;
15mod evm;
16mod hook;
17mod observe;
18mod ops;
19mod outcome_store;
20mod payments;
21mod retry;
22mod solana;
23
24use chio_core::capability::scope::MonetaryAmount;
25use serde::{Deserialize, Serialize};
26
27pub use automation::{
28 assess_watchdog_execution, build_bond_watchdog_job, build_settlement_watchdog_job,
29 SettlementAutomationExecution, SettlementAutomationOutcome, SettlementAutomationTriggerKind,
30 SettlementWatchdogJob, SettlementWatchdogKind, CHIO_SETTLEMENT_AUTOMATION_JOB_SCHEMA,
31};
32pub use ccip::{
33 prepare_ccip_settlement_message, reconcile_ccip_delivery, CcipDeliveryObservation,
34 CcipLaneConfig, CcipMessageStatus, CcipReconciliationOutcome, CcipSettlementMessage,
35 CcipSettlementPayload, CHIO_CCIP_SETTLEMENT_MESSAGE_SCHEMA,
36};
37pub use config::{
38 settlement_devnet_rpc_egress_contract, DevnetContracts, DevnetMocks, EvidenceSubstrateMode,
39 LocalDevnetDeployment, SettlementAmountTier, SettlementChainConfig, SettlementEvidenceConfig,
40 SettlementOracleAuthority, SettlementOracleConfig, SettlementPolicyConfig,
41};
42pub use evm::{
43 build_failure_receipt, build_reversal_receipt, confirm_transaction, estimate_call_gas,
44 finalize_bond_lock, finalize_escrow_dispatch, prepare_authorized_channel_merkle_release,
45 prepare_bond_expiry, prepare_bond_impair, prepare_bond_lock,
46 prepare_bond_proof_root_publication, prepare_bond_release, prepare_dual_sign_release,
47 prepare_erc20_approval, prepare_escrow_refund, prepare_merkle_release,
48 prepare_merkle_release_root_publication, prepare_web3_escrow_dispatch, read_bond_snapshot,
49 read_escrow_snapshot, scale_chio_amount_to_token_minor_units,
50 scale_token_minor_units_to_chio_amount, static_validate_call, submit_call, BondLockRequest,
51 DualSignReleaseInput, EscrowDispatchRequest, EscrowExecutionAmount, EscrowSnapshot,
52 EvmBondSnapshot, EvmLogEntry, EvmSignature, EvmTransactionReceipt,
53 PreparedAuthorizedChannelMerkleReleaseV1, PreparedBondExpiry, PreparedBondImpair,
54 PreparedBondLock, PreparedBondProofRoot, PreparedBondRelease, PreparedDualSignRelease,
55 PreparedErc20Approval, PreparedEscrowCreate, PreparedEscrowRefund, PreparedEvmCall,
56 PreparedEvmSubmission, PreparedMerkleRelease, PreparedRootPublication,
57 SettlementAnchorContentBinding,
58};
59pub use hook::{
60 SettlementFailureClass, SettlementFailureCode, SettlementFailureCodeParseError,
61 SettlementFailureReason, SettlementHook, SettlementHookError, SettlementIdempotencyKey,
62 SettlementObservation, SettlementOutcome, SettlementSkipReason, SETTLEMENT_OBSERVATION_SCHEMA,
63 SETTLEMENT_OUTCOME_SCHEMA,
64};
65pub use observe::{
66 inspect_finality, inspect_finality_for_receipt, observe_bond, project_escrow_execution_receipt,
67 BondLifecycleObservation, BondLifecycleStatus, EscrowExecutionProjection,
68 EscrowLifecycleStatus, ExecutionProjectionInput, SettlementFinalityAssessment,
69 SettlementFinalityStatus, SettlementRecoveryAction,
70};
71pub use ops::{
72 classify_settlement_lane, ensure_settlement_operation_allowed, SettlementAlertSeverity,
73 SettlementControlChangeRecord, SettlementControlState, SettlementEmergencyControls,
74 SettlementEmergencyMode, SettlementIncidentAlert, SettlementIndexerCursor,
75 SettlementIndexerCursorInput, SettlementIndexerStatus, SettlementLaneRuntimeStatus,
76 SettlementLaneRuntimeStatusInput, SettlementOperationKind, SettlementRecoveryRecord,
77 SettlementRuntimeReport, SettlementRuntimeStatus, CHIO_SETTLE_RUNTIME_REPORT_SCHEMA,
78};
79pub use outcome_store::{
80 validate_settlement_claim, SettlementAttemptClaim, SettlementClaimValidationError,
81 SettlementOutcomeStore, SettlementRoute, SettlementRouteError, SettlementRouteErrorClass,
82 SettlementRoutingInput, SettlementStoreBinding, MAX_SETTLEMENT_CLAIM_BATCH,
83 MAX_SETTLEMENT_LEASE_MS, MAX_SETTLEMENT_WORKER_ID_BYTES,
84};
85pub use payments::{
86 approval_binding_from_governed, build_x402_payment_requirements, evaluate_circle_nanopayment,
87 prepare_paymaster_compatibility, prepare_transfer_with_authorization,
88 validate_offchain_settlement_receipt, ApprovalBinding, CircleNanopaymentPolicy, Eip3009Domain,
89 Eip3009NonceStore, Erc4337PaymasterPolicy, InMemoryEip3009NonceStore, NonceOutcome,
90 OffchainSettlementReceiptArtifact, PreparedCircleNanopayment, PreparedPaymasterCompatibility,
91 PreparedTransferWithAuthorization, RailBinding, TransferWithAuthorizationInput,
92 X402PaymentRequirements, X402SettlementMode, CHIO_OFFCHAIN_SETTLEMENT_RECEIPT_SCHEMA,
93 DEFAULT_MAX_EIP3009_NONCE_ENTRIES,
94};
95pub use retry::{
96 classify_attempt, DeadLetterRecord, RetryDecision, RetryPolicy, RetryPolicyError,
97 DEFAULT_BACKOFF_CAP_MS, DEFAULT_BACKOFF_MULTIPLIER, DEFAULT_INITIAL_BACKOFF_MS,
98 DEFAULT_MAX_RETRIES, SETTLE_DEAD_LETTER_SCHEMA,
99};
100pub use solana::{
101 compare_commitments, prepare_solana_settlement, verify_solana_binding_and_receipt,
102 CommitmentConsistencyReport, PreparedSolanaSettlement, SolanaSettlementConfig,
103 SolanaSettlementRequest, SOLANA_ED25519_PROGRAM_ID,
104};
105
106pub const SETTLEMENT_COMPLETION_FLOW_ROW_ID_PREFIX: &str = "economic-completion-flow:";
107
108#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
109#[serde(deny_unknown_fields)]
110pub struct SettlementCommitment {
111 pub chain_id: String,
112 pub lane_kind: String,
113 pub capability_commitment: String,
114 pub receipt_reference: String,
115 pub operator_identity: String,
116 pub settlement_amount: MonetaryAmount,
117}
118
119pub fn settlement_completion_flow_row_id(receipt_id: &str) -> Result<String, SettlementError> {
120 if receipt_id.trim().is_empty() {
121 return Err(SettlementError::invalid_input(
122 "completion-flow binding requires a non-empty receipt id",
123 ));
124 }
125 if receipt_id.trim() != receipt_id {
126 return Err(SettlementError::invalid_input(
127 "completion-flow receipt id must not contain surrounding whitespace",
128 ));
129 }
130 Ok(format!(
131 "{SETTLEMENT_COMPLETION_FLOW_ROW_ID_PREFIX}{receipt_id}"
132 ))
133}
134
135pub fn settlement_completion_flow_receipt_id(row_id: &str) -> Result<&str, SettlementError> {
136 let receipt_id = row_id
137 .strip_prefix(SETTLEMENT_COMPLETION_FLOW_ROW_ID_PREFIX)
138 .ok_or_else(|| {
139 SettlementError::invalid_input(format!(
140 "completion-flow row id `{row_id}` does not carry the expected prefix"
141 ))
142 })?;
143 if receipt_id.trim().is_empty() {
144 return Err(SettlementError::invalid_input(format!(
145 "completion-flow row id `{row_id}` does not carry the expected prefix"
146 )));
147 }
148 if receipt_id.trim() != receipt_id {
149 return Err(SettlementError::invalid_input(
150 "completion-flow row id receipt id must not contain surrounding whitespace",
151 ));
152 }
153 Ok(receipt_id)
154}
155
156#[derive(Debug, thiserror::Error)]
157pub enum SettlementError {
158 #[error("invalid input: {0}")]
159 InvalidInput(String),
160
161 #[error("invalid dispatch: {0}")]
162 InvalidDispatch(String),
163
164 #[error("invalid binding: {0}")]
165 InvalidBinding(String),
166
167 #[error("unsupported operation: {0}")]
168 Unsupported(String),
169
170 #[error("rpc error: {0}")]
171 Rpc(String),
172
173 #[error("serialization error: {0}")]
174 Serialization(String),
175
176 #[error("signature error: {0}")]
177 Signature(String),
178
179 #[error("verification error: {0}")]
180 Verification(String),
181}
182
183impl SettlementError {
184 pub(crate) fn invalid_input(message: impl Into<String>) -> Self {
185 Self::InvalidInput(message.into())
186 }
187}
188
189#[cfg(test)]
190mod tests {
191 use super::{
192 settlement_completion_flow_receipt_id, settlement_completion_flow_row_id, SettlementError,
193 SETTLEMENT_COMPLETION_FLOW_ROW_ID_PREFIX,
194 };
195
196 use chio_test_support::prelude::*;
197
198 #[test]
199 fn invalid_input_constructor_preserves_message() {
200 let error = SettlementError::invalid_input("rpc_url must not be empty");
201 assert!(matches!(
202 error,
203 SettlementError::InvalidInput(message) if message == "rpc_url must not be empty"
204 ));
205 }
206
207 #[test]
208 fn completion_flow_receipt_ids_reject_surrounding_whitespace() {
209 let error = settlement_completion_flow_row_id(" receipt-1")
210 .test_expect_err("padded receipt id rejected when building row id");
211 assert!(error.to_string().contains("surrounding whitespace"));
212
213 let row_id = format!("{SETTLEMENT_COMPLETION_FLOW_ROW_ID_PREFIX}receipt-1 ");
214 let error = settlement_completion_flow_receipt_id(&row_id)
215 .test_expect_err("padded receipt id rejected when parsing row id");
216 assert!(error.to_string().contains("surrounding whitespace"));
217 }
218}