Skip to content

Commit 22c20c2

Browse files
Merge pull request #65 from keljoshX/feature/multi-currency-price-feed
Multi Currency Price Feed Feature
2 parents 23f3038 + 79ba29a commit 22c20c2

File tree

4 files changed

+235
-0
lines changed

4 files changed

+235
-0
lines changed

index.js

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
require('dotenv').config();
22
const express = require('express');
33
const cors = require('cors');
4+
const { getUSDCToFiatRates, getXLMToUSDCPath } = require('./services/priceFeedService');
45
const { checkAndInitializeLease, getLeases, saveLeases } = require('./worker');
56
const leaseRoutes = require('./src/routes/leaseRoutes');
67
const ownerRoutes = require('./src/routes/ownerRoutes');
@@ -500,6 +501,55 @@ app.post('/sign-lease', async (req, res) => {
500501
res.json({ message: `Lease ${leaseId} signed by ${role}.`, state: leases[leaseId] });
501502
});
502503

504+
/**
505+
* Get current USDC to Fiat exchange rates.
506+
* Query params: currencies (comma-separated list of fiat currency codes, e.g., 'ngn,eur')
507+
*/
508+
app.get('/api/price-feed', async (req, res) => {
509+
try {
510+
const { currencies } = req.query;
511+
const currencyList = currencies ? currencies.split(',') : ['ngn', 'eur', 'usd'];
512+
const rates = await getUSDCToFiatRates(currencyList);
513+
res.json({
514+
success: true,
515+
rates,
516+
base_currency: 'USDC'
517+
});
518+
} catch (error) {
519+
res.status(500).json({
520+
success: false,
521+
message: error.message
522+
});
523+
}
524+
});
525+
526+
/**
527+
* Calculate the best path for XLM to USDC payment.
528+
* Query params: amount (destination USDC amount)
529+
*/
530+
app.get('/api/calculate-path-payment', async (req, res) => {
531+
try {
532+
const { amount } = req.query;
533+
if (!amount) {
534+
return res.status(400).json({
535+
success: false,
536+
message: 'Destination amount is required.'
537+
});
538+
}
539+
540+
const pathDetails = await getXLMToUSDCPath(amount);
541+
res.json({
542+
success: true,
543+
...pathDetails
544+
});
545+
} catch (error) {
546+
res.status(500).json({
547+
success: false,
548+
message: error.message
549+
});
550+
}
551+
});
552+
503553
// Asset metadata endpoints
504554
app.get('/api/asset/:id/metadata', async (req, res) => {
505555
try {

package-lock.json

Lines changed: 29 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

services/priceFeedService.js

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
const axios = require('axios');
2+
const { Horizon, Asset } = require('@stellar/stellar-sdk');
3+
4+
const COINGECKO_API_URL = 'https://api.coingecko.com/api/v3';
5+
const STELLAR_HORIZON_URL = 'https://horizon-testnet.stellar.org';
6+
const server = new Horizon.Server(STELLAR_HORIZON_URL);
7+
8+
// USDC on Stellar Testnet (Circle issuer)
9+
const USDC_ASSET = new Asset(
10+
'USDC',
11+
'GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5'
12+
);
13+
14+
/**
15+
* Fetch USDC to Fiat exchange rates.
16+
* @param {string[]} currencies - Array of fiat currency codes (e.g., ['ngn', 'eur']).
17+
* @returns {Promise<Object>} - Exchange rates.
18+
*/
19+
async function getUSDCToFiatRates(currencies = ['ngn', 'eur', 'usd']) {
20+
try {
21+
// USDC's ID on CoinGecko is 'usd-coin'
22+
const response = await axios.get(`${COINGECKO_API_URL}/simple/price`, {
23+
params: {
24+
ids: 'usd-coin',
25+
vs_currencies: currencies.join(','),
26+
},
27+
});
28+
return response.data['usd-coin'];
29+
} catch (error) {
30+
console.error('Error fetching fiat rates from CoinGecko:', error.message);
31+
// Fallback or rethrow
32+
throw new Error('Failed to fetch fiat exchange rates.');
33+
}
34+
}
35+
36+
/**
37+
* Calculate the best path for XLM to USDC payment.
38+
* @param {string} destinationAmount - Amount of USDC required by the landlord.
39+
* @returns {Promise<Object>} - Path payment details.
40+
*/
41+
async function getXLMToUSDCPath(destinationAmount) {
42+
try {
43+
const paths = await server.strictReceivePaths(
44+
[Asset.native()], // Source asset (XLM)
45+
USDC_ASSET, // Destination asset (USDC)
46+
destinationAmount // Destination amount
47+
).call();
48+
49+
if (paths.records && paths.records.length > 0) {
50+
// Sort by source amount ascending to find the most cost-effective path
51+
const sortedPaths = paths.records.sort((a, b) => parseFloat(a.source_amount) - parseFloat(b.source_amount));
52+
const bestPath = sortedPaths[0];
53+
54+
return {
55+
sourceAsset: 'XLM',
56+
sourceAmount: bestPath.source_amount,
57+
destinationAsset: 'USDC',
58+
destinationAmount: destinationAmount,
59+
path: bestPath.path,
60+
price: bestPath.source_amount / destinationAmount,
61+
};
62+
} else {
63+
throw new Error('No path found for XLM to USDC.');
64+
}
65+
} catch (error) {
66+
console.error('Error finding Stellar path:', error.message);
67+
throw new Error('Failed to calculate path payment.');
68+
}
69+
}
70+
71+
module.exports = {
72+
getUSDCToFiatRates,
73+
getXLMToUSDCPath,
74+
};

tests/priceFeed.test.js

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
const request = require('supertest');
2+
const app = require('../index');
3+
const axios = require('axios');
4+
const { Horizon } = require('@stellar/stellar-sdk');
5+
6+
// Mock axios
7+
jest.mock('axios');
8+
9+
// Mock Horizon server
10+
jest.mock('@stellar/stellar-sdk', () => {
11+
const originalModule = jest.requireActual('@stellar/stellar-sdk');
12+
return {
13+
...originalModule,
14+
Horizon: {
15+
Server: jest.fn().mockImplementation(() => ({
16+
strictReceivePaths: jest.fn().mockReturnValue({
17+
call: jest.fn().mockResolvedValue({
18+
records: [
19+
{
20+
source_amount: '10.5',
21+
path: [],
22+
},
23+
],
24+
}),
25+
}),
26+
})),
27+
},
28+
};
29+
});
30+
31+
describe('Price Feed API', () => {
32+
describe('GET /api/price-feed', () => {
33+
it('should return USDC to Fiat rates', async () => {
34+
const mockRates = {
35+
'usd-coin': {
36+
ngn: 1500,
37+
eur: 0.92,
38+
usd: 1,
39+
},
40+
};
41+
axios.get.mockResolvedValue({ data: mockRates });
42+
43+
const response = await request(app).get('/api/price-feed?currencies=ngn,eur,usd');
44+
45+
expect(response.status).toBe(200);
46+
expect(response.body.success).toBe(true);
47+
expect(response.body.rates).toEqual(mockRates['usd-coin']);
48+
expect(response.body.base_currency).toBe('USDC');
49+
});
50+
51+
it('should handle errors when fetching rates', async () => {
52+
axios.get.mockRejectedValue(new Error('API Error'));
53+
54+
const response = await request(app).get('/api/price-feed');
55+
56+
expect(response.status).toBe(500);
57+
expect(response.body.success).toBe(false);
58+
expect(response.body.message).toBe('Failed to fetch fiat exchange rates.');
59+
});
60+
});
61+
62+
describe('GET /api/calculate-path-payment', () => {
63+
it('should return path details for XLM to USDC', async () => {
64+
const response = await request(app).get('/api/calculate-path-payment?amount=100');
65+
66+
expect(response.status).toBe(200);
67+
expect(response.body.success).toBe(true);
68+
expect(response.body.sourceAsset).toBe('XLM');
69+
expect(response.body.sourceAmount).toBe('10.5');
70+
expect(response.body.destinationAsset).toBe('USDC');
71+
expect(response.body.destinationAmount).toBe('100');
72+
});
73+
74+
it('should return 400 if amount is missing', async () => {
75+
const response = await request(app).get('/api/calculate-path-payment');
76+
77+
expect(response.status).toBe(400);
78+
expect(response.body.success).toBe(false);
79+
expect(response.body.message).toBe('Destination amount is required.');
80+
});
81+
});
82+
});

0 commit comments

Comments
 (0)