commit 5be1c86dc740f8bd6eea1c25301929d698c5eacb
parent 4b9a168bc7e97e48f1d11175a51da426f1cfe178
Author: Antoine A <>
Date: Mon, 13 Dec 2021 19:33:30 +0100
wire-gateway: handle transfer request idempotence
Diffstat:
7 files changed, 74 insertions(+), 16 deletions(-)
diff --git a/script/test_gateway.sh b/script/test_gateway.sh
@@ -112,6 +112,15 @@ for endpoint in incoming outgoing; do
echo ""
done
+echo "----- Transfer idempotence -----"
+DATA="{\"request_uid\":\"0ZSX8SH0M30KHX8K3Y1DAMVGDQV82XEF9DG1HC4QMQ3QWYT4AF00ZSX8SH0M30KHX8K3Y1DAMVGDQV82XEF9DG1HC4QMQ3QWYT4AF00\",\"amount\":\"BTC:0.000034\",\"exchange_base_url\":\"$BASE_URL\",\"wtid\":\"0ZSX8SH0M30KHX8K3Y1DAMVGDQV82XEF9DG1HC4QMQ3QWYT4AF00\",\"credit_account\":\"payto://bitcoin/$ADDRESS\"}"
+echo -n "Same:"
+test `curl -w %{http_code} -s -o /dev/null -H "Content-Type: application/json" -d $DATA ${BANK_ENDPOINT}transfer` -eq 200 && echo -n " OK" || echo -n " Failed"
+test `curl -w %{http_code} -s -o /dev/null -H "Content-Type: application/json" -d $DATA ${BANK_ENDPOINT}transfer` -eq 200 && echo " OK" || echo " Failed"
+echo -n "Collision:"
+DATA="{\"request_uid\":\"0ZSX8SH0M30KHX8K3Y1DAMVGDQV82XEF9DG1HC4QMQ3QWYT4AF00ZSX8SH0M30KHX8K3Y1DAMVGDQV82XEF9DG1HC4QMQ3QWYT4AF00\",\"amount\":\"BTC:0.000042\",\"exchange_base_url\":\"$BASE_URL\",\"wtid\":\"0ZSX8SH0M30KHX8K3Y1DAMVGDQV82XEF9DG1HC4QMQ3QWYT4AF00\",\"credit_account\":\"payto://bitcoin/$ADDRESS\"}"
+test `curl -w %{http_code} -s -o /dev/null -H "Content-Type: application/json" -d $DATA ${BANK_ENDPOINT}transfer` -eq 409 && echo " OK" || echo " Failed"
+
echo "----- Security -----"
# Generate big random file
diff --git a/test.conf b/test.conf
@@ -1,5 +1,5 @@
[main]
-BASE_URL = test.com
+BASE_URL = http://test.com
DB_URL = postgres://localhost/postgres?user=postgres&password=password
PORT = 8060
PAYTO = payto://bitcoin/bcrt1qgkgxkjj27g3f7s87mcvjjsghay7gh34cx39prj
diff --git a/wire-gateway/db/schema.sql b/wire-gateway/db/schema.sql
@@ -26,5 +26,6 @@ CREATE TABLE tx_out (
credit_acc TEXT NOT NULL,
exchange_url TEXT NOT NULL,
status SMALLINT NOT NULL,
- txid BYTEA UNIQUE
+ txid BYTEA UNIQUE,
+ request_uid BYTEA NOT NULL UNIQUE
);
\ No newline at end of file
diff --git a/wire-gateway/src/api_common.rs b/wire-gateway/src/api_common.rs
@@ -26,7 +26,7 @@ pub struct ErrorDetail {
}
/// <https://docs.taler.net/core/api-common.html#tsref-type-Timestamp>
-#[derive(Debug, Clone, Copy)]
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Timestamp {
Never,
Time(SystemTime),
@@ -262,11 +262,11 @@ fn test_amount() {
pub enum ParseBase32Error {
#[error("Invalid Crockford’s base32 format")]
Format,
- #[error("Invalid length: expected {0} got {1}")]
+ #[error("Invalid length: expected {0} bytes got {1}")]
Length(usize, usize),
}
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct Base32<const L: usize>([u8; L]);
impl<const L: usize> From<[u8; L]> for Base32<L> {
diff --git a/wire-gateway/src/api_wire.rs b/wire-gateway/src/api_wire.rs
@@ -10,7 +10,7 @@ pub struct TransferResponse {
}
/// <https://docs.taler.net/core/api-wire.html#tsref-type-TransferRequest>
-#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
+#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
pub struct TransferRequest {
pub request_uid: HashCode,
pub amount: Amount,
@@ -79,4 +79,4 @@ pub struct HistoryParams {
pub start: Option<u64>,
pub delta: i64,
pub long_pool_ms: Option<u64>,
-}
-\ No newline at end of file
+}
diff --git a/wire-gateway/src/error.rs b/wire-gateway/src/error.rs
@@ -63,6 +63,14 @@ impl ServerError {
}
}
+ pub fn status(status: StatusCode) -> Self {
+ Self::new(
+ status,
+ Content::None,
+ status.canonical_reason().unwrap_or("").to_string(),
+ )
+ }
+
pub fn code(status: StatusCode, code: ErrorCode) -> Self {
Self::detail(
status,
@@ -80,6 +88,15 @@ impl ServerError {
}
}
+impl From<tokio_postgres::Error> for ServerError {
+ fn from(e: tokio_postgres::Error) -> Self {
+ ServerError::catch_code(
+ e,
+ StatusCode::BAD_GATEWAY,
+ ErrorCode::GENERIC_DB_FETCH_FAILED,
+ )
+ }
+}
pub trait CatchResult<T: Sized> {
fn unexpected(self) -> Result<T, ServerError>;
fn catch_code(self, status: StatusCode, code: ErrorCode) -> Result<T, ServerError>;
diff --git a/wire-gateway/src/main.rs b/wire-gateway/src/main.rs
@@ -63,7 +63,7 @@ async fn main() {
.map(|it| match it {
Host::Tcp(it) => it.to_string(),
#[cfg(target_os = "linux")]
- Host::Unix(it) => it.to_str().unwrap().to_string()
+ Host::Unix(it) => it.to_str().unwrap().to_string(),
})
.collect(),
);
@@ -197,17 +197,49 @@ async fn router(
ErrorCode::GENERIC_PARAMETER_MALFORMED,
));
}
- let timestamp = Timestamp::now();
let db = state.pool.get().await.catch_code(
StatusCode::GATEWAY_TIMEOUT,
ErrorCode::GENERIC_DB_FETCH_FAILED,
)?;
- let row = db.query_one("INSERT INTO tx_out (_date, amount, wtid, debit_acc, credit_acc, exchange_url, status) VALUES (now(), $1, $2, $3, $4, $5, $6) RETURNING id", &[
- &request.amount.to_string(), &request.wtid.as_ref(), &state.pay_to, &request.credit_account.to_string(), &request.exchange_base_url.to_string(), &0i16
- ]).await.catch_code(
- StatusCode::BAD_GATEWAY,
- ErrorCode::GENERIC_DB_FETCH_FAILED,
- )?;
+ // Handle idempotence, check previous transaction with the same request_uid
+ let row = db.query_opt("SELECT amount, exchange_url, wtid, credit_acc, id, _date FROM tx_out WHERE request_uid = $1", &[&request.request_uid.as_ref()])
+ .await?;
+ if let Some(row) = row {
+ let prev = TransferRequest {
+ request_uid: request.request_uid.clone(),
+ amount: Amount::from_str(row.get(0)).unwrap(),
+ exchange_base_url: Url::parse(row.get(1)).unwrap(),
+ wtid: {
+ let slice: &[u8] = row.get(2);
+ let array: [u8; 32] = slice.try_into().unwrap();
+ ShortHashCode::from(array)
+ },
+ credit_account: Url::parse(row.get(3)).unwrap(),
+ };
+ if prev == request {
+ // Idempotence
+ return Ok(encode_body(
+ parts,
+ StatusCode::OK,
+ &TransferResponse {
+ timestamp: Timestamp::Time(row.get(5)),
+ row_id: {
+ let id: i32 = row.get(4);
+ SafeUint64::try_from(id as u64).unwrap()
+ },
+ },
+ )
+ .await
+ .unexpected()?);
+ } else {
+ return Err(ServerError::status(StatusCode::CONFLICT));
+ }
+ }
+
+ let timestamp = Timestamp::now();
+ let row = db.query_one("INSERT INTO tx_out (_date, amount, wtid, debit_acc, credit_acc, exchange_url, status, request_uid) VALUES (now(), $1, $2, $3, $4, $5, $6, $7) RETURNING id", &[
+ &request.amount.to_string(), &request.wtid.as_ref(), &state.pay_to, &request.credit_account.to_string(), &request.exchange_base_url.to_string(), &0i16, &request.request_uid.as_ref()
+ ]).await?;
encode_body(
parts,
StatusCode::OK,