mirror of
https://github.com/Such-Software/smirk-backend-core.git
synced 2026-10-17 15:31:38 +02:00
First wallet-handler slice. Thin, non-custodial, JWT-gated proxies over the
ElectrumClient — the wallet derives addresses and signs locally; these
endpoints only relay reads and a finalized broadcast.
Endpoints (POST, under /api/v1/wallet/utxo, asset = btc|ltc in body):
- balance -> {confirmed, unconfirmed} (separate integers; client sums)
- utxos -> [{txid, vout, value, height}]
- history -> [{txid, height, fee}]
- tip -> {height}
- fee -> {sat_per_vb}
- broadcast-> {txid}
- JWT-gated for abuse control; the client-supplied address is NOT persisted
against the user (no address<->identity graph stored here).
- Feature-gated dispatch: unknown/disabled asset -> 400; the Electrum client
validates the address + network before any network call.
- broadcast validates tx_hex (non-empty, even-length, hex, <=200 KB) before
reaching the node.
- Registered in build_router + openapi.json (6 paths, btc_ltc tag); spec
regenerated, drift-clean.
5 L1 integration tests (auth gate on balance/broadcast/fee; unknown-asset and
malformed-tx-hex 400s — all deterministic, no network) + tx_hex unit test.
89 lines
2.5 KiB
Rust
89 lines
2.5 KiB
Rust
//! L1 integration: BTC/LTC wallet endpoints through the real router.
|
|
//!
|
|
//! These assert the deterministic, no-network paths: the JWT gate, asset
|
|
//! validation, and broadcast tx-hex validation — all of which resolve before any
|
|
//! Electrum call. The funded/networked happy paths (real balance/broadcast) live
|
|
//! in the separate gitignored L3 harness.
|
|
|
|
mod common;
|
|
|
|
use axum::http::StatusCode;
|
|
use serde_json::json;
|
|
|
|
#[tokio::test]
|
|
async fn balance_requires_a_token() {
|
|
let app = require_app!();
|
|
let (status, _) = app
|
|
.request(
|
|
"POST",
|
|
"/api/v1/wallet/utxo/balance",
|
|
None,
|
|
Some(
|
|
json!({ "asset": "btc", "address": "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4" }),
|
|
),
|
|
)
|
|
.await;
|
|
assert_eq!(status, StatusCode::UNAUTHORIZED);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn balance_rejects_unknown_asset() {
|
|
let app = require_app!();
|
|
let (_uid, access, _r) = app.mint_session().await;
|
|
let (status, body) = app
|
|
.request(
|
|
"POST",
|
|
"/api/v1/wallet/utxo/balance",
|
|
Some(&access),
|
|
Some(json!({ "asset": "doge", "address": "x" })),
|
|
)
|
|
.await;
|
|
assert_eq!(status, StatusCode::BAD_REQUEST);
|
|
assert_eq!(body["code"], "VALIDATION_ERROR");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn broadcast_requires_a_token() {
|
|
let app = require_app!();
|
|
let (status, _) = app
|
|
.request(
|
|
"POST",
|
|
"/api/v1/wallet/utxo/broadcast",
|
|
None,
|
|
Some(json!({ "asset": "btc", "tx_hex": "deadbeef" })),
|
|
)
|
|
.await;
|
|
assert_eq!(status, StatusCode::UNAUTHORIZED);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn broadcast_rejects_malformed_tx_hex() {
|
|
let app = require_app!();
|
|
let (_uid, access, _r) = app.mint_session().await;
|
|
// Authed + valid asset, but the tx hex is non-hex -> 400 before any network.
|
|
let (status, body) = app
|
|
.request(
|
|
"POST",
|
|
"/api/v1/wallet/utxo/broadcast",
|
|
Some(&access),
|
|
Some(json!({ "asset": "btc", "tx_hex": "nothex!!" })),
|
|
)
|
|
.await;
|
|
assert_eq!(status, StatusCode::BAD_REQUEST);
|
|
assert_eq!(body["code"], "VALIDATION_ERROR");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn fee_requires_a_token() {
|
|
let app = require_app!();
|
|
let (status, _) = app
|
|
.request(
|
|
"POST",
|
|
"/api/v1/wallet/utxo/fee",
|
|
None,
|
|
Some(json!({ "asset": "btc", "blocks": 6 })),
|
|
)
|
|
.await;
|
|
assert_eq!(status, StatusCode::UNAUTHORIZED);
|
|
}
|