Summary
Build multi-asset donations via Stellar path payments — a donor contributes in XLM, EURC, or any asset with DEX liquidity, and the treasury receives USDC, atomically, in a single transaction. New feature; donations today assume the donor already holds the treasury asset (server/src/services/horizon-verify.service.ts verifies a plain payment).
Background
Every donor turned away for holding "the wrong asset" is funding LearnVault never received. A European sponsor holds EURC; a Stellar-native holds XLM; a regional donor holds an anchored local-currency token. Asking each of them to go swap first loses most of them at that step.
Stellar's path_payment_strict_receive does the conversion inside the payment itself through the built-in DEX and AMM liquidity pools — no external swap, no bridge, no second transaction that can half-fail. The treasury's accounting stays denominated in one asset while donors pay in whatever they hold.
What to build
1. Path discovery
GET /api/donate/paths?from=XLM&amount=100 → call Horizon's /paths/strict-send and /paths/strict-receive, return viable payment paths with the estimated destination amount.
- Show the donor the real rate and the price impact before they sign. A donor who discovers slippage after the fact does not donate twice.
2. Donation transaction builder
POST /api/donate/build constructs a path_payment_strict_receive where:
destAsset = treasury USDC, destAmount = the exact amount the treasury should receive
sendMax = quoted amount plus a donor-selected slippage tolerance (default 0.5%)
path = the discovered intermediate assets
- destination = the treasury account
- Return XDR for wallet signing. The server never holds donor keys.
3. Trustline preflight
- Check whether the donor holds a trustline for the asset they are sending, and whether the treasury has one for USDC.
- If the treasury lacks a trustline for an incoming path, that path must be excluded from the results rather than offered and then failing at submission.
4. Verification and accounting
- Extend
horizon-verify.service.ts to recognize path payments: verify the destination amount and asset actually credited to the treasury, not the source amount. Crediting a donor for sendMax rather than what arrived is the bug this feature invites — guard it with a test.
- Record both legs (
sent_asset/sent_amount and received_usdc) so impact reporting stays denominated in USDC.
- Migration
server/src/db/migrations/029_multi_asset_donations.sql plus its .undo.sql adding the source-asset columns to the existing donation table.
5. Frontend
- Asset picker on the donate flow, defaulting to assets the donor actually holds (read their balances).
- Live quote: "You send ~102.3 XLM → Treasury receives 10.00 USDC", refreshing on a timer, with the slippage control visible.
- Clear failure message when no path exists, naming the asset rather than showing a generic error.
6. Tests
- Path lookup with a mocked Horizon.
sendMax correctly reflects slippage tolerance.
- Verification credits the received USDC amount, not the sent amount.
- No-path-available and missing-trustline cases are handled and surfaced.
Acceptance criteria
Notes for contributors
Summary
Build multi-asset donations via Stellar path payments — a donor contributes in XLM, EURC, or any asset with DEX liquidity, and the treasury receives USDC, atomically, in a single transaction. New feature; donations today assume the donor already holds the treasury asset (
server/src/services/horizon-verify.service.tsverifies a plain payment).Background
Every donor turned away for holding "the wrong asset" is funding LearnVault never received. A European sponsor holds EURC; a Stellar-native holds XLM; a regional donor holds an anchored local-currency token. Asking each of them to go swap first loses most of them at that step.
Stellar's
path_payment_strict_receivedoes the conversion inside the payment itself through the built-in DEX and AMM liquidity pools — no external swap, no bridge, no second transaction that can half-fail. The treasury's accounting stays denominated in one asset while donors pay in whatever they hold.What to build
1. Path discovery
GET /api/donate/paths?from=XLM&amount=100→ call Horizon's/paths/strict-sendand/paths/strict-receive, return viable payment paths with the estimated destination amount.2. Donation transaction builder
POST /api/donate/buildconstructs apath_payment_strict_receivewhere:destAsset= treasury USDC,destAmount= the exact amount the treasury should receivesendMax= quoted amount plus a donor-selected slippage tolerance (default 0.5%)path= the discovered intermediate assets3. Trustline preflight
4. Verification and accounting
horizon-verify.service.tsto recognize path payments: verify the destination amount and asset actually credited to the treasury, not the source amount. Crediting a donor forsendMaxrather than what arrived is the bug this feature invites — guard it with a test.sent_asset/sent_amountandreceived_usdc) so impact reporting stays denominated in USDC.server/src/db/migrations/029_multi_asset_donations.sqlplus its.undo.sqladding the source-asset columns to the existing donation table.5. Frontend
6. Tests
sendMaxcorrectly reflects slippage tolerance.Acceptance criteria
Notes for contributors
strict_receive(notstrict_send) so the treasury receives an exact, predictable amount — this keeps treasury accounting clean.