Offramp Service Integration
This document provides comprehensive guidance on integrating offramp services with the Rahat platform for token-to-fiat conversion and local payment processing.
Overview
Offramp services enable the conversion of blockchain tokens to local currency and facilitate distribution to beneficiaries through various payment methods. The integration supports multiple payment providers and ensures seamless cash distribution workflows.
Architecture
Core Components
1. Offramp Service
- Purpose: Token-to-fiat conversion and local payment processing
- Integration: REST API with authentication
- Features: Instant conversion, bank transfers, VPA payments, mobile money
2. Payment Providers
- Purpose: Local financial service provider integration
- Features: Multiple provider support, transaction tracking
- Methods: Bank transfers, mobile money, VPA (Virtual Payment Address)
3. Payout Management
- Purpose: Orchestrate the entire payout process
- Features: Batch processing, status tracking, error handling
Integration Setup
1. Environment Configuration
Configure the following environment variables for offramp service integration:
# Offramp Service Settings
OFFRAMP_URL=https://api.offramp-service.com
OFFRAMP_APP_ID=your_app_id
OFFRAMP_ACCESS_TOKEN=your_access_token
OFFRAMP_DEFAULT_PAYMENT_PROVIDER=provider_id
# Transaction Limits
OFFRAMP_MIN_AMOUNT=10
OFFRAMP_MAX_AMOUNT=10000
OFFRAMP_DAILY_LIMIT=50000
2. Database Settings
Store offramp configuration in the settings table:
INSERT INTO settings (name, value, description) VALUES
('OFFRAMP_SETTINGS', '{
"URL": "https://api.offramp-service.com",
"APP_ID": "your_app_id",
"ACCESS_TOKEN": "your_access_token",
"DEFAULT_PAYMENT_PROVIDER": "provider_id",
"TRANSACTION_LIMITS": {
"MIN_AMOUNT": 10,
"MAX_AMOUNT": 10000,
"DAILY_LIMIT": 50000
}
}', 'Offramp service configuration');
Service Implementation
1. Offramp Service Class
import { Injectable, HttpService } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class OfframpService {
constructor(
private readonly httpService: HttpService,
private readonly configService: ConfigService,
private readonly coreClient: ClientProxy
) {}
/**
* Fetch offramp service settings from database
*/
async fetchOfframpSettings(): Promise<{
url: string;
appId: string;
accessToken: string;
defaultPaymentProvider: string;
transactionLimits: {
minAmount: number;
maxAmount: number;
dailyLimit: number;
};
}> {
const settings = await this.coreClient.send({
cmd: 'SETTINGS.GET',
data: { name: 'OFFRAMP_SETTINGS' }
});
return {
url: settings.value.URL,
appId: settings.value.APP_ID,
accessToken: settings.value.ACCESS_TOKEN,
defaultPaymentProvider: settings.value.DEFAULT_PAYMENT_PROVIDER,
transactionLimits: settings.value.TRANSACTION_LIMITS
};
}
/**
* Get offramp wallet address for token transfers
*/
async getOfframpWalletAddress(): Promise<string> {
const offrampSettings = await this.fetchOfframpSettings();
const response = await this.httpService.axiosRef.get(
`${offrampSettings.url}/app/${offrampSettings.appId}`,
{
headers: {
'APP_ID': offrampSettings.appId,
'Authorization': `Bearer ${offrampSettings.accessToken}`
}
}
);
return response.data.data.wallet;
}
/**
* Get available payment providers
*/
async getPaymentProviders(): Promise<IPaymentProvider[]> {
const offrampSettings = await this.fetchOfframpSettings();
const response = await this.httpService.axiosRef.get(
`${offrampSettings.url}/payment-providers`,
{
headers: {
'APP_ID': offrampSettings.appId,
'Authorization': `Bearer ${offrampSettings.accessToken}`
}
}
);
return response.data.data;
}
/**
* Process instant offramp conversion
*/
async instantOfframp(offrampPayload: OfframpRequest): Promise<OfframpResponse> {
const offrampSettings = await this.fetchOfframpSettings();
const response = await this.httpService.axiosRef.post(
`${offrampSettings.url}/offramp-request/instant`,
offrampPayload,
{
headers: {
'APP_ID': offrampSettings.appId,
'Authorization': `Bearer ${offrampSettings.accessToken}`,
'Content-Type': 'application/json'
}
}
);
return response.data.data;
}
/**
* Add bulk transactions to offramp queue
*/
async addBulkToOfframpQueue(transactions: FSPOfframpDetails[]): Promise<void> {
const offrampSettings = await this.fetchOfframpSettings();
await this.httpService.axiosRef.post(
`${offrampSettings.url}/offramp-request/bulk`,
{ transactions },
{
headers: {
'APP_ID': offrampSettings.appId,
'Authorization': `Bearer ${offrampSettings.accessToken}`,
'Content-Type': 'application/json'
}
}
);
}
}
2. Data Interfaces
// Payment Provider Interface
interface IPaymentProvider {
id: string;
name: string;
type: 'BANK' | 'MOBILE_MONEY' | 'VPA' | 'CASH';
country: string;
currency: string;
supportedMethods: string[];
processingTime: string;
fees: {
percentage: number;
fixed: number;
};
limits: {
minAmount: number;
maxAmount: number;
dailyLimit: number;
};
}
// Offramp Request Interface
interface OfframpRequest {
tokenAmount: number;
paymentProviderId: string;
transactionHash: string;
senderAddress: string;
xref: string;
paymentDetails: {
creditorAgent?: string;
creditorAccount?: string;
creditorName?: string;
vpa?: string;
};
}
// Offramp Response Interface
interface OfframpResponse {
id: string;
status: 'PENDING' | 'PROCESSING' | 'COMPLETED' | 'FAILED';
transactionId: string;
amount: number;
currency: string;
paymentProvider: string;
processingTime: number;
fees: number;
errorMessage?: string;
}
// FSP Offramp Details Interface
interface FSPOfframpDetails {
amount: number;
beneficiaryWalletAddress: string;
beneficiaryBankDetails: {
bankName: string;
accountNumber: string;
accountName: string;
ifscCode?: string;
};
payoutUUID: string;
payoutProcessorId: string;
beneficiaryPhoneNumber: string;
offrampWalletAddress: string;
offrampType: string;
transactionHash?: string;
}
Integration Workflow
1. Payout Initiation
@Injectable()
export class PayoutsService {
constructor(
private readonly offrampService: OfframpService,
private readonly stellarService: StellarService,
private readonly prisma: PrismaService
) {}
async triggerPayout(uuid: string): Promise<any> {
const payoutDetails = await this.findOne(uuid);
if (payoutDetails.isPayoutTriggered) {
throw new RpcException(
`Payout with UUID '${uuid}' has already been triggered`
);
}
const BeneficiaryPayoutDetails = await this.fetchBeneficiaryPayoutDetails(uuid);
const offrampWalletAddress = await this.offrampService.getOfframpWalletAddress();
const stellerOfframpQueuePayload: FSPPayoutDetails[] =
BeneficiaryPayoutDetails.map((beneficiary) => ({
amount: beneficiary.amount,
beneficiaryWalletAddress: beneficiary.walletAddress,
beneficiaryBankDetails: beneficiary.bankDetails,
payoutUUID: uuid,
payoutProcessorId: payoutDetails.payoutProcessorId,
beneficiaryPhoneNumber: beneficiary.phoneNumber,
offrampWalletAddress,
offrampType: payoutExtras.paymentProviderType,
}));
await this.stellarService.addBulkToTokenTransferQueue(
stellerOfframpQueuePayload
);
return 'Payout Initiated Successfully';
}
}
2. Token Transfer Process
@Processor(BQUEUE.STELLAR)
export class StellarProcessor {
@Process(JOBS.STELLAR.TRANSFER_TO_OFFRAMP)
async processTransferToOfframp(job: Job) {
const payload: FSPPayoutDetails = job.data;
try {
// Transfer tokens from beneficiary to offramp wallet
const transactionResult = await this.stellarService.transferTokens({
fromAddress: payload.beneficiaryWalletAddress,
toAddress: payload.offrampWalletAddress,
amount: payload.amount,
assetCode: 'RAHAT',
assetIssuer: process.env.STELLAR_ASSET_ISSUER
});
// Update payload with transaction hash
payload.transactionHash = transactionResult.hash;
// Add to offramp queue for cash conversion
await this.offrampService.addBulkToOfframpQueue([payload]);
return transactionResult;
} catch (error) {
this.logger.error(`Token transfer failed: ${error.message}`);
throw error;
}
}
}
3. Offramp Processing
@Processor(BQUEUE.OFFRAMP)
export class OfframpProcessor {
@Process(JOBS.OFFRAMP.PROCESS_CONVERSION)
async processOfframpConversion(job: Job) {
const payload: FSPOfframpDetails = job.data;
try {
const offrampRequest = await this.generateOfframpPayload(
payload.offrampType,
payload
);
const offrampResponse = await this.offrampService.instantOfframp(offrampRequest);
// Update beneficiary redeem status
await this.prisma.beneficiaryRedeem.update({
where: {
payoutUUID_beneficiaryId: {
payoutUUID: payload.payoutUUID,
beneficiaryId: payload.beneficiaryId
}
},
data: {
status: offrampResponse.status,
offrampResponse: offrampResponse,
transactionHash: payload.transactionHash
}
});
return offrampResponse;
} catch (error) {
this.logger.error(`Offramp conversion failed: ${error.message}`);
// Update status to failed
await this.prisma.beneficiaryRedeem.update({
where: {
payoutUUID_beneficiaryId: {
payoutUUID: payload.payoutUUID,
beneficiaryId: payload.beneficiaryId
}
},
data: {
status: 'FAILED',
errorMessage: error.message,
numberOfAttempts: { increment: 1 }
}
});
throw error;
}
}
private async generateOfframpPayload(
offrampType: string,
fspOfframpDetails: FSPOfframpDetails
): Promise<OfframpRequest> {
let offrampRequest: OfframpRequest = {
tokenAmount: fspOfframpDetails.amount,
paymentProviderId: fspOfframpDetails.payoutProcessorId,
transactionHash: fspOfframpDetails.transactionHash,
senderAddress: fspOfframpDetails.beneficiaryWalletAddress,
xref: fspOfframpDetails.payoutUUID,
paymentDetails: {}
};
if (offrampType.toLowerCase() === 'bank') {
offrampRequest.paymentDetails = {
creditorAgent: getBankId(fspOfframpDetails.beneficiaryBankDetails.bankName),
creditorAccount: fspOfframpDetails.beneficiaryBankDetails.accountNumber,
creditorName: fspOfframpDetails.beneficiaryBankDetails.accountName,
};
} else if (offrampType.toLowerCase() === 'vpa') {
offrampRequest.paymentDetails = {
vpa: fspOfframpDetails.beneficiaryPhoneNumber,
};
}
return offrampRequest;
}
}
Payment Provider Integration
1. Bank Transfer Integration
// Bank transfer configuration
interface BankTransferConfig {
bankName: string;
accountNumber: string;
accountName: string;
ifscCode?: string;
routingNumber?: string;
}
// Get bank ID mapping
function getBankId(bankName: string): string {
const bankMapping = {
'SBI': 'SBIN0000001',
'HDFC': 'HDFC0000001',
'ICICI': 'ICIC0000001',
// Add more bank mappings
};
return bankMapping[bankName] || bankName;
}
2. VPA (Virtual Payment Address) Integration
// VPA payment configuration
interface VPAConfig {
vpa: string; // Format: user@upi
beneficiaryName?: string;
}
// VPA validation
function validateVPA(vpa: string): boolean {
const vpaRegex = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+$/;
return vpaRegex.test(vpa);
}
3. Mobile Money Integration
// Mobile money configuration
interface MobileMoneyConfig {
phoneNumber: string;
provider: 'MPESA' | 'AIRTEL_MONEY' | 'MTN_MOBILE_MONEY';
country: string;
}
Error Handling and Retry Logic
1. Error Types
enum OfframpErrorType {
INSUFFICIENT_FUNDS = 'INSUFFICIENT_FUNDS',
INVALID_ACCOUNT = 'INVALID_ACCOUNT',
NETWORK_ERROR = 'NETWORK_ERROR',
TIMEOUT = 'TIMEOUT',
RATE_LIMIT = 'RATE_LIMIT',
INVALID_VPA = 'INVALID_VPA',
BANK_NOT_SUPPORTED = 'BANK_NOT_SUPPORTED'
}
2. Retry Configuration
interface RetryConfig {
maxAttempts: number;
backoffDelay: number;
exponentialBackoff: boolean;
}
const defaultRetryConfig: RetryConfig = {
maxAttempts: 3,
backoffDelay: 1000, // 1 second
exponentialBackoff: true
};
3. Error Handling Implementation
async processOfframpWithRetry(
payload: FSPOfframpDetails,
retryConfig: RetryConfig = defaultRetryConfig
): Promise<OfframpResponse> {
let lastError: Error;
for (let attempt = 1; attempt <= retryConfig.maxAttempts; attempt++) {
try {
const offrampRequest = await this.generateOfframpPayload(
payload.offrampType,
payload
);
return await this.offrampService.instantOfframp(offrampRequest);
} catch (error) {
lastError = error;
if (attempt === retryConfig.maxAttempts) {
throw error;
}
// Calculate delay with exponential backoff
const delay = retryConfig.exponentialBackoff
? retryConfig.backoffDelay * Math.pow(2, attempt - 1)
: retryConfig.backoffDelay;
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw lastError;
}
Monitoring and Analytics
1. Transaction Tracking
interface TransactionMetrics {
totalTransactions: number;
successfulTransactions: number;
failedTransactions: number;
averageProcessingTime: number;
totalAmount: number;
successRate: number;
}
async getTransactionMetrics(timeRange: DateRange): Promise<TransactionMetrics> {
const transactions = await this.prisma.beneficiaryRedeem.findMany({
where: {
createdAt: {
gte: timeRange.start,
lte: timeRange.end
}
}
});
const successful = transactions.filter(t => t.status === 'COMPLETED');
const failed = transactions.filter(t => t.status === 'FAILED');
return {
totalTransactions: transactions.length,
successfulTransactions: successful.length,
failedTransactions: failed.length,
averageProcessingTime: this.calculateAverageProcessingTime(transactions),
totalAmount: transactions.reduce((sum, t) => sum + t.amount, 0),
successRate: (successful.length / transactions.length) * 100
};
}
2. Status Tracking
enum PayoutStatus {
PENDING = 'PENDING',
PROCESSING = 'PROCESSING',
COMPLETED = 'COMPLETED',
FAILED = 'FAILED',
CANCELLED = 'CANCELLED'
}
interface BeneficiaryRedeem {
uuid: string;
beneficiaryId: string;
payoutUUID: string;
amount: number;
status: PayoutStatus;
transactionHash?: string;
offrampResponse?: any;
errorMessage?: string;
numberOfAttempts: number;
createdAt: Date;
updatedAt: Date;
}
Security Considerations
1. Authentication
// Secure API authentication
const headers = {
'APP_ID': offrampSettings.appId,
'Authorization': `Bearer ${offrampSettings.accessToken}`,
'X-Request-ID': generateRequestId(),
'X-Timestamp': Date.now().toString()
};
2. Data Validation
// Validate offramp request
function validateOfframpRequest(request: OfframpRequest): boolean {
if (!request.tokenAmount || request.tokenAmount <= 0) {
throw new Error('Invalid token amount');
}
if (!request.paymentProviderId) {
throw new Error('Payment provider ID is required');
}
if (!request.transactionHash) {
throw new Error('Transaction hash is required');
}
return true;
}
3. Rate Limiting
// Implement rate limiting
@Injectable()
export class RateLimitService {
private requestCounts = new Map<string, number>();
async checkRateLimit(identifier: string, limit: number): Promise<boolean> {
const currentCount = this.requestCounts.get(identifier) || 0;
if (currentCount >= limit) {
return false;
}
this.requestCounts.set(identifier, currentCount + 1);
return true;
}
}
Testing
1. Unit Tests
describe('OfframpService', () => {
let service: OfframpService;
let httpService: HttpService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
OfframpService,
{
provide: HttpService,
useValue: {
axiosRef: {
get: jest.fn(),
post: jest.fn()
}
}
}
],
}).compile();
service = module.get<OfframpService>(OfframpService);
httpService = module.get<HttpService>(HttpService);
});
it('should fetch offramp settings', async () => {
const mockSettings = {
URL: 'https://api.test.com',
APP_ID: 'test_app_id',
ACCESS_TOKEN: 'test_token'
};
jest.spyOn(service, 'fetchOfframpSettings').mockResolvedValue(mockSettings);
const result = await service.fetchOfframpSettings();
expect(result.url).toBe(mockSettings.URL);
});
});
2. Integration Tests
describe('Offramp Integration', () => {
it('should process complete offramp workflow', async () => {
// Test complete workflow from token transfer to cash distribution
const payoutUUID = 'test-payout-uuid';
// Trigger payout
const result = await payoutsService.triggerPayout(payoutUUID);
expect(result).toBe('Payout Initiated Successfully');
// Verify offramp processing
const redeemStatus = await prisma.beneficiaryRedeem.findMany({
where: { payoutUUID }
});
expect(redeemStatus.length).toBeGreaterThan(0);
expect(redeemStatus[0].status).toBe('COMPLETED');
});
});
Best Practices
1. Configuration Management
- Store sensitive configuration in environment variables
- Use database settings for dynamic configuration
- Implement configuration validation
2. Error Handling
- Implement comprehensive error handling
- Use retry logic with exponential backoff
- Log detailed error information for debugging
3. Monitoring
- Track transaction success rates
- Monitor processing times
- Set up alerts for failed transactions
4. Security
- Use secure authentication
- Validate all input data
- Implement rate limiting
- Encrypt sensitive data
5. Performance
- Use batch processing for large volumes
- Implement caching for frequently accessed data
- Optimize database queries
Troubleshooting
Common Issues
-
Authentication Errors
- Verify APP_ID and ACCESS_TOKEN
- Check token expiration
- Ensure proper headers
-
Network Timeouts
- Increase timeout values
- Implement retry logic
- Check network connectivity
-
Invalid Payment Details
- Validate bank account numbers
- Check VPA format
- Verify beneficiary information
-
Rate Limiting
- Implement request throttling
- Use bulk endpoints for multiple transactions
- Monitor API usage limits
Debug Tools
// Enable debug logging
const debugConfig = {
enableDebugLogs: true,
logLevel: 'DEBUG',
includeRequestData: true
};
// Debug logging implementation
if (debugConfig.enableDebugLogs) {
console.log('Offramp Request:', JSON.stringify(request, null, 2));
console.log('Offramp Response:', JSON.stringify(response, null, 2));
}
This comprehensive documentation provides all the necessary information for implementing and maintaining offramp service integration with the Rahat platform.