diff --git a/README.md b/README.md index 42f92b3..aa55d60 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,13 @@ -# SofizPay SDK for Java/Kotlin -
- SofizPay Logo - -

๐Ÿš€ A powerful Java/Kotlin SDK for SofizPay payment processing

- + SofizPay Logo + +

SofizPay Java / Kotlin SDK

+

The official Java and Kotlin SDK for secure digital payments on the SofizPay platform.

+ [![Maven Central](https://img.shields.io/maven-central/v/io.github.kenandarabeh/sofizpay-sdk-java.svg)](https://search.maven.org/artifact/io.github.kenandarabeh/sofizpay-sdk-java) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) - [![GitHub Stars](https://img.shields.io/github/stars/kenandarabeh/sofizpay-sdk-java.svg)](https://github.com/kenandarabeh/sofizpay-sdk-java/stargazers) - [![Issues](https://img.shields.io/github/issues/kenandarabeh/sofizpay-sdk-java.svg)](https://github.com/kenandarabeh/sofizpay-sdk-java/issues) [![Java 8+](https://img.shields.io/badge/Java-8%2B-orange.svg)](https://www.oracle.com/java/) - [![Kotlin](https://img.shields.io/badge/Kotlin-1.9.0-blue.svg)](https://kotlinlang.org/) + [![Kotlin](https://img.shields.io/badge/Kotlin-1.9%2B-blue.svg)](https://kotlinlang.org/)
--- @@ -18,56 +15,42 @@ ## ๐Ÿ“‹ Table of Contents - [Overview](#overview) -- [Features](#features) - [Installation](#installation) - [Quick Start](#quick-start) +- [Core Methods](#core-methods) - [API Reference](#api-reference) -- [Usage Examples](#usage-examples) +- [Digital Services (Missions)](#digital-services-missions) +- [Bank Integration (CIB)](#bank-integration-cib) - [Real-time Transaction Monitoring](#real-time-transaction-monitoring) -- [Banking Integration](#banking-integration) +- [Webhook Signature Verification](#webhook-signature-verification) +- [Response Format](#response-format) - [Error Handling](#error-handling) -- [Best Practices](#best-practices) +- [Security Best Practices](#security-best-practices) - [Java vs Kotlin](#java-vs-kotlin) -- [Testing](#testing) -- [Contributing](#contributing) +- [Use Cases](#use-cases) - [Support](#support) -- [License](#license) --- ## ๐ŸŒŸ Overview -SofizPay SDK is a comprehensive Java/Kotlin library for secure payment processing with real-time transaction monitoring, banking integration, and advanced payment management capabilities. +The SofizPay Java/Kotlin SDK is an enterprise-ready library for integrating **DZT digital payments** into JVM applications โ€” Android apps, Spring Boot backends, Gradle projects, and Kotlin Multiplatform. It offers full feature parity with the JS, Python, PHP, and Dart SDKs including exhaustive transaction history, real-time polling streams, CIB bank payments, Mission digital services, and RSA webhook verification. **Key Benefits:** -- ๐Ÿ” Secure payment processing -- โšก Real-time transaction monitoring with callbacks -- ๐ŸŽฏ Simple, intuitive API for both Java and Kotlin -- ๐Ÿฆ Banking transaction support -- ๐Ÿ“Š Comprehensive transaction history and search -- ๐Ÿ” Advanced signature verification -- ๐ŸŒ Testnet and Production environment support - ---- - -## โœจ Features - -- โœ… **Send Payments**: Secure payment transfers with memo support -- โœ… **Transaction History**: Retrieve and filter transaction records with pagination -- โœ… **Balance Checking**: Real-time balance queries -- โœ… **Transaction Search**: Find transactions by memo, hash, or account -- โœ… **Real-time Streams**: Live transaction monitoring with customizable callbacks -- โœ… **Banking Transactions**: Create bank transactions for deposits -- โœ… **Account Management**: Create new accounts and manage payment credentials -- โœ… **Signature Verification**: RSA signature validation for secure communications -- โœ… **Error Handling**: Robust error management and detailed reporting -- โœ… **Kotlin Support**: Modern Kotlin implementation with data classes and coroutines-ready +- โ˜• Works with Java 8+ and Kotlin 1.9+ +- ๐Ÿ“Š Exhaustive 24-transaction history (Path Payments, Trustlines, Account Creation) +- ๐Ÿฆ CIB/Dahabia bank deposit links +- ๐Ÿ“ฑ Phone, Internet & Game recharges (Mission APIs) +- ๐Ÿ”ด Real-time transaction polling with callbacks +- ๐Ÿ” RSA-SHA256 webhook signature verification +- ๐Ÿ”’ `AutoCloseable` โ€” safe resource management with try-with-resources --- ## ๐Ÿ“ฆ Installation -### Gradle (Groovy) +### Gradle (Groovy DSL) + ```gradle dependencies { implementation 'io.github.kenandarabeh:sofizpay-sdk-java:1.0.5-SNAPSHOT' @@ -75,6 +58,7 @@ dependencies { ``` ### Gradle (Kotlin DSL) + ```kotlin dependencies { implementation("io.github.kenandarabeh:sofizpay-sdk-java:1.0.5-SNAPSHOT") @@ -82,6 +66,7 @@ dependencies { ``` ### Maven + ```xml io.github.kenandarabeh @@ -91,6 +76,7 @@ dependencies { ``` ### Build from Source + ```bash git clone https://github.com/kenandarabeh/sofizpay-sdk-java.git cd sofizpay-sdk-java @@ -101,775 +87,604 @@ cd sofizpay-sdk-java ## ๐Ÿš€ Quick Start -### Java Example +### Java ```java import com.sofizpay.sdk.SofizPayStellarSDK; public class QuickStart { public static void main(String[] args) { - // Initialize SDK (testnet by default) - try (SofizPayStellarSDK sdk = new SofizPayStellarSDK(true)) { - - // Create payment data - SofizPayStellarSDK.PaymentData paymentData = new SofizPayStellarSDK.PaymentData(); - paymentData.secret = "YOUR_SECRET_KEY"; - paymentData.destination = "DESTINATION_ADDRESS"; - paymentData.amount = "10.0"; - paymentData.memo = "Payment for services"; - - // Send payment - SofizPayStellarSDK.PaymentResult result = sdk.submit(paymentData); - + // isTestnet = false โ†’ connects to mainnet + try (SofizPayStellarSDK sdk = new SofizPayStellarSDK(false)) { + + // 1. Check DZT balance + SofizPayStellarSDK.BalanceResult balance = sdk.getBalance("YOUR_PUBLIC_KEY"); + if (balance.success) { + System.out.println("๐Ÿ’ฐ Balance: " + balance.balance + " DZT"); + } + + // 2. Send a DZT payment + SofizPayStellarSDK.PaymentData data = new SofizPayStellarSDK.PaymentData(); + data.secret = "YOUR_SECRET_KEY"; + data.destination = "RECIPIENT_PUBLIC_KEY"; + data.amount = "100.0"; + data.memo = "Invoice #1234"; + + SofizPayStellarSDK.PaymentResult result = sdk.submit(data); + if (result.success) { - System.out.println("โœ… Payment successful! TX: " + result.transactionId); + System.out.println("โœ… Payment sent! TX: " + result.transactionId); } else { - System.out.println("โŒ Payment failed: " + result.message); + System.out.println("โŒ Failed: " + result.message); } } } } ``` -### Kotlin Example +### Kotlin ```kotlin import com.sofizpay.sdk.SofizPayStellarSDK fun main() { - // Initialize SDK (testnet by default) - SofizPayStellarSDK(isTestnet = true).use { sdk -> - - // Create payment data - val paymentData = SofizPayStellarSDK.PaymentData( - secret = "YOUR_SECRET_KEY", - destination = "DESTINATION_ADDRESS", - amount = "10.0", - memo = "Payment for services" - ) - - // Send payment - val result = sdk.submit(paymentData) - - if (result.success) { - println("โœ… Payment successful! TX: ${result.transactionId}") - } else { - println("โŒ Payment failed: ${result.message}") - } + SofizPayStellarSDK(false).use { sdk -> // false = mainnet + + // 1. Check DZT balance + val balance = sdk.getBalance("YOUR_PUBLIC_KEY") + if (balance.success) println("๐Ÿ’ฐ Balance: ${balance.balance} DZT") + + // 2. Send a DZT payment + val result = sdk.submit(SofizPayStellarSDK.PaymentData( + secret = "YOUR_SECRET_KEY", + destination = "RECIPIENT_PUBLIC_KEY", + amount = "100.0", + memo = "Invoice #1234" + )) + + if (result.success) println("โœ… TX: ${result.transactionId}") + else println("โŒ Failed: ${result.message}") } } ``` --- -## ๐Ÿ“š API Reference +## ๐Ÿ”ง Core Methods -### Core Payment Methods +### `getBalance(String accountId)` -#### `submit()` - Send Payment -Send payments with memo support. +Returns the DZT balance for a Stellar account. -**Java:** ```java -PaymentData paymentData = new PaymentData(); -paymentData.secret = "SECRET_KEY"; -paymentData.destination = "DESTINATION_ADDRESS"; -paymentData.amount = "10.0"; -paymentData.memo = "Payment memo"; - -PaymentResult result = sdk.submit(paymentData); +BalanceResult balance = sdk.getBalance("GCAZI...YOUR_PUBLIC_KEY"); +// balance.success โ†’ true/false +// balance.balance โ†’ "1500.0000000" +// balance.accountId โ†’ "GCAZI..." +// balance.message โ†’ "Balance retrieved successfully" ``` -**Kotlin:** -```kotlin -val paymentData = PaymentData( - secret = "SECRET_KEY", - destination = "DESTINATION_ADDRESS", - amount = "10.0", - memo = "Payment memo" -) +--- -val result = sdk.submit(paymentData) -``` +### `submit(PaymentData data)` -#### `getBalance()` - Check Balance -Get account balance. +Submits a DZT payment to the Stellar network. ```java -BalanceResult balance = sdk.getBalance("ACCOUNT_ADDRESS"); -System.out.println("Balance: " + balance.balance); +PaymentData data = new PaymentData(); +data.secret = "SXXX...YOUR_SECRET_KEY"; // 56-char seed starting with 'S' +data.destination = "GXXX...RECIPIENT"; // Recipient's Stellar public key +data.amount = "250.50"; // Amount in DZT (as string) +data.memo = "Order #5567"; // Optional (max 28 chars) + +PaymentResult result = sdk.submit(data); +// result.success โ†’ true/false +// result.transactionId โ†’ "abc123...hash" +// result.message โ†’ "Payment submitted successfully" ``` -#### `getPublicKey()` - Extract Public Key -Extract public key from secret key. +--- + +### `getPublicKey(String secretKey)` + +Derives the Stellar public key from a secret seed without a network call. ```java -PublicKeyResult result = sdk.getPublicKey("SECRET_KEY"); -String publicKey = result.publicKey; +PublicKeyResult result = sdk.getPublicKey("SXXX...SECRET"); +if (result.success) { + System.out.println("Public key: " + result.publicKey); +} ``` -### Transaction Management +--- + +### `getTransactions(String accountId, Integer limit)` -#### `getTransactions()` - Transaction History -Get paginated transaction history for an account. +Fetches **exhaustive transaction history** using the Stellar `/operations?join=transactions` endpoint. Captures the full 24-transaction parity set. ```java -TransactionHistoryResult result = sdk.getTransactions("ACCOUNT_ID", 50); -for (TransactionInfo tx : result.transactions) { - System.out.println("TX: " + tx.hash + " Amount: " + tx.amount); +TransactionHistoryResult history = sdk.getTransactions("YOUR_PUBLIC_KEY", 100); + +for (TransactionInfo tx : history.transactions) { + System.out.printf("[%s] %-16s โ€” %s %s%n", + tx.timestamp, + tx.type.toUpperCase(), + tx.amount, + tx.asset_code != null ? tx.asset_code : "DZT" + ); } ``` -#### `searchTransactionsByMemo()` - Search by Memo -Find transactions containing specific memo text. +**`TransactionInfo` fields:** + +| Field | Type | Description | +|-------|------|-------------| +| `id` | `String` | Transaction hash | +| `hash` | `String` | Transaction hash | +| `type` | `String` | `sent` / `received` / `trustline` / `account_created` | +| `amount` | `String` | Transferred amount | +| `from` | `String` | Sender's public key | +| `to` | `String` | Recipient's public key | +| `asset_code` | `String` | `DZT` or `XLM` | +| `memo` | `String` | Transaction memo | +| `created_at` | `String` | ISO 8601 timestamp | +| `successful` | `boolean` | Whether the transaction succeeded | + +--- + +### `searchTransactionsByMemo(String accountId, String memo, int limit)` + +Finds transactions matching a specific memo string. ```java -SearchTransactionsResult result = sdk.searchTransactionsByMemo("ACCOUNT_ID", "invoice-123", 10); +SearchTransactionsResult results = sdk.searchTransactionsByMemo( + "YOUR_PUBLIC_KEY", "Order #12345", 10 +); + +System.out.println("Found: " + results.totalFound + " transactions"); ``` -#### `getTransactionByHash()` - Get by Hash -Retrieve specific transaction by hash. +--- + +### `getTransactionByHash(String hash)` + +Retrieves a specific transaction by its hash. ```java -TransactionByHashResult result = sdk.getTransactionByHash("TRANSACTION_HASH"); +TransactionByHashResult result = sdk.getTransactionByHash("abc123...hash"); if (result.found) { - TransactionInfo tx = result.transaction; - System.out.println("Found transaction: " + tx.amount); + System.out.println("Amount: " + result.transaction.amount); +} else { + System.out.println("Transaction not found"); } ``` -### Account Management +--- -#### `createAccount()` - Create New Account -Generate a new account keypair. +## ๐Ÿ“š API Reference -```java -AccountCreationResult result = sdk.createAccount(); -System.out.println("Account ID: " + result.accountId); -System.out.println("Secret Key: " + result.secretKey); -``` +### Full Method Table + +| Method | Parameters | Returns | Description | +|--------|-----------|---------|-------------| +| `submit(data)` | `PaymentData` | `PaymentResult` | Submit DZT payment | +| `getBalance(accountId)` | `String` | `BalanceResult` | Get DZT balance | +| `getPublicKey(secretKey)` | `String` | `PublicKeyResult` | Derive public key from secret | +| `getTransactions(accountId, limit)` | `String, Integer` | `TransactionHistoryResult` | Full transaction history | +| `getTransactionByHash(hash)` | `String` | `TransactionByHashResult` | Find specific transaction | +| `searchTransactionsByMemo(accountId, memo, limit)` | `String, String, int` | `SearchTransactionsResult` | Search by memo | +| `startTransactionStream(accountId, callback)` | `String, TransactionCallback` | `StreamResult` | Start real-time monitoring | +| `stopTransactionStream(accountId)` | `String` | `StreamResult` | Stop monitoring | +| `getStreamStatus(accountId)` | `String` | `StreamStatusResult` | Check stream status | +| `makeCIBTransaction(data)` | `CIBTransactionData` | `CIBTransactionResult` | Create bank payment link | +| `checkCIBStatus(orderId)` | `String` | `ServiceResult` | Check CIB status | +| `rechargePhone(data)` | `Map` | `ServiceResult` | Phone recharge | +| `rechargeInternet(data)` | `Map` | `ServiceResult` | Internet recharge | +| `rechargeGame(data)` | `Map` | `ServiceResult` | Game top-up | +| `payBill(data)` | `Map` | `ServiceResult` | Bill payment | +| `getProducts(encryptedSk)` | `String` | `ServiceResult` | List available services | +| `getOperationHistory(encSk, limit, offset)` | `String, int, int` | `ServiceResult` | Mission history | +| `getOperationDetails(operationId, encSk)` | `String, String` | `ServiceResult` | Single operation details | +| `verifySignature(message, signatureUrlSafe)` | `String, String` | `boolean` | Validate RSA webhook | +| `createAccount()` | โ€” | `AccountCreationResult` | Generate new keypair | +| `fundAccountFromFaucet(accountId)` | `String` | `FundResult` | Fund via testnet faucet | +| `close()` | โ€” | `void` | Release all resources | + +--- -#### `fundAccountFromFaucet()` - Fund from Testnet Faucet -Fund account with testnet funds (testnet only). +## ๐Ÿ“ฑ Digital Services (Missions) + +Mission APIs let your users spend DZT on real-world digital services. All Mission calls require the user's `encrypted_sk` โ€” not the raw secret key. + +### Phone Recharge ```java -FundResult result = sdk.fundAccountFromFaucet("ACCOUNT_ID"); +Map data = new HashMap<>(); +data.put("encrypted_sk", "USER_ENCRYPTED_SECRET_KEY"); +data.put("phone", "0661000000"); +data.put("operator", "mobilis"); // "mobilis" | "djezzy" | "ooredoo" +data.put("amount", 100); +data.put("offer", "pix"); // Offer type from getProducts() + +ServiceResult result = sdk.rechargePhone(data); if (result.success) { - System.out.println("Account funded successfully"); + System.out.println("โœ… Phone recharged!"); } ``` -### Banking Integration +### Kotlin Phone Recharge -#### `makeCIBTransaction()` - Bank Transaction -Create bank transactions for deposits. +```kotlin +val data = mapOf( + "encrypted_sk" to "USER_ENCRYPTED_SECRET_KEY", + "phone" to "0661000000", + "operator" to "mobilis", + "amount" to 100, + "offer" to "pix" +) + +val result = sdk.rechargePhone(data) +if (result.success) println("โœ… Recharged!") +``` + +### Internet Recharge (Idoom 4G) ```java -CIBTransactionData cibData = new CIBTransactionData(); -cibData.account = "YOUR_SECRET_KEY"; -cibData.amount = "150"; -cibData.full_name = "Ahmed"; -cibData.phone = "+213*********"; -cibData.email = "ahmed@sofizpay.com"; -cibData.memo = "Payment"; -cibData.return_url = "https://yoursite.com/payment-success"; -cibData.redirect = true; +Map data = new HashMap<>(); +data.put("encrypted_sk", "USER_ENCRYPTED_SECRET_KEY"); +data.put("phone", "0661000000"); +data.put("amount", 200); +data.put("offer", "idoom_1gb"); -CIBTransactionResult result = sdk.makeCIBTransaction(cibData); +ServiceResult result = sdk.rechargeInternet(data); ``` -### Security +### Game Top-up (FreeFire, PUBG) -#### `verifySignature()` - RSA Signature Verification -Verify RSA signatures for secure communication. +```java +Map data = new HashMap<>(); +data.put("encrypted_sk", "USER_ENCRYPTED_SECRET_KEY"); +data.put("game", "freefire"); +data.put("player_id", "123456789"); +data.put("amount", 500); + +ServiceResult result = sdk.rechargeGame(data); +``` + +### Get Available Products ```java -boolean isValid = sdk.verifySignature("message", "base64_signature"); -if (isValid) { - System.out.println("Signature is valid"); +ServiceResult products = sdk.getProducts("USER_ENCRYPTED_SK"); +if (products.success) { + System.out.println("Products: " + products.data); } ``` ---- +### Operation History & Details -## ๐Ÿ”„ Real-time Transaction Monitoring +```java +// Last 10 operations +ServiceResult history = sdk.getOperationHistory("USER_ENCRYPTED_SK", 10, 0); -Monitor transactions in real-time with customizable callbacks. +// Single operation details +ServiceResult details = sdk.getOperationDetails("OPERATION_ID", "USER_ENCRYPTED_SK"); +``` -### Java Implementation +--- -```java -// Define callback -TransactionCallback callback = new TransactionCallback() { - @Override - public void onNewTransaction(TransactionInfo transaction) { - System.out.println("New transaction: " + transaction.type + - " Amount: " + transaction.amount); - - if ("received".equals(transaction.type)) { - handleIncomingPayment(transaction); - } - } -}; +## ๐Ÿฆ Bank Integration (CIB) -// Start monitoring -StreamResult streamResult = sdk.startTransactionStream("ACCOUNT_ID", callback); +Generate a Dahabia/CIB bank payment link for your customers. -// Check status -StreamStatusResult status = sdk.getStreamStatus("ACCOUNT_ID"); -System.out.println("Stream active: " + status.active); +```java +CIBTransactionData cibData = new CIBTransactionData(); +cibData.account = "YOUR_STELLAR_PUBLIC_KEY"; // Your SofizPay account +cibData.amount = "2500"; // Amount in DZT +cibData.full_name = "Ahmed Benali"; +cibData.phone = "0661234567"; +cibData.email = "ahmed@example.com"; +cibData.memo = "Order #789"; // Optional +cibData.return_url = "https://yoursite.com/callback"; // Optional +cibData.redirect = false; -// Stop monitoring -StreamResult stopResult = sdk.stopTransactionStream("ACCOUNT_ID"); +CIBTransactionResult result = sdk.makeCIBTransaction(cibData); + +if (result.success && result.data != null) { + String paymentUrl = (String) result.data.get("payment_url"); + System.out.println("Redirect customer to: " + paymentUrl); +} ``` -### Kotlin Implementation +### Kotlin CIB Example ```kotlin -// Start monitoring with lambda -val streamResult = sdk.startTransactionStream("ACCOUNT_ID") { transaction -> - println("New transaction: ${transaction.type} Amount: ${transaction.amount}") - - if (transaction.type == "received") { - handleIncomingPayment(transaction) - } +val cibData = SofizPayStellarSDK.CIBTransactionData().apply { + account = "YOUR_STELLAR_PUBLIC_KEY" + amount = "2500" + full_name = "Ahmed Benali" + phone = "0661234567" + email = "ahmed@example.com" + memo = "Order #789" + return_url = "https://yoursite.com/callback" + redirect = false } -// Check status -val status = sdk.getStreamStatus("ACCOUNT_ID") -println("Stream active: ${status.active}") - -// Stop monitoring -val stopResult = sdk.stopTransactionStream("ACCOUNT_ID") +val result = sdk.makeCIBTransaction(cibData) ``` ---- - -## ๐Ÿฆ Banking Integration - -Complete banking integration for payment processing. +### Check CIB Status ```java -public class PaymentProcessor { - private final SofizPayStellarSDK sdk; - - public PaymentProcessor() { - this.sdk = new SofizPayStellarSDK(false); // Production - } - - public void processDeposit(String account, String amount, String fullName, String phone, String email) { - CIBTransactionData cibData = new CIBTransactionData(); - cibData.account = account; - cibData.amount = amount; - cibData.full_name = fullName; - cibData.phone = phone; - cibData.email = email; - cibData.memo = "Payment deposit"; - cibData.return_url = "https://yoursite.com/payment-success"; - cibData.redirect = true; - - CIBTransactionResult result = sdk.makeCIBTransaction(cibData); - - if (result.success) { - System.out.println("Transaction successful"); - Map responseData = result.data; - // Process response data - } else { - System.err.println("Transaction failed: " + result.message); - } - } - - public void close() { - sdk.close(); - } +ServiceResult status = sdk.checkCIBStatus("ORDER_NUMBER"); +if (status.success) { + System.out.println("Payment status: " + ((Map) status.data).get("status")); } ``` --- -## ๐Ÿ’ก Usage Examples +## ๐Ÿ”ด Real-time Transaction Monitoring + +Monitor an account for new transactions using scheduled polling (every 5 seconds by default). -### Complete Payment System (Java) +### Java Callback ```java -public class PaymentSystem { - private final SofizPayStellarSDK sdk; - - public PaymentSystem() { - this.sdk = new SofizPayStellarSDK(true); // Testnet - } - - public void processPayment(String secretKey, String destination, - String amount, String memo) { - try { - // Check balance first - String accountId = sdk.getPublicKey(secretKey).publicKey; - BalanceResult balance = sdk.getBalance(accountId); - - System.out.println("Current balance: " + balance.balance); - - // Create payment - PaymentData paymentData = new PaymentData(); - paymentData.secret = secretKey; - paymentData.destination = destination; - paymentData.amount = amount; - paymentData.memo = memo; - - // Submit payment - PaymentResult result = sdk.submit(paymentData); - - if (result.success) { - System.out.println("โœ… Payment successful!"); - System.out.println("Transaction ID: " + result.transactionId); - - // Start monitoring for confirmation - startPaymentMonitoring(accountId, result.transactionId); - } else { - System.err.println("โŒ Payment failed: " + result.message); - } - - } catch (Exception e) { - System.err.println("Error processing payment: " + e.getMessage()); +TransactionCallback callback = new TransactionCallback() { + @Override + public void onNewTransaction(TransactionInfo tx) { + if ("received".equals(tx.type)) { + System.out.println("๐Ÿ’ธ Received " + tx.amount + " DZT from " + tx.from); } } - - private void startPaymentMonitoring(String accountId, String expectedTxId) { - TransactionCallback callback = new TransactionCallback() { - @Override - public void onNewTransaction(TransactionInfo transaction) { - if (expectedTxId.equals(transaction.hash)) { - System.out.println("โœ… Payment confirmed!"); - sdk.stopTransactionStream(accountId); - } - } - }; - - sdk.startTransactionStream(accountId, callback); - } - - public void close() { - sdk.close(); - } -} -``` +}; -### E-commerce Integration (Kotlin) +// Start monitoring +StreamResult startResult = sdk.startTransactionStream("YOUR_PUBLIC_KEY", callback); +System.out.println("Stream started: " + startResult.success); -```kotlin -class ECommercePaymentProcessor(private val isTestnet: Boolean = true) : AutoCloseable { - private val sdk = SofizPayStellarSDK(isTestnet) - - suspend fun processOrder(order: Order): PaymentResult { - return try { - // Validate order - if (order.amount <= 0) { - return PaymentResult(false, "Invalid amount", null) - } - - // Create payment - val paymentData = SofizPayStellarSDK.PaymentData( - secret = order.customerSecretKey, - destination = getCompanyAddress(), - amount = order.amount.toString(), - memo = "Order #${order.id}" - ) - - // Submit payment - val result = sdk.submit(paymentData) - - if (result.success) { - // Log successful payment - logPayment(order.id, result.transactionId!!) - - // Start monitoring for confirmation - startOrderMonitoring(order) - } - - result - } catch (e: Exception) { - PaymentResult(false, "Payment processing error: ${e.message}", null) - } - } - - private fun startOrderMonitoring(order: Order) { - val companyAccountId = getCompanyAddress() - - sdk.startTransactionStream(companyAccountId) { transaction -> - if (transaction.memo == "Order #${order.id}" && transaction.type == "received") { - println("โœ… Order ${order.id} payment confirmed!") - fulfillOrder(order) - sdk.stopTransactionStream(companyAccountId) - } - } - } - - private fun getCompanyAddress(): String = "COMPANY_ADDRESS" - private fun logPayment(orderId: String, txId: String) { /* Log to database */ } - private fun fulfillOrder(order: Order) { /* Fulfill order */ } - - override fun close() = sdk.close() -} +// Check status +StreamStatusResult status = sdk.getStreamStatus("YOUR_PUBLIC_KEY"); +System.out.println("Active: " + status.active); -data class Order( - val id: String, - val customerSecretKey: String, - val amount: Double, - val items: List -) +// Stop monitoring +sdk.stopTransactionStream("YOUR_PUBLIC_KEY"); ``` -### Transaction Analytics +### Kotlin Lambda -```java -public class TransactionAnalytics { - private final SofizPayStellarSDK sdk; - - public TransactionAnalytics() { - this.sdk = new SofizPayStellarSDK(false); // Production - } - - public void generateReport(String accountId, int days) { - try { - // Get recent transactions - TransactionHistoryResult result = sdk.getTransactions(accountId, 200); - - double totalReceived = 0; - double totalSent = 0; - int receivedCount = 0; - int sentCount = 0; - - for (TransactionInfo tx : result.transactions) { - if ("received".equals(tx.type)) { - totalReceived += Double.parseDouble(tx.amount); - receivedCount++; - } else if ("sent".equals(tx.type)) { - totalSent += Double.parseDouble(tx.amount); - sentCount++; - } - } - - System.out.println("=== Transaction Report ==="); - System.out.println("Total Received: " + totalReceived + " (" + receivedCount + " transactions)"); - System.out.println("Total Sent: " + totalSent + " (" + sentCount + " transactions)"); - System.out.println("Net Flow: " + (totalReceived - totalSent)); - - } catch (Exception e) { - System.err.println("Error generating report: " + e.getMessage()); - } - } - - public void close() { - sdk.close(); +```kotlin +val startResult = sdk.startTransactionStream("YOUR_PUBLIC_KEY") { tx -> + when (tx.type) { + "received" -> println("๐Ÿ’ธ Received ${tx.amount} DZT") + "sent" -> println("๐Ÿ“ค Sent ${tx.amount} DZT") } } ``` --- -## โš ๏ธ Error Handling +## ๐Ÿ”’ Webhook Signature Verification -All methods return structured response objects with consistent error handling: +Verify that incoming SofizPay webhook events are authentic using RSA-SHA256. ```java -// Java -PaymentResult result = sdk.submit(paymentData); -if (result.success) { - System.out.println("Success: " + result.transactionId); -} else { - System.err.println("Error: " + result.message); -} -``` +// Spring Boot webhook handler +@PostMapping("/webhook/sofizpay") +public ResponseEntity sofizpayWebhook(@RequestBody Map payload) { + String message = payload.get("message"); + String signatureB64 = payload.get("signature_url_safe"); -```kotlin -// Kotlin -val result = sdk.submit(paymentData) -if (result.success) { - println("Success: ${result.transactionId}") -} else { - println("Error: ${result.message}") -} -``` + boolean isValid = sdk.verifySignature(message, signatureB64); -### Common Error Messages + if (!isValid) { + return ResponseEntity.status(400).body(Map.of("error", "Invalid signature")); + } -- `"Secret key is required."` -- `"Destination address is required."` -- `"Amount is required."` -- `"Bad request: Invalid destination address"` -- `"Network error: Connection timeout"` -- `"Insufficient balance for transaction"` + // โœ… Process the verified payment event + System.out.println("Confirmed payment: " + message); + return ResponseEntity.ok(Map.of("status", "received")); +} +``` -### Exception Handling +### Kotlin Spring Boot -```java -try (SofizPayStellarSDK sdk = new SofizPayStellarSDK()) { - // SDK operations -} catch (Exception e) { - System.err.println("SDK error: " + e.getMessage()); - e.printStackTrace(); +```kotlin +@PostMapping("/webhook/sofizpay") +fun webhook(@RequestBody payload: Map): ResponseEntity<*> { + val isValid = sdk.verifySignature( + payload["message"] ?: "", + payload["signature_url_safe"] ?: "" + ) + + return if (isValid) ResponseEntity.ok(mapOf("status" to "received")) + else ResponseEntity.badRequest().body(mapOf("error" to "Invalid signature")) } ``` --- -## ๐Ÿ† Best Practices - -### Security -```java -// โœ… Store secret keys securely -String secretKey = System.getenv("SECRET_KEY"); // From environment -// โŒ Never hardcode secret keys in source code - -// โœ… Validate inputs -if (amount == null || amount.isEmpty()) { - throw new IllegalArgumentException("Amount is required"); -} +## ๐Ÿ“ค Response Format -// โœ… Use try-with-resources for automatic cleanup -try (SofizPayStellarSDK sdk = new SofizPayStellarSDK()) { - // Use SDK -} // Automatically closed -``` +All methods return typed result objects. The base structure includes `success` and `message`: -### Performance ```java -// โœ… Reuse SDK instances -private static final SofizPayStellarSDK sdk = new SofizPayStellarSDK(); +// All result objects share these fields: +result.success // boolean โ€” true on success +result.message // String โ€” human-readable status message -// โœ… Use reasonable limits for transaction queries -TransactionHistoryResult result = sdk.getTransactions(accountId, 50); // Not 1000+ +// Payment-specific additions: +result.transactionId // String โ€” Stellar transaction hash -// โœ… Stop streams when no longer needed -sdk.stopTransactionStream(accountId); +// Balance-specific: +result.balance // String โ€” current DZT balance + +// Transaction history: +result.transactions // List ``` -### Error Resilience +Always check `result.success` before accessing specific fields: + ```java -// โœ… Always check success status +PaymentResult result = sdk.submit(data); if (result.success) { - // Process successful result + logPayment(result.transactionId); } else { - // Handle error gracefully - logError("Payment failed", result.message); -} - -// โœ… Implement retry logic for network operations -public PaymentResult submitWithRetry(PaymentData data, int maxRetries) { - for (int i = 0; i < maxRetries; i++) { - PaymentResult result = sdk.submit(data); - if (result.success || !isRetryableError(result.message)) { - return result; - } - try { - Thread.sleep(1000 * (i + 1)); // Exponential backoff - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - break; - } - } - return new PaymentResult(false, "Max retries exceeded", null); + log.error("Payment failed: {}", result.message); } ``` --- -## ๐Ÿ”„ Java vs Kotlin - -This SDK provides both Java and Kotlin implementations with identical functionality: - -### Java Strengths -- **Familiar Syntax**: Traditional Java patterns -- **Enterprise Ready**: Mature ecosystem -- **Explicit Types**: Clear type declarations -- **Wide Adoption**: Large developer community - -### Kotlin Advantages -- **Concise Syntax**: Less boilerplate code -- **Null Safety**: Compile-time null checks -- **Data Classes**: Built-in equals/hashCode/toString -- **Lambda Support**: Modern functional programming +## โš ๏ธ Error Handling -### Migration Path -You can easily migrate from Java to Kotlin implementation: +The SDK captures all exceptions and returns them as `success: false` with a descriptive `message`. ```java -// Java -PaymentData data = new PaymentData(); -data.secret = "SECRET"; -data.destination = "DEST"; -data.amount = "10.0"; +PaymentResult result = sdk.submit(invalidData); + +if (!result.success) { + String msg = result.message; + + if (msg.contains("Secret key is required")) { + System.err.println("โŒ No secret key provided."); + } else if (msg.contains("Bad request")) { + System.err.println("โŒ Check destination address format."); + } else if (msg.contains("Network error")) { + System.err.println("โŒ Network unreachable โ€” retry later."); + } else { + System.err.println("โŒ Unexpected error: " + msg); + } +} ``` -```kotlin -// Kotlin -val data = PaymentData( - secret = "SECRET", - destination = "DEST", - amount = "10.0" -) -``` +**Common error messages:** ---- +| Error | Cause | +|-------|-------| +| `Secret key is required.` | Empty `data.secret` | +| `Destination address is required.` | Empty `data.destination` | +| `Amount is required.` | Empty `data.amount` | +| `Bad request: ...` | Invalid destination or insufficient balance | +| `Network error: ...` | Connectivity issue with Stellar Horizon | +| `Error extracting public key: ...` | Malformed secret key format | -## ๐Ÿงช Testing +--- -### Unit Tests -```bash -./gradlew test -``` +## ๐Ÿ›ก๏ธ Security Best Practices -### Integration Tests -```bash -./gradlew integrationTest -``` +| Rule | Why | +|------|-----| +| โŒ Never hardcode secret keys | Source code may be committed or deployed | +| โœ… Use environment variables | `System.getenv("SOFIZPAY_SECRET")` | +| โœ… Use try-with-resources | Ensures `sdk.close()` is always called | +| โœ… Verify webhook signatures | Prevents forged payment notifications | +| โœ… Use `encrypted_sk` for Missions | Protects the raw secret key | -### Test Example ```java -@Test -public void testPaymentSubmission() { - SofizPayStellarSDK sdk = new SofizPayStellarSDK(true); // Testnet - - // Create test account - AccountCreationResult account = sdk.createAccount(); - - // Fund account - FundResult fundResult = sdk.fundAccountFromFaucet(account.accountId); - assertTrue(fundResult.success); - - // Test payment - PaymentData paymentData = new PaymentData(); - paymentData.secret = account.secretKey; - paymentData.destination = "DESTINATION_ADDRESS"; - paymentData.amount = "1.0"; - paymentData.memo = "Test payment"; - - PaymentResult result = sdk.submit(paymentData); - assertTrue(result.success); - assertNotNull(result.transactionId); - - sdk.close(); +// โœ… Correct โ€” from environment +PaymentData data = new PaymentData(); +data.secret = System.getenv("SOFIZPAY_SECRET_KEY"); + +try (SofizPayStellarSDK sdk = new SofizPayStellarSDK(false)) { + PaymentResult result = sdk.submit(data); } + +// โŒ Never do this +data.secret = "SXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"; ``` --- -## ๐Ÿค Contributing - -We welcome contributions! Here's how to get started: +## ๐Ÿ”„ Java vs Kotlin -### Development Setup -```bash -# Clone repository -git clone https://github.com/kenandarabeh/sofizpay-sdk-java.git -cd sofizpay-sdk-java +Both styles are fully supported with identical functionality. -# Build project -./gradlew build +### Java (verbose, explicit) -# Run tests -./gradlew test +```java +PaymentData data = new PaymentData(); +data.secret = System.getenv("SOFIZPAY_SECRET"); +data.destination = "GXXX..."; +data.amount = "100"; +data.memo = "Payment"; -# Run examples -./gradlew runJavaExample -./gradlew runKotlinExample +PaymentResult result = sdk.submit(data); ``` -### Contribution Guidelines +### Kotlin (concise, idiomatic) -1. **Fork** the repository -2. **Create** feature branch: `git checkout -b feature/amazing-feature` -3. **Write** tests for new functionality -4. **Ensure** all tests pass: `./gradlew test` -5. **Commit** changes: `git commit -m 'Add amazing feature'` -6. **Push** to branch: `git push origin feature/amazing-feature` -7. **Open** a Pull Request - -### Code Style -- Follow Java/Kotlin conventions -- Add Javadoc/KDoc for public methods -- Include unit tests for new features -- Use meaningful variable names -- Handle errors gracefully +```kotlin +val result = sdk.submit(PaymentData( + secret = System.getenv("SOFIZPAY_SECRET") ?: "", + destination = "GXXX...", + amount = "100", + memo = "Payment" +)) +``` --- -## ๐Ÿ“ž Support - -- ๐Ÿ“– [Documentation](https://github.com/kenandarabeh/sofizpay-sdk-java#readme) -- ๐Ÿ› [Report Issues](https://github.com/kenandarabeh/sofizpay-sdk-java/issues) -- ๐Ÿ’ฌ [Discussions](https://github.com/kenandarabeh/sofizpay-sdk-java/discussions) -- โญ [Star the Project](https://github.com/kenandarabeh/sofizpay-sdk-java) -- ๐Ÿ“ง [Email Support](mailto:support@sofizpay.com) - -### Getting Help - -1. **Check the documentation** above -2. **Search existing issues** on GitHub -3. **Join our community** discussions -4. **Contact support** for enterprise needs - ---- +## ๐Ÿ’ก Use Cases -## ๐Ÿ“„ License +### Spring Boot Payment Service -This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. +```java +@Service +public class PaymentService { + private final SofizPayStellarSDK sdk = new SofizPayStellarSDK(false); + + public PaymentResult processPayment(String destination, String amount, String orderId) { + PaymentData data = new PaymentData(); + data.secret = System.getenv("SOFIZPAY_SECRET_KEY"); + data.destination = destination; + data.amount = amount; + data.memo = "Order #" + orderId; + + return sdk.submit(data); + } + @PreDestroy + public void onShutdown() { + sdk.close(); + } +} ``` -MIT License -Copyright (c) 2025 SofizPay +### Transaction Analytics Report -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +```java +public void generateReport(String accountId) { + TransactionHistoryResult history = sdk.getTransactions(accountId, 200); + + double totalReceived = 0, totalSent = 0; + for (TransactionInfo tx : history.transactions) { + double amount = Double.parseDouble(tx.amount); + if ("received".equals(tx.type)) totalReceived += amount; + else if ("sent".equals(tx.type)) totalSent += amount; + } -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. + System.out.printf("๐Ÿ“Š Total Received: %.4f DZT%n", totalReceived); + System.out.printf("๐Ÿ“Š Total Sent: %.4f DZT%n", totalSent); + System.out.printf("๐Ÿ“Š Net Flow: %.4f DZT%n", totalReceived - totalSent); +} ``` --- -## ๐Ÿ™ Acknowledgments +## ๐Ÿ“ž Support -- **Open Source Community** - For continuous improvement and feedback -- **Java/Kotlin Ecosystem** - For providing robust development tools -- **OkHttp** - Reliable HTTP client for network operations -- **Gson** - JSON serialization and deserialization -- **Gradle** - Build automation and dependency management -- **JetBrains** - Kotlin language development +- ๐ŸŒ **Website**: [SofizPay.com](https://sofizpay.com) +- ๐Ÿ“š **Full Docs**: [GitHub Repository](https://github.com/kenandarabeh/sofizpay-sdk-java#readme) +- ๐Ÿ› **Bug Reports**: [Open an Issue](https://github.com/kenandarabeh/sofizpay-sdk-java/issues) +- ๐Ÿ’ฌ **Discussions**: [Community Forum](https://github.com/kenandarabeh/sofizpay-sdk-java/discussions) --- -## ๐Ÿ”ฎ Roadmap +## License -### Upcoming Features -- [ ] **WebSocket Streams** - Real-time WebSocket transaction monitoring -- [ ] **Multi-signature Support** - Advanced security with multiple signatures -- [ ] **Advanced Analytics** - Built-in transaction analytics and reporting -- [ ] **Spring Boot Integration** - Auto-configuration for Spring applications -- [ ] **Reactive Streams** - RxJava/Reactor support for reactive applications -- [ ] **Mobile SDK** - Android and iOS native SDKs - -### Version History -- **v1.0.5-SNAPSHOT** - Latest development version with enhanced features -- **v1.0.0** - Initial release with core payment functionality -- **v1.0.0-kotlin** - Kotlin implementation with modern language features +MIT ยฉ [SofizPay Team](https://github.com/kenandarabeh) --- -
-

๐Ÿš€ Ready to integrate secure payments?

-

Start building with SofizPay SDK today!

- -

- GitHub โ€ข - Maven Central โ€ข - Support โ€ข - Contact -

- -

Made with โค๏ธ by the SofizPay Team

-
+**Built with โค๏ธ for Java & Kotlin developers | Version `1.0.5`** diff --git a/examples/ExhaustiveSDKTest.java b/examples/ExhaustiveSDKTest.java new file mode 100644 index 0000000..cc5cc4d --- /dev/null +++ b/examples/ExhaustiveSDKTest.java @@ -0,0 +1,140 @@ +package com.sofizpay.sdk; + +import java.util.HashMap; +import java.util.Map; +import java.util.List; + +public class ExhaustiveSDKTest { + + public static void main(String[] args) { + // Test Credentials + String pub = "GB3R3DRQXBPSC2XSFLPDRVCAVRCVJXAPJGBPMJ45JBRJC5QJPM7QTUSO"; + String encSk = "SCILSE4IMSKSZ7PPDP26CXOYFXWLUER47X5ROMYE6XLWSCZX2UPFKBCO"; + String txHash = "0000000000000000000000000000000000000000000000000000000000000000"; + + SofizPayStellarSDK sdk = new SofizPayStellarSDK(false); // Mainnet + System.out.println("--- Starting SofizPay Java SDK Test (v" + sdk.getVersion() + ") ---"); + + try { + // 1. Core: Fetch Balance + SofizPayStellarSDK.BalanceResult balance = sdk.getBalance(pub); + System.out.println("1. Get Balance: " + balance.balance + " (Success: " + balance.success + ")"); + + // 2. Core: Transaction History + SofizPayStellarSDK.TransactionHistoryResult history = sdk.getTransactions(pub, 5); + System.out.println("2. Get Transactions: " + history.transactions.size() + " items"); + + // 3. Core: Public Key Discovery + SofizPayStellarSDK.PublicKeyResult pkResult = sdk.getPublicKey(pub); + System.out.println("3. Get Public Key (Validation): " + (pkResult.success ? "Success" : "Expected fail")); + + // 4. Core: Search by Memo + SofizPayStellarSDK.SearchTransactionsResult search = sdk.searchTransactionsByMemo(pub, "test", 2); + System.out.println("4. Search by Memo: Found " + search.transactions.size() + " matches"); + + // 5. Core: Transaction by Hash + SofizPayStellarSDK.TransactionByHashResult txResult = sdk.getTransactionByHash(txHash); + System.out.println("5. Get Transaction by Hash: Found=" + txResult.found); + + // 6. CIB: Create Transaction + SofizPayStellarSDK.CIBTransactionData cibData = new SofizPayStellarSDK.CIBTransactionData(); + cibData.account = pub; + cibData.amount = "100.0"; + cibData.full_name = "Java SDK Tester"; + cibData.phone = "0661000000"; + cibData.email = "test@sofizpay.com"; + cibData.memo = "Test CIB Pay"; + cibData.redirect = false; + + SofizPayStellarSDK.CIBTransactionResult cibCreate = sdk.makeCIBTransaction(cibData); + System.out.println("6. CIB Create: Success=" + cibCreate.success); + + // 7. CIB: Check Status + if (cibCreate.success && cibCreate.data != null) { + String orderNo = "1234567890"; // Mock + SofizPayStellarSDK.ServiceResult status = sdk.checkCIBStatus(orderNo); + System.out.println("7. CIB Status: Success=" + status.success); + } else { + System.out.println("7. CIB Status: Skipped"); + } + + // 8. Services: Get Products + SofizPayStellarSDK.ServiceResult products = sdk.getProducts(encSk); + System.out.println("8. Get Products: Success=" + products.success); + + // 9. Services: Operation History + SofizPayStellarSDK.ServiceResult opHistory = sdk.getOperationHistory(encSk, 10, 0); + System.out.println("9. Operation History: Success=" + opHistory.success); + + // 10. Services: Operation Details + SofizPayStellarSDK.ServiceResult details = sdk.getOperationDetails("OP_12345", encSk); + System.out.println("10. Operation Details: Success=" + details.success); + + // 11. Mission: Phone Recharge + Map rechargeData = new HashMap<>(); + rechargeData.put("encrypted_sk", encSk); + rechargeData.put("phone", "0661000000"); + rechargeData.put("operator", "mobilis"); + rechargeData.put("amount", 100); + rechargeData.put("offer", "pix"); + + SofizPayStellarSDK.ServiceResult recharge = sdk.rechargePhone(rechargeData); + System.out.println("11. Recharge Phone: Success=" + recharge.success); + + // 12. Mission: Internet Recharge + Map internetData = new HashMap<>(); + internetData.put("encrypted_sk", encSk); + internetData.put("phone", "0661000000"); + internetData.put("operator", "idoom"); + internetData.put("amount", 2000); + internetData.put("offer", "adsl"); + + SofizPayStellarSDK.ServiceResult internet = sdk.rechargeInternet(internetData); + System.out.println("12. Recharge Internet: Success=" + internet.success); + + // 13. Mission: Game Recharge + Map gameData = new HashMap<>(); + gameData.put("encrypted_sk", encSk); + gameData.put("operator", "freefire"); + gameData.put("playerId", "123456789"); + gameData.put("amount", 100); + gameData.put("offer", "diamonds"); + + SofizPayStellarSDK.ServiceResult game = sdk.rechargeGame(gameData); + System.out.println("13. Recharge Game: Success=" + game.success); + + // 14. Mission: Pay Bill + Map billData = new HashMap<>(); + billData.put("encrypted_sk", encSk); + billData.put("operator", "sonelgaz"); + billData.put("bill_id", "BILL_999"); + billData.put("amount", 5500); + + SofizPayStellarSDK.ServiceResult bill = sdk.payBill(billData); + System.out.println("14. Pay Bill: Success=" + bill.success); + + // 15. Utility: Signature Verification + boolean isValid = sdk.verifySignature("test_message", "jHrONYl2NuBhjAYTgRq3xwRuW2ZYZIQlx1VWgiObu5FrSnY78pQ"); + System.out.println("15. Signature Verification: " + isValid); + + // 16. Stream Monitoring + System.out.println("16. Stream - Starting..."); + SofizPayStellarSDK.StreamResult stream = sdk.startTransactionStream(pub, new SofizPayStellarSDK.TransactionCallback() { + @Override + public void onNewTransaction(SofizPayStellarSDK.TransactionInfo tx) { + System.out.println("STREAM EVENT: " + tx.hash); + } + }); + System.out.println("Stream started: " + stream.success); + sdk.stopTransactionStream(pub); + + } catch (Exception e) { + System.err.println("Critical Test Failure: " + e.getMessage()); + e.printStackTrace(); + } finally { + sdk.close(); + } + + System.out.println("--- SDK Test Completed ---"); + } +} diff --git a/src/main/java/com/sofizpay/sdk/SofizPayStellarSDK.java b/src/main/java/com/sofizpay/sdk/SofizPayStellarSDK.java index 37d51c7..db3b599 100644 --- a/src/main/java/com/sofizpay/sdk/SofizPayStellarSDK.java +++ b/src/main/java/com/sofizpay/sdk/SofizPayStellarSDK.java @@ -103,22 +103,22 @@ public BalanceResult getBalance(String accountId) { for (AccountResponse.Balance balance : account.getBalances()) { if (balance.getAssetType().equals("native")) { - return new BalanceResult(true, "Balance retrieved successfully", balance.getBalance()); + return new BalanceResult(true, "Balance retrieved successfully", balance.getBalance(), accountId); } else if (balance.getAssetCode() != null && balance.getAssetCode().equals(DZT_ASSET_CODE)) { - return new BalanceResult(true, "Balance retrieved successfully", balance.getBalance()); + return new BalanceResult(true, "Balance retrieved successfully", balance.getBalance(), accountId); } } List balances = account.getBalances(); if (!balances.isEmpty()) { AccountResponse.Balance nativeBalance = balances.get(0); - return new BalanceResult(true, "Balance not found, showing native balance", nativeBalance.getBalance()); + return new BalanceResult(true, "Balance not found, showing native balance", nativeBalance.getBalance(), accountId); } - return new BalanceResult(true, "No balances found", "0"); + return new BalanceResult(true, "No balances found", "0", accountId); } catch (Exception e) { - return new BalanceResult(false, "Error fetching balance: " + e.getMessage(), "0"); + return new BalanceResult(false, "Error fetching balance: " + e.getMessage(), "0", accountId); } } @@ -181,70 +181,125 @@ public PaymentResult submit(PaymentData paymentData) { } } - public TransactionHistoryResult getTransactions(String accountId, int limit) { + public TransactionHistoryResult getTransactions(String accountId, Integer limit) { + List allTransactions = new ArrayList<>(); + String cursor = ""; + boolean hasMore = true; + int pageSize = (limit == null || limit > 200) ? 200 : limit; + try { - Page transactionsPage = server.transactions() - .forAccount(accountId) - .limit(limit) - .order(RequestBuilder.Order.DESC) - .execute(); - - List transactions = new ArrayList<>(); - - for (TransactionResponse tx : transactionsPage.getRecords()) { - TransactionInfo txInfo = new TransactionInfo(); - txInfo.id = tx.getHash(); - txInfo.hash = tx.getHash(); - txInfo.created_at = tx.getCreatedAt(); - txInfo.source_account = tx.getSourceAccount(); - txInfo.fee_charged = String.valueOf(tx.getFeeCharged()); - txInfo.successful = tx.getSuccessful(); - txInfo.operation_count = tx.getOperationCount(); - - if (tx.getMemo() != null) { - txInfo.memo = tx.getMemo().toString(); + while (hasMore) { + // โ”€โ”€ Comprehensive: Use /operations for 100% history coverage โ”€โ”€ + HttpUrl.Builder urlBuilder = HttpUrl.parse(server.getHorizonUrl() + "accounts/" + accountId + "/operations") + .newBuilder() + .addQueryParameter("limit", String.valueOf(pageSize)) + .addQueryParameter("order", "desc") + .addQueryParameter("join", "transactions"); + + if (!cursor.isEmpty()) { + urlBuilder.addQueryParameter("cursor", cursor); } - - try { - Page operations = server.operations().forTransaction(tx.getHash()).execute(); - List operationRecords = operations.getRecords(); - - for (org.stellar.sdk.responses.operations.OperationResponse operation : operationRecords) { - if (operation.getType().equals("payment")) { - PaymentOperationResponse paymentOp = (PaymentOperationResponse) operation; - txInfo.amount = paymentOp.getAmount(); - txInfo.from = paymentOp.getFrom(); - txInfo.to = paymentOp.getTo(); - - if (paymentOp.getFrom().equals(accountId)) { - txInfo.type = "sent"; - } else if (paymentOp.getTo().equals(accountId)) { - txInfo.type = "received"; - } - - if (paymentOp.getAsset().getType().equals("native")) { - txInfo.asset_type = "native"; - txInfo.asset_code = "DZT"; - } else { - txInfo.asset_type = "credit_alphanum4"; - txInfo.asset_code = paymentOp.getAsset().toString(); + + Request request = new Request.Builder() + .url(urlBuilder.build()) + .get() + .addHeader("Accept", "application/json") + .build(); + + try (Response response = httpClient.newCall(request).execute()) { + if (!response.isSuccessful()) { + throw new IOException("Unexpected code " + response); + } + + String responseBody = response.body().string(); + Map data = gson.fromJson(responseBody, Map.class); + Map embedded = (Map) data.get("_embedded"); + List> records = (List>) embedded.get("records"); + + if (records == null || records.isEmpty()) { + break; + } + + for (Map op : records) { + Map txDetail = (Map) op.get("transaction"); + String type = (String) op.get("type"); + + boolean isAdded = false; + TransactionInfo txInfo = new TransactionInfo(); + txInfo.id = (String) op.get("transaction_hash"); + txInfo.transactionId = (String) op.get("transaction_hash"); + txInfo.hash = (String) op.get("transaction_hash"); + txInfo.created_at = (String) op.get("created_at"); + txInfo.timestamp = (String) op.get("created_at"); + + // 1. Handle Payments (Direct & Path) + boolean isPaymentType = "payment".equals(type) || + "path_payment_strict_receive".equals(type) || + "path_payment_strict_send".equals(type); + + if (isPaymentType && DZT_ASSET_CODE.equals(op.get("asset_code")) && DZT_ISSUER.equals(op.get("asset_issuer"))) { + txInfo.type = accountId.equals(op.get("from")) ? "sent" : "received"; + txInfo.amount = (String) op.get("amount"); + txInfo.from = (String) op.get("from"); + txInfo.to = (String) (op.get("to") != null ? op.get("to") : op.get("destination")); + txInfo.asset_code = (String) op.get("asset_code"); + txInfo.asset_type = (String) op.get("asset_type"); + isAdded = true; + } + // 2. Handle Trustline (DZT) + else if ("change_trust".equals(type) && DZT_ASSET_CODE.equals(op.get("asset_code")) && DZT_ISSUER.equals(op.get("asset_issuer"))) { + txInfo.type = "trustline"; + txInfo.amount = "0"; + txInfo.asset_code = (String) op.get("asset_code"); + txInfo.to = "Trustline Created"; + isAdded = true; + } + // 3. Handle Account Creation + else if ("create_account".equals(type) && accountId.equals(op.get("account"))) { + txInfo.type = "account_created"; + txInfo.amount = (String) op.get("starting_balance"); + txInfo.from = (String) op.get("funder"); + txInfo.to = accountId; + txInfo.asset_code = "XLM"; + isAdded = true; + } + + if (isAdded) { + if (txDetail != null) { + txInfo.memo = (String) txDetail.get("memo"); + txInfo.successful = Boolean.TRUE.equals(txDetail.get("successful")); + txInfo.fee_charged = String.valueOf(txDetail.get("fee_charged")); + Object opCount = txDetail.get("operation_count"); + if (opCount instanceof Double) { + txInfo.operation_count = ((Double) opCount).intValue(); + } } + allTransactions.add(txInfo); + } + + if (limit != null && allTransactions.size() >= limit) { + hasMore = false; break; } } - } catch (Exception e) { - System.err.println("Cannot fetch operation details for transaction: " + tx.getHash()); + + if (!hasMore) break; + + if (records.size() < pageSize) { + hasMore = false; + } else { + cursor = (String) records.get(records.size() - 1).get("paging_token"); + } } - - transactions.add(txInfo); } - + return new TransactionHistoryResult( - true, - "Fetched all transactions (" + transactions.size() + " transactions)", - transactions + true, + "Fetched " + allTransactions.size() + " DZT-related activities", + allTransactions, + accountId ); - + } catch (Exception e) { return new TransactionHistoryResult(false, "Error fetching transactions: " + e.getMessage(), new ArrayList<>()); } @@ -264,8 +319,10 @@ public SearchTransactionsResult searchTransactionsByMemo(String accountId, Strin if (tx.getMemo() != null && tx.getMemo().toString().contains(memo)) { TransactionInfo txInfo = new TransactionInfo(); txInfo.id = tx.getHash(); + txInfo.transactionId = tx.getHash(); txInfo.hash = tx.getHash(); txInfo.created_at = tx.getCreatedAt(); + txInfo.timestamp = tx.getCreatedAt(); txInfo.source_account = tx.getSourceAccount(); txInfo.memo = tx.getMemo().toString(); txInfo.successful = tx.getSuccessful(); @@ -292,14 +349,21 @@ public SearchTransactionsResult searchTransactionsByMemo(String accountId, Strin } } + List limitedTransactions = matchingTransactions.size() > limit + ? matchingTransactions.subList(0, limit) + : matchingTransactions; + return new SearchTransactionsResult( - true, - "Found " + matchingTransactions.size() + " transactions containing \"" + memo + "\"", - matchingTransactions + true, + matchingTransactions.size() + " transactions found matching memo", + limitedTransactions, + matchingTransactions.size(), + memo, + accountId ); - + } catch (Exception e) { - return new SearchTransactionsResult(false, "Error searching transactions: " + e.getMessage(), new ArrayList<>()); + return new SearchTransactionsResult(false, "Error: " + e.getMessage(), new ArrayList<>(), 0, memo, accountId); } } @@ -310,8 +374,10 @@ public TransactionByHashResult getTransactionByHash(String transactionHash) { TransactionInfo txInfo = new TransactionInfo(); txInfo.id = transaction.getHash(); + txInfo.transactionId = transaction.getHash(); txInfo.hash = transaction.getHash(); txInfo.created_at = transaction.getCreatedAt(); + txInfo.timestamp = transaction.getCreatedAt(); txInfo.source_account = transaction.getSourceAccount(); txInfo.successful = transaction.getSuccessful(); txInfo.fee_charged = String.valueOf(transaction.getFeeCharged()); @@ -611,6 +677,158 @@ public void close() { } } } + + // --- Service & Mission APIs --- + + public ServiceResult rechargePhone(Map data) { + return performServiceOperation(data); + } + + public ServiceResult rechargeInternet(Map data) { + return performServiceOperation(data); + } + + public ServiceResult rechargeGame(Map data) { + return performServiceOperation(data); + } + + public ServiceResult payBill(Map data) { + return performServiceOperation(data); + } + + private ServiceResult performServiceOperation(Map data) { + try { + String json = gson.toJson(data); + RequestBody body = RequestBody.create(json, MediaType.parse("application/json; charset=utf-8")); + Request request = new Request.Builder() + .url("https://www.sofizpay.com/services/operation_post") + .post(body) + .build(); + + try (Response response = httpClient.newCall(request).execute()) { + String responseBody = response.body() != null ? response.body().string() : ""; + if (response.isSuccessful()) { + Object responseData = gson.fromJson(responseBody, Object.class); + return new ServiceResult(true, "Operation successful", responseData); + } else { + return new ServiceResult(false, "Operation failed: " + response.code() + " - " + responseBody, null); + } + } + } catch (Exception e) { + return new ServiceResult(false, "Error: " + e.getMessage(), null); + } + } + + public ServiceResult getOperationDetails(String operationId, String encryptedSecretKey) { + try { + HttpUrl url = HttpUrl.parse("https://www.sofizpay.com/operation-details/" + operationId + "/") + .newBuilder() + .addQueryParameter("encrypted_sk", encryptedSecretKey) + .build(); + Request request = new Request.Builder().url(url).get().build(); + + try (Response response = httpClient.newCall(request).execute()) { + String responseBody = response.body() != null ? response.body().string() : ""; + if (response.isSuccessful()) { + Object responseData = gson.fromJson(responseBody, Object.class); + return new ServiceResult(true, "Details fetched successfully", responseData); + } else { + return new ServiceResult(false, "Failed to fetch details: " + response.code(), null); + } + } + } catch (Exception e) { + return new ServiceResult(false, "Error: " + e.getMessage(), null); + } + } + + public ServiceResult getOperationHistory(String encryptedSecretKey, int limit, int offset) { + try { + HttpUrl url = HttpUrl.parse("https://sofizpay.com/services/operation-history/") + .newBuilder() + .addQueryParameter("encrypted_sk", encryptedSecretKey) + .addQueryParameter("limit", String.valueOf(limit)) + .addQueryParameter("offset", String.valueOf(offset)) + .build(); + Request request = new Request.Builder().url(url).get().build(); + + try (Response response = httpClient.newCall(request).execute()) { + String responseBody = response.body() != null ? response.body().string() : ""; + if (response.isSuccessful()) { + Object responseData = gson.fromJson(responseBody, Object.class); + return new ServiceResult(true, "History fetched successfully", responseData); + } else { + return new ServiceResult(false, "Failed to fetch history: " + response.code(), null); + } + } + } catch (Exception e) { + return new ServiceResult(false, "Error: " + e.getMessage(), null); + } + } + + public ServiceResult getProducts(String encryptedSecretKey) { + try { + Map data = new HashMap<>(); + if (encryptedSecretKey != null) { + data.put("encrypted_sk", encryptedSecretKey); + } + String json = gson.toJson(data); + RequestBody body = RequestBody.create(json, MediaType.parse("application/json")); + + Request request = new Request.Builder() + .url("https://sofizpay.com/services/get_products/") + .method("GET", body) + .build(); + + try (Response response = httpClient.newCall(request).execute()) { + String responseBody = response.body() != null ? response.body().string() : ""; + if (response.isSuccessful()) { + Object responseData = gson.fromJson(responseBody, Object.class); + return new ServiceResult(true, "Products fetched successfully", responseData); + } else { + return new ServiceResult(false, "Failed to fetch products: " + response.code(), null); + } + } + } catch (Exception e) { + return new ServiceResult(false, "Error: " + e.getMessage(), null); + } + } + + public ServiceResult checkCIBStatus(String cibTransactionId) { + try { + HttpUrl url = HttpUrl.parse("https://www.sofizpay.com/cib-transaction-check/") + .newBuilder() + .addQueryParameter("order_number", cibTransactionId) + .build(); + Request request = new Request.Builder().url(url).get().build(); + + try (Response response = httpClient.newCall(request).execute()) { + String responseBody = response.body() != null ? response.body().string() : ""; + if (response.isSuccessful()) { + Object responseData = gson.fromJson(responseBody, Object.class); + return new ServiceResult(true, "Status fetched successfully", responseData); + } else { + return new ServiceResult(false, "Failed to fetch status: " + response.code(), null); + } + } + } catch (Exception e) { + return new ServiceResult(false, "Error: " + e.getMessage(), null); + } + } + + public static class ServiceResult { + public boolean success; + public String message; + public Object data; + public String timestamp; + + public ServiceResult(boolean success, String message, Object data) { + this.success = success; + this.message = message; + this.data = data; + this.timestamp = Instant.now().toString(); + } + } + public static class PaymentData { public String secret; @@ -623,11 +841,15 @@ public static class PaymentResult { public boolean success; public String message; public String transactionId; + public String transactionHash; + public String timestamp; public PaymentResult(boolean success, String message, String transactionId) { this.success = success; this.message = message; this.transactionId = transactionId; + this.transactionHash = transactionId; + this.timestamp = Instant.now().toString(); } } @@ -635,11 +857,13 @@ public static class PublicKeyResult { public boolean success; public String message; public String publicKey; + public String timestamp; public PublicKeyResult(boolean success, String message, String publicKey) { this.success = success; this.message = message; this.publicKey = publicKey; + this.timestamp = Instant.now().toString(); } } @@ -647,18 +871,26 @@ public static class BalanceResult { public boolean success; public String message; public String balance; + public String publicKey; + public String asset_code = "DZT"; + public String asset_issuer = "GCAZI7YBLIDJWIVEL7ETNAZGPP3LC24NO6KAOBWZHUERXQ7M5BC52DLV"; + public String timestamp; - public BalanceResult(boolean success, String message, String balance) { + public BalanceResult(boolean success, String message, String balance, String publicKey) { this.success = success; this.message = message; this.balance = balance; + this.publicKey = publicKey; + this.timestamp = Instant.now().toString(); } } public static class TransactionInfo { public String id; + public String transactionId; public String hash; public String created_at; + public String timestamp; public String source_account; public String memo; public String amount; @@ -670,17 +902,24 @@ public static class TransactionInfo { public String fee_charged; public boolean successful; public int operation_count; + public String status = "completed"; } public static class TransactionHistoryResult { public boolean success; public String message; public List transactions; + public int total; + public String publicKey; + public String timestamp; - public TransactionHistoryResult(boolean success, String message, List transactions) { + public TransactionHistoryResult(boolean success, String message, List transactions, String publicKey) { this.success = success; this.message = message; this.transactions = transactions; + this.total = transactions.size(); + this.publicKey = publicKey; + this.timestamp = Instant.now().toString(); } } @@ -688,11 +927,21 @@ public static class SearchTransactionsResult { public boolean success; public String message; public List transactions; + public int total; + public int totalFound; + public String searchMemo; + public String publicKey; + public String timestamp; - public SearchTransactionsResult(boolean success, String message, List transactions) { + public SearchTransactionsResult(boolean success, String message, List transactions, int totalFound, String searchMemo, String publicKey) { this.success = success; this.message = message; this.transactions = transactions; + this.total = transactions.size(); + this.totalFound = totalFound; + this.searchMemo = searchMemo; + this.publicKey = publicKey; + this.timestamp = Instant.now().toString(); } } @@ -702,6 +951,7 @@ public static class TransactionByHashResult { public boolean found; public TransactionInfo transaction; public String hash; + public String timestamp; public TransactionByHashResult(boolean success, String message, boolean found, TransactionInfo transaction, String hash) { this.success = success; @@ -709,6 +959,7 @@ public TransactionByHashResult(boolean success, String message, boolean found, T this.found = found; this.transaction = transaction; this.hash = hash; + this.timestamp = Instant.now().toString(); } }