Skip to content

Commit

Permalink
allow to get account balance
Browse files Browse the repository at this point in the history
  • Loading branch information
joksas committed Aug 18, 2024
1 parent fbe6b0b commit 00f1b9d
Show file tree
Hide file tree
Showing 4 changed files with 198 additions and 1 deletion.
5 changes: 4 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
authors = ["RSS Blue", "Dovydas Joksas"]
name = "v4v"
version = "0.4.0"
version = "0.4.1"
edition = "2021"
description = "Value-for-value helper utilities for Podcasting 2.0"
license = "MIT OR Apache-2.0"
Expand All @@ -18,4 +18,7 @@ base64 = "0.22.1"
hmac-sha256 = "1.1.7"
http02 = { package = "http", version = "0.2.12" }
http1 = { package = "http", version = "1.1.0" }
reqwest = "0.12.5"
serde = {version="1.0.208", features=["derive"]}
serde_json = "1.0.125"
time = "0.3.36"
41 changes: 41 additions & 0 deletions src/alby/api.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
use crate::alby::helpers::{make_request, RequestArgs, RequestError};

/// Alby API functions related to the account.
///
/// Read more at
/// <https://guides.getalby.com/developer-guide/v/alby-wallet-api/reference/api-reference/account>.
pub mod account {
use super::*;

/// Response body for a successful [get_balance] request.
#[derive(Debug, serde::Deserialize)]
pub struct GetBalanceResponse {
/// The balance amount.
pub balance: u64,
/// The currency of the balance.
pub currency: String,
/// The unit of the balance (e.g., "sat" for satoshis).
pub unit: String,
}

/// Arguments for [get_balance].
pub struct GetBalanceArgs<'a> {
/// User agent string.
pub user_agent: &'a str,
/// Bearer token for authentication.
pub token: &'a str,
}

/// Get Alby account balance.
pub async fn get_balance(args: GetBalanceArgs<'_>) -> Result<GetBalanceResponse, RequestError> {
let request_args = RequestArgs {
user_agent: args.user_agent,
method: reqwest::Method::GET,
url: "https://api.getalby.com/balance",
token: args.token,
body: None,
};

make_request(request_args).await
}
}
151 changes: 151 additions & 0 deletions src/alby/helpers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
use serde::de::DeserializeOwned;
use std::fmt;

/// Alby error response from 400 and 500 status codes.
#[derive(Debug, serde::Deserialize)]
pub struct ErrorResponse {
/// Alby error code.
pub code: u32,
/// Indicates if it is an error.
pub error: bool,
/// Alby error message.
pub message: String,
}

/// Alby API request error.
#[derive(Debug)]
pub enum RequestError {
/// Failed to create auth header.
AuthHeaderCreation(reqwest::header::InvalidHeaderValue),
/// Failed to create reqwest client.
ClientCreation(reqwest::Error),
/// Failed to send request.
RequestSend(reqwest::Error),
/// Failed to read response body.
ResponseBodyRead(reqwest::Error),
/// Failed to parse response body.
ResponseParse(serde_json::Error),
/// Bad request (400).
BadRequest(ErrorResponse),
/// Internal server error (500).
InternalServerError(ErrorResponse),
/// Unexpected status code.
UnexpectedStatus {
/// Status code.
status: reqwest::StatusCode,
/// Response body.
body: String,
},
}

impl fmt::Display for RequestError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
RequestError::AuthHeaderCreation(e) => write!(f, "Failed to create auth header: {}", e),
RequestError::ClientCreation(e) => write!(f, "Failed to create reqwest client: {}", e),
RequestError::RequestSend(e) => write!(f, "Failed to send request: {}", e),
RequestError::ResponseBodyRead(e) => write!(f, "Failed to read response body: {}", e),
RequestError::ResponseParse(e) => write!(f, "Failed to parse response body: {}", e),
RequestError::BadRequest(e) => {
write!(f, "Bad request (400): {} (code: {})", e.message, e.code)
}
RequestError::InternalServerError(e) => write!(
f,
"Internal server error (500): {} (code: {})",
e.message, e.code
),
RequestError::UnexpectedStatus { status, body } => {
write!(f, "Unexpected status code: {}. Body: {}", status, body)
}
}
}
}

impl std::error::Error for RequestError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
RequestError::AuthHeaderCreation(e) => Some(e),
RequestError::ClientCreation(e) => Some(e),
RequestError::RequestSend(e) => Some(e),
RequestError::ResponseBodyRead(e) => Some(e),
RequestError::ResponseParse(e) => Some(e),
RequestError::BadRequest(_)
| RequestError::InternalServerError(_)
| RequestError::UnexpectedStatus { .. } => None,
}
}
}

impl From<reqwest::header::InvalidHeaderValue> for RequestError {
fn from(error: reqwest::header::InvalidHeaderValue) -> Self {
RequestError::AuthHeaderCreation(error)
}
}

impl From<reqwest::Error> for RequestError {
fn from(error: reqwest::Error) -> Self {
if error.is_builder() {
RequestError::ClientCreation(error)
} else if error.is_request() {
RequestError::RequestSend(error)
} else {
RequestError::ResponseBodyRead(error)
}
}
}

impl From<serde_json::Error> for RequestError {
fn from(error: serde_json::Error) -> Self {
RequestError::ResponseParse(error)
}
}

/// Arguments for making a request.
pub struct RequestArgs<'a> {
/// User agent string.
pub user_agent: &'a str,
/// HTTP method.
pub method: reqwest::Method,
/// URL to send the request to.
pub url: &'a str,
/// Bearer token for authentication.
pub token: &'a str,
/// Optional request body.
pub body: Option<&'a str>,
}

pub async fn make_request<T: DeserializeOwned>(args: RequestArgs<'_>) -> Result<T, RequestError> {
let mut headers = reqwest::header::HeaderMap::new();
headers.insert(
reqwest::header::AUTHORIZATION,
reqwest::header::HeaderValue::from_str(&format!("Bearer {}", args.token))?,
);

let client = reqwest::Client::builder()
.default_headers(headers)
.user_agent(args.user_agent)
.timeout(std::time::Duration::from_secs(10))
.build()?;

let response = client
.request(args.method, args.url)
.body(args.body.unwrap_or_default().to_string())
.send()
.await?;

let status = response.status();
let body = response.text().await?;

match status.as_u16() {
200 => Ok(serde_json::from_str(&body)?),
400 => {
let error_response: ErrorResponse = serde_json::from_str(&body)?;
Err(RequestError::BadRequest(error_response))
}
500 => {
let error_response: ErrorResponse = serde_json::from_str(&body)?;
Err(RequestError::InternalServerError(error_response))
}
_ => Err(RequestError::UnexpectedStatus { status, body }),
}
}
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
pub mod alby {
/// Alby API types and functions.
pub mod api;
/// Helper functions.
mod helpers;
/// Alby webhook utilities.
pub mod webhooks;
}
Expand Down

0 comments on commit 00f1b9d

Please sign in to comment.