Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added
- `data::Error::InsufficientFunds { have, need }` (amounts in atto): a wallet whose payment-token
balance is below the payment total now fails with this instead of a generic
`Error::Payment(String)`, so consumers such as the mobile SDK can offer "top up your wallet"
without matching on message text (#152). It covers every wallet-backed payment path: the
journaled native path that file and data uploads use, the wave-batch and Merkle batch paths, and
single-chunk payment. Nothing has been paid when it is returned, because the wallet checks its
balance before sending. A shortfall after earlier batches of the same upload were paid still
surfaces as `PartialUpload`, so the spend and the stored chunks are still reported, and it is now
kept typed in the new `PartialUpload::cause` field (also set when storing the paid batches
failed too). Code that builds `PartialUpload` or matches it without `..` must add the field. The
CLI's message changes from `payment error: Insufficient tokens to pay for quotes. Have: …` to
`insufficient funds: wallet has … atto, payment needs … atto`. A shortfall of gas (ETH) is not
detected up front and is unchanged.

## [0.3.12] - 2026-10-22

### Changed (breaking — `ant-core` browser API)
Expand Down
1 change: 1 addition & 0 deletions ant-core/src/data/client/adaptive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3122,6 +3122,7 @@ mod tests {
gas_cost_wei: 0,
}),
reason: "r".to_string(),
cause: None,
}),
),
];
Expand Down
4 changes: 2 additions & 2 deletions ant-core/src/data/client/batch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ impl SingleNodeQuotePayment {
.pay_for_quotes(quote_payments)
.await
.map_err(|PayForQuotesError(err, _)| {
Error::Payment(format!("Failed to pay for quotes: {err}"))
Error::from_payment_failure(format!("Failed to pay for quotes: {err}"))
})?;

let mut result_hashes = Vec::new();
Expand Down Expand Up @@ -543,7 +543,7 @@ impl Client {
.pay_for_quotes(all_payments)
.await
.map_err(|PayForQuotesError(err, _)| {
Error::Payment(format!("Batch payment failed: {err}"))
Error::from_payment_failure(format!("Batch payment failed: {err}"))
})?;

info!(
Expand Down
9 changes: 9 additions & 0 deletions ant-core/src/data/client/file/native.rs
Original file line number Diff line number Diff line change
Expand Up @@ -690,6 +690,7 @@ fn partial_upload_after_fatal(
total_chunks,
spend: Box::new(spend),
reason,
cause: None,
}
}

Expand Down Expand Up @@ -1039,6 +1040,7 @@ fn settlement_refusal_after_paid_waves(
the {total_stored} chunk(s) in earlier wave(s) were already paid for and stored, \
and the remaining {remaining_count} chunk(s) were neither quoted nor paid. {refusal}"
),
cause: None,
}
}

Expand Down Expand Up @@ -2262,6 +2264,7 @@ impl Client {
gas_cost_wei: 0,
}),
reason: "finalize_upload: chunk storage failed after retries".into(),
cause: None,
});
}
let chunks_stored = already_stored_count + wave_result.stored.len();
Expand Down Expand Up @@ -3696,6 +3699,7 @@ mod tests {
gas_cost_wei: 0,
}),
reason: "merkle chunk store aborted".into(),
cause: None,
});
let outcome = assemble_merkle_finalize_outcome(
store_result,
Expand Down Expand Up @@ -3780,6 +3784,7 @@ mod tests {
gas_cost_wei: 0,
}),
reason: "quorum shortfall".into(),
cause: None,
});
let outcome = assemble_merkle_finalize_outcome(
first_pass,
Expand Down Expand Up @@ -4295,6 +4300,7 @@ mod tests {
gas_cost_wei: 11,
}),
reason: "wave store failed after retries".to_string(),
cause: None,
};

let outcome = fold_single_wave(Err(err)).unwrap();
Expand Down Expand Up @@ -4350,10 +4356,12 @@ mod tests {
total_chunks,
spend,
reason,
cause,
} = err
else {
panic!("expected PartialUpload, got: {err:?}");
};
assert!(cause.is_none());
assert_eq!(got_stored, stored);
assert_eq!(stored_count, 2);
assert_eq!(failed_count, 3);
Expand Down Expand Up @@ -4771,6 +4779,7 @@ impl Client {
gas_cost_wei: batch_result.gas_cost_wei,
}),
reason,
cause: None,
});
}

Expand Down
4 changes: 3 additions & 1 deletion ant-core/src/data/client/merkle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1097,7 +1097,9 @@ impl Client {
prepared.merkle_payment_timestamp,
)
.await
.map_err(|e| Error::Payment(format!("Merkle batch payment failed: {e}")))?;
.map_err(|e| {
Error::from_payment_failure(format!("Merkle batch payment failed: {e}"))
})?;

info!(
"Merkle payment succeeded: winner pool {}",
Expand Down
12 changes: 11 additions & 1 deletion ant-core/src/data/client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,7 @@ mod settlement_refusal_tests {
/// network stress these are how transport failures surface)
/// - `PartialUpload` -> `NetworkError` (literal capacity signal: some
/// chunks could not be stored)
/// - `AlreadyStored`, `Encryption`, `Crypto`, `Payment`,
/// - `AlreadyStored`, `Encryption`, `Crypto`, `Payment`, `InsufficientFunds`,
/// `Serialization`, `InvalidData`, `NotFound`, `SignatureVerification`,
/// `Config`, `InsufficientDiskSpace`, `CostEstimationInconclusive`,
/// `Cancelled` -> `ApplicationError` (would happen on a perfectly
Expand Down Expand Up @@ -327,6 +327,7 @@ pub(crate) fn classify_error(err: &Error) -> Outcome {
| Error::Encryption(_)
| Error::Crypto(_)
| Error::Payment(_)
| Error::InsufficientFunds { .. }
| Error::Serialization(_)
| Error::InvalidData(_)
// A definitively absent record, reported over a working link —
Expand Down Expand Up @@ -1105,6 +1106,13 @@ mod tests {
),
(Error::Storage("s".to_string()), Outcome::NetworkError),
(Error::Payment("p".to_string()), Outcome::ApplicationError),
(
Error::InsufficientFunds {
have: ant_protocol::evm::Amount::ZERO,
need: ant_protocol::evm::Amount::from(1),
},
Outcome::ApplicationError,
),
(Error::Protocol("p".to_string()), Outcome::NetworkError),
(
Error::InvalidData("d".to_string()),
Expand Down Expand Up @@ -1156,6 +1164,7 @@ mod tests {
gas_cost_wei: 0,
}),
reason: "r".to_string(),
cause: None,
},
Outcome::NetworkError,
),
Expand Down Expand Up @@ -1284,6 +1293,7 @@ mod tests {
| Error::InsufficientPeers(_)
| Error::Storage(_)
| Error::Payment(_)
| Error::InsufficientFunds { .. }
| Error::Protocol(_)
| Error::InvalidData(_)
| Error::NotFound(_)
Expand Down
2 changes: 1 addition & 1 deletion ant-core/src/data/client/native_payment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ async fn execute_once<A: UploadAdapter, F: Fn(&UploadState) -> bool + Send + Syn
wallet
.prepare_payment(request)
.await
.map_err(Error::Payment)
.map_err(Error::from_payment_failure)
};
let signed = match prepared {
Ok(signed) => signed,
Expand Down
9 changes: 5 additions & 4 deletions ant-core/src/data/client/payment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,10 +129,11 @@ impl Client {
info!("Payment total: {} atto", payment.total_amount());

// 4. Pay on-chain
let tx_hashes = payment
.pay(wallet)
.await
.map_err(|e| Error::Payment(format!("On-chain payment failed: {e}")))?;
let tx_hashes = payment.pay(wallet).await.map_err(|e| match e {
// Already typed by `pay`; wrapping it would demote it to `Payment`.
Error::InsufficientFunds { .. } => e,
e => Error::Payment(format!("On-chain payment failed: {e}")),
})?;

info!(
"On-chain payment succeeded: {} transactions",
Expand Down
Loading
Loading