Skip to content

Commit 6b2918e

Browse files
Merge #88
88: feat: retry nilauth payment r=mfontanini a=mfontanini This adds retries on the payment subscription request in the nilauth client, similar to the changes in nuc-py. Co-authored-by: Matias Fontanini <[email protected]>
2 parents 1ff9b07 + 936a0d3 commit 6b2918e

File tree

3 files changed

+147
-29
lines changed

3 files changed

+147
-29
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

libs/nilauth-client/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,13 @@ chrono = { version = "0.4", features = ["serde"] }
99
hex = { version = "0.4", features = ["serde"] }
1010
nillion-chain-client = { path = "../nillion-chain/client" }
1111
nillion-nucs = { path = "../nucs" }
12+
tracing = "0.1"
1213
rand = "0.8"
1314
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
1415
serde = { version = "1", features = ["derive"] }
1516
serde_json = "1"
1617
thiserror = "1"
18+
tokio = { version = "1.44", features = ["time"] }
1719

1820
[dev-dependencies]
1921
tokio = { version = "1.44", features = ["rt-multi-thread", "macros"] }

libs/nilauth-client/src/client.rs

Lines changed: 144 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,25 @@ use nillion_nucs::{
1111
},
1212
token::{Did, ProofHash, TokenBody},
1313
};
14-
use serde::{Deserialize, Serialize};
14+
use reqwest::Response;
15+
use serde::{de::DeserializeOwned, Deserialize, Serialize};
1516
use serde_json::json;
16-
use std::{iter, time::Duration};
17+
use std::{fmt::Display, iter, time::Duration};
18+
use tokio::time::sleep;
19+
use tracing::warn;
1720

1821
const TOKEN_REQUEST_EXPIRATION: Duration = Duration::from_secs(60);
1922
const REQUEST_TIMEOUT: Duration = Duration::from_secs(30);
23+
const TX_RETRY_ERROR_CODE: &str = "TRANSACTION_NOT_COMMITTED";
24+
static PAYMENT_TX_RETRIES: &[Duration] = &[
25+
Duration::from_secs(1),
26+
Duration::from_secs(2),
27+
Duration::from_secs(3),
28+
Duration::from_secs(5),
29+
Duration::from_secs(10),
30+
Duration::from_secs(10),
31+
Duration::from_secs(10),
32+
];
2033

2134
/// An interface to interact with nilauth.
2235
#[async_trait]
@@ -59,8 +72,11 @@ pub enum RequestTokenError {
5972
#[error("invalid public key")]
6073
InvalidPublicKey,
6174

62-
#[error("request: {0}")]
63-
Request(#[from] reqwest::Error),
75+
#[error("http: {0}")]
76+
Http(#[from] reqwest::Error),
77+
78+
#[error("request: {0:?}")]
79+
Request(RequestError),
6480
}
6581

6682
/// An error when paying a subscription.
@@ -78,18 +94,27 @@ pub enum PaySubscriptionError {
7894
#[error("invalid public key")]
7995
InvalidPublicKey,
8096

81-
#[error("request: {0}")]
82-
Request(#[from] reqwest::Error),
97+
#[error("http: {0}")]
98+
Http(#[from] reqwest::Error),
8399

84100
#[error("making payment: {0}")]
85101
Payment(String),
102+
103+
#[error("server could not validate payment: {tx_hash}")]
104+
PaymentValidation { tx_hash: TxHash, payload: String },
105+
106+
#[error("request: {0:?}")]
107+
Request(RequestError),
86108
}
87109

88110
/// An error when fetching the subscription cost.
89111
#[derive(Debug, thiserror::Error)]
90112
pub enum SubscriptionCostError {
91-
#[error("request: {0}")]
92-
Request(#[from] reqwest::Error),
113+
#[error("http: {0}")]
114+
Http(#[from] reqwest::Error),
115+
116+
#[error("request: {0:?}")]
117+
Request(RequestError),
93118
}
94119

95120
/// An error when revoking a token.
@@ -113,24 +138,57 @@ pub enum RevokeTokenError {
113138
#[error("building invocation: {0}")]
114139
BuildInvocation(#[from] NucTokenBuildError),
115140

116-
#[error("request: {0}")]
117-
Request(#[from] reqwest::Error),
141+
#[error("http: {0}")]
142+
Http(#[from] reqwest::Error),
143+
144+
#[error("request: {0:?}")]
145+
Request(RequestError),
118146
}
119147

120148
/// An error when requesting the information about a nilauth instance.
121149
#[derive(Debug, thiserror::Error)]
122150
pub enum AboutError {
123-
#[error("request: {0}")]
124-
Request(#[from] reqwest::Error),
151+
#[error("http: {0}")]
152+
Http(#[from] reqwest::Error),
153+
154+
#[error("request: {0:?}")]
155+
Request(RequestError),
125156
}
126157

127158
/// An error when looking up revoked tokens.
128159
#[derive(Debug, thiserror::Error)]
129160
pub enum LookupRevokedTokensError {
130-
#[error("request: {0}")]
131-
Request(#[from] reqwest::Error),
161+
#[error("http: {0}")]
162+
Http(#[from] reqwest::Error),
163+
164+
#[error("request: {0:?}")]
165+
Request(RequestError),
166+
}
167+
168+
// implement `From<RequestError>` for a list of types.
169+
macro_rules! impl_from_request_error {
170+
($t:ty) => {
171+
impl From<RequestError> for $t {
172+
fn from(e: RequestError) -> Self {
173+
Self::Request(e)
174+
}
175+
}
176+
};
177+
($t:ty, $($rest:ty),+) => {
178+
impl_from_request_error!($t);
179+
impl_from_request_error!($($rest),+);
180+
};
132181
}
133182

183+
impl_from_request_error!(
184+
RequestTokenError,
185+
PaySubscriptionError,
186+
SubscriptionCostError,
187+
RevokeTokenError,
188+
AboutError,
189+
LookupRevokedTokensError
190+
);
191+
134192
/// The default nilauth client that hits the actual service.
135193
pub struct DefaultNilauthClient {
136194
client: reqwest::Client,
@@ -147,14 +205,45 @@ impl DefaultNilauthClient {
147205
let base_url = &self.base_url;
148206
format!("{base_url}{path}")
149207
}
208+
209+
async fn parse_reponse<T, E>(response: Response) -> Result<T, E>
210+
where
211+
T: DeserializeOwned,
212+
E: From<reqwest::Error> + From<RequestError>,
213+
{
214+
if response.status().is_success() {
215+
Ok(response.json().await?)
216+
} else {
217+
let error: RequestError = response.json().await?;
218+
Err(error.into())
219+
}
220+
}
221+
222+
async fn post<R, O, E>(&self, url: &str, request: &R) -> Result<O, E>
223+
where
224+
R: Serialize,
225+
O: DeserializeOwned,
226+
E: From<reqwest::Error> + From<RequestError>,
227+
{
228+
let response = self.client.post(url).json(&request).send().await?;
229+
Self::parse_reponse(response).await
230+
}
231+
232+
async fn get<O, E>(&self, url: &str) -> Result<O, E>
233+
where
234+
O: DeserializeOwned,
235+
E: From<reqwest::Error> + From<RequestError>,
236+
{
237+
let response = self.client.get(url).send().await?;
238+
Self::parse_reponse(response).await
239+
}
150240
}
151241

152242
#[async_trait]
153243
impl NilauthClient for DefaultNilauthClient {
154244
async fn about(&self) -> Result<About, AboutError> {
155245
let url = self.make_url("/about");
156-
let about = self.client.get(url).send().await?.json().await?;
157-
Ok(about)
246+
self.get(&url).await
158247
}
159248

160249
async fn request_token(&self, key: &SecretKey) -> Result<String, RequestTokenError> {
@@ -172,9 +261,8 @@ impl NilauthClient for DefaultNilauthClient {
172261
let request =
173262
CreateNucRequest { public_key, signature: signature.to_bytes().into(), payload: payload.into_bytes() };
174263
let url = self.make_url("/api/v1/nucs/create");
175-
let response: CreateNucResponse =
176-
self.client.post(url).json(&request).send().await?.error_for_status()?.json().await?;
177-
Ok(response.token)
264+
let response: Result<CreateNucResponse, RequestTokenError> = self.post(&url, &request).await;
265+
Ok(response?.token)
178266
}
179267

180268
async fn pay_subscription(
@@ -194,15 +282,27 @@ impl NilauthClient for DefaultNilauthClient {
194282

195283
let public_key = key.to_sec1_bytes().as_ref().try_into().map_err(|_| PaySubscriptionError::InvalidPublicKey)?;
196284
let url = self.make_url("/api/v1/payments/validate");
197-
let request = ValidatePaymentRequest { tx_hash: tx_hash.clone(), payload: payload.into_bytes(), public_key };
198-
self.client.post(url).json(&request).send().await?.error_for_status()?;
199-
Ok(TxHash(tx_hash))
285+
let request =
286+
ValidatePaymentRequest { tx_hash: tx_hash.clone(), payload: payload.as_bytes().to_vec(), public_key };
287+
let tx_hash = TxHash(tx_hash);
288+
for delay in PAYMENT_TX_RETRIES {
289+
let response: Result<(), PaySubscriptionError> = self.post(&url, &request).await;
290+
match response {
291+
Ok(_) => return Ok(tx_hash),
292+
Err(PaySubscriptionError::Request(e)) if e.error_code == TX_RETRY_ERROR_CODE => {
293+
warn!("Server could not validate payment, retyring in {delay:?}");
294+
sleep(*delay).await;
295+
}
296+
Err(e) => return Err(e),
297+
};
298+
}
299+
Err(PaySubscriptionError::PaymentValidation { tx_hash, payload })
200300
}
201301

202302
async fn subscription_cost(&self) -> Result<TokenAmount, SubscriptionCostError> {
203303
let url = self.make_url("/api/v1/payments/cost");
204-
let response: GetCostResponse = self.client.get(url).send().await?.error_for_status()?.json().await?;
205-
Ok(TokenAmount::Unil(response.cost_unils))
304+
let response: Result<GetCostResponse, SubscriptionCostError> = self.get(&url).await;
305+
Ok(TokenAmount::Unil(response?.cost_unils))
206306
}
207307

208308
async fn revoke_token(&self, token: &NucTokenEnvelope, key: &SecretKey) -> Result<(), RevokeTokenError> {
@@ -219,8 +319,8 @@ impl NilauthClient for DefaultNilauthClient {
219319
.build(&key.into())?;
220320
let header_value = format!("Bearer {invocation}");
221321
let url = self.make_url("/api/v1/revocations/revoke");
222-
self.client.post(url).header("Authorization", header_value).send().await?.error_for_status()?;
223-
Ok(())
322+
let response = self.client.post(url).header("Authorization", header_value).send().await?;
323+
Self::parse_reponse(response).await
224324
}
225325

226326
async fn lookup_revoked_tokens(
@@ -230,16 +330,21 @@ impl NilauthClient for DefaultNilauthClient {
230330
let hashes = iter::once(envelope.token()).chain(envelope.proofs()).map(|t| t.compute_hash()).collect();
231331
let request = LookupRevokedTokensRequest { hashes };
232332
let url = self.make_url("/api/v1/revocations/lookup");
233-
let response: LookupRevokedTokensResponse =
234-
self.client.post(url).json(&request).send().await?.error_for_status()?.json().await?;
235-
Ok(response.revoked)
333+
let response: Result<LookupRevokedTokensResponse, LookupRevokedTokensError> = self.post(&url, &request).await;
334+
Ok(response?.revoked)
236335
}
237336
}
238337

239338
/// A transaction hash.
240339
#[derive(Clone, Debug, PartialEq)]
241340
pub struct TxHash(pub String);
242341

342+
impl Display for TxHash {
343+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
344+
write!(f, "{}", self.0)
345+
}
346+
}
347+
243348
/// Information about a nilauth server.
244349
#[derive(Clone, Deserialize)]
245350
pub struct About {
@@ -327,3 +432,13 @@ pub struct RevokedToken {
327432
/// The timestamp at which the token was revoked.
328433
pub revoked_at: DateTime<Utc>,
329434
}
435+
436+
/// An error when performing a request.
437+
#[derive(Clone, Debug, Deserialize)]
438+
pub struct RequestError {
439+
/// The error message.
440+
pub message: String,
441+
442+
/// The error code.
443+
pub error_code: String,
444+
}

0 commit comments

Comments
 (0)