Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Batch Payments Feature #155

Closed
wants to merge 9 commits into from
Closed

Conversation

eshanvaid
Copy link
Contributor

This PR introduces the batch payment feature to our CLI. This feature allows the user to execute multiple payments simultaneously using a CSV file containing the details of the recipients.

The command syntax is as follows:
batch --csv <file-path>

The CSV file needs to adhere to this format: the columns should be - username, {cents or sats}, memo(optional).

Example:

username cents memo
test_user_a 3 happy easter
test_user_b 8

The implementation consists of four key steps, which are carried out by their corresponding functions:

  • check_file_exists - This function checks if the input file exists.
  • validate_csv - This function validates the CSV file. It checks whether the headers and rows are in the correct format, and whether the usernames in the rows are valid.
  • check_sufficient_balance - This function checks if the total amount specified in the CSV file is less than or equal to the user's current wallet balance.
  • execute_batch_payment - This function performs the payment transactions. The mode of payment (cents or sats) is determined based on the corresponding header in the CSV file.

Related issue: #148

As this is a significant feature addition, thorough testing has been performed to ensure its correctness and reliability.

@eshanvaid eshanvaid added the enhancement New feature or request label Jun 14, 2023
@eshanvaid
Copy link
Contributor Author

eshanvaid commented Jun 14, 2023

Note that this branch was created from add-balance-command. Since that branch has not been merged into the master yet, all the commits from add-balance-command are currently visible in this PR as well. Once PR #152 is merged, I will rebase this branch with the latest master which will then show only the changes relevant to this feature

let mut reader = csv::ReaderBuilder::new().delimiter(b',').from_path(file)?;
let headers = reader.headers()?.clone();

if &headers[0] != "username"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we could make this feature proof and go with lightning address? or maybe lightning address has not been implemented yet from the cli?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We currently haven't implemented Lightning Address in the CLI, but it's on our to-do list. Once we've got that (probably within this week), we'll tweak the batch payments to work with it.

pub fn is_empty(&self) -> bool {
self.payments.is_empty()
}
let wallet_type = if headers.get(1) == Some("cents") {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are we assuming that it we sent sats, it's from the btc wallet, and if we send cents, it's from the usd wallet?

I don't think we should make this assumption. we can send "100 dollar worth of bitcoin" from the bitcoin wallet.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think typically people right not send "cents" or "cents equivalent", so we could start with this one.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Batch payments is built upon intraledger payments, which does not support sending cents from a Bitcoin wallet or sats from a USD wallet. Due to this, our current implementation assumes that if the amount is in cents, it is sent from the USD wallet and vice versa. Once we incorporate the capability for conversions in intraledger payments, we will extend the batch payments feature to allow this.


Ok(())
pub fn check_sufficient_balance(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should also check the sending limits.

@@ -1,92 +0,0 @@
use galoy_cli::batch::Batch;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is the tests deleted?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the tests you're referring to were associated with the previous iteration of batch payments, which are no longer relevant.
Once this PR is approved and the implementation is finalized, I'll be introducing new e2e tests for batch in a subsequent PR

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once this PR is approved and the implementation is finalized, I'll be introducing new e2e tests for batch in a subsequent PR

imo, unless exception for good reasons, tests should come with PRs, not after. otherwise it's too easy to move on to other things and never do the tests.

client,
price,
}
// Utility function to read and validate the CSV file
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment is redundant as it adds no additional information than the name of the function. Delete it - or alternatively if you want to publish this as a library turn in into a doc style comment (///) and add something useful.

file: &str,
) -> anyhow::Result<(Vec<csv::StringRecord>, Wallet)> {
let mut reader = csv::ReaderBuilder::new().delimiter(b',').from_path(file)?;
let headers = reader.headers()?.clone();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why clone?

let mut reader = csv::ReaderBuilder::new().delimiter(b',').from_path(file)?;
let headers = reader.headers()?.clone();

if &headers[0] != "username"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should not be using magic values... all hard coded values must be declared as constants.

pub fn is_empty(&self) -> bool {
self.payments.is_empty()
}
let wallet_type = if headers.get(1) == Some("cents") {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be handled via FromStr / parse

let payment_btc: Decimal = payment.usd / self.price;
payment.sats = Some(payment_btc * dec!(100_000_000));
}
let amount = record.get(1).ok_or(anyhow::anyhow!("Amount is missing"))?;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find the syntax:

 record.get(1).context("Amount is missing")?;

more concise.
Need to use anyhow::Context for it to work.

"payment to {username} of sats {amount}, usd {usd}: {:?}",
res
);
match wallet_type {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How are you dealing with partial failures? What if the first 5 out of 10 payments complete but then there is an error - how is this reported to avoid a double payment on re-try?


pub mod server;

impl From<&WalletCurrency> for Wallet {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like putting From implementations into their own file eg src/client/convert.rs.

batch.execute().context("can't make payment successfully")?;

Ok(())
pub fn batch_payment(self, file: String) -> anyhow::Result<String> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having batch as a submodule of client is IMO significantly bloating its responsibility. Typically a client is a thin wrapper around some network calls. More complex coordination that requires multiple round trips needs to be put in a use-case / app layer that uses the client as a dependency.

@@ -15,6 +15,8 @@ use std::fs::{self};
mod constants;
mod token;

use galoy_cli::types::*;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

main.rs is completely bloated. It should just call into something that gets exposed via lib - which is where the use cases should be defined.

IMO this project is getting large enough that a purely design - improvement / refactoring iteration should be executed to improve things before implementing further complex use cases.

At this stage it should still be fairly simple to clean things up - later that might not be the case anymore.

@@ -0,0 +1,16 @@
use rust_decimal::Decimal;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having a types.rs file is a code smell that indicates a lack of domain oriented design-thinking.

@sandipndev sandipndev closed this Jul 26, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants