UNPKG

medusa-payment-solana

Version:

Solana crypto currency payment provider for MedusaJS 2.0

429 lines 18.2 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const utils_1 = require("@medusajs/framework/utils"); const errors_1 = require("./providers/solana-payment/errors"); const solana_client_1 = __importDefault(require("./providers/solana-payment/solana-client")); const utils_2 = require("./providers/solana-payment/utils"); class SolanaPaymentProviderService extends utils_1.AbstractPaymentProvider { async initialize() { this.solanaClient = new solana_client_1.default({ rpcUrl: this.options_.rpcUrl, mnemonic: this.options_.passPhrase, coldStorageWallet: this.options_.coldStorageWallet, currencyConverter: { provider: this.options_.currencyConverter?.provider || 'default', apiKey: this.options_.currencyConverter?.apiKey } }); } constructor(container, options) { super(container, options); this.logger_ = container.logger; this.options_ = options; // Initialize the provider asynchronously this.initialize().catch(error => { this.logger_.error(`Error initializing Solana payment provider: ${error}`); throw error; }); } /** * Validate the options provided to the payment provider */ static validateOptions(options) { if (!options.passPhrase) { throw new errors_1.SolanaPaymentError(errors_1.SolanaPaymentError.Types.INVALID_DATA, 'Solana mnemonic phrase is required'); } } /** * Initialize a new payment session */ async initiatePayment(input) { try { const { amount, currency_code, context } = input; this.logger_.info('Initiating Solana payment with amount: ' + amount + ' and currency: ' + currency_code); // Generate a unique payment ID const paymentId = (0, utils_2.generatePaymentId)(); // Convert the amount to SOL const solAmount = await this.solanaClient.convertToSol(Number(amount), currency_code); this.logger_.info(`Converted ${amount} ${currency_code} to ${solAmount} SOL`); const solana_one_time_address = this.solanaClient.generateAddress(paymentId); // Calculate expiration timestamp based on configuration const expirationSeconds = this.options_.sessionExpirationSeconds || 60 * 5; // Default to 5 minutes if not set const expirationDate = new Date(); expirationDate.setSeconds(expirationDate.getSeconds() + expirationSeconds); // Create payment details const paymentDetails = { id: paymentId, amount: Number(amount), currency_code, sol_amount: solAmount, received_sol_amount: 0, solana_one_time_address, status: 'pending', created_at: new Date(), updated_at: new Date(), expiration_date: expirationDate }; this.logger_.info('Created payment details:' + JSON.stringify(paymentDetails)); // Create a description for the payment const customContext = context; const description = (0, utils_2.createPaymentDescription)(customContext?.order_id || 'unknown', Number(amount), currency_code, solAmount); this.logger_.info(`Initiated Solana payment: ${paymentId} for ${solAmount} SOL`); // Return with additional data that will be stored with the payment session return { id: paymentId, data: { ...paymentDetails, description, }, }; } catch (error) { this.logger_.error(`Error initiating Solana payment: ${error}`); throw error; } } /** * Renews the payment details with a new price and expiration date. */ async renewPayment(paymentDetails) { this.logger_.info(`Renewing payment session: ${paymentDetails.id}`); // Update the price to the current conversion rate const newSolAmount = await this.solanaClient.convertToSol(paymentDetails.amount, paymentDetails.currency_code); this.logger_.info(`Updated price for ${paymentDetails.id} from ${paymentDetails.sol_amount} SOL to ${newSolAmount} SOL`); // Calculate a new expiration timestamp const expirationSeconds = this.options_.sessionExpirationSeconds || 60 * 5; // Default to 5 minutes const newExpirationDate = new Date(); newExpirationDate.setSeconds(newExpirationDate.getSeconds() + expirationSeconds); const updatedDetails = { ...paymentDetails, sol_amount: newSolAmount, updated_at: new Date(), expiration_date: newExpirationDate, status: 'pending' // Reset status to pending }; return updatedDetails; } /** * Authorize a payment */ async authorizePayment(input) { try { const { data } = input; if (!data) { throw new errors_1.SolanaPaymentError(errors_1.SolanaPaymentError.Types.INVALID_DATA, 'No payment data found'); } const initialPaymentDetails = data; // 1. Get blockchain data const { receivedAmount, lastTransactionTime } = await this.solanaClient.checkPayment(initialPaymentDetails); // Create a new object with the updated received amount const paymentDetails = { ...initialPaymentDetails, received_sol_amount: receivedAmount, }; const expirationDate = paymentDetails.expiration_date ? new Date(paymentDetails.expiration_date) : new Date(0); const isExpired = new Date() > expirationDate; // Scenario A: Session is NOT expired if (!isExpired) { if (receivedAmount >= paymentDetails.sol_amount) { this.logger_.info(`Payment authorized (on time): ${paymentDetails.id}`); return { status: 'authorized', data: { ...paymentDetails, status: 'authorized', updated_at: new Date() }, }; } // Not enough paid yet, still pending return { status: 'pending', data: { ...paymentDetails, updated_at: new Date() } }; } // Scenario B: Session IS expired this.logger_.info(`Payment session expired: ${paymentDetails.id}. Checking payment conditions...`); // Case B1: User paid on time, but check happens later than expiration time const paidOnTime = lastTransactionTime && lastTransactionTime <= expirationDate && receivedAmount >= paymentDetails.sol_amount; if (paidOnTime) { this.logger_.info(`Payment authorized (late confirmation, paid on time): ${paymentDetails.id}`); return { status: 'authorized', data: { ...paymentDetails, status: 'authorized', updated_at: new Date() }, }; } // Case B2: User paid late or underpaid. Renew the price and check against it. const newSolAmount = await this.solanaClient.convertToSol(paymentDetails.amount, paymentDetails.currency_code); if (receivedAmount >= newSolAmount) { this.logger_.info(`Payment authorized (paid late, but covered new price): ${paymentDetails.id}`); return { status: 'authorized', data: { ...paymentDetails, status: 'authorized', updated_at: new Date() }, }; } // If none of the above, renew the session with new price and expiration const renewedDetails = await this.renewPayment(paymentDetails); const finalDetails = { ...renewedDetails, received_sol_amount: receivedAmount, // carry over the received amount }; this.logger_.info(`Payment session renewed with new price: ${finalDetails.sol_amount} SOL. Still pending.`); return { status: 'pending', data: finalDetails, }; } catch (error) { this.logger_.error(`Error authorizing Solana payment: ${error}`); throw error; } } /** * Capture an authorized payment */ async capturePayment(input) { try { const { data } = input; if (!data) { throw new errors_1.SolanaPaymentError(errors_1.SolanaPaymentError.Types.INVALID_DATA, 'No payment data found'); } const paymentDetails = data; // Mark payment as captured const updatedData = { ...paymentDetails, status: 'captured', updated_at: new Date(), }; this.logger_.info(`Payment captured: ${paymentDetails.id}`); // Transfer funds to cold storage wallet try { const txSignature = await this.solanaClient.transferToColdStorage(paymentDetails.id); this.logger_.info(`Funds transferred to cold storage: ${txSignature}`); } catch (error) { this.logger_.error(`Error transferring funds to cold storage: ${error}`); // Notify admin about the failure // this.notifyAdmin({ // type: 'cold_storage_transfer_failed', // paymentId: paymentDetails.id, // error: error instanceof Error ? error.message : 'Unknown error' // }); // We still consider the payment captured even if the transfer fails // since the funds are in our one-time wallet } return { data: updatedData, }; } catch (error) { this.logger_.error(`Error capturing Solana payment: ${error}`); throw error; } } /** * Cancel a payment */ async cancelPayment(input) { try { const { data } = input; if (!data) { throw new errors_1.SolanaPaymentError(errors_1.SolanaPaymentError.Types.INVALID_DATA, 'No payment data found'); } const paymentDetails = data; // For Solana, we can only mark the payment as canceled // Actual refund would need to be handled separately if payment was already received this.logger_.info(`Payment canceled: ${paymentDetails.id}`); const updatedData = { ...paymentDetails, status: 'canceled', updated_at: new Date(), }; return { data: updatedData, }; } catch (error) { this.logger_.error(`Error canceling Solana payment: ${error}`); throw error; } } /** * Refund a captured payment */ async refundPayment(input) { try { const { data, amount } = input; if (!data) { throw new errors_1.SolanaPaymentError(errors_1.SolanaPaymentError.Types.INVALID_DATA, 'No payment data found'); } const paymentDetails = data; // For Solana, refunds would need to be handled manually // Here we just mark the payment as refunded this.logger_.info(`Payment refund initiated: ${paymentDetails.id} for ${amount}`); const updatedData = { ...paymentDetails, status: 'refunded', updated_at: new Date(), }; return { data: updatedData, }; } catch (error) { this.logger_.error(`Error refunding Solana payment: ${error}`); throw error; } } /** * Get the status of a payment * This function is mainely used by webhook based payment modules, but is required by medusa core. * It is UNCLEAR what data structure it is called with when called by medusa. Beware! */ async getPaymentStatus(input) { try { const { data } = input; if (!data) { throw new errors_1.SolanaPaymentError(errors_1.SolanaPaymentError.Types.INVALID_DATA, 'No payment data found'); } const paymentDetails = data; // If payment is already in a final state, return that state if (paymentDetails.status === 'captured') { return { status: 'captured' }; } else if (paymentDetails.status === 'canceled') { return { status: 'canceled' }; } else if (paymentDetails.status === 'requires_more') { return { status: 'requires_more' }; } // Note: the payment is never "authorized" in Solana, it is either pending or captured // Payment not yet received return { status: 'pending', data: paymentDetails }; } catch (error) { this.logger_.error(`Error getting Solana payment status: ${error}`); throw error; } } /** * Retrieve payment data */ async retrievePayment(input) { try { const { data } = input; if (!data) { throw new errors_1.SolanaPaymentError(errors_1.SolanaPaymentError.Types.INVALID_DATA, 'No payment data found'); } // For Solana, we just return the stored payment data // In a more advanced implementation, we might fetch additional data from the blockchain return data; } catch (error) { this.logger_.error(`Error retrieving Solana payment: ${error}`); throw error; } } /** * Update payment data */ async updatePayment(input) { try { const { data, amount, currency_code, context } = input; if (!data) { throw new errors_1.SolanaPaymentError(errors_1.SolanaPaymentError.Types.INVALID_DATA, 'No payment data found'); } const paymentDetails = data; // Convert the new amount to SOL const solAmount = await this.solanaClient.convertToSol(Number(amount), currency_code); // Create a new description for the payment const customContext = context; const description = (0, utils_2.createPaymentDescription)(customContext?.order_id || 'unknown', Number(amount), currency_code, solAmount); // Update payment details const updatedData = { ...paymentDetails, amount: Number(amount), currency_code, sol_amount: solAmount, updated_at: new Date(), description, }; this.logger_.info(`Payment updated: ${paymentDetails.id} to ${solAmount} SOL`); // Return the updated data return { data: updatedData }; } catch (error) { this.logger_.error(`Error updating Solana payment: ${error}`); throw error; } } /** * Delete a payment session */ async deletePayment(input) { try { const { data } = input; if (!data) { throw new errors_1.SolanaPaymentError(errors_1.SolanaPaymentError.Types.INVALID_DATA, 'No payment data found'); } const paymentDetails = data; this.logger_.info(`Payment deleted: ${paymentDetails.id}`); // For Solana, we just mark the session as deleted // No actual deletion from the blockchain is possible return {}; } catch (error) { this.logger_.error(`Error deleting Solana payment: ${error}`); throw error; } } /** * Process webhook events * This could be used to handle notifications from a service monitoring the blockchain */ async getWebhookActionAndData(payload) { try { const { data } = payload; // This is a placeholder for handling webhook events // In a real implementation, you might receive notifications from a service // that monitors the blockchain for transactions to your wallet if (!data || !data.type) { return { action: 'not_supported', }; } switch (data.type) { case 'payment_received': return { action: 'authorized', data: { session_id: String(data.session_id || ''), amount: Number(data.amount || 0), }, }; case 'payment_confirmed': return { action: 'captured', data: { session_id: String(data.session_id || ''), amount: Number(data.amount || 0), }, }; default: return { action: 'not_supported', }; } } catch (error) { this.logger_.error(`Error processing Solana webhook: ${error}`); return { action: 'failed', }; } } } SolanaPaymentProviderService.identifier = 'solana'; exports.default = SolanaPaymentProviderService; //# sourceMappingURL=service.js.map