|
| 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