Skip to content

Commit 3614f30

Browse files
committed
feat(1503): implement Stellar path payment operations support
- Add sendAsset, receiveAsset, slippageTolerance parameters to POST /donations endpoint - Update createDonationSchema to include cross-asset fields - Enhance DonationService to automatically discover paths for cross-asset donations - Apply slippage tolerance to minimum destination amount - Support path payment transactions with automatic asset conversion - Default slippageTolerance to 1% if not specified Fixes #1503
1 parent 4883de4 commit 3614f30

3 files changed

Lines changed: 65 additions & 7 deletions

File tree

src/routes/donations/create.js

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,7 @@ router.post('/', payloadSizeLimiter(ENDPOINT_LIMITS.singleDonation), donationRat
282282
return await processCustodialDonation(req, res, next);
283283
}
284284

285-
const { amount, currency, donor, recipient, memo, memoType, notes, tags, encryptMemo, anonymous, sourceAsset, sourceAmount } = req.body;
285+
const { amount, currency, donor, recipient, memo, memoType, notes, tags, encryptMemo, anonymous, sourceAsset, sourceAmount, sendAsset, receiveAsset, slippageTolerance } = req.body;
286286

287287
if (!amount || !recipient) {
288288
throw new ValidationError('Missing required fields: amount, recipient', null, ERROR_CODES.MISSING_REQUIRED_FIELD);
@@ -313,6 +313,32 @@ router.post('/', payloadSizeLimiter(ENDPOINT_LIMITS.singleDonation), donationRat
313313
}
314314
}
315315

316+
let normalizedSendAsset = null;
317+
let normalizedReceiveAsset = null;
318+
let normalizedSlippageTolerance = null;
319+
if (sendAsset || receiveAsset) {
320+
if (!sendAsset || !receiveAsset) {
321+
return res.status(400).json({
322+
success: false,
323+
error: { code: 'VALIDATION_ERROR', message: 'Both sendAsset and receiveAsset must be provided for cross-asset donations' }
324+
});
325+
}
326+
normalizedSendAsset = parseAssetInput(sendAsset, 'sendAsset');
327+
normalizedReceiveAsset = parseAssetInput(receiveAsset, 'receiveAsset');
328+
329+
if (slippageTolerance !== undefined && slippageTolerance !== null) {
330+
if (typeof slippageTolerance !== 'number' || slippageTolerance < 0 || slippageTolerance > 1) {
331+
return res.status(400).json({
332+
success: false,
333+
error: { code: 'VALIDATION_ERROR', message: 'slippageTolerance must be a number between 0 and 1' }
334+
});
335+
}
336+
normalizedSlippageTolerance = slippageTolerance;
337+
} else {
338+
normalizedSlippageTolerance = 0.01;
339+
}
340+
}
341+
316342
if (memo || memoType) {
317343
const memoValidator = require('../../utils/memoValidator');
318344
const memoValidation = memoValidator.validateWithType(memo || '', memoType || 'text');
@@ -356,6 +382,9 @@ router.post('/', payloadSizeLimiter(ENDPOINT_LIMITS.singleDonation), donationRat
356382
memo,
357383
sourceAsset: normalizedSourceAsset,
358384
sourceAmount: sourceAmountValidation ? sourceAmountValidation.value : undefined,
385+
sendAsset: normalizedSendAsset,
386+
receiveAsset: normalizedReceiveAsset,
387+
slippageTolerance: normalizedSlippageTolerance,
359388
memoType: memoType || 'text',
360389
notes,
361390
tags,

src/routes/donations/helpers.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,10 @@ const createDonationSchema = validateSchema({
7373
notes: { type: 'string', required: false, nullable: true },
7474
tags: { type: 'array', required: false, nullable: true },
7575
sourceAsset: { type: 'string', required: false, nullable: true },
76-
sourceAmount: { types: ['number', 'numberString'], required: false, nullable: true }
76+
sourceAmount: { types: ['number', 'numberString'], required: false, nullable: true },
77+
sendAsset: { types: ['string', 'object'], required: false, nullable: true },
78+
receiveAsset: { types: ['string', 'object'], required: false, nullable: true },
79+
slippageTolerance: { type: 'number', required: false, nullable: true }
7780
}
7881
}
7982
});

src/services/DonationService.js

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -710,6 +710,9 @@ class DonationService {
710710
anonymous = false,
711711
sourceAsset,
712712
sourceAmount,
713+
sendAsset,
714+
receiveAsset,
715+
slippageTolerance,
713716
validAfter = 0,
714717
validBefore = 0,
715718
memoEnvelope = null,
@@ -801,11 +804,22 @@ class DonationService {
801804
}
802805

803806
const sourceAssetProvided = sourceAsset !== undefined && sourceAsset !== null;
804-
const normalizedDestAsset = DEFAULT_DESTINATION_ASSET;
805-
const normalizedSourceAsset = sourceAssetProvided
807+
const sendAssetProvided = sendAsset !== undefined && sendAsset !== null;
808+
const receiveAssetProvided = receiveAsset !== undefined && receiveAsset !== null;
809+
810+
let normalizedDestAsset = DEFAULT_DESTINATION_ASSET;
811+
let normalizedSourceAsset = sourceAssetProvided
806812
? parseAssetInput(sourceAsset, 'sourceAsset')
807813
: normalizedDestAsset;
814+
815+
// Handle sendAsset/receiveAsset for path payment donations
816+
if (sendAssetProvided && receiveAssetProvided) {
817+
normalizedSourceAsset = parseAssetInput(sendAsset, 'sendAsset');
818+
normalizedDestAsset = parseAssetInput(receiveAsset, 'receiveAsset');
819+
}
820+
808821
const normalizedSourceAmount = sourceAmount ?? xlmAmount;
822+
const normalizedSlippageTolerance = slippageTolerance ?? 0.01;
809823
const sourceSecret = this.resolvePaymentSourceSecret(sanitizedDonor);
810824
let stellarResult = null;
811825
let paymentMethod = 'record_only';
@@ -815,12 +829,15 @@ class DonationService {
815829

816830
if (sourceSecret && sanitizedRecipient) {
817831
await this.checkRecipientAccountExists(sanitizedRecipient);
818-
if (!sourceAssetProvided) {
832+
833+
const isPathPayment = sourceAssetProvided || (sendAssetProvided && receiveAssetProvided && !isSameAsset(normalizedSourceAsset, normalizedDestAsset));
834+
835+
if (!isPathPayment) {
819836
// Set correlation ID on StellarService for this request
820837
if (correlationId) {
821838
this.stellarService.setCorrelationId(correlationId);
822839
}
823-
840+
824841
stellarResult = await this.stellarService.sendDonation({
825842
sourceSecret,
826843
destinationPublic: sanitizedRecipient,
@@ -837,6 +854,11 @@ class DonationService {
837854
this.stellarService.setCorrelationId(correlationId);
838855
}
839856

857+
// Set correlation ID on StellarService for this request
858+
if (correlationId) {
859+
this.stellarService.setCorrelationId(correlationId);
860+
}
861+
840862
const estimate = await this.stellarService.discoverBestPath({
841863
sourceAsset: normalizedSourceAsset,
842864
sourceAmount: normalizedSourceAmount.toString(),
@@ -851,12 +873,16 @@ class DonationService {
851873
selectedPath = estimate.path || [];
852874
conversionRate = estimate.conversionRate;
853875

876+
// Apply slippage tolerance to minimum destination amount
877+
const estimatedDestAmount = parseFloat(estimate.destAmount);
878+
const minDestAmount = estimatedDestAmount * (1 - normalizedSlippageTolerance);
879+
854880
try {
855881
stellarResult = await this.stellarService.pathPayment(
856882
normalizedSourceAsset,
857883
normalizedSourceAmount.toString(),
858884
normalizedDestAsset,
859-
estimate.destAmount,
885+
minDestAmount.toString(),
860886
selectedPath,
861887
{
862888
sourceSecret,

0 commit comments

Comments
 (0)