Skip to content

Commit

Permalink
add wallet balance endpoint (#56)
Browse files Browse the repository at this point in the history
  • Loading branch information
tsachiherman authored Feb 12, 2024
1 parent 470c257 commit d2393a7
Show file tree
Hide file tree
Showing 5 changed files with 255 additions and 30 deletions.
85 changes: 85 additions & 0 deletions gateway-types/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
//! Shared types between XPS Gateawy and client (libxmtp)
pub mod error;

use ethers::types::U256;
use ethers::types::{Address, Bytes as EthersBytes, Signature};
use ethers::utils::format_units;
use std::fmt;

use serde::{Deserialize, Serialize};
use std::fmt::Display;

/// Address of the did:ethr Registry on Sepolia
pub const DID_ETH_REGISTRY: &str = "0xd1D374DDE031075157fDb64536eF5cC13Ae75000";
Expand Down Expand Up @@ -48,6 +52,47 @@ pub struct GrantInstallationResult {
pub transaction: String,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub enum Unit {
Eth,
Other(String),
}

/// WalletBalance used as the return value for the balance rpc endpoint.
#[derive(Serialize, Deserialize, Clone)]
pub struct WalletBalance {
/// The balance for the wallet
#[serde(rename = "balance")]
pub balance: U256,
/// The unit used for the balance
#[serde(rename = "unit")]
pub unit: Unit,
}

impl Display for WalletBalance {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.unit {
Unit::Eth => {
let ether_balance =
format_units(self.balance, 18) // 18 decimal places for Ether
.unwrap_or_else(|_| "failed to convert balance".to_string());
write!(f, "{} ETH", ether_balance)
}
Unit::Other(unit_name) => write!(f, "{} {}", self.balance, unit_name),
}
}
}

// Assuming you have a Display implementation for Unit as well
impl Display for Unit {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Unit::Eth => write!(f, "ETH"),
Unit::Other(value) => write!(f, "{}", value),
}
}
}

#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct SendMessageResult {
pub status: Status,
Expand Down Expand Up @@ -89,4 +134,44 @@ mod tests {
assert_eq!(format!("{}", Status::Success), "success");
assert_eq!(format!("{}", Status::Failed), "failed");
}

#[test]
fn test_wallet_balance_display() {
assert_eq!(
format!(
"{}",
WalletBalance {
balance: U256::from(123456789),
unit: Unit::Eth
}
),
"0.000000000123456789 ETH"
);
assert_eq!(
format!(
"{}",
WalletBalance {
balance: U256::from(987654321),
unit: Unit::Eth
}
),
"0.000000000987654321 ETH"
);
assert_eq!(
format!(
"{}",
WalletBalance {
balance: U256::from(500),
unit: Unit::Other("BTC".to_string())
}
),
"500 BTC"
);
}

#[test]
fn test_unit_display() {
assert_eq!(format!("{}", Unit::Eth), "ETH");
assert_eq!(format!("{}", Unit::Other("ABC".to_string())), "ABC");
}
}
84 changes: 83 additions & 1 deletion xps-gateway/src/rpc/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use ethers::core::types::Signature;
use ethers::prelude::*;
use jsonrpsee::{proc_macros::rpc, types::ErrorObjectOwned};

use gateway_types::{GrantInstallationResult, KeyPackageResult};
use gateway_types::{GrantInstallationResult, KeyPackageResult, WalletBalance};
use gateway_types::{Message, SendMessageResult};
use lib_didethresolver::types::XmtpAttribute;

Expand Down Expand Up @@ -518,4 +518,86 @@ pub trait Xps {

#[method(name = "walletAddress")]
async fn wallet_address(&self) -> Result<Address, ErrorObjectOwned>;

/// ### Documentation for JSON RPC Endpoint: `balance`
/// ---
/// #### Endpoint Name: `balance`
/// #### Description:
/// The `balance` endpoint retrieves the current balance of the internal wallet managed by the server. This endpoint is essential for applications that need to display or monitor the wallet's balance, especially in the context of cryptocurrency transactions or account management.
/// #### Request:
/// - **Method:** `POST`
/// - **URL:** `/rpc/v1/balance`
/// - **Headers:**
/// - `Content-Type: application/json`
/// - **Body:**
/// - **JSON Object:**
/// - `jsonrpc`: `"2.0"`
/// - `method`: `"balance"`
/// - `params`: Array (optional parameters as required)
/// - `id`: Request identifier (integer or string)
/// **Example Request Body:**
/// ```json
/// {
/// "jsonrpc": "2.0",
/// "method": "balance",
/// "params": [],
/// "id": 1
/// }
/// ```
/// #### Response:
/// - **Success Status Code:** `200 OK`
/// - **Error Status Codes:**
/// - `400 Bad Request` - Invalid request format or parameters.
/// - `500 Internal Server Error` - Server or wallet-related error.
/// **Success Response Body:**
/// ```json
/// {
/// "jsonrpc": "2.0",
/// "result": {
/// "balance": "100.0 ETH",
/// "unit": "ETH"
/// },
/// "id": 1
/// }
/// ```
/// **Error Response Body:**
/// ```json
/// {
/// "jsonrpc": "2.0",
/// "error": {
/// "code": -32602,
/// "message": "Invalid parameters"
/// },
/// "id": 1
/// }
/// ```
/// #### Error Handling:
/// - **Invalid Parameters:** Check if the request body is properly formatted and includes valid parameters.
/// - **Wallet or Server Errors:** Ensure that the server and wallet are operational. Consult server logs for detailed error information.
/// #### Security Considerations:
/// - **Authentication and Authorization:** Implement robust authentication and authorization checks to ensure only authorized users can access wallet balance information.
/// - **Secure Communication:** Utilize HTTPS to encrypt data in transit and prevent eavesdropping.
/// #### Usage Example:
/// ```javascript
/// const requestBody = {
/// jsonrpc: "2.0",
/// method: "balance",
/// params: [],
/// id: 1
/// };
/// fetch('https://server.example.com/rpc/v1/balance', {
/// method: 'POST',
/// headers: {
/// 'Content-Type': 'application/json'
/// },
/// body: JSON.stringify(requestBody)
/// })
/// .then(response => response.json())
/// .then(data => console.log('Wallet Balance:', data.result))
/// .catch(error => console.error('Error:', error));
/// ```
/// </div>
/// ```
#[method(name = "balance")]
async fn balance(&self) -> Result<WalletBalance, ErrorObjectOwned>;
}
53 changes: 43 additions & 10 deletions xps-gateway/src/rpc/methods.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,13 @@ use super::api::*;

use async_trait::async_trait;
use ethers::prelude::*;
use ethers::{core::types::Signature, providers::Middleware};
use gateway_types::{GrantInstallationResult, KeyPackageResult, SendMessageResult};
use ethers::{
core::types::Signature,
providers::{Middleware, ProviderError},
};
use gateway_types::{
GrantInstallationResult, KeyPackageResult, SendMessageResult, Unit, WalletBalance,
};
use jsonrpsee::types::ErrorObjectOwned;
use lib_didethresolver::types::XmtpAttribute;
use messaging::MessagingOperations;
Expand Down Expand Up @@ -103,6 +108,34 @@ impl<P: Middleware + 'static> XpsServer for XpsMethods<P> {
Ok(self.wallet.address())
}

/// Fetches the current balance of the wallet in Ether.
///
/// This asynchronous method queries the Ethereum blockchain to get the current balance
/// of the associated wallet address, converting the result from wei (the smallest unit
/// of Ether) to Ether for more understandable reading.
///
/// # Returns
/// - `Ok(WalletBalance)`: On success, returns a `WalletBalance` struct containing the
/// wallet's balance formatted as a string in Ether, along with the unit "ETH".
/// - `Err(ErrorObjectOwned)`: On failure, returns an error object detailing why the
/// balance could not be fetched or converted.
///
async fn balance(&self) -> Result<WalletBalance, ErrorObjectOwned> {
// Fetch the balance in wei (the smallest unit of Ether) from the blockchain.
let wei_balance: U256 = self
.signer
.provider()
.get_balance(self.wallet.address(), None)
.await
.map_err::<RpcError<P>, _>(RpcError::from)?;

// Return the balance in Ether as a WalletBalance object.
Ok(WalletBalance {
balance: wei_balance,
unit: Unit::Eth,
})
}

async fn fetch_key_packages(&self, did: String) -> Result<KeyPackageResult, ErrorObjectOwned> {
log::debug!("xps_fetchKeyPackages called");
let result = self
Expand All @@ -119,20 +152,20 @@ impl<P: Middleware + 'static> XpsServer for XpsMethods<P> {
enum RpcError<M: Middleware> {
/// A public key parameter was invalid
#[error(transparent)]
ContactOperation(#[from] ContactOperationError<M>),
Contact(#[from] ContactOperationError<M>),
/// Error occurred while querying the balance.
#[error(transparent)]
Balance(#[from] ProviderError),
#[error(transparent)]
MessagingOperation(#[from] MessagingOperationError<M>),
Messaging(#[from] MessagingOperationError<M>),
}

impl<M: Middleware> From<RpcError<M>> for ErrorObjectOwned {
fn from(error: RpcError<M>) -> Self {
match error {
RpcError::ContactOperation(c) => {
ErrorObjectOwned::owned(-31999, c.to_string(), None::<()>)
}
RpcError::MessagingOperation(m) => {
ErrorObjectOwned::owned(-31999, m.to_string(), None::<()>)
}
RpcError::Contact(c) => ErrorObjectOwned::owned(-31999, c.to_string(), None::<()>),
RpcError::Balance(c) => ErrorObjectOwned::owned(-31999, c.to_string(), None::<()>),
RpcError::Messaging(m) => ErrorObjectOwned::owned(-31999, m.to_string(), None::<()>),
}
}
}
59 changes: 42 additions & 17 deletions xps-gateway/tests/integration_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ use std::str::FromStr;

use anyhow::Error;

use ethers::{
signers::{LocalWallet, Signer},
types::Bytes,
utils::keccak256,
};
use ethers::providers::Middleware;
use ethers::types::{Address, Bytes, TransactionRequest, U256};
use ethers::utils::keccak256;
use ethers::{signers::LocalWallet, signers::Signer};
use gateway_types::{Message, Status, Unit};
use integration_util::*;
use jsonrpsee::core::ClientError;
use lib_didethresolver::{
did_registry::RegistrySignerExt,
Expand All @@ -17,11 +18,6 @@ use lib_didethresolver::{
use messaging::ConversationSignerExt;
use xps_gateway::rpc::{XpsClient, DEFAULT_ATTRIBUTE_VALIDITY};

use ethers::types::{Address, U256};
use gateway_types::{Message, Status};

use integration_util::*;

#[tokio::test]
async fn test_say_hello() -> Result<(), Error> {
with_xps_client(None, |client, _, _, _| async move {
Expand Down Expand Up @@ -51,10 +47,10 @@ async fn test_send_message() -> Result<(), Error> {
.await?;

let message = Message {
conversation_id: conversation_id,
payload: payload,
conversation_id,
payload,
identity: me.address(),
signature: signature,
signature,
};

let pre_nonce = context.conversation.nonce(me.address()).call().await?;
Expand Down Expand Up @@ -91,10 +87,10 @@ async fn test_send_message_fail() -> Result<(), Error> {
.await?;

let message = Message {
conversation_id: conversation_id,
payload: payload,
conversation_id,
payload,
identity: me.address(),
signature: signature,
signature,
};

let pre_nonce = context.conversation.nonce(me.address()).call().await?;
Expand Down Expand Up @@ -395,6 +391,36 @@ async fn test_revoke_installation() -> Result<(), Error> {
.await
}

#[tokio::test]
async fn test_balance() -> Result<(), Error> {
with_xps_client(None, |client, context, _resolver, _anvil| async move {
// by default, we have no balance. verify that.
let mut balance = client.balance().await?;
assert_eq!(balance.balance, U256::from(0));
assert_eq!(balance.unit, Unit::Eth);

// fund the wallet account.
let accounts = context.signer.get_accounts().await?;
let from = accounts[1];
let tx = TransactionRequest::new()
.to(client.wallet_address().await?)
.value(5_000_000_000_000_000_000_000_u128)
.from(from);
context.signer.send_transaction(tx, None).await?.await?;

// check to see if the balance gets updated.
balance = client.balance().await?;
assert_eq!(
balance.balance,
U256::from(5_000_000_000_000_000_000_000_u128)
);
assert_eq!(balance.unit, Unit::Eth);

Ok(())
})
.await
}

#[tokio::test]
async fn test_fetch_key_packages() -> Result<(), Error> {
with_xps_client(None, |client, context, _, anvil| async move {
Expand All @@ -421,7 +447,6 @@ async fn test_fetch_key_packages() -> Result<(), Error> {
.unwrap()
]
);

Ok(())
})
.await
Expand Down
Loading

0 comments on commit d2393a7

Please sign in to comment.