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

feat/add-support-for-wasm32 #83

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 43 additions & 19 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,30 +9,54 @@ repository = "https://github.com/collinsmuriuki/mpesa-rust"
readme = "./README.md"
license = "MIT"

[features]
default = [
"account_balance",
"b2b",
"b2c",
"bill_manager",
"c2b_register",
"c2b_simulate",
"express_request",
"transaction_reversal",
"transaction_status",
c12i marked this conversation as resolved.
Show resolved Hide resolved
]
account_balance = []
b2b = ["dep:base64", "dep:x509-parser", "dep:rsa", "dep:rand"]
b2c = ["dep:base64", "dep:x509-parser", "dep:rsa", "dep:rand"]
bill_manager = ["dep:chrono"]
c2b_register = []
c2b_simulate = []
express_request = ["dep:chrono", "dep:base64"]
transaction_reversal = ["dep:base64", "dep:x509-parser", "dep:rsa", "dep:rand"]
transaction_status = ["dep:base64", "dep:x509-parser", "dep:rsa", "dep:rand"]


[dependencies]
chrono = {version = "0.4", optional = true, default-features = false, features = ["clock", "serde"] }
openssl = {version = "0.10", optional = true}
reqwest = {version = "0.11", features = ["json"]}
secrecy = "0.8.0"
serde = {version="1.0", features= ["derive"]}
base64 = { version = "0.21", optional = true }
chrono = { version = "0.4", optional = true, default-features = false, features = [
"clock",
"serde",
] }
rand = { version = "0.8", optional = true, default-features = false, features = [
"std",
] }
reqwest = { version = "0.11", features = ["json"] }
rsa = { version = "0.9", optional = true }
secrecy = "0.8"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde_repr = "0.1"
thiserror = "1.0.37"
wiremock = "0.5"
thiserror = "1.0"
x509-parser = { version = "0.15", optional = true }


[dev-dependencies]
dotenv = "0.15"
tokio = {version = "1", features = ["rt", "rt-multi-thread", "macros"]}
tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros"] }
wiremock = "0.5"

[features]
default = ["account_balance", "b2b", "b2c", "bill_manager", "c2b_register", "c2b_simulate", "express_request", "transaction_reversal", "transaction_status"]
account_balance = ["dep:openssl"]
b2b = ["dep:openssl"]
b2c = ["dep:openssl"]
bill_manager = ["dep:chrono"]
c2b_register = []
c2b_simulate = []
express_request = ["dep:chrono"]
transaction_reversal = ["dep:openssl"]
transaction_status= ["dep:openssl"]

# Support Wasm
[target.'cfg(target_arch = "wasm32")'.dependencies]
getrandom = { version = "0.2", features = ["js"] }
Comment on lines +61 to +62
Copy link
Owner

Choose a reason for hiding this comment

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

79 changes: 52 additions & 27 deletions src/client.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
use std::cell::RefCell;

use base64::engine::general_purpose;
use base64::Engine as _;
use reqwest::Client as HttpClient;
use rsa::{BigUint, Pkcs1v15Encrypt};
use secrecy::{ExposeSecret, Secret};
use serde_json::Value;

use crate::environment::ApiEnvironment;
use crate::services::{
AccountBalanceBuilder, B2bBuilder, B2cBuilder, BulkInvoiceBuilder, C2bRegisterBuilder,
Expand All @@ -6,18 +15,10 @@ use crate::services::{
TransactionStatusBuilder,
};
use crate::{ApiError, MpesaError};
use openssl::base64;
use openssl::rsa::Padding;
use openssl::x509::X509;
use reqwest::Client as HttpClient;
use secrecy::{ExposeSecret, Secret};
use serde_json::Value;
use std::cell::RefCell;

/// Source: [test credentials](https://developer.safaricom.co.ke/test_credentials)
const DEFAULT_INITIATOR_PASSWORD: &str = "Safcom496!";
/// Get current package version from metadata
const CARGO_PACKAGE_VERSION: &str = env!("CARGO_PKG_VERSION");

/// `Result` enum type alias
pub type MpesaResult<T> = Result<T, MpesaError>;
Expand Down Expand Up @@ -47,13 +48,16 @@ impl<'mpesa, Env: ApiEnvironment> Mpesa<Env> {
/// # Panics
/// This method can panic if a TLS backend cannot be initialized for the internal http_client
pub fn new<S: Into<String>>(client_key: S, client_secret: S, environment: Env) -> Self {
#[cfg(target_arch = "wasm32")]
let http_client = HttpClient::new();

#[cfg(not(target_arch = "wasm32"))]
let http_client = HttpClient::builder()
.connect_timeout(std::time::Duration::from_millis(10_000))
.user_agent(format!("mpesa-rust@{CARGO_PACKAGE_VERSION}"))
// TODO: Potentialy return a `Result` enum from Mpesa::new?
// Making assumption that creation of http client cannot fail
.user_agent(format!("mpesa-rust@{}", env!("CARGO_PKG_VERSION")))
.build()
.expect("Error building http client");

Self {
client_key: client_key.into(),
client_secret: Secret::new(client_secret.into()),
Expand Down Expand Up @@ -508,30 +512,51 @@ impl<'mpesa, Env: ApiEnvironment> Mpesa<Env> {
///
/// # Errors
/// Returns `EncryptionError` variant of `MpesaError`

pub(crate) fn gen_security_credentials(&self) -> MpesaResult<String> {
Copy link
Owner

Choose a reason for hiding this comment

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

Suggestion, perhaps we could keep the original implementation under #[cfg(not(target_arch = "wasm32"))] and the wasm implementation under #[cfg(target_arch = "wasm32")]

let pem = self.environment.get_certificate().as_bytes();
let cert = X509::from_pem(pem)?;
// getting the public and rsa keys
let pub_key = cert.public_key()?;
let rsa_key = pub_key.rsa()?;
// configuring the buffer
let buf_len = pub_key.size();
let mut buffer = vec![0; buf_len];

rsa_key.public_encrypt(
self.initiator_password().as_bytes(),
&mut buffer,
Padding::PKCS1,
)?;
Ok(base64::encode_block(&buffer))

let (_, cert) = x509_parser::pem::parse_x509_pem(pem)
.map_err(|e| MpesaError::EncryptionError(e.to_string()))?;

let cert = cert
.parse_x509()
.map_err(|e| MpesaError::EncryptionError(e.to_string()))?;

let key = cert
.public_key()
.parsed()
.map_err(|e| MpesaError::EncryptionError(e.to_string()))?;

let rsa = match key {
x509_parser::public_key::PublicKey::RSA(rsa_key) => rsa_key,
_ => unreachable!("Invalid public key type"),
};

let value = rsa::RsaPublicKey::new(
BigUint::from_bytes_be(rsa.modulus),
BigUint::from_bytes_be(rsa.exponent),
)
.map_err(|e| MpesaError::EncryptionError(e.to_string()))?;

let value = value
.encrypt(
&mut rand::rngs::OsRng,
Pkcs1v15Encrypt,
self.initiator_password().as_bytes(),
)
.map_err(|e| MpesaError::EncryptionError(e.to_string()))?;

let value = general_purpose::STANDARD.encode(value);

Ok(value)
}
}

#[cfg(test)]
mod tests {
use crate::Sandbox;

use super::*;
use crate::Sandbox;

#[test]
fn test_setting_initator_password() {
Expand Down
3 changes: 2 additions & 1 deletion src/constants.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use std::fmt::{Display, Formatter, Result as FmtResult};

use chrono::prelude::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_repr::{Deserialize_repr, Serialize_repr};
use std::fmt::{Display, Formatter, Result as FmtResult};

/// Mpesa command ids
#[derive(Debug, Serialize, Deserialize)]
Expand Down
4 changes: 3 additions & 1 deletion src/environment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@
//! and the `public key` an X509 certificate used for encrypting initiator passwords. You can read more about that from
//! the Safaricom API [docs](https://developer.safaricom.co.ke/docs?javascript#security-credentials).

use std::convert::TryFrom;
use std::str::FromStr;

use crate::MpesaError;
use std::{convert::TryFrom, str::FromStr};

#[derive(Debug, Clone)]
/// Enum to map to desired environment so as to access certificate
Expand Down
6 changes: 4 additions & 2 deletions src/errors.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use std::env::VarError;
use std::fmt;

use serde::{Deserialize, Serialize};
use std::{env::VarError, fmt};

/// Mpesa error stack
#[derive(thiserror::Error, Debug)]
Expand Down Expand Up @@ -41,7 +43,7 @@ pub enum MpesaError {
#[error("An error has occured while retreiving an environmental variable")]
EnvironmentalVariableError(#[from] VarError),
#[error("An error has occurred while generating security credentials")]
EncryptionError(#[from] openssl::error::ErrorStack),
EncryptionError(String),
#[error("{0}")]
Message(&'static str),
}
Expand Down
3 changes: 2 additions & 1 deletion src/services/account_balance.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
use serde::{Deserialize, Serialize};

use crate::client::MpesaResult;
use crate::constants::{CommandId, IdentifierTypes};
use crate::environment::ApiEnvironment;
use crate::{Mpesa, MpesaError};
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize)]
/// Account Balance payload
Expand Down
3 changes: 2 additions & 1 deletion src/services/b2b.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
use serde::{Deserialize, Serialize};

use crate::client::{Mpesa, MpesaResult};
use crate::constants::{CommandId, IdentifierTypes};
use crate::environment::ApiEnvironment;
use crate::errors::MpesaError;
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize)]
struct B2bPayload<'mpesa> {
Expand Down
3 changes: 2 additions & 1 deletion src/services/b2c.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use serde::{Deserialize, Serialize};

use crate::client::MpesaResult;
use crate::environment::ApiEnvironment;
use crate::{CommandId, Mpesa, MpesaError};
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize)]
/// Payload to allow for b2c transactions:
Expand Down
3 changes: 2 additions & 1 deletion src/services/bill_manager/bulk_invoice.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
use serde::Deserialize;

use crate::client::{Mpesa, MpesaResult};
use crate::constants::Invoice;
use crate::environment::ApiEnvironment;
use crate::errors::MpesaError;
use serde::Deserialize;

#[derive(Clone, Debug, Deserialize)]
pub struct BulkInvoiceResponse {
Expand Down
3 changes: 2 additions & 1 deletion src/services/bill_manager/cancel_invoice.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use serde::{Deserialize, Serialize};

use crate::client::{Mpesa, MpesaResult};
use crate::environment::ApiEnvironment;
use crate::errors::MpesaError;
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
Expand Down
3 changes: 2 additions & 1 deletion src/services/bill_manager/onboard.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
use serde::{Deserialize, Serialize};

use crate::client::{Mpesa, MpesaResult};
use crate::constants::SendRemindersTypes;
use crate::environment::ApiEnvironment;
use crate::errors::MpesaError;
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize)]
/// Payload to opt you in as a biller to the bill manager features.
Expand Down
3 changes: 2 additions & 1 deletion src/services/bill_manager/onboard_modify.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
use serde::{Deserialize, Serialize};

use crate::client::{Mpesa, MpesaResult};
use crate::constants::SendRemindersTypes;
use crate::environment::ApiEnvironment;
use crate::errors::MpesaError;
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize)]
/// Payload to modify opt-in details to the bill manager api.
Expand Down
5 changes: 3 additions & 2 deletions src/services/bill_manager/reconciliation.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
use chrono::prelude::{DateTime, Utc};
use serde::{Deserialize, Serialize};

use crate::client::{Mpesa, MpesaResult};
use crate::environment::ApiEnvironment;
use crate::errors::MpesaError;
use chrono::prelude::{DateTime, Utc};
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
Expand Down
5 changes: 3 additions & 2 deletions src/services/bill_manager/single_invoice.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
use chrono::prelude::{DateTime, Utc};
use serde::Deserialize;

use crate::client::{Mpesa, MpesaResult};
use crate::constants::{Invoice, InvoiceItem};
use crate::environment::ApiEnvironment;
use crate::errors::MpesaError;
use chrono::prelude::{DateTime, Utc};
use serde::Deserialize;

#[derive(Clone, Debug, Deserialize)]
pub struct SingleInvoiceResponse {
Expand Down
3 changes: 2 additions & 1 deletion src/services/c2b_register.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
use serde::{Deserialize, Serialize};

use crate::client::{Mpesa, MpesaResult};
use crate::constants::ResponseType;
use crate::environment::ApiEnvironment;
use crate::errors::MpesaError;
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize)]
/// Payload to register the 3rd party’s confirmation and validation URLs to M-Pesa
Expand Down
3 changes: 2 additions & 1 deletion src/services/c2b_simulate.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
use serde::{Deserialize, Serialize};

use crate::client::{Mpesa, MpesaResult};
use crate::constants::CommandId;
use crate::environment::ApiEnvironment;
use crate::errors::MpesaError;
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize)]
/// Payload to make payment requests from C2B.
Expand Down
10 changes: 6 additions & 4 deletions src/services/express_request.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
use base64::engine::general_purpose;
use base64::Engine as _;
use chrono::prelude::Local;
use serde::{Deserialize, Serialize};

use crate::client::{Mpesa, MpesaResult};
use crate::constants::CommandId;
use crate::environment::ApiEnvironment;
use crate::errors::MpesaError;
use chrono::prelude::Local;
use openssl::base64;
use serde::{Deserialize, Serialize};

/// Source: [test credentials](https://developer.safaricom.co.ke/test_credentials)
static DEFAULT_PASSKEY: &str = "bfb279f9aa9bdbcf158e97dd71a467cd2e0c893059b10f78e6b72ada1ed2c919";
Expand Down Expand Up @@ -100,7 +102,7 @@ impl<'mpesa, Env: ApiEnvironment> MpesaExpressRequestBuilder<'mpesa, Env> {
/// Returns the encoded password and a timestamp string
fn generate_password_and_timestamp(&self) -> (String, String) {
let timestamp = Local::now().format("%Y%m%d%H%M%S").to_string();
let encoded_password = base64::encode_block(
let encoded_password = general_purpose::STANDARD.encode(
format!(
"{}{}{}",
self.business_short_code(),
Expand Down
12 changes: 3 additions & 9 deletions src/services/transaction_reversal.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,6 @@
use serde::Deserialize;
use serde::Serialize;

use crate::ApiEnvironment;
use crate::CommandId;
use crate::IdentifierTypes;
use crate::Mpesa;
use crate::MpesaError;
use crate::MpesaResult;
use serde::{Deserialize, Serialize};

use crate::{ApiEnvironment, CommandId, IdentifierTypes, Mpesa, MpesaError, MpesaResult};

#[derive(Debug, Serialize)]
pub struct TransactionReversalPayload<'mpesa> {
Expand Down
Loading
Loading