Skip to content

Commit 8f2e1ab

Browse files
Kingsley4867kodinaka30-ship-it
andauthored
feat: add Stellar Path Payment for stealth payments guide (#71) (#114)
Co-authored-by: kodinaka30-ship-it <kodinaka30@gmail.com>
1 parent 1648101 commit 8f2e1ab

2 files changed

Lines changed: 289 additions & 0 deletions

File tree

docs.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@
150150
"guides/stellar-federation",
151151
"guides/stellar-custom-assets",
152152
"guides/stellar/stellar-liquidity-pool-swap",
153+
"guides/stellar/stellar-path-payment",
153154
"guides/wraith-names-stellar",
154155
"guides/ops/self-hosted-deployment"
155156
]
Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
---
2+
title: "Stellar Path Payments for Stealth Payments"
3+
description: "Pay in any held asset on Stellar while the recipient receives their target token at a stealth address via DEX path payments and Soroban stealth announcements."
4+
keywords: "Stellar, path payment, stealth address, Horizon, DEX, XLM, USDC, slippage, strict send, strict receive, Soroban"
5+
---
6+
7+
Stellar Path Payments allow a sender to pay using asset A (such as XLM or BTC) while the recipient receives asset B (such as USDC) in a single atomic transaction. The Stellar network automatically routes the swap through order books, liquidity pools, or a combination of both.
8+
9+
When combined with Wraith Protocol stealth addresses, path payments enable powerful "pay in any asset" flows: senders pay in whatever token they hold, and recipients receive their requested token at an un-linkable, one-time stealth address.
10+
11+
> **Prerequisite reading:** Review the [Stellar Quickstart](/guides/stellar/stellar-quickstart) to understand how stealth meta-addresses, stealth keys, and announcement scanning work on Stellar.
12+
13+
---
14+
15+
## Path Payment Concepts
16+
17+
Stellar provides two distinct path payment operations:
18+
19+
1. **`PathPaymentStrictReceive`**: The sender specifies the *exact amount of target asset* the recipient must receive (`destAmount`), along with a *maximum amount of source asset* they are willing to spend (`sendMax`).
20+
2. **`PathPaymentStrictSend`**: The sender specifies the *exact amount of source asset* to spend (`sendAmount`), along with a *minimum amount of target asset* the recipient must receive (`destMin`).
21+
22+
For stealth payments, **`PathPaymentStrictReceive`** is commonly preferred because invoice amounts are usually denominated in a target currency (e.g., exactly 50 USDC for a service), while the sender pays with whatever asset is available in their wallet (e.g., XLM).
23+
24+
### Operational Sequence
25+
26+
```
27+
[Sender Wallet]
28+
│ (Pays XLM)
29+
30+
[Stellar DEX / Pools] ──► Path Routing (XLM → Liquidity Pool / Order Book → USDC)
31+
32+
▼ (Receives USDC)
33+
[Stealth Address] ◄───── Announce Event (Soroban Contract) ◄───── [Recipient Scans]
34+
```
35+
36+
---
37+
38+
## Off-Chain Path Finding via Horizon
39+
40+
Before building a transaction, you must query Horizon to find an available DEX path and determine the required source asset amount.
41+
42+
### Querying `strictReceivePaths`
43+
44+
To find how much XLM is needed to deliver a fixed amount of USDC:
45+
46+
```typescript
47+
import { Horizon, Asset } from "@stellar/stellar-sdk";
48+
49+
// Initialize Horizon server (Testnet by default)
50+
const server = new Horizon.Server("https://horizon-testnet.stellar.org");
51+
52+
const usdcIssuer = "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5";
53+
const destinationAsset = new Asset("USDC", usdcIssuer);
54+
const sourceAsset = Asset.native(); // XLM
55+
const targetAmount = "50"; // Recipient gets 50 USDC
56+
57+
// Find paths that deliver exactly 50 USDC
58+
const pathResponse = await server
59+
.strictReceivePaths(sourceAsset, targetAmount)
60+
.destinationAsset(destinationAsset)
61+
.call();
62+
63+
if (pathResponse.records.length === 0) {
64+
throw new Error("No path found between XLM and USDC. Check DEX liquidity.");
65+
}
66+
67+
// Select the path requiring the minimum source asset amount
68+
const bestPath = pathResponse.records.reduce((prev, curr) =>
69+
parseFloat(curr.source_amount) < parseFloat(prev.source_amount) ? curr : prev
70+
);
71+
72+
console.log("Estimated XLM required:", bestPath.source_amount);
73+
console.log("Intermediate path hops:", bestPath.path);
74+
```
75+
76+
### Querying `strictSendPaths`
77+
78+
Alternatively, if the sender wants to spend a fixed 100 XLM and maximize recipient USDC output:
79+
80+
```typescript
81+
const sendResponse = await server
82+
.strictSendPaths(sourceAsset, "100")
83+
.destinationAsset(destinationAsset)
84+
.call();
85+
86+
if (sendResponse.records.length === 0) {
87+
throw new Error("No path found for sending 100 XLM to USDC.");
88+
}
89+
90+
const bestSendPath = sendResponse.records[0];
91+
console.log("Estimated USDC received:", bestSendPath.destination_amount);
92+
```
93+
94+
---
95+
96+
## Wrapping in a Stealth Announcement
97+
98+
Classic path payments transfer tokens on-chain but do not emit stealth announcements. To ensure the recipient can detect and spend the payment, the transaction must include both:
99+
100+
1. **Path Payment Operation**: Transfers the converted asset to the one-time `stealthAddress`.
101+
2. **Soroban Announcement Contract Call**: Emits a Soroban event containing the `ephemeralPubKey`, `viewTag`, `stealthAddress`, and asset details.
102+
103+
Combining both operations in a single Stellar transaction guarantees atomicity: either both succeed in the same ledger block or the entire transaction fails.
104+
105+
---
106+
107+
## End-to-End Example: XLM → USDC to Stealth Address
108+
109+
This complete example derives a stealth address, finds an off-chain path from XLM to USDC, calculates slippage protection, and submits an atomic transaction on Stellar Testnet.
110+
111+
### Prerequisites
112+
113+
```bash
114+
npm install @wraith-protocol/sdk @stellar/stellar-sdk
115+
```
116+
117+
### Complete Code Example
118+
119+
```typescript
120+
import {
121+
Horizon,
122+
Asset,
123+
Operation,
124+
TransactionBuilder,
125+
Keypair,
126+
Networks,
127+
} from "@stellar/stellar-sdk";
128+
import {
129+
decodeStealthMetaAddress,
130+
generateStealthAddress,
131+
getDeployment,
132+
createAnnounceOperation,
133+
} from "@wraith-protocol/sdk/chains/stellar";
134+
135+
// 1. Network Configuration
136+
// Default: Stellar Testnet
137+
const HORIZON_URL = "https://horizon-testnet.stellar.org";
138+
const NETWORK_PASSPHRASE = Networks.TESTNET;
139+
140+
// NOTE FOR MAINNET:
141+
// For production, use:
142+
// const HORIZON_URL = "https://horizon.stellar.org";
143+
// const NETWORK_PASSPHRASE = Networks.PUBLIC;
144+
145+
const server = new Horizon.Server(HORIZON_URL);
146+
const deployment = getDeployment("stellar");
147+
148+
// Sender Keypair
149+
const senderKeypair = Keypair.fromSecret("S..."); // Replace with sender secret key
150+
const senderAccount = await server.loadAccount(senderKeypair.publicKey());
151+
152+
// Assets
153+
const sourceAsset = Asset.native(); // XLM
154+
const usdcIssuer = "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"; // Testnet USDC issuer
155+
const destAsset = new Asset("USDC", usdcIssuer);
156+
const destAmount = "50"; // 50 USDC
157+
158+
// 2. Generate Stealth Address from Recipient Meta-Address
159+
const recipientMetaAddress = "st:xlm:02a1b2c3...03d4e5f6...";
160+
const { spendingPubKey, viewingPubKey } = decodeStealthMetaAddress(recipientMetaAddress);
161+
162+
const stealth = generateStealthAddress(spendingPubKey, viewingPubKey);
163+
const stealthAddress = stealth.stealthAddress; // G... public key for one-time address
164+
165+
console.log("Derived Stealth Address:", stealthAddress);
166+
console.log("Ephemeral Public Key:", stealth.ephemeralPubKey);
167+
168+
// 3. Off-Chain Path Finding via Horizon
169+
const pathResponse = await server
170+
.strictReceivePaths(sourceAsset, destAmount)
171+
.destinationAsset(destAsset)
172+
.call();
173+
174+
if (pathResponse.records.length === 0) {
175+
throw new Error("No DEX path found for XLM -> USDC swap.");
176+
}
177+
178+
const bestPath = pathResponse.records[0];
179+
const estimatedSourceAmount = parseFloat(bestPath.source_amount);
180+
181+
// 4. Calculate Slippage (e.g., 2% tolerance)
182+
const SLIPPAGE_BPS = 200; // 2%
183+
const sendMax = (estimatedSourceAmount * (1 + SLIPPAGE_BPS / 10000)).toFixed(7);
184+
185+
console.log(`Sending max ${sendMax} XLM to receive exactly ${destAmount} USDC`);
186+
187+
// Format intermediate path assets for Operation
188+
const intermediatePath = bestPath.path.map((p) =>
189+
p.asset_type === "native"
190+
? Asset.native()
191+
: new Asset(p.asset_code, p.asset_issuer)
192+
);
193+
194+
// 5. Construct Soroban Stealth Announcement Operation
195+
const announceOp = createAnnounceOperation({
196+
contractId: deployment.announcerContractId,
197+
ephemeralPubKey: stealth.ephemeralPubKey,
198+
viewTag: stealth.viewTag,
199+
stealthAddress: stealthAddress,
200+
asset: "USDC",
201+
amount: destAmount,
202+
});
203+
204+
// 6. Build Atomic Transaction
205+
const transaction = new TransactionBuilder(senderAccount, {
206+
fee: "100000", // Account for inclusion fee + Soroban resource fee
207+
networkPassphrase: NETWORK_PASSPHRASE,
208+
})
209+
.addOperation(
210+
Operation.pathPaymentStrictReceive({
211+
sendAsset: sourceAsset,
212+
sendMax: sendMax,
213+
destination: stealthAddress,
214+
destAsset: destAsset,
215+
destAmount: destAmount,
216+
path: intermediatePath,
217+
})
218+
)
219+
.addOperation(announceOp)
220+
.setTimeout(60) // 60-second expiration window
221+
.build();
222+
223+
// 7. Sign & Submit
224+
transaction.sign(senderKeypair);
225+
226+
try {
227+
const result = await server.submitTransaction(transaction);
228+
console.log("Path payment + stealth announcement submitted successfully!");
229+
console.log("Transaction Hash:", result.hash);
230+
} catch (error) {
231+
console.error("Transaction failed:", error);
232+
}
233+
```
234+
235+
---
236+
237+
## Failure Modes and Recovery Strategies
238+
239+
When executing path payments with stealth announcements, handle these primary network failure modes gracefully:
240+
241+
### 1. No Path Found (`op_no_destination`, `op_too_few_offers`)
242+
243+
- **Cause**: Horizon returns an empty `records` array because there is insufficient liquidity across DEX order books and pools for the requested asset pair.
244+
- **Handling**:
245+
- Verify that the asset issuer addresses and asset codes are valid.
246+
- Fall back to checking liquidity pool swaps via [Stellar Liquidity Pool Swaps](/guides/stellar/stellar-liquidity-pool-swap).
247+
- Offer the user the choice to pay directly in the destination asset if already held.
248+
249+
### 2. Slippage / Send Max Exceeded (`op_over_source_max`, `op_under_dest_min`)
250+
251+
- **Cause**: Market price moved between off-chain path query and ledger inclusion, causing the source asset cost to exceed `sendMax` (`PATH_PAYMENT_STRICT_RECEIVE_OVER_SOURCE_MAX`) or destination output to fall below `destMin` (`PATH_PAYMENT_STRICT_SEND_UNDER_DEST_MIN`).
252+
- **Handling**:
253+
- Re-fetch path recommendations immediately prior to transaction construction.
254+
- Increase slippage budget (e.g. from 1% to 2% or 3%) for volatile assets.
255+
- Use realistic price buffers based on order book depth.
256+
257+
| Slippage | Basis Points | `sendMax` for 100 XLM estimated cost |
258+
|---|---|---|
259+
| 0.5% | 50 | 100.50 XLM |
260+
| 1.0% | 100 | 101.00 XLM |
261+
| 2.0% | 200 | 102.00 XLM |
262+
| 5.0% | 500 | 105.00 XLM |
263+
264+
### 3. Expired Transaction / Timeout (`tx_too_late`)
265+
266+
- **Cause**: Ledger submission exceeded `setTimeout()` duration during network congestion or due to low inclusion fees.
267+
- **Handling**:
268+
- Re-query Horizon for fresh path rates before rebuilding an expired transaction.
269+
- Use fee estimation helpers to adjust base fee during network traffic surges.
270+
- Set reasonable timeouts (30-60 seconds).
271+
272+
### 4. Destination Trustline / Unfunded Account (`op_no_trust`, `op_no_destination`)
273+
274+
- **Cause**: Receiving non-native tokens (such as USDC) requires the target account to hold a trustline or Soroban Asset Certificate (SAC) approval. If the stealth address is new and un-funded, sending custom assets directly will fail with `op_no_destination` or `op_no_trust`.
275+
- **Handling**:
276+
- If the stealth address is brand new, include an `Operation.createAccount` operation in the transaction prior to asset transfer to fund minimum XLM reserves.
277+
- Use SAC wrappers or trustline initialization patterns as detailed in [Stellar Custom Assets](/guides/stellar-custom-assets).
278+
279+
---
280+
281+
## Related Guides
282+
283+
- [Stellar Quickstart](/guides/stellar/stellar-quickstart) — derive stealth keys and run client-side payment scanners
284+
- [Stellar Managed Quickstart](/guides/stellar-quickstart) — TEE-managed stealth agent walkthrough
285+
- [Stellar Liquidity Pool Swaps](/guides/stellar/stellar-liquidity-pool-swap) — direct constant-product AMM swaps
286+
- [Stellar Custom Assets](/guides/stellar-custom-assets) — USDC trustlines and SAC asset compatibility
287+
- [Stellar Fee Estimation](/guides/stellar-fees) — calculate transaction inclusion and Soroban resource fees
288+
- [Stellar Troubleshooting](/guides/stellar-troubleshooting) — resolve transaction error codes and simulation failures

0 commit comments

Comments
 (0)