stellarskills 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/cli.js ADDED
@@ -0,0 +1,496 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+ import { fileURLToPath } from 'url';
6
+ import { spawn, execSync } from 'child_process';
7
+
8
+ // Resolve directory paths properly in ES modules
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = path.dirname(__filename);
11
+ const rootDir = path.resolve(__dirname, '..');
12
+
13
+ const REPO_BASE_URL = 'https://raw.githubusercontent.com/ggoldani/stellarskills/main';
14
+
15
+ const args = process.argv.slice(2);
16
+
17
+ function printHelp() {
18
+ console.log(`
19
+ 🚀 StellarSkills CLI
20
+
21
+ Usage:
22
+ stellarskills <command> [arguments]
23
+
24
+ Commands:
25
+ list List all available skills.
26
+ get <skill> Print the raw Markdown content of a specific skill.
27
+ url <skill> Print the direct raw GitHub URL of a specific skill.
28
+ combine <skill1> [skill2] ... Combine multiple skills into a single Markdown output.
29
+ search "<query>" Search across all skills for a specific term.
30
+ copy <skill1> [skill2] ... Copy combined skills directly to your system clipboard.
31
+ rules <ide> <skill1> ... Append combined skills directly to your .cursorrules, .clinerules, or .windsurfrules.
32
+ index Output a full, agent-friendly Markdown map of all skills and their descriptions.
33
+ doctor Verify if your local environment is ready for Stellar and Soroban development.
34
+ system <skills> [--instruction] Output the ultimate AI System Prompt containing an expert persona and requested skills.
35
+
36
+ Examples:
37
+ npx stellarskills list
38
+ npx stellarskills get soroban
39
+ npx stellarskills url accounts
40
+ npx stellarskills combine accounts soroban security > prompt.txt
41
+ npx stellarskills search "trustline"
42
+ npx stellarskills copy dex
43
+ npx stellarskills rules cursor accounts soroban
44
+ npx stellarskills index
45
+ `);
46
+ }
47
+
48
+ function getAvailableSkills() {
49
+ const skills = ['root']; // Base skill file at root
50
+
51
+ try {
52
+ const items = fs.readdirSync(rootDir);
53
+ for (const item of items) {
54
+ if (item === 'node_modules' || item === 'bin' || item.startsWith('.')) continue;
55
+
56
+ const fullPath = path.join(rootDir, item);
57
+ if (fs.statSync(fullPath).isDirectory()) {
58
+ const skillFilePath = path.join(fullPath, 'SKILL.md');
59
+ if (fs.existsSync(skillFilePath)) {
60
+ skills.push(item);
61
+ }
62
+ }
63
+ }
64
+ } catch (error) {
65
+ console.error('Error reading skills directory:', error.message);
66
+ }
67
+
68
+ return skills;
69
+ }
70
+
71
+ function handleList() {
72
+ const skills = getAvailableSkills();
73
+ console.log('📚 Available Stellar Skills:\n');
74
+ skills.forEach(skill => {
75
+ console.log(` - ${skill}`);
76
+ });
77
+ console.log('\nRun `stellarskills get <skill>` to output its content.');
78
+ }
79
+
80
+ function getSkillPath(skill) {
81
+ if (skill === 'root') {
82
+ return path.join(rootDir, 'SKILL.md');
83
+ }
84
+ return path.join(rootDir, skill, 'SKILL.md');
85
+ }
86
+
87
+ function readSkillContent(skillPath) {
88
+ let content = fs.readFileSync(skillPath, 'utf-8');
89
+ // Strip YAML frontmatter if it exists, handling both \n and \r\n
90
+ const frontmatterRegex = /^---\r?\n[\s\S]*?\r?\n---\r?\n/;
91
+ if (frontmatterRegex.test(content)) {
92
+ content = content.replace(frontmatterRegex, '').trimStart();
93
+ }
94
+ return content;
95
+ }
96
+
97
+ function handleGet(skill) {
98
+ if (!skill) {
99
+ console.error('Error: You must provide a skill name.\nExample: stellarskills get soroban');
100
+ process.exit(1);
101
+ }
102
+
103
+ const availableSkills = getAvailableSkills();
104
+ if (!availableSkills.includes(skill)) {
105
+ console.error(`Error: Skill '${skill}' not found.`);
106
+ console.log('Run `stellarskills list` to see available skills.');
107
+ process.exit(1);
108
+ }
109
+
110
+ const skillPath = getSkillPath(skill);
111
+ if (fs.existsSync(skillPath)) {
112
+ const content = readSkillContent(skillPath);
113
+ console.log(content);
114
+ } else {
115
+ console.error(`Error: Skill '${skill}' not found.`);
116
+ console.log('Run `stellarskills list` to see available skills.');
117
+ process.exit(1);
118
+ }
119
+ }
120
+
121
+ function handleUrl(skill) {
122
+ if (!skill) {
123
+ console.error('Error: You must provide a skill name.\nExample: stellarskills url soroban');
124
+ process.exit(1);
125
+ }
126
+
127
+ const availableSkills = getAvailableSkills();
128
+ if (!availableSkills.includes(skill)) {
129
+ console.error(`Error: Skill '${skill}' not found.`);
130
+ console.log('Run `stellarskills list` to see available skills.');
131
+ process.exit(1);
132
+ }
133
+
134
+ if (skill === 'root') {
135
+ console.log(`${REPO_BASE_URL}/SKILL.md`);
136
+ } else {
137
+ console.log(`${REPO_BASE_URL}/${skill}/SKILL.md`);
138
+ }
139
+ }
140
+
141
+ function handleCombine(requestedSkills) {
142
+ if (!requestedSkills || requestedSkills.length === 0) {
143
+ console.error('Error: You must provide at least one skill name to combine.\nExample: stellarskills combine accounts soroban security');
144
+ process.exit(1);
145
+ }
146
+
147
+ const availableSkills = getAvailableSkills();
148
+ const missingSkills = requestedSkills.filter(skill => !availableSkills.includes(skill));
149
+
150
+ if (missingSkills.length > 0) {
151
+ console.error(`Error: The following requested skills were not found: ${missingSkills.join(', ')}`);
152
+ console.log('Run `stellarskills list` to see available skills.');
153
+ process.exit(1);
154
+ }
155
+
156
+ const contents = requestedSkills.map(skill => {
157
+ const skillPath = getSkillPath(skill);
158
+ try {
159
+ return readSkillContent(skillPath);
160
+ } catch (e) {
161
+ console.error(`Error: Could not read file for skill '${skill}': ${e.message}`);
162
+ process.exit(1);
163
+ }
164
+ });
165
+
166
+ const separator = '\n\n--------------------------------------------------------------------------------\n\n';
167
+ const combinedOutput = contents.join(separator);
168
+
169
+ console.log(combinedOutput);
170
+ }
171
+
172
+ function handleCopy(requestedSkills) {
173
+ if (!requestedSkills || requestedSkills.length === 0) {
174
+ console.error('Error: You must provide at least one skill name to copy.\nExample: stellarskills copy accounts soroban');
175
+ process.exit(1);
176
+ }
177
+
178
+ const availableSkills = getAvailableSkills();
179
+ const missingSkills = requestedSkills.filter(skill => !availableSkills.includes(skill));
180
+
181
+ if (missingSkills.length > 0) {
182
+ console.error(`Error: The following requested skills were not found: ${missingSkills.join(', ')}`);
183
+ process.exit(1);
184
+ }
185
+
186
+ const contents = requestedSkills.map(skill => {
187
+ try {
188
+ return readSkillContent(getSkillPath(skill));
189
+ } catch (e) {
190
+ console.error(`Error: Could not read file for skill '${skill}': ${e.message}`);
191
+ process.exit(1);
192
+ }
193
+ });
194
+
195
+ const separator = '\n\n--------------------------------------------------------------------------------\n\n';
196
+ const combinedOutput = contents.join(separator);
197
+
198
+ let command;
199
+ switch (process.platform) {
200
+ case 'darwin':
201
+ command = 'pbcopy';
202
+ break;
203
+ case 'win32':
204
+ command = 'clip';
205
+ break;
206
+ case 'linux':
207
+ // Fallback xclip for linux. If missing, it will error out cleanly.
208
+ command = 'xclip -selection clipboard';
209
+ break;
210
+ default:
211
+ console.error('Error: Clipboard copy is not supported on this operating system.');
212
+ process.exit(1);
213
+ }
214
+
215
+ try {
216
+ const child = spawn(command, { shell: true, stdio: ['pipe', 'ignore', 'ignore'] });
217
+ child.stdin.write(combinedOutput);
218
+ child.stdin.end();
219
+
220
+ child.on('close', (code) => {
221
+ if (code === 0) {
222
+ console.log(`\n✅ Copied the raw Markdown for [${requestedSkills.join(', ')}] to your clipboard.\n`);
223
+ } else {
224
+ console.error(`\n❌ Failed to copy to clipboard. Ensure '${command}' is installed on your system.\n`);
225
+ }
226
+ });
227
+ } catch (error) {
228
+ console.error(`\n❌ Failed to copy to clipboard: ${error.message}\n`);
229
+ }
230
+ }
231
+
232
+ function handleSearch(query) {
233
+ if (!query) {
234
+ console.error('Error: You must provide a search query.\nExample: stellarskills search "trustline"');
235
+ process.exit(1);
236
+ }
237
+
238
+ const availableSkills = getAvailableSkills();
239
+ const lowerQuery = query.toLowerCase();
240
+ let foundAny = false;
241
+
242
+ console.log(`\n🔍 Searching for "${query}"...\n`);
243
+
244
+ for (const skill of availableSkills) {
245
+ const skillPath = getSkillPath(skill);
246
+ try {
247
+ const content = fs.readFileSync(skillPath, 'utf-8');
248
+ if (content.toLowerCase().includes(lowerQuery)) {
249
+ console.log(` ✅ Found in: ${skill}`);
250
+ foundAny = true;
251
+ }
252
+ } catch (e) {
253
+ // Gracefully skip files that cannot be read
254
+ }
255
+ }
256
+
257
+ if (!foundAny) {
258
+ console.log(` ❌ No results found across available skills.`);
259
+ }
260
+
261
+ console.log(); // Trailing newline for clean output
262
+ }
263
+
264
+ function handleRules(ide, requestedSkills) {
265
+ if (!ide || !requestedSkills || requestedSkills.length === 0) {
266
+ console.error('Error: You must provide an IDE and at least one skill.\nExample: stellarskills rules cursor accounts soroban');
267
+ process.exit(1);
268
+ }
269
+
270
+ const supportedIdes = {
271
+ 'cursor': '.cursorrules',
272
+ 'cline': '.clinerules',
273
+ 'windsurf': '.windsurfrules'
274
+ };
275
+
276
+ const filename = supportedIdes[ide.toLowerCase()];
277
+ if (!filename) {
278
+ console.error(`Error: Unsupported IDE '${ide}'. Supported IDEs are: ${Object.keys(supportedIdes).join(', ')}`);
279
+ process.exit(1);
280
+ }
281
+
282
+ const availableSkills = getAvailableSkills();
283
+ const missingSkills = requestedSkills.filter(skill => !availableSkills.includes(skill));
284
+
285
+ if (missingSkills.length > 0) {
286
+ console.error(`Error: The following requested skills were not found: ${missingSkills.join(', ')}`);
287
+ process.exit(1);
288
+ }
289
+
290
+ const contents = requestedSkills.map(skill => {
291
+ try {
292
+ return readSkillContent(getSkillPath(skill));
293
+ } catch (e) {
294
+ console.error(`Error: Could not read file for skill '${skill}': ${e.message}`);
295
+ process.exit(1);
296
+ }
297
+ });
298
+
299
+ const separator = '\n\n--------------------------------------------------------------------------------\n\n';
300
+ const combinedOutput = separator + contents.join(separator) + '\n';
301
+ const targetPath = path.join(process.cwd(), filename);
302
+
303
+ try {
304
+ fs.appendFileSync(targetPath, combinedOutput, 'utf-8');
305
+ console.log(`\n✅ Appended Stellar knowledge [${requestedSkills.join(', ')}] to ${filename} in the current directory.\n`);
306
+ } catch (error) {
307
+ console.error(`\n❌ Failed to write to ${filename}: ${error.message}\n`);
308
+ process.exit(1);
309
+ }
310
+ }
311
+
312
+ function handleIndex() {
313
+ const availableSkills = getAvailableSkills();
314
+ console.log('## StellarSkills Index\n');
315
+
316
+ availableSkills.forEach(skill => {
317
+ const skillPath = getSkillPath(skill);
318
+ let description = 'No description available.';
319
+
320
+ try {
321
+ const content = fs.readFileSync(skillPath, 'utf-8');
322
+ const lines = content.split('\n');
323
+ for (const line of lines) {
324
+ if (line.trim().startsWith('> ')) {
325
+ // Remove the '> ' prefix
326
+ description = line.trim().substring(2).trim();
327
+ break;
328
+ }
329
+ }
330
+ } catch (e) {
331
+ // Ignore read errors
332
+ }
333
+
334
+ console.log(`- **${skill}**: ${description}`);
335
+ });
336
+ console.log();
337
+ }
338
+
339
+ function handleDoctor() {
340
+ console.log('🩺 StellarSkills Environment Doctor\n');
341
+ console.log('Checking your system for required Stellar & Soroban tools...\n');
342
+
343
+ const checks = [
344
+ { name: 'Node.js', cmd: 'node -v', isRequired: true },
345
+ { name: 'Rust Compiler', cmd: 'rustc --version', isRequired: true },
346
+ { name: 'Cargo', cmd: 'cargo --version', isRequired: true },
347
+ { name: 'Stellar CLI', cmd: 'stellar --version', isRequired: true },
348
+ { name: 'WASM Target (wasm32-unknown-unknown)', cmd: 'rustup target list', isRequired: true, validator: (out) => out.includes('wasm32-unknown-unknown (installed)') }
349
+ ];
350
+
351
+ let allPassed = true;
352
+
353
+ checks.forEach(check => {
354
+ try {
355
+ // Execute the command synchronously
356
+ const output = execSync(check.cmd, { stdio: 'pipe', encoding: 'utf-8' }).trim();
357
+
358
+ // If a custom validator exists, run it
359
+ if (check.validator && !check.validator(output)) {
360
+ throw new Error('Validation failed');
361
+ }
362
+
363
+ // Keep output concise (first line only unless validator is used)
364
+ let shortOutput = output.split('\n')[0];
365
+ if (check.validator) {
366
+ shortOutput = 'installed';
367
+ }
368
+
369
+ console.log(`✅ [Installed] ${check.name}: ${shortOutput}`);
370
+ } catch (error) {
371
+ console.log(`❌ [Missing] ${check.name}`);
372
+ if (check.isRequired) {
373
+ allPassed = false;
374
+ }
375
+ }
376
+ });
377
+
378
+ console.log('\n---');
379
+ if (allPassed) {
380
+ console.log('🎉 Your environment is fully configured to build on Stellar & Soroban!');
381
+ } else {
382
+ console.log('⚠️ Your environment is missing required dependencies.');
383
+ console.log('👉 Agents: Please install the missing tools using `cargo install --locked stellar-cli` or `rustup target add wasm32-unknown-unknown` before attempting to compile contracts.');
384
+ }
385
+ }
386
+
387
+ function handleSystem(argsArray) {
388
+ let requestedSkills = [];
389
+ let instruction = '';
390
+
391
+ // Parse arguments to separate skills and the optional --instruction flag
392
+ for (let i = 0; i < argsArray.length; i++) {
393
+ if (argsArray[i] === '--instruction') {
394
+ if (i + 1 < argsArray.length) {
395
+ instruction = argsArray[i + 1];
396
+ i++; // skip the instruction text
397
+ } else {
398
+ console.error('Error: --instruction requires a string argument.');
399
+ process.exit(1);
400
+ }
401
+ } else {
402
+ requestedSkills.push(argsArray[i]);
403
+ }
404
+ }
405
+
406
+ if (requestedSkills.length === 0) {
407
+ console.error('Error: You must provide at least one skill name.\nExample: stellarskills system soroban dex --instruction "Create an AMM pool"');
408
+ process.exit(1);
409
+ }
410
+
411
+ const availableSkills = getAvailableSkills();
412
+ const missingSkills = requestedSkills.filter(skill => !availableSkills.includes(skill));
413
+
414
+ if (missingSkills.length > 0) {
415
+ console.error(`Error: The following requested skills were not found: ${missingSkills.join(', ')}`);
416
+ process.exit(1);
417
+ }
418
+
419
+ const contents = requestedSkills.map(skill => {
420
+ try {
421
+ return readSkillContent(getSkillPath(skill));
422
+ } catch (e) {
423
+ console.error(`Error: Could not read file for skill '${skill}': ${e.message}`);
424
+ process.exit(1);
425
+ }
426
+ });
427
+
428
+ const separator = '\n\n--------------------------------------------------------------------------------\n\n';
429
+ const combinedKnowledge = contents.join(separator);
430
+
431
+ const expertPersona = `You are an expert software engineer specializing in the Stellar Network and Soroban smart contracts.
432
+ You must strictly adhere to the following architectural rules:
433
+ 1. Stellar uses typed Operations, not EVM calldata.
434
+ 2. Smart contracts are written in memory-safe Rust and compiled to WASM.
435
+ 3. You must always use the \`stellar-cli\` tool to build, simulate, and deploy contracts. You must NEVER use the deprecated \`soroban-cli\`.
436
+ 4. You must never invent or hallucinate EVM-like concepts (like msg.sender or Solidity mappings) when writing Soroban.
437
+
438
+ Below is your verified knowledge base to complete the user's task. Read it carefully.
439
+
440
+ ### STELLAR KNOWLEDGE BASE:
441
+
442
+ ${combinedKnowledge}
443
+ `;
444
+
445
+ let finalOutput = expertPersona;
446
+
447
+ if (instruction) {
448
+ finalOutput += `\n### YOUR TASK:\n\n${instruction}\n`;
449
+ }
450
+
451
+ console.log(finalOutput);
452
+ }
453
+
454
+ const command = args[0];
455
+
456
+ switch (command) {
457
+ case 'list':
458
+ handleList();
459
+ break;
460
+ case 'get':
461
+ handleGet(args[1]);
462
+ break;
463
+ case 'url':
464
+ handleUrl(args[1]);
465
+ break;
466
+ case 'combine':
467
+ handleCombine(args.slice(1));
468
+ break;
469
+ case 'search':
470
+ handleSearch(args[1]);
471
+ break;
472
+ case 'copy':
473
+ handleCopy(args.slice(1));
474
+ break;
475
+ case 'rules':
476
+ handleRules(args[1], args.slice(2));
477
+ break;
478
+ case 'index':
479
+ handleIndex();
480
+ break;
481
+ case 'doctor':
482
+ handleDoctor();
483
+ break;
484
+ case 'system':
485
+ handleSystem(args.slice(1));
486
+ break;
487
+ case 'help':
488
+ case '--help':
489
+ case '-h':
490
+ default:
491
+ if (command && command !== 'help' && command !== '--help' && command !== '-h') {
492
+ console.error(`Unknown command: ${command}`);
493
+ }
494
+ printHelp();
495
+ break;
496
+ }
package/dex/SKILL.md ADDED
@@ -0,0 +1,170 @@
1
+ ---
2
+ name: stellarskills-dex
3
+ description: Stellar's built-in order book, Automated Market Makers (AMM), Liquidity Pools, and Path Payments.
4
+ ---
5
+
6
+ # STELLARSKILLS — DEX & AMM
7
+
8
+ > Stellar's built-in order book, Automated Market Makers (AMM), Liquidity Pools, and Path Payments.
9
+
10
+ ---
11
+
12
+ ## The Built-in DEX
13
+
14
+ Stellar is unique among layer-1s: it has an order book natively built into the protocol. You don't need a smart contract (like Uniswap) to trade assets; it's a core protocol operation.
15
+
16
+ Features:
17
+ - Completely on-chain limit order book.
18
+ - No front-running via mempool (transactions apply deterministically).
19
+ - Trades execute immediately if they cross the spread.
20
+
21
+ ### Create an Offer (Limit Order)
22
+
23
+ Use `ManageBuyOffer` or `ManageSellOffer`. They are effectively identical, just framed differently (I want to buy X vs I want to sell Y).
24
+
25
+ ```javascript
26
+ import { Operation, Asset } from "@stellar/stellar-sdk";
27
+
28
+ const USDC = new Asset("USDC", "GA5...");
29
+ const XLM = Asset.native();
30
+
31
+ // "I want to sell 100 USDC to buy XLM at a price of 10 XLM per USDC"
32
+ const sellOffer = Operation.manageSellOffer({
33
+ selling: USDC,
34
+ buying: XLM,
35
+ amount: "100", // Amount of USDC I'm selling
36
+ price: "10.0", // 10 XLM / 1 USDC
37
+ offerId: "0", // 0 means create a new offer
38
+ });
39
+ ```
40
+
41
+ ### Update or Cancel an Offer
42
+
43
+ To update, provide the existing `offerId`.
44
+ To cancel, update the offer but set `amount: "0"`.
45
+
46
+ ```javascript
47
+ const cancelOffer = Operation.manageSellOffer({
48
+ selling: USDC,
49
+ buying: XLM,
50
+ amount: "0", // Cancel
51
+ price: "10.0", // Price must still match original
52
+ offerId: "12345", // ID from previous transaction effect
53
+ });
54
+ ```
55
+
56
+ ---
57
+
58
+ ## Automated Market Makers (AMM) / Liquidity Pools
59
+
60
+ Stellar also supports protocol-level AMMs. Users can provide liquidity to pools (CPMM: $x \times y = k$) and earn a 0.3% protocol fee on swaps.
61
+
62
+ ### Get a Liquidity Pool ID
63
+
64
+ Pools are identified by a deterministic hash of their assets and fee.
65
+
66
+ ```javascript
67
+ import { LiquidityPoolAsset } from "@stellar/stellar-sdk";
68
+
69
+ // Pool for XLM / USDC (assets must be sorted lexicographically)
70
+ const lpAsset = new LiquidityPoolAsset(Asset.native(), USDC, LiquidityPoolFeeV18);
71
+ const poolId = lpAsset.getLiquidityPoolId();
72
+ ```
73
+
74
+ ### Provide Liquidity (Deposit)
75
+
76
+ ```javascript
77
+ const deposit = Operation.liquidityPoolDeposit({
78
+ liquidityPoolId: poolId,
79
+ maxAmountA: "100", // Max XLM willing to deposit
80
+ maxAmountB: "10", // Max USDC willing to deposit
81
+ minPrice: "9.5", // Slippage protection: min A/B price
82
+ maxPrice: "10.5", // Slippage protection: max A/B price
83
+ });
84
+ ```
85
+
86
+ ### Withdraw Liquidity
87
+
88
+ ```javascript
89
+ const withdraw = Operation.liquidityPoolWithdraw({
90
+ liquidityPoolId: poolId,
91
+ amount: "50", // Amount of pool shares to redeem
92
+ minAmountA: "90", // Slippage: minimum XLM to receive
93
+ minAmountB: "9", // Slippage: minimum USDC to receive
94
+ });
95
+ ```
96
+
97
+ ---
98
+
99
+ ## Path Payments (Swaps)
100
+
101
+ Path payments are the most powerful DEX feature. They allow an account to send Asset A, route it through the DEX (order books or AMMs), and deliver Asset B to the recipient — all in one atomic transaction.
102
+
103
+ **If the DEX cannot satisfy the exchange rate requested, the entire transaction fails.**
104
+
105
+ ### Strict Send (Known Input, Variable Output)
106
+
107
+ "I want to spend exactly 10 USDC, give the recipient as much BRL as possible."
108
+
109
+ ```javascript
110
+ const strictSend = Operation.pathPaymentStrictSend({
111
+ sendAsset: USDC,
112
+ sendAmount: "10.0", // Exactly 10 USDC spent
113
+ destination: recipientKey,
114
+ destAsset: BRL,
115
+ destMin: "45.0", // Slippage: tx fails if recipient gets < 45 BRL
116
+ path: [], // Optional intermediary assets
117
+ });
118
+ ```
119
+
120
+ ### Strict Receive (Variable Input, Known Output)
121
+
122
+ "The recipient must receive exactly 50 BRL, spend as little of my USDC as possible."
123
+
124
+ ```javascript
125
+ const strictReceive = Operation.pathPaymentStrictReceive({
126
+ sendAsset: USDC,
127
+ sendMax: "11.0", // Slippage: tx fails if costs > 11 USDC
128
+ destination: recipientKey,
129
+ destAsset: BRL,
130
+ destAmount: "50.0", // Exactly 50 BRL received
131
+ path: [], // Optional intermediary assets
132
+ });
133
+ ```
134
+
135
+ ### Path Finding
136
+
137
+ You usually don't need to specify the `path` array manually. Let Horizon find the best path across order books and AMMs.
138
+
139
+ ```javascript
140
+ const server = new Horizon.Server("https://horizon.stellar.org");
141
+
142
+ // Find best path for a Strict Receive
143
+ const paths = await server.strictReceivePaths({
144
+ sourceAssets: [USDC, XLM], // What assets do I have to spend?
145
+ destinationAsset: BRL,
146
+ destinationAmount: "50.0",
147
+ }).call();
148
+
149
+ const bestPath = paths.records[0];
150
+ console.log(`Spend ${bestPath.source_amount} of ${bestPath.source_asset_code}`);
151
+ console.log(`Path:`, bestPath.path);
152
+ ```
153
+
154
+ Pass `bestPath.path` into the `path` array of your Operation.
155
+
156
+ ---
157
+
158
+ ## Common Errors
159
+
160
+ | Error | Meaning | Fix |
161
+ |-------|---------|-----|
162
+ | `op_underfunded` | Not enough asset to create offer or swap | Check balances |
163
+ | `op_cross_self` | Trying to match against your own offer | Cancel existing offer first |
164
+ | `op_over_source_max` | (Strict Receive) Slippage hit, cost too high | Adjust `sendMax` |
165
+ | `op_under_dest_min` | (Strict Send) Slippage hit, output too low | Adjust `destMin` |
166
+ | `op_no_trust` | Recipient lacks trustline for destination asset | Recipient must `changeTrust` |
167
+
168
+ ---
169
+
170
+ *raw.githubusercontent.com/ggoldani/stellarskills/main/dex — MIT License*