#![allow(unsafe_op_in_unsafe_fn)]
#![allow(non_local_definitions)]
#![allow(clippy::inherent_to_string)]
use pyo3::exceptions::{PyRuntimeError, PyValueError};
use pyo3::prelude::*;
use serde_json::json;
use crate::{Amount, CryptoVerifier, Keypair, Transaction, TxId, Unsigned, Verified, WalletId};
#[pyclass(name = "Amount")]
#[derive(Clone, Debug)]
pub struct PyAmount {
inner: Amount,
}
impl Default for PyPaymentEngine {
fn default() -> Self {
Self::new()
}
}
#[pymethods]
impl PyAmount {
#[new]
pub fn new(value: i128) -> PyResult<Self> {
Amount::new(value)
.map(|inner| PyAmount { inner })
.map_err(|e| PyValueError::new_err(format!("Invalid amount: {:?}", e)))
}
#[getter]
pub fn value(&self) -> i128 {
self.inner.value()
}
pub fn __repr__(&self) -> String {
format!("Amount({})", self.inner.value())
}
pub fn __add__(&self, other: &PyAmount) -> PyResult<PyAmount> {
Ok(PyAmount {
inner: self.inner.clone() + other.inner.clone(),
})
}
pub fn __sub__(&self, other: &PyAmount) -> PyResult<PyAmount> {
Ok(PyAmount {
inner: self.inner.clone() - other.inner.clone(),
})
}
pub fn to_json(&self) -> String {
json!({"value": self.inner.value()}).to_string()
}
}
#[pyclass(name = "WalletId")]
#[derive(Clone, Debug)]
pub struct PyWalletId {
inner: WalletId,
}
#[pymethods]
impl PyWalletId {
#[staticmethod]
pub fn from_bank(account: &str) -> Self {
PyWalletId {
inner: WalletId::Bank(crate::wallet::BankAccount(account.to_string())),
}
}
#[staticmethod]
pub fn from_crypto(address: &str) -> Self {
PyWalletId {
inner: WalletId::Crypto(address.to_string()),
}
}
pub fn __repr__(&self) -> String {
format!("WalletId('{}')", self.inner)
}
pub fn __str__(&self) -> String {
self.inner.to_string()
}
pub fn to_string(&self) -> String {
self.inner.to_string()
}
pub fn is_bank(&self) -> bool {
matches!(self.inner, WalletId::Bank(_))
}
pub fn is_crypto(&self) -> bool {
matches!(self.inner, WalletId::Crypto(_))
}
}
#[pyclass(name = "Transaction")]
#[derive(Debug)]
pub struct PyTransaction {
inner: Transaction<Verified>,
}
#[pymethods]
impl PyTransaction {
#[staticmethod]
pub fn create_unsigned(
id: &str,
from_wallet: &PyWalletId,
to_wallet: &PyWalletId,
amount: &PyAmount,
nonce: u64,
) -> PyResult<PyTransaction> {
let tx_id = TxId(id.to_string());
let unsigned = Transaction::<Unsigned>::new(
tx_id,
from_wallet.inner.clone(),
to_wallet.inner.clone(),
amount.inner.clone(),
nonce,
);
let keypair = Keypair::generate();
let signed = unsigned.sign(&keypair);
match signed.verify() {
Ok(verified) => Ok(PyTransaction { inner: verified }),
Err(e) => Err(PyRuntimeError::new_err(e)),
}
}
#[getter]
pub fn id(&self) -> String {
self.inner.id.0.clone()
}
#[getter]
pub fn from_wallet(&self) -> PyWalletId {
PyWalletId {
inner: self.inner.from.clone(),
}
}
#[getter]
pub fn to_wallet(&self) -> PyWalletId {
PyWalletId {
inner: self.inner.to.clone(),
}
}
#[getter]
pub fn amount(&self) -> PyAmount {
PyAmount {
inner: self.inner.amount.clone(),
}
}
#[getter]
pub fn nonce(&self) -> u64 {
self.inner.nonce
}
#[getter]
pub fn state(&self) -> String {
"Verified".to_string()
}
pub fn to_json(&self) -> PyResult<String> {
let data = json!({
"id": self.inner.id.0,
"from": self.inner.from.to_string(),
"to": self.inner.to.to_string(),
"amount": self.inner.amount.value(),
"nonce": self.inner.nonce,
"state": "Verified",
});
serde_json::to_string(&data).map_err(|e| PyRuntimeError::new_err(e.to_string()))
}
pub fn verify_signature(&self) -> PyResult<bool> {
if let Some(sig) = &self.inner.signature {
let message = format!(
"{}:{}:{}:{}:{}",
self.inner.id.0,
self.inner.from,
self.inner.to,
self.inner.amount.value(),
self.inner.nonce
)
.into_bytes();
match CryptoVerifier::verify(&message, sig, &self.inner.from) {
Ok(_) => Ok(true),
Err(_) => Ok(false),
}
} else {
Ok(false)
}
}
}
#[pyclass(name = "PaymentEngine")]
#[derive(Debug)]
pub struct PyPaymentEngine {
}
#[pymethods]
impl PyPaymentEngine {
#[new]
pub fn new() -> Self {
PyPaymentEngine {}
}
#[pyo3(signature = (id, from_wallet, to_wallet, amount, nonce))]
pub fn create_transaction(
&self,
id: &str,
from_wallet: &PyWalletId,
to_wallet: &PyWalletId,
amount: &PyAmount,
nonce: u64,
) -> PyResult<PyTransaction> {
PyTransaction::create_unsigned(id, from_wallet, to_wallet, amount, nonce)
}
pub fn create_wallet(&self, wallet_type: &str, identifier: &str) -> PyResult<PyWalletId> {
match wallet_type.to_lowercase().as_str() {
"bank" => Ok(PyWalletId::from_bank(identifier)),
"crypto" => Ok(PyWalletId::from_crypto(identifier)),
_ => Err(PyValueError::new_err(
"Wallet type must be 'bank' or 'crypto'",
)),
}
}
pub fn create_amount(&self, value: i128) -> PyResult<PyAmount> {
PyAmount::new(value)
}
pub fn verify_transaction(&self, tx: &PyTransaction) -> PyResult<bool> {
tx.verify_signature()
}
}
#[pymodule]
pub fn zaru_core(_py: Python<'_>, m: &PyModule) -> PyResult<()> {
m.add_class::<PyAmount>()?;
m.add_class::<PyWalletId>()?;
m.add_class::<PyTransaction>()?;
m.add_class::<PyPaymentEngine>()?;
m.add("__version__", env!("CARGO_PKG_VERSION"))?;
m.add("__author__", env!("CARGO_PKG_AUTHORS"))?;
#[pyfunction]
fn version() -> String {
env!("CARGO_PKG_VERSION").to_string()
}
m.add_function(wrap_pyfunction!(version, m)?)?;
#[pyfunction]
fn generate_keypair() -> String {
let keypair = Keypair::generate();
hex::encode(keypair.verifying.as_bytes())
}
m.add_function(wrap_pyfunction!(generate_keypair, m)?)?;
Ok(())
}