From b271d89e79894a3b4af4c3db508355e166dbee27 Mon Sep 17 00:00:00 2001 From: Sam Holmes Date: Wed, 20 Aug 2025 14:12:22 -0700 Subject: [PATCH 01/11] docs: Add comprehensive documentation for developers and AI agents - Create AGENTS.md with build commands and code style guidelines - Add TypeScript style guide covering imports, types, and conventions - Document swap plugin architecture patterns and best practices - Capture critical business rules for DEX wallet requirements - Create step-by-step guide for adding new exchanges - Track deprecated APIs that need migration - Reference Edge company-wide conventions - Update README with quick links to key documentation This documentation provides a knowledge base for both human developers and AI coding assistants working on the edge-exchange-plugins project. --- AGENTS.md | 44 ++++ README.md | 30 ++- .../business-rules/wallet-validation-rules.md | 84 +++++++ docs/conventions/edge-company-conventions.md | 67 ++++++ docs/conventions/typescript-style-guide.md | 142 ++++++++++++ docs/guides/adding-new-exchange.md | 208 ++++++++++++++++++ docs/patterns/swap-plugin-architecture.md | 192 ++++++++++++++++ docs/references/api-deprecations.md | 64 ++++++ 8 files changed, 820 insertions(+), 11 deletions(-) create mode 100644 AGENTS.md create mode 100644 docs/business-rules/wallet-validation-rules.md create mode 100644 docs/conventions/edge-company-conventions.md create mode 100644 docs/conventions/typescript-style-guide.md create mode 100644 docs/guides/adding-new-exchange.md create mode 100644 docs/patterns/swap-plugin-architecture.md create mode 100644 docs/references/api-deprecations.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..c6ea0e25 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,44 @@ +# Agent Guidelines for edge-exchange-plugins + +## Build/Test/Lint Commands + +- **Test**: `npm test` (single test: `npm test -- test/path/to/file.test.ts`) +- **Lint**: `npm run lint` (auto-fix: `npm run fix`) +- **Type check**: `npm run types` +- **Build**: `npm run prepare` (runs clean, compile, types, and webpack) +- **Verify all**: `npm run verify` (build + lint + types + test) + +## Code Style Guidelines + +- **TypeScript**: Strict mode enabled, use type imports (`import type { ... }`) +- **Imports**: Sort with `simple-import-sort`, no default exports +- **Formatting**: 2-space indentation, semicolons required, trailing commas +- **Naming**: camelCase for variables/functions, PascalCase for types/interfaces +- **Files**: Use `.ts` extension, organize by feature in `src/swap/` +- **Async**: Always use async/await over promises, handle errors with try/catch +- **Exports**: Named exports only, group related functionality +- **Dependencies**: Use `cleaners` for runtime validation, `biggystring` for numbers +- **Constants**: UPPER_SNAKE_CASE for true constants, extract magic numbers +- **Error handling**: Throw specific Edge error types (e.g., SwapCurrencyError) + +## Documentation Index + +### Setup & Configuration + +- `README.md` - **When to read**: Initial setup, installation, adding exchanges + - **Summary**: Setup instructions, development guide, PR requirements + +### Detailed Documentation + +- `docs/conventions/edge-company-conventions.md` - **When to read**: Starting development + - **Summary**: Company-wide Edge conventions, git workflow, PR rules +- `docs/conventions/typescript-style-guide.md` - **When to read**: Writing new code + - **Summary**: Import rules, type safety, error handling, naming conventions +- `docs/patterns/swap-plugin-architecture.md` - **When to read**: Creating new plugins + - **Summary**: Plugin structure, categories, common patterns, best practices +- `docs/business-rules/wallet-validation-rules.md` - **When to read**: Implementing DEX plugins + - **Summary**: Critical wallet requirements for DEX/DeFi integrations +- `docs/guides/adding-new-exchange.md` - **When to read**: Adding exchange support + - **Summary**: Step-by-step guide for new exchange integration +- `docs/references/api-deprecations.md` - **When to read**: Seeing deprecation warnings + - **Summary**: Deprecated APIs, migration paths, impact assessment diff --git a/README.md b/README.md index 34de6d29..50668867 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,14 @@ This library exports a collection of exchange-rate & swap plugins for use with [ Please see [index.js](./src/index.js) for the list of plugins in this repo. These are compatible with edge-core-js v0.19.37 or later. +## Quick Links + +- [Edge Company Conventions](https://github.com/EdgeApp/edge-conventions) - Company-wide development standards +- [TypeScript Style Guide](docs/conventions/typescript-style-guide.md) - Project-specific code conventions +- [Adding New Exchange Guide](docs/guides/adding-new-exchange.md) - Step-by-step integration guide +- [Plugin Architecture](docs/patterns/swap-plugin-architecture.md) - Understanding plugin patterns +- [Agent Guidelines](AGENTS.md) - For AI coding assistants + ## Installing Fist, add this library to your project: @@ -17,21 +25,21 @@ yarn add edge-exchange-plugins For Node.js, you should call `addEdgeCorePlugins` to register these plugins with edge-core-js: ```js -const { addEdgeCorePlugins, lockEdgeCorePlugins } = require('edge-core-js') -const plugins = require('edge-exchange-plugins') +const { addEdgeCorePlugins, lockEdgeCorePlugins } = require("edge-core-js"); +const plugins = require("edge-exchange-plugins"); -addEdgeCorePlugins(plugins) +addEdgeCorePlugins(plugins); // Once you are done adding plugins, call this: -lockEdgeCorePlugins() +lockEdgeCorePlugins(); ``` You can also add plugins individually if you want to be more picky: ```js addEdgeCorePlugins({ - thorchain: plugins.thorchain -}) + thorchain: plugins.thorchain, +}); ``` ### Browser @@ -50,12 +58,12 @@ and then adjust your script URL to http://localhost:8083/edge-exchange-plugins.j This package will automatically install itself using React Native autolinking. To integrate the plugins with edge-core-js, add its URI to the context component: ```jsx -import { pluginUri } from 'edge-exchange-plugins' +import { pluginUri } from "edge-exchange-plugins"; +/>; ``` To debug this project, run `yarn start` to start a Webpack server, and then use `debugUri` instead of `pluginUri`. @@ -90,7 +98,7 @@ Please be aware that when considering merging pull requests for additional excha - Accompanying PR submitted to `edge-reports` that fetches transaction data to your exchange that is credited to Edge users - Rebase of your branch upon this repo's `master` branch. For more info: -https://github.com/edx/edx-platform/wiki/How-to-Rebase-a-Pull-Request + https://github.com/edx/edx-platform/wiki/How-to-Rebase-a-Pull-Request - Accompanying PR submitted to `edge-react-gui` that includes (but is not limited to) the following: - - Small 64x64 pixel square logos with a white background - - 600x210 pixel horizontal logo for your exchange, with **no** empty space around the logo (we will add this programatically within the app + - Small 64x64 pixel square logos with a white background + - 600x210 pixel horizontal logo for your exchange, with **no** empty space around the logo (we will add this programatically within the app diff --git a/docs/business-rules/wallet-validation-rules.md b/docs/business-rules/wallet-validation-rules.md new file mode 100644 index 00000000..128900d1 --- /dev/null +++ b/docs/business-rules/wallet-validation-rules.md @@ -0,0 +1,84 @@ +# Wallet Validation Rules + +**Date**: 2025-08-20 + +## Critical Business Rules + +### Same Wallet Requirements + +Several DEX plugins **must** use the same wallet for source and destination: + +1. **XRP DEX** (`src/swap/defi/xrpDex.ts`) + + ```typescript + // Source and dest wallet must be the same + if (request.fromWallet !== request.toWallet) { + throw new Error("XRP DEX must use same wallet for source and destination"); + } + ``` + +2. **0x Gasless** (`src/swap/defi/0x/0xGasless.ts`) + + ```typescript + // The fromWallet and toWallet must be of the same because the swap + ``` + +3. **Fantom Sonic Upgrade** (`src/swap/defi/fantomSonicUpgrade.ts`) + ```typescript + if (fromAddress !== toAddress) { + throw new Error("From and to addresses must be the same"); + } + ``` + +### Chain Validation + +Uniswap V2-based plugins validate that both wallets are on the same chain: + +1. **TombSwap** (`src/swap/defi/uni-v2-based/plugins/tombSwap.ts`) +2. **SpookySwap** (`src/swap/defi/uni-v2-based/plugins/spookySwap.ts`) + ```typescript + // Sanity check: Both wallets should be of the same chain. + ``` + +## Rationale + +### DEX Same-Wallet Requirement + +- DEX swaps happen in a single transaction on-chain +- The wallet executing the swap receives the output tokens +- Cross-wallet swaps would require additional transfer transactions + +### Chain Validation + +- Prevents accidental cross-chain swap attempts +- Ensures contract addresses are valid for the target chain +- Protects users from losing funds due to chain mismatches + +## Implementation Guidelines + +When implementing a new DEX plugin: + +1. **Always validate wallet compatibility** early in `fetchSwapQuote` +2. **Throw descriptive errors** that explain the limitation +3. **Document the requirement** in the plugin's swapInfo + +Example validation: + +```typescript +async fetchSwapQuote(request: EdgeSwapRequest): Promise { + // Validate same wallet requirement for DEX + if (request.fromWallet !== request.toWallet) { + throw new Error(`${swapInfo.displayName} requires same wallet for swap`) + } + + // Continue with quote logic... +} +``` + +## Exceptions + +Centralized exchange plugins (`/central/`) typically support cross-wallet swaps because: + +- They use deposit addresses +- The exchange handles the actual swap +- Funds can be sent to any destination address diff --git a/docs/conventions/edge-company-conventions.md b/docs/conventions/edge-company-conventions.md new file mode 100644 index 00000000..c2dbc299 --- /dev/null +++ b/docs/conventions/edge-company-conventions.md @@ -0,0 +1,67 @@ +# Edge Company Conventions + +**Date**: 2025-08-20 + +## Overview + +This document references the company-wide Edge development conventions that apply to all Edge projects, including edge-exchange-plugins. + +## Edge Conventions Repository + +The official Edge conventions are maintained at: https://github.com/EdgeApp/edge-conventions + +### Key Convention Categories + +1. **Code Conventions** + + - [JavaScript Code Conventions](https://github.com/EdgeApp/edge-conventions/blob/master/code/javascriptCode.md) + - [JavaScript Project Setup](https://github.com/EdgeApp/edge-conventions/blob/master/code/javascriptSetup.md) + - [React Conventions](https://github.com/EdgeApp/edge-conventions/blob/master/code/react.md) + - [Redux Conventions](https://github.com/EdgeApp/edge-conventions/blob/master/code/redux.md) + +2. **Git Conventions** + + - [Commit Rules](https://github.com/EdgeApp/edge-conventions/blob/master/git/commit.md) + - [Pull Request Rules](https://github.com/EdgeApp/edge-conventions/blob/master/git/pr.md) + - [Git "Future Commit" Workflow](https://github.com/EdgeApp/edge-conventions/blob/master/git/future-commit.md) + +3. **Documentation Standards** + - [Documentation Conventions](https://github.com/EdgeApp/edge-conventions/blob/master/docs.md) + +## How These Apply to edge-exchange-plugins + +### Code Standards + +While edge-exchange-plugins uses TypeScript (not plain JavaScript), many principles from the JavaScript conventions still apply: + +- Consistent formatting and style +- Clear naming conventions +- Proper error handling patterns + +### Git Workflow + +All Edge projects follow the same git conventions: + +- **Commit messages** should follow the Edge commit rules +- **Pull requests** must meet the PR requirements +- **Branching** follows the documented patterns + +### Additional Project-Specific Conventions + +This project extends the Edge conventions with TypeScript-specific rules documented in: + +- [TypeScript Style Guide](./typescript-style-guide.md) - Project-specific TypeScript conventions + +## Important Notes + +1. **Company conventions take precedence** - When in doubt, follow the Edge conventions +2. **TypeScript additions** - This project adds TypeScript-specific rules on top of the base conventions +3. **PR requirements** - All PRs must follow both Edge conventions and project-specific requirements + +## Quick Reference + +For edge-exchange-plugins developers: + +1. Read the [Edge conventions](https://github.com/EdgeApp/edge-conventions) first +2. Then read our [TypeScript Style Guide](./typescript-style-guide.md) for project-specific rules +3. Follow the [PR rules](https://github.com/EdgeApp/edge-conventions/blob/master/git/pr.md) when submitting changes diff --git a/docs/conventions/typescript-style-guide.md b/docs/conventions/typescript-style-guide.md new file mode 100644 index 00000000..c60170ee --- /dev/null +++ b/docs/conventions/typescript-style-guide.md @@ -0,0 +1,142 @@ +# TypeScript Style Guide + +**Date**: 2025-08-20 + +## Import Conventions + +### Always use type imports for types + +```typescript +// ✅ Good +import type { EdgeSwapQuote, EdgeCurrencyWallet } from "edge-core-js/types"; + +// ❌ Bad +import { EdgeSwapQuote, EdgeCurrencyWallet } from "edge-core-js/types"; +``` + +### Import sorting is enforced + +- Imports are automatically sorted by `simple-import-sort` ESLint plugin +- Order: external packages first, then internal modules +- No default exports are used in this codebase + +## Type Safety + +### Strict TypeScript is enabled + +```json +{ + "compilerOptions": { + "strict": true + } +} +``` + +### Use cleaners for runtime validation + +```typescript +// Always validate external data with cleaners +import { asObject, asString, asNumber } from "cleaners"; + +const asApiResponse = asObject({ + rate: asNumber, + currency: asString, +}); +``` + +### Use biggystring for numeric operations + +```typescript +// ✅ Good - use biggystring for comparisons +import { gt, lt } from "biggystring"; +if (gt(amount, maxAmount)) throw new SwapAboveLimitError(); + +// ❌ Bad - don't use Number for crypto amounts +if (Number(amount) > Number(maxAmount)) throw new SwapAboveLimitError(); +``` + +## Error Handling + +### Throw specific Edge error types + +```typescript +// ✅ Good +throw new SwapCurrencyError(swapInfo, fromCurrencyCode, toCurrencyCode); +throw new SwapAboveLimitError(swapInfo, max); +throw new SwapBelowLimitError(swapInfo, min); + +// ❌ Bad +throw new Error("Invalid currency"); +``` + +### Always handle async errors with try/catch + +```typescript +async function fetchQuote(): Promise { + try { + const response = await fetch(url); + return processResponse(response); + } catch (error) { + throw new SwapCurrencyError(swapInfo, "BTC", "ETH"); + } +} +``` + +## Naming Conventions + +### Variables and functions: camelCase + +```typescript +const swapRequest = { ... } +function calculateExchangeRate() { ... } +``` + +### Types and interfaces: PascalCase + +```typescript +interface SwapOrder { ... } +type EdgeSwapRequestPlugin = { ... } +``` + +### Constants: UPPER_SNAKE_CASE + +```typescript +const MAX_RETRIES = 3; +const API_BASE_URL = "https://api.exchange.com"; +``` + +### Files: camelCase with .ts extension + +- `src/swap/central/changenow.ts` +- `src/util/swapHelpers.ts` + +## Code Organization + +### Named exports only + +```typescript +// ✅ Good +export const swapInfo = { ... } +export function makePlugin() { ... } + +// ❌ Bad +export default makePlugin +``` + +### Extract magic numbers as constants + +```typescript +// ✅ Good +const EXPIRATION_TIME_SECONDS = 600 +if (Date.now() / 1000 > quote.expirationDate + EXPIRATION_TIME_SECONDS) { ... } + +// ❌ Bad +if (Date.now() / 1000 > quote.expirationDate + 600) { ... } +``` + +## References + +- ESLint config: `.eslintrc.json` +- TypeScript config: `tsconfig.json` +- Editor config: `.editorconfig` +- [Edge Development Conventions](https://github.com/EdgeApp/edge-conventions) - Company-wide conventions for all Edge projects diff --git a/docs/guides/adding-new-exchange.md b/docs/guides/adding-new-exchange.md new file mode 100644 index 00000000..17b2e96e --- /dev/null +++ b/docs/guides/adding-new-exchange.md @@ -0,0 +1,208 @@ +# Adding a New Exchange Plugin + +**Date**: 2025-08-20 + +## Overview + +This guide walks through adding a new exchange plugin to edge-exchange-plugins. Exchange plugins enable Edge Wallet to perform cryptocurrency swaps through various providers. + +## Prerequisites + +1. Clone edge-exchange-plugins and edge-react-gui as peers: + + ```bash + git clone git@github.com:EdgeApp/edge-exchange-plugins.git + git clone git@github.com:EdgeApp/edge-react-gui.git + ``` + +2. Build edge-exchange-plugins: + + ```bash + cd edge-exchange-plugins + yarn + yarn prepare + ``` + +3. Link to edge-react-gui: + ```bash + cd ../edge-react-gui + yarn updot edge-exchange-plugins + yarn prepare + yarn prepare.ios # For iOS development + ``` + +## Implementation Steps + +### 1. Choose Plugin Type + +Determine if your exchange is: + +- **Centralized** (API-based): Place in `src/swap/central/` +- **Decentralized** (DEX): Place in `src/swap/defi/` + +### 2. Create Plugin File + +Create your plugin file following the naming convention: + +```typescript +// src/swap/central/myexchange.ts +import { + EdgeCorePluginOptions, + EdgeSwapInfo, + EdgeSwapPlugin, +} from "edge-core-js/types"; + +const pluginId = "myexchange"; + +export const swapInfo: EdgeSwapInfo = { + pluginId, + isDex: false, // true for DEX + displayName: "My Exchange", + supportEmail: "support@myexchange.com", +}; + +export function makeMyExchangePlugin( + opts: EdgeCorePluginOptions +): EdgeSwapPlugin { + // Implementation +} +``` + +### 3. Implement Required Methods + +Your plugin must implement `fetchSwapQuote`: + +```typescript +async fetchSwapQuote(request: EdgeSwapRequest): Promise { + // 1. Validate supported currencies + const { fromCurrencyCode, toCurrencyCode } = request + + // 2. Convert Edge request to your API format + const apiRequest = await convertRequest(request) + + // 3. Call your exchange API + const quote = await fetchQuoteFromApi(apiRequest) + + // 4. Validate limits + if (lt(quote.fromAmount, MIN_AMOUNT)) { + throw new SwapBelowLimitError(swapInfo, MIN_AMOUNT) + } + + // 5. Return EdgeSwapQuote + return makeSwapPluginQuote({ + request, + swapInfo, + // Your quote details + }) +} +``` + +### 4. Add to Index + +Export your plugin in `src/index.ts`: + +```typescript +import { makeMyExchangePlugin } from "./swap/central/myexchange"; + +const plugins = { + // ... existing plugins + myexchange: makeMyExchangePlugin, +}; +``` + +### 5. Configure in edge-react-gui + +1. Add logo assets: + + - 64x64 pixel square logo (white background) + - 600x210 pixel horizontal logo (no empty space) + +2. Update environment config in `env.json` + +3. Search for "changelly" in edge-react-gui and make similar changes for your plugin + +## Testing Your Plugin + +### Local Testing + +1. Disable other exchanges in Settings > Exchange Settings +2. Test swaps with your plugin enabled +3. Verify error handling for: + - Unsupported currencies + - Below/above limits + - Network errors + +### Test Coverage + +Create tests in `test/myexchange.test.ts`: + +```typescript +describe("MyExchange Plugin", () => { + it("should fetch a valid quote", async () => { + const plugin = makeMyExchangePlugin({ initOptions: { apiKey: "test" } }); + const quote = await plugin.fetchSwapQuote(mockRequest); + expect(quote.fromNativeAmount).to.equal("1000000000"); + }); +}); +``` + +## Submission Requirements + +Before submitting a PR: + +1. **Add transaction reporting** - Submit PR to edge-reports for crediting Edge users +2. **Rebase on master** - Keep your branch up to date +3. **Include assets** - Logo files in edge-react-gui PR +4. **Test thoroughly** - All edge cases and error conditions + +## Common Patterns + +### Currency Code Mapping + +If your API uses different currency codes: + +```typescript +const currencyMap: StringMap = { + USDT: "USDT20", // Your API code + BTC: "BTC", +}; +``` + +### Rate Limiting + +Handle API rate limits gracefully: + +```typescript +const RATE_LIMIT_MS = 1000; +let lastCallTime = 0; + +async function throttledFetch() { + const now = Date.now(); + const timeSinceLastCall = now - lastCallTime; + if (timeSinceLastCall < RATE_LIMIT_MS) { + await new Promise((resolve) => + setTimeout(resolve, RATE_LIMIT_MS - timeSinceLastCall) + ); + } + lastCallTime = Date.now(); + // Make API call +} +``` + +## Debugging Tips + +1. **Enable your plugin only** in Exchange Settings +2. **Check logs** for API responses and errors +3. **Use test wallets** with small amounts +4. **Run local webpack server** for hot reloading: + ```bash + yarn start # In edge-exchange-plugins + ``` + +## Support + +For questions or issues: + +- Review existing plugins for examples +- Check closed PRs for similar implementations +- Contact Edge team for API access or integration support diff --git a/docs/patterns/swap-plugin-architecture.md b/docs/patterns/swap-plugin-architecture.md new file mode 100644 index 00000000..747cc80b --- /dev/null +++ b/docs/patterns/swap-plugin-architecture.md @@ -0,0 +1,192 @@ +# Swap Plugin Architecture + +**Date**: 2025-08-20 + +## Overview + +Edge exchange plugins follow a consistent architecture pattern for implementing swap providers. This document describes the standard patterns used across all swap plugins. + +## Plugin Structure + +### Basic Plugin Factory Pattern + +```typescript +export function makeSwapPlugin(opts: EdgeCorePluginOptions): EdgeSwapPlugin { + const initOptions = asInitOptions(opts.initOptions); + + return { + swapInfo, + + async fetchSwapQuote(request: EdgeSwapRequest): Promise { + // Implementation + }, + }; +} +``` + +### SwapInfo Object + +Every plugin must export a `swapInfo` object: + +```typescript +export const swapInfo: EdgeSwapInfo = { + pluginId: "changenow", + isDex: false, // true for DEX plugins + displayName: "ChangeNOW", + supportEmail: "support@changenow.io", +}; +``` + +## Plugin Categories + +### 1. Centralized Exchange Plugins (`src/swap/central/`) + +- Direct API integration with centralized exchanges +- Examples: ChangeHero, ChangeNOW, Godex, LetsExchange +- Pattern: API key authentication, order creation, status polling + +### 2. DEX/DeFi Plugins (`src/swap/defi/`) + +- On-chain decentralized exchanges +- Examples: THORChain, Uniswap V2 forks, 0x Protocol +- Pattern: Smart contract interaction, gas estimation, slippage handling + +### 3. Transfer Plugin (`src/swap/transfer.ts`) + +- Special plugin for same-currency transfers between wallets +- No actual swap, just moves funds + +## Common Patterns + +### Currency Code Validation + +```typescript +// Check supported currencies +const isFromCurrencySupported = checkInvalidCodes( + fromCodes, + InvalidCurrencyCodes +); +const isToCurrencySupported = checkInvalidCodes(toCodes, InvalidCurrencyCodes); + +if (!isFromCurrencySupported || !isToCurrencySupported) { + throw new SwapCurrencyError( + swapInfo, + request.fromCurrencyCode, + request.toCurrencyCode + ); +} +``` + +### Quote Request Pattern + +```typescript +async function fetchSwapQuote(request: EdgeSwapRequest): Promise { + // 1. Validate currencies + checkInvalidCodes(...) + + // 2. Convert request to internal format + const convertedRequest = await convertRequest(request) + + // 3. Fetch quote from API + const apiQuote = await fetchQuoteFromApi(convertedRequest) + + // 4. Validate limits + if (lt(amount, min)) throw new SwapBelowLimitError(swapInfo, min) + if (gt(amount, max)) throw new SwapAboveLimitError(swapInfo, max) + + // 5. Create and return EdgeSwapQuote + return makeSwapPluginQuote({ + // Quote details + }) +} +``` + +### Approval Pattern (DEX) + +```typescript +const spendInfo: EdgeSpendInfo = { + // Approval transaction first + spendTargets: [ + { + nativeAmount: "0", + publicAddress: APPROVAL_CONTRACT, + }, + ], + // Then swap transaction +}; +``` + +## Utility Functions + +### makeSwapPluginQuote + +Standard utility for creating quotes: + +```typescript +import { makeSwapPluginQuote } from "../../util/swapHelpers"; + +const quote = await makeSwapPluginQuote({ + request, + swapInfo, + fetchSwapQuote, + checkWhitelistedMainnetCodes, + // Additional quote details +}); +``` + +### convertRequest + +Converts Edge request format to plugin-specific format: + +```typescript +const convertedRequest = await convertRequest(request); +// Returns: amount, fromAddress, toAddress, etc. +``` + +## Error Handling Patterns + +### Standard Swap Errors + +- `SwapCurrencyError`: Unsupported currency pair +- `SwapBelowLimitError`: Amount too small +- `SwapAboveLimitError`: Amount too large +- `InsufficientFundsError`: Not enough balance + +### Network Error Handling + +```typescript +try { + const response = await fetch(url); +} catch (error) { + // Log and rethrow with context + console.error(`${pluginId} fetchQuote error:`, error); + throw error; +} +``` + +## Testing Patterns + +### Mock Data + +- Use `test/fake*.ts` files for mock currency info +- Create standardized test cases in `test/` directory + +### Integration Tests + +```typescript +describe("SwapPlugin", () => { + it("should fetch quote", async () => { + const quote = await plugin.fetchSwapQuote(mockRequest); + expect(quote.fromNativeAmount).to.equal("1000000000"); + }); +}); +``` + +## Best Practices + +1. **Always validate currency codes** before making API calls +2. **Use biggystring** for all numeric comparisons +3. **Include detailed metadata** in quotes (order ID, etc.) +4. **Handle rate limiting** with appropriate delays +5. **Log errors with context** for debugging +6. **Test with mainnet and testnet** currency codes diff --git a/docs/references/api-deprecations.md b/docs/references/api-deprecations.md new file mode 100644 index 00000000..56243def --- /dev/null +++ b/docs/references/api-deprecations.md @@ -0,0 +1,64 @@ +# API Deprecations + +**Date**: 2025-08-20 + +## Overview + +This document tracks deprecated APIs in edge-core-js that are still used in the codebase. These should be migrated when possible. + +## Deprecated APIs Currently in Use + +### fetchCors + +**Status**: Deprecated +**Used in**: `src/swap/central/changehero.ts` (line 184) +**Migration**: Use standard `fetch` API with appropriate CORS headers + +### denominationToNative / nativeToDenomination + +**Status**: Deprecated +**Used extensively in**: + +- `src/swap/central/changehero.ts` (multiple lines) +- Various other swap plugins + +**Current usage example**: + +```typescript +// Deprecated +const nativeAmount = denominationToNative(denominationAmount, currencyInfo); +const denominationAmount = nativeToDenomination(nativeAmount, currencyInfo); +``` + +**Migration path**: Use the new conversion utilities from edge-core-js when available. + +## Migration Strategy + +1. **Track deprecation warnings** during build/development +2. **Update incrementally** - migrate one plugin at a time +3. **Test thoroughly** - ensure numeric precision is maintained +4. **Coordinate with edge-core-js** updates + +## Impact Assessment + +### High Priority + +- `denominationToNative` / `nativeToDenomination` - Used for critical amount calculations + +### Medium Priority + +- `fetchCors` - Can be replaced with standard fetch + +## Testing Deprecation Fixes + +When migrating deprecated APIs: + +1. **Preserve exact numeric behavior** - Use test cases with known values +2. **Check edge cases** - Very large/small amounts, different decimal places +3. **Verify cross-plugin compatibility** - Ensure all plugins work together + +## Notes + +- Deprecation warnings appear as HINT messages during TypeScript compilation +- Some deprecations may require waiting for edge-core-js updates +- Always maintain backward compatibility during migration From 8cd93846e274a3aa3ffafe091b1cab2d8336735b Mon Sep 17 00:00:00 2001 From: Sam Holmes Date: Wed, 20 Aug 2025 14:17:15 -0700 Subject: [PATCH 02/11] docs: Add debug server instructions for faster development - Document DEBUG_EXCHANGE env setting in edge-react-gui - Present debug server as recommended development approach - Clarify difference between debug server and direct linking methods - Update debugging tips to emphasize hot-reload benefits --- docs/guides/adding-new-exchange.md | 50 +++++++++++++++++++++++++++--- 1 file changed, 46 insertions(+), 4 deletions(-) diff --git a/docs/guides/adding-new-exchange.md b/docs/guides/adding-new-exchange.md index 17b2e96e..75597fe0 100644 --- a/docs/guides/adding-new-exchange.md +++ b/docs/guides/adding-new-exchange.md @@ -8,6 +8,8 @@ This guide walks through adding a new exchange plugin to edge-exchange-plugins. ## Prerequisites +### Option 1: Debug Server (Recommended for Development) + 1. Clone edge-exchange-plugins and edge-react-gui as peers: ```bash @@ -15,6 +17,46 @@ This guide walks through adding a new exchange plugin to edge-exchange-plugins. git clone git@github.com:EdgeApp/edge-react-gui.git ``` +2. Set up debug mode in edge-react-gui: + + ```json + // edge-react-gui/env.json + { + "DEBUG_EXCHANGE": true + } + ``` + +3. Start the development server: + ```bash + cd edge-exchange-plugins + yarn + yarn start # Runs webpack dev server on localhost:8083 + ``` + +This approach uses a local webpack server that hot-reloads your changes, making development faster and easier. + +### Option 2: Direct Linking (For Production Testing) + +1. Clone edge-exchange-plugins and edge-react-gui as peers (same as above) + +2. Build edge-exchange-plugins: + + ```bash + cd edge-exchange-plugins + yarn + yarn prepare + ``` + +3. Link to edge-react-gui: + ```bash + cd ../edge-react-gui + yarn updot edge-exchange-plugins + yarn prepare + yarn prepare.ios # For iOS development + ``` + +This approach builds and links the plugins directly into edge-react-gui, which is closer to production behavior but requires rebuilding after each change. + 2. Build edge-exchange-plugins: ```bash @@ -194,10 +236,10 @@ async function throttledFetch() { 1. **Enable your plugin only** in Exchange Settings 2. **Check logs** for API responses and errors 3. **Use test wallets** with small amounts -4. **Run local webpack server** for hot reloading: - ```bash - yarn start # In edge-exchange-plugins - ``` +4. **Use debug server** for faster development: + - Set `DEBUG_EXCHANGE: true` in edge-react-gui's env.json + - Run `yarn start` in edge-exchange-plugins + - Changes will hot-reload without rebuilding ## Support From dbb6b8c7e0e3c9c4a1fb7f9987e5bd8f46847eab Mon Sep 17 00:00:00 2001 From: Sam Holmes Date: Wed, 20 Aug 2025 16:44:16 -0700 Subject: [PATCH 03/11] docs: Add comprehensive plugin expectations and requirements - Document chain support mapping and parameter access patterns - Emphasize critical fee estimation via transaction creation - Add destination tag/memo handling requirements - Clarify fixed vs variable quote specifications - Document reverse quote implementation - Expand error handling requirements with specific examples - Add tokenId to contract address mapping explanation - Include best practices for all new requirements --- docs/patterns/swap-plugin-architecture.md | 207 ++++++++++++++++++++++ 1 file changed, 207 insertions(+) diff --git a/docs/patterns/swap-plugin-architecture.md b/docs/patterns/swap-plugin-architecture.md index 747cc80b..7c076a39 100644 --- a/docs/patterns/swap-plugin-architecture.md +++ b/docs/patterns/swap-plugin-architecture.md @@ -56,6 +56,207 @@ export const swapInfo: EdgeSwapInfo = { - Special plugin for same-currency transfers between wallets - No actual swap, just moves funds +## Plugin Expectations and Requirements + +### Chain Support Mapping + +Plugins must properly map Edge currency codes to exchange-specific codes: + +```typescript +// Map Edge pluginId to your exchange's chain identifiers +const CHAIN_ID_MAP: Record = { + bitcoin: "btc", + ethereum: "eth", + binancesmartchain: "bsc", + // Add all supported chains +}; + +// For EVM chains, get chain ID from currency info +const chainId = fromWallet.currencyInfo.defaultSettings?.otherSettings?.chainId; +``` + +### Accessing Request Parameters + +The `EdgeSwapRequest` provides all necessary information: + +```typescript +interface EdgeSwapRequest { + fromWallet: EdgeCurrencyWallet; // Source wallet + toWallet: EdgeCurrencyWallet; // Destination wallet + fromTokenId: string | null; // Token contract or null for native + toTokenId: string | null; // Token contract or null for native + nativeAmount: string; // Amount in smallest unit + quoteFor: "from" | "to" | "max"; // Quote direction +} + +// Access plugin IDs +const fromPluginId = request.fromWallet.currencyInfo.pluginId; +const toPluginId = request.toWallet.currencyInfo.pluginId; + +// Get EVM chain ID +const evmChainId = + request.fromWallet.currencyInfo.defaultSettings?.otherSettings?.chainId; + +// Convert tokenId to contract address +const contractAddress = request.fromTokenId; // tokenId IS the contract address for EVM +``` + +### Transaction Fee Estimation + +**Critical**: Plugins must create actual transactions to get accurate fee estimates: + +```typescript +// For DEX plugins - create the actual swap transaction +const swapTx = await makeSwapTransaction(params); +const networkFee = swapTx.networkFee; + +// For centralized exchanges - estimate deposit transaction +const depositAddress = await getDepositAddress(); +const spendInfo: EdgeSpendInfo = { + tokenId: request.fromTokenId, + spendTargets: [ + { + nativeAmount: request.nativeAmount, + publicAddress: depositAddress, + }, + ], +}; +const tx = await request.fromWallet.makeSpend(spendInfo); +const networkFee = tx.networkFee; + +// Return fee in the quote +return { + ...quote, + networkFee: { + currencyCode: fromWallet.currencyInfo.currencyCode, + nativeAmount: networkFee, + }, +}; +``` + +### Destination Tags and Memos + +Handle destination tags/memos for chains that require them: + +```typescript +// Check if destination requires memo +const { publicAddress, tag } = await getAddress(request.toWallet); + +// For XRP, XLM, etc. +if (tag != null) { + spendInfo.spendTargets[0].uniqueIdentifier = tag; +} + +// For Cosmos-based chains +if (memo != null) { + spendInfo.memo = memo; +} + +// Include in quote approval info +quote.approveInfo = { + ...approveInfo, + customFee: { + nativeAmount: networkFee, + }, +}; +``` + +### Quote Types: Fixed vs Variable + +Specify quote behavior clearly: + +```typescript +interface EdgeSwapQuote { + isEstimate: boolean; // true for variable rates, false for fixed + expirationDate?: Date; // Required for fixed quotes + + // For fixed quotes + guaranteedAmount?: string; // The guaranteed output amount + + // For variable quotes + minReceiveAmount?: string; // Minimum possible output + maxReceiveAmount?: string; // Maximum possible output +} + +// Fixed quote example +return { + ...quote, + isEstimate: false, + expirationDate: new Date(Date.now() + 600 * 1000), // 10 minutes + toNativeAmount: guaranteedAmount, +}; + +// Variable quote example +return { + ...quote, + isEstimate: true, + toNativeAmount: estimatedAmount, + minReceiveAmount: minAmount, +}; +``` + +### Reverse Quotes + +Support both forward and reverse quotes: + +```typescript +async function fetchSwapQuote( + request: EdgeSwapRequest +): Promise { + const { quoteFor } = request; + + switch (quoteFor) { + case "from": + // User specified source amount, calculate destination + return fetchForwardQuote(request); + + case "to": + // User specified destination amount, calculate source + return fetchReverseQuote(request); + + case "max": + // Use maximum available balance + const maxAmount = await getMaxSwappable(request); + return fetchQuoteWithAmount(request, maxAmount); + } +} +``` + +### Error Handling Requirements + +Provide specific, actionable error messages: + +```typescript +// Currency not supported +throw new SwapCurrencyError(swapInfo, fromCode, toCode); + +// Amount validation +if (lt(amount, minimum)) { + throw new SwapBelowLimitError(swapInfo, minimum, fromCode); +} +if (gt(amount, maximum)) { + throw new SwapAboveLimitError(swapInfo, maximum, fromCode); +} + +// Network or API errors +try { + const response = await fetch(url); +} catch (error) { + console.error(`${pluginId} network error:`, error); + // Include request ID if available for support + throw new Error(`Network error: ${error.message}. Request ID: ${requestId}`); +} + +// Insufficient liquidity +if (response.error === "INSUFFICIENT_LIQUIDITY") { + throw new InsufficientLiquidityError(swapInfo, { + fromCode, + toCode, + amount, + }); +} +``` + ## Common Patterns ### Currency Code Validation @@ -190,3 +391,9 @@ describe("SwapPlugin", () => { 4. **Handle rate limiting** with appropriate delays 5. **Log errors with context** for debugging 6. **Test with mainnet and testnet** currency codes +7. **Create actual transactions** for accurate fee estimation +8. **Handle destination tags/memos** for chains that require them +9. **Specify quote type** (fixed vs variable) clearly +10. **Support all quote directions** (from, to, max) +11. **Map chains correctly** using pluginId and chain IDs +12. **Provide specific error messages** with support context From 3c93bc133058381e15de5a551789af63de3ff3db Mon Sep 17 00:00:00 2001 From: Sam Holmes Date: Wed, 20 Aug 2025 17:55:44 -0700 Subject: [PATCH 04/11] fix: Correct chain ID access and Edge type usage in docs - Remove incorrect usage of deprecated defaultSettings for chain ID - Clarify that pluginId uniquely identifies the network - Update type annotations to use proper Edge types (EdgeTokenId) - Explain that tokenId is null for native/mainnet tokens - Document EdgeCurrencyWallet structure with currencyInfo/currencyConfig - Emphasize pluginId as the primary chain identifier --- docs/patterns/swap-plugin-architecture.md | 47 +++++++++++++++-------- 1 file changed, 32 insertions(+), 15 deletions(-) diff --git a/docs/patterns/swap-plugin-architecture.md b/docs/patterns/swap-plugin-architecture.md index 7c076a39..f4dc6d95 100644 --- a/docs/patterns/swap-plugin-architecture.md +++ b/docs/patterns/swap-plugin-architecture.md @@ -60,19 +60,25 @@ export const swapInfo: EdgeSwapInfo = { ### Chain Support Mapping -Plugins must properly map Edge currency codes to exchange-specific codes: +Plugins must properly map Edge pluginIds to exchange-specific identifiers: ```typescript // Map Edge pluginId to your exchange's chain identifiers -const CHAIN_ID_MAP: Record = { +const PLUGIN_ID_MAP: Record = { bitcoin: "btc", ethereum: "eth", binancesmartchain: "bsc", + avalanche: "avax", // Add all supported chains }; -// For EVM chains, get chain ID from currency info -const chainId = fromWallet.currencyInfo.defaultSettings?.otherSettings?.chainId; +// The pluginId uniquely identifies the network +const fromPluginId = request.fromWallet.currencyInfo.pluginId; // e.g. 'ethereum' +const toPluginId = request.toWallet.currencyInfo.pluginId; // e.g. 'bitcoin' + +// Map to your exchange's format +const fromChain = PLUGIN_ID_MAP[fromPluginId]; +const toChain = PLUGIN_ID_MAP[toPluginId]; ``` ### Accessing Request Parameters @@ -81,24 +87,35 @@ The `EdgeSwapRequest` provides all necessary information: ```typescript interface EdgeSwapRequest { - fromWallet: EdgeCurrencyWallet; // Source wallet + fromWallet: EdgeCurrencyWallet; // Source wallet with currencyInfo and currencyConfig toWallet: EdgeCurrencyWallet; // Destination wallet - fromTokenId: string | null; // Token contract or null for native - toTokenId: string | null; // Token contract or null for native - nativeAmount: string; // Amount in smallest unit + fromTokenId: EdgeTokenId | null; // Token identifier or null for native/mainnet token + toTokenId: EdgeTokenId | null; // Token identifier or null for native/mainnet token + nativeAmount: string; // Amount in smallest unit (satoshis, wei, etc.) quoteFor: "from" | "to" | "max"; // Quote direction } -// Access plugin IDs -const fromPluginId = request.fromWallet.currencyInfo.pluginId; +// Access wallet information +const fromWallet = request.fromWallet; +const fromCurrencyInfo = fromWallet.currencyInfo; // EdgeCurrencyInfo +const fromCurrencyConfig = fromWallet.currencyConfig; // EdgeCurrencyConfig + +// Plugin IDs uniquely identify the network +const fromPluginId = fromCurrencyInfo.pluginId; // e.g. 'ethereum', 'bitcoin' const toPluginId = request.toWallet.currencyInfo.pluginId; -// Get EVM chain ID -const evmChainId = - request.fromWallet.currencyInfo.defaultSettings?.otherSettings?.chainId; +// Token handling +const fromTokenId = request.fromTokenId; // null for mainnet token (ETH, BTC, etc.) +const toTokenId = request.toTokenId; // string for tokens (contract address on EVM) + +// For EVM chains, tokenId is the contract address +if (fromTokenId != null && fromPluginId === "ethereum") { + const contractAddress = fromTokenId; // This IS the contract address +} -// Convert tokenId to contract address -const contractAddress = request.fromTokenId; // tokenId IS the contract address for EVM +// Get currency codes +const fromCurrencyCode = request.fromCurrencyCode; // e.g. 'ETH', 'USDC' +const toCurrencyCode = request.toCurrencyCode; ``` ### Transaction Fee Estimation From 040b93b2d28c1f5454e2de28db54f68245cccd86 Mon Sep 17 00:00:00 2001 From: Sam Holmes Date: Wed, 20 Aug 2025 17:57:36 -0700 Subject: [PATCH 05/11] fix: Make tokenId documentation chain-agnostic - Remove EVM-specific references for tokenId - Clarify tokenId applies to any chain with tokens - Explain tokenId format varies by chain type - Use more generic language (native currency vs tokens) --- docs/patterns/swap-plugin-architecture.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/docs/patterns/swap-plugin-architecture.md b/docs/patterns/swap-plugin-architecture.md index f4dc6d95..34a91dec 100644 --- a/docs/patterns/swap-plugin-architecture.md +++ b/docs/patterns/swap-plugin-architecture.md @@ -89,8 +89,8 @@ The `EdgeSwapRequest` provides all necessary information: interface EdgeSwapRequest { fromWallet: EdgeCurrencyWallet; // Source wallet with currencyInfo and currencyConfig toWallet: EdgeCurrencyWallet; // Destination wallet - fromTokenId: EdgeTokenId | null; // Token identifier or null for native/mainnet token - toTokenId: EdgeTokenId | null; // Token identifier or null for native/mainnet token + fromTokenId: EdgeTokenId | null; // Token identifier or null for native currency + toTokenId: EdgeTokenId | null; // Token identifier or null for native currency nativeAmount: string; // Amount in smallest unit (satoshis, wei, etc.) quoteFor: "from" | "to" | "max"; // Quote direction } @@ -105,12 +105,13 @@ const fromPluginId = fromCurrencyInfo.pluginId; // e.g. 'ethereum', 'bitcoin' const toPluginId = request.toWallet.currencyInfo.pluginId; // Token handling -const fromTokenId = request.fromTokenId; // null for mainnet token (ETH, BTC, etc.) -const toTokenId = request.toTokenId; // string for tokens (contract address on EVM) +const fromTokenId = request.fromTokenId; // null for native/mainnet currency (ETH, BTC, etc.) +const toTokenId = request.toTokenId; // string identifier for tokens -// For EVM chains, tokenId is the contract address -if (fromTokenId != null && fromPluginId === "ethereum") { - const contractAddress = fromTokenId; // This IS the contract address +// For chains with tokens, tokenId identifies the specific token +if (fromTokenId != null) { + // This is a token swap, not the native currency + // The tokenId format depends on the chain (contract address, asset ID, etc.) } // Get currency codes From 0272d62995da8daa85b9c95019add3f5b556a7c2 Mon Sep 17 00:00:00 2001 From: Sam Holmes Date: Wed, 20 Aug 2025 18:02:48 -0700 Subject: [PATCH 06/11] docs: Enhance swap plugin architecture documentation - Add comprehensive plugin system fundamentals section - Document how plugins attach to edge-core-js via factory pattern - Explain EdgeCorePluginOptions and plugin lifecycle - Correct API usage based on actual Edge types: - Fix currency code access (must be derived from wallet/tokenId) - Update to use networkFees array instead of networkFee - Correct memo API to use memos array - Fix EdgeSwapQuote interface properties - Add getCurrencyCode helper function example - Clarify factory function signatures and registration --- docs/patterns/swap-plugin-architecture.md | 146 ++++++++++++++++------ 1 file changed, 105 insertions(+), 41 deletions(-) diff --git a/docs/patterns/swap-plugin-architecture.md b/docs/patterns/swap-plugin-architecture.md index 34a91dec..bd53d2d6 100644 --- a/docs/patterns/swap-plugin-architecture.md +++ b/docs/patterns/swap-plugin-architecture.md @@ -4,39 +4,98 @@ ## Overview -Edge exchange plugins follow a consistent architecture pattern for implementing swap providers. This document describes the standard patterns used across all swap plugins. +Edge exchange plugins follow a consistent architecture pattern for implementing swap providers. This document describes the standard patterns used across all swap plugins and how they integrate with edge-core-js. -## Plugin Structure +## Plugin System Fundamentals -### Basic Plugin Factory Pattern +### How Plugins Attach to Core + +Edge plugins are registered with edge-core-js through a plugin map that gets passed during context creation: + +```typescript +// In src/index.ts - the main entry point +import { make0xGaslessPlugin } from "./swap/defi/0x/0xGasless"; +import { makeChangeNowPlugin } from "./swap/central/changenow"; +import { makeThorchainPlugin } from "./swap/defi/thorchain/thorchain"; + +const plugins = { + // Plugin ID maps to factory function + "0xgasless": make0xGaslessPlugin, + changenow: makeChangeNowPlugin, + thorchain: makeThorchainPlugin, + // ... more plugins +}; + +// When edge-core-js initializes, it calls these factory functions +// with EdgeCorePluginOptions to create the actual plugin instances +``` + +### Plugin Factory Pattern + +Every swap plugin exports a factory function that creates the plugin instance: ```typescript -export function makeSwapPlugin(opts: EdgeCorePluginOptions): EdgeSwapPlugin { +// Factory function signature - always follows this pattern +export function makeMyExchangePlugin( + opts: EdgeCorePluginOptions +): EdgeSwapPlugin { + // EdgeCorePluginOptions provides: + // - initOptions: API keys and config from the app + // - io: Network, crypto, and storage functions + // - log: Scoped logging for this plugin + // - pluginDisklet: Plugin-specific storage + + // Validate init options (API keys, etc.) const initOptions = asInitOptions(opts.initOptions); + // Return the EdgeSwapPlugin interface return { - swapInfo, - - async fetchSwapQuote(request: EdgeSwapRequest): Promise { + swapInfo, // Static metadata about this plugin + + // Required: Main quote fetching method + async fetchSwapQuote( + request: EdgeSwapRequest, + userSettings: JsonObject | undefined, + opts: { infoPayload: JsonObject; promoCode?: string } + ): Promise { // Implementation }, + + // Optional: Check if plugin needs activation + checkSettings: (userSettings: JsonObject) => EdgeSwapPluginStatus, }; } + +// Init options validation using cleaners +const asInitOptions = asObject({ + apiKey: asString, + affiliateId: asOptional(asString), +}); ``` ### SwapInfo Object -Every plugin must export a `swapInfo` object: +Every plugin must export a `swapInfo` object that identifies the plugin: ```typescript export const swapInfo: EdgeSwapInfo = { - pluginId: "changenow", - isDex: false, // true for DEX plugins - displayName: "ChangeNOW", + pluginId: "changenow", // Unique identifier matching src/index.ts + isDex: false, // true for DEX, false/undefined for CEX + displayName: "ChangeNOW", // User-facing name supportEmail: "support@changenow.io", }; ``` +The `pluginId` in `swapInfo` must match the key used in `src/index.ts` for proper registration. + +### Plugin Lifecycle + +1. **Registration**: Plugin factory functions are exported from `src/index.ts` +2. **Initialization**: Edge-core-js calls factory functions with `EdgeCorePluginOptions` +3. **Configuration**: Plugins receive API keys via `initOptions` and runtime settings via `userSettings` +4. **Quote Requests**: Core calls `fetchSwapQuote` when users request swaps +5. **Quote Execution**: Returned quotes include an `approve()` method for execution + ## Plugin Categories ### 1. Centralized Exchange Plugins (`src/swap/central/`) @@ -114,9 +173,27 @@ if (fromTokenId != null) { // The tokenId format depends on the chain (contract address, asset ID, etc.) } -// Get currency codes -const fromCurrencyCode = request.fromCurrencyCode; // e.g. 'ETH', 'USDC' -const toCurrencyCode = request.toCurrencyCode; +// Get currency codes from the wallets +// Note: EdgeSwapRequest doesn't have fromCurrencyCode/toCurrencyCode directly +const fromCurrencyCode = getCurrencyCode( + request.fromWallet, + request.fromTokenId +); +const toCurrencyCode = getCurrencyCode(request.toWallet, request.toTokenId); + +// Helper to get currency code from wallet and tokenId +function getCurrencyCode( + wallet: EdgeCurrencyWallet, + tokenId: EdgeTokenId +): string { + if (tokenId == null) { + // Native currency + return wallet.currencyInfo.currencyCode; + } + // Token - look it up in the wallet's token map + const token = wallet.currencyConfig.allTokens[tokenId]; + return token?.currencyCode ?? "UNKNOWN"; +} ``` ### Transaction Fee Estimation @@ -126,7 +203,7 @@ const toCurrencyCode = request.toCurrencyCode; ```typescript // For DEX plugins - create the actual swap transaction const swapTx = await makeSwapTransaction(params); -const networkFee = swapTx.networkFee; +const networkFee = swapTx.networkFees[0]; // networkFees is an array of EdgeTxAmount // For centralized exchanges - estimate deposit transaction const depositAddress = await getDepositAddress(); @@ -140,34 +217,21 @@ const spendInfo: EdgeSpendInfo = { ], }; const tx = await request.fromWallet.makeSpend(spendInfo); -const networkFee = tx.networkFee; +const networkFee = tx.networkFees[0]; // networkFees is an array of EdgeTxAmount // Return fee in the quote return { ...quote, networkFee: { - currencyCode: fromWallet.currencyInfo.currencyCode, - nativeAmount: networkFee, + tokenId: networkFee.tokenId, + nativeAmount: networkFee.nativeAmount, + currencyCode: getCurrencyCode(fromWallet, networkFee.tokenId), // deprecated but still required }, }; -``` -### Destination Tags and Memos - -Handle destination tags/memos for chains that require them: - -```typescript -// Check if destination requires memo -const { publicAddress, tag } = await getAddress(request.toWallet); - -// For XRP, XLM, etc. -if (tag != null) { - spendInfo.spendTargets[0].uniqueIdentifier = tag; -} - -// For Cosmos-based chains +// For chains with memo support if (memo != null) { - spendInfo.memo = memo; + spendInfo.memos = [{ type: "text", value: memo }]; } // Include in quote approval info @@ -184,16 +248,16 @@ quote.approveInfo = { Specify quote behavior clearly: ```typescript +// EdgeSwapQuote properties for quote types interface EdgeSwapQuote { isEstimate: boolean; // true for variable rates, false for fixed - expirationDate?: Date; // Required for fixed quotes - - // For fixed quotes - guaranteedAmount?: string; // The guaranteed output amount + expirationDate?: Date; // When this quote expires + canBePartial?: boolean; // Can fulfill partially + maxFulfillmentSeconds?: number; // Max time to complete + minReceiveAmount?: string; // Worst-case receive amount - // For variable quotes - minReceiveAmount?: string; // Minimum possible output - maxReceiveAmount?: string; // Maximum possible output + fromNativeAmount: string; // Input amount + toNativeAmount: string; // Output amount (estimated or guaranteed) } // Fixed quote example From f7dc56922cfdc30172c21b2d303fa2c97506d767 Mon Sep 17 00:00:00 2001 From: Sam Holmes Date: Fri, 22 Aug 2025 13:01:19 -0700 Subject: [PATCH 07/11] refactor: Replace currency code terminology with symbol mapping MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Update fetchSwapQuote to use pluginId/tokenId → symbol mapping - Add comprehensive mapping examples for chains and tokens - Include EVM chain ID mapping for exchanges that need it - Emphasize API-driven validation over hardcoded values - Fix duplicate section in prerequisites - Update error handling to reference assets instead of currencies --- docs/guides/adding-new-exchange.md | 81 ++++++++++++++++++------------ 1 file changed, 50 insertions(+), 31 deletions(-) diff --git a/docs/guides/adding-new-exchange.md b/docs/guides/adding-new-exchange.md index 75597fe0..80b8cb53 100644 --- a/docs/guides/adding-new-exchange.md +++ b/docs/guides/adding-new-exchange.md @@ -57,22 +57,6 @@ This approach uses a local webpack server that hot-reloads your changes, making This approach builds and links the plugins directly into edge-react-gui, which is closer to production behavior but requires rebuilding after each change. -2. Build edge-exchange-plugins: - - ```bash - cd edge-exchange-plugins - yarn - yarn prepare - ``` - -3. Link to edge-react-gui: - ```bash - cd ../edge-react-gui - yarn updot edge-exchange-plugins - yarn prepare - yarn prepare.ios # For iOS development - ``` - ## Implementation Steps ### 1. Choose Plugin Type @@ -116,25 +100,27 @@ Your plugin must implement `fetchSwapQuote`: ```typescript async fetchSwapQuote(request: EdgeSwapRequest): Promise { - // 1. Validate supported currencies - const { fromCurrencyCode, toCurrencyCode } = request + // 1. Map Edge pluginId/tokenId to your exchange's symbols + const fromSymbol = mapToExchangeSymbol(request.fromWallet, request.fromTokenId) + const toSymbol = mapToExchangeSymbol(request.toWallet, request.toTokenId) // 2. Convert Edge request to your API format - const apiRequest = await convertRequest(request) + const apiRequest = await convertRequest(request, fromSymbol, toSymbol) - // 3. Call your exchange API + // 3. Call your exchange API for validation and quote const quote = await fetchQuoteFromApi(apiRequest) - // 4. Validate limits - if (lt(quote.fromAmount, MIN_AMOUNT)) { - throw new SwapBelowLimitError(swapInfo, MIN_AMOUNT) + // 4. Validate based on API response (not hardcoded values) + // The API should return supported assets, limits, and region restrictions + if (quote.error) { + handleApiError(quote.error, request) } // 5. Return EdgeSwapQuote return makeSwapPluginQuote({ request, swapInfo, - // Your quote details + // Your quote details from API }) } ``` @@ -170,8 +156,9 @@ const plugins = { 1. Disable other exchanges in Settings > Exchange Settings 2. Test swaps with your plugin enabled 3. Verify error handling for: - - Unsupported currencies - - Below/above limits + - Unsupported assets (pluginId/tokenId combinations) + - Below/above limits from API + - Region restrictions - Network errors ### Test Coverage @@ -199,14 +186,46 @@ Before submitting a PR: ## Common Patterns -### Currency Code Mapping +### PluginId/TokenId to Symbol Mapping -If your API uses different currency codes: +Map Edge's pluginId and tokenId to your exchange's symbols: ```typescript -const currencyMap: StringMap = { - USDT: "USDT20", // Your API code - BTC: "BTC", +// Map Edge pluginId to your exchange's chain identifiers +const CHAIN_MAP: Record = { + bitcoin: "btc", + ethereum: "eth", + binancesmartchain: "bsc", + avalanche: "avax", + // Add all supported chains +}; + +// Helper to convert Edge wallet/tokenId to exchange symbol +function mapToExchangeSymbol( + wallet: EdgeCurrencyWallet, + tokenId: EdgeTokenId +): string { + const pluginId = wallet.currencyInfo.pluginId; + const chainSymbol = CHAIN_MAP[pluginId]; + + if (tokenId == null) { + // Native currency + return chainSymbol; + } + + // For tokens, you may need additional mapping + // based on your exchange's token symbol format + const token = wallet.currencyConfig.allTokens[tokenId]; + return mapTokenToSymbol(chainSymbol, token); +} + +// For exchanges that use EVM chain IDs +const EVM_CHAIN_ID_TO_PLUGIN: Record = { + "1": "ethereum", // Ethereum Mainnet + "56": "binancesmartchain", // BSC + "137": "polygon", // Polygon + "43114": "avalanche", // Avalanche C-Chain + // Add other EVM chains as needed }; ``` From 6e5b73f21b55f1767e63745bc88dcb5ac168ff57 Mon Sep 17 00:00:00 2001 From: Sam Holmes Date: Fri, 22 Aug 2025 13:02:45 -0700 Subject: [PATCH 08/11] docs: Document proper error throwing patterns with priority MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add comprehensive error handling section with priority order - Show API-driven validation instead of hardcoded values - Document error priority: region → asset support → limits - Update quote request pattern to use API validation - Replace currency validation with asset validation via API --- docs/guides/adding-new-exchange.md | 45 ++++++++- docs/patterns/swap-plugin-architecture.md | 114 +++++++++++++--------- 2 files changed, 109 insertions(+), 50 deletions(-) diff --git a/docs/guides/adding-new-exchange.md b/docs/guides/adding-new-exchange.md index 80b8cb53..f5b3a273 100644 --- a/docs/guides/adding-new-exchange.md +++ b/docs/guides/adding-new-exchange.md @@ -111,10 +111,8 @@ async fetchSwapQuote(request: EdgeSwapRequest): Promise { const quote = await fetchQuoteFromApi(apiRequest) // 4. Validate based on API response (not hardcoded values) - // The API should return supported assets, limits, and region restrictions - if (quote.error) { - handleApiError(quote.error, request) - } + // Throw errors in priority order based on API response + validateQuoteResponse(quote, request, swapInfo) // 5. Return EdgeSwapQuote return makeSwapPluginQuote({ @@ -229,6 +227,45 @@ const EVM_CHAIN_ID_TO_PLUGIN: Record = { }; ``` +### Error Handling + +Always throw errors based on API response, with proper priority: + +```typescript +function validateQuoteResponse( + quote: ApiQuoteResponse, + request: EdgeSwapRequest, + swapInfo: EdgeSwapInfo +): void { + // Priority 1: Region restrictions + if (quote.regionRestricted) { + throw new SwapPermissionError(swapInfo, "geoRestriction"); + } + + // Priority 2: Asset support + if (!quote.fromAssetSupported || !quote.toAssetSupported) { + throw new SwapCurrencyError(swapInfo, request); + } + + // Priority 3: Amount limits (from API, not hardcoded) + if (quote.belowMinimum) { + throw new SwapBelowLimitError( + swapInfo, + quote.minAmount, // Use API's minimum + quote.limitAsset // 'from' or 'to' + ); + } + + if (quote.aboveMaximum) { + throw new SwapAboveLimitError( + swapInfo, + quote.maxAmount, // Use API's maximum + quote.limitAsset + ); + } +} +``` + ### Rate Limiting Handle API rate limits gracefully: diff --git a/docs/patterns/swap-plugin-architecture.md b/docs/patterns/swap-plugin-architecture.md index bd53d2d6..e188f7f0 100644 --- a/docs/patterns/swap-plugin-architecture.md +++ b/docs/patterns/swap-plugin-architecture.md @@ -306,81 +306,103 @@ async function fetchSwapQuote( ### Error Handling Requirements -Provide specific, actionable error messages: +Always throw errors based on API response with proper priority: ```typescript -// Currency not supported -throw new SwapCurrencyError(swapInfo, fromCode, toCode); +// Priority order for error handling +function validateApiResponse( + apiResponse: any, + request: EdgeSwapRequest, + swapInfo: EdgeSwapInfo +): void { + // 1. Region restrictions (highest priority) + if (apiResponse.geoRestricted || apiResponse.regionError) { + throw new SwapPermissionError(swapInfo, "geoRestriction"); + } -// Amount validation -if (lt(amount, minimum)) { - throw new SwapBelowLimitError(swapInfo, minimum, fromCode); -} -if (gt(amount, maximum)) { - throw new SwapAboveLimitError(swapInfo, maximum, fromCode); + // 2. Asset support + if (!apiResponse.pairSupported || apiResponse.assetError) { + throw new SwapCurrencyError(swapInfo, request); + } + + // 3. Amount limits (use API values, not hardcoded) + if (apiResponse.amountTooLow) { + throw new SwapBelowLimitError( + swapInfo, + apiResponse.minAmount, // From API + apiResponse.limitSide // 'from' or 'to' + ); + } + + if (apiResponse.amountTooHigh) { + throw new SwapAboveLimitError( + swapInfo, + apiResponse.maxAmount, // From API + apiResponse.limitSide + ); + } + + // 4. Other errors (liquidity, network issues, etc.) + if (apiResponse.insufficientLiquidity) { + throw new InsufficientFundsError(swapInfo, request); + } } -// Network or API errors +// Network errors try { const response = await fetch(url); } catch (error) { console.error(`${pluginId} network error:`, error); - // Include request ID if available for support - throw new Error(`Network error: ${error.message}. Request ID: ${requestId}`); -} - -// Insufficient liquidity -if (response.error === "INSUFFICIENT_LIQUIDITY") { - throw new InsufficientLiquidityError(swapInfo, { - fromCode, - toCode, - amount, - }); + throw error; } ``` ## Common Patterns -### Currency Code Validation +### Asset Validation ```typescript -// Check supported currencies -const isFromCurrencySupported = checkInvalidCodes( - fromCodes, - InvalidCurrencyCodes -); -const isToCurrencySupported = checkInvalidCodes(toCodes, InvalidCurrencyCodes); +// Map pluginId/tokenId to exchange symbols +const fromSymbol = mapToExchangeSymbol(request.fromWallet, request.fromTokenId); +const toSymbol = mapToExchangeSymbol(request.toWallet, request.toTokenId); -if (!isFromCurrencySupported || !isToCurrencySupported) { - throw new SwapCurrencyError( - swapInfo, - request.fromCurrencyCode, - request.toCurrencyCode - ); +// Let the API validate supported assets +const apiResponse = await fetchQuote(fromSymbol, toSymbol, amount); + +// Validate based on API response +if (!apiResponse.assetSupported) { + throw new SwapCurrencyError(swapInfo, request); } ``` ### Quote Request Pattern ```typescript -async function fetchSwapQuote(request: EdgeSwapRequest): Promise { - // 1. Validate currencies - checkInvalidCodes(...) +async function fetchSwapQuote( + request: EdgeSwapRequest +): Promise { + // 1. Map Edge identifiers to exchange symbols + const fromSymbol = mapToExchangeSymbol( + request.fromWallet, + request.fromTokenId + ); + const toSymbol = mapToExchangeSymbol(request.toWallet, request.toTokenId); - // 2. Convert request to internal format - const convertedRequest = await convertRequest(request) + // 2. Convert request to exchange API format + const convertedRequest = await convertRequest(request, fromSymbol, toSymbol); - // 3. Fetch quote from API - const apiQuote = await fetchQuoteFromApi(convertedRequest) + // 3. Fetch quote from API (includes validation data) + const apiQuote = await fetchQuoteFromApi(convertedRequest); - // 4. Validate limits - if (lt(amount, min)) throw new SwapBelowLimitError(swapInfo, min) - if (gt(amount, max)) throw new SwapAboveLimitError(swapInfo, max) + // 4. Validate based on API response with proper priority + validateApiResponse(apiQuote, request, swapInfo); // 5. Create and return EdgeSwapQuote return makeSwapPluginQuote({ - // Quote details - }) + request, + swapInfo, + // Quote details from API + }); } ``` From 13b7d31a7b875f692a2d7f7a514a0688ff5653a6 Mon Sep 17 00:00:00 2001 From: Sam Holmes Date: Fri, 22 Aug 2025 13:03:21 -0700 Subject: [PATCH 09/11] docs: Emphasize API-driven validation principle - Add explicit section on API-driven validation - Show BAD vs GOOD examples of hardcoded vs API validation - Update best practices to prioritize API validation - Clarify what exchange APIs should return - Reinforce no hardcoded limits or asset lists --- docs/patterns/swap-plugin-architecture.md | 51 +++++++++++++++++------ 1 file changed, 39 insertions(+), 12 deletions(-) diff --git a/docs/patterns/swap-plugin-architecture.md b/docs/patterns/swap-plugin-architecture.md index e188f7f0..21006ff8 100644 --- a/docs/patterns/swap-plugin-architecture.md +++ b/docs/patterns/swap-plugin-architecture.md @@ -117,6 +117,32 @@ The `pluginId` in `swapInfo` must match the key used in `src/index.ts` for prope ## Plugin Expectations and Requirements +### API-Driven Validation Principle + +**Critical**: All validation must come from your exchange's API response, not hardcoded values: + +```typescript +// ❌ BAD - Hardcoded validation +const MIN_BTC = "0.001"; +const MAX_BTC = "10"; +if (lt(amount, MIN_BTC)) throw new SwapBelowLimitError(swapInfo, MIN_BTC); + +// ✅ GOOD - API-driven validation +const apiResponse = await fetchQuote(request); +if (apiResponse.belowMinimum) { + throw new SwapBelowLimitError(swapInfo, apiResponse.minAmount); +} +``` + +Your exchange API should return: + +- Supported asset pairs (not hardcoded lists) +- Current min/max limits for the specific pair +- Region restrictions for the user's location +- Available liquidity and rates + +## Plugin Expectations and Requirements + ### Chain Support Mapping Plugins must properly map Edge pluginIds to exchange-specific identifiers: @@ -489,15 +515,16 @@ describe("SwapPlugin", () => { ## Best Practices -1. **Always validate currency codes** before making API calls -2. **Use biggystring** for all numeric comparisons -3. **Include detailed metadata** in quotes (order ID, etc.) -4. **Handle rate limiting** with appropriate delays -5. **Log errors with context** for debugging -6. **Test with mainnet and testnet** currency codes -7. **Create actual transactions** for accurate fee estimation -8. **Handle destination tags/memos** for chains that require them -9. **Specify quote type** (fixed vs variable) clearly -10. **Support all quote directions** (from, to, max) -11. **Map chains correctly** using pluginId and chain IDs -12. **Provide specific error messages** with support context +1. **Use API for all validation** - Never hardcode limits, supported assets, or restrictions +2. **Map pluginId/tokenId to symbols** - Always translate Edge identifiers to exchange symbols +3. **Use biggystring** for all numeric comparisons +4. **Include detailed metadata** in quotes (order ID, etc.) +5. **Handle rate limiting** with appropriate delays +6. **Log errors with context** for debugging +7. **Test with mainnet and testnet** assets +8. **Create actual transactions** for accurate fee estimation +9. **Handle destination tags/memos** for chains that require them +10. **Specify quote type** (fixed vs variable) clearly +11. **Support all quote directions** (from, to, max) +12. **Follow error priority** - Region → Asset support → Limits +13. **Provide specific error messages** with support context From b2bd54349f6ab15b700733ef744bc24fcea96a69 Mon Sep 17 00:00:00 2001 From: Sam Holmes Date: Fri, 22 Aug 2025 13:04:26 -0700 Subject: [PATCH 10/11] refactor: Remove remaining currency code references - Replace currency code helpers with exchange symbol mapping - Update error throwing examples to use request object - Fix deprecated currencyCode field in networkFee - Ensure consistent use of pluginId/tokenId terminology --- docs/conventions/typescript-style-guide.md | 7 +++-- docs/patterns/swap-plugin-architecture.md | 34 +++++++--------------- 2 files changed, 15 insertions(+), 26 deletions(-) diff --git a/docs/conventions/typescript-style-guide.md b/docs/conventions/typescript-style-guide.md index c60170ee..8716c62a 100644 --- a/docs/conventions/typescript-style-guide.md +++ b/docs/conventions/typescript-style-guide.md @@ -61,9 +61,10 @@ if (Number(amount) > Number(maxAmount)) throw new SwapAboveLimitError(); ```typescript // ✅ Good -throw new SwapCurrencyError(swapInfo, fromCurrencyCode, toCurrencyCode); -throw new SwapAboveLimitError(swapInfo, max); -throw new SwapBelowLimitError(swapInfo, min); +throw new SwapCurrencyError(swapInfo, request); +throw new SwapAboveLimitError(swapInfo, max, "from"); +throw new SwapBelowLimitError(swapInfo, min, "to"); +throw new SwapPermissionError(swapInfo, "geoRestriction"); // ❌ Bad throw new Error("Invalid currency"); diff --git a/docs/patterns/swap-plugin-architecture.md b/docs/patterns/swap-plugin-architecture.md index 21006ff8..4acf5cb3 100644 --- a/docs/patterns/swap-plugin-architecture.md +++ b/docs/patterns/swap-plugin-architecture.md @@ -199,26 +199,19 @@ if (fromTokenId != null) { // The tokenId format depends on the chain (contract address, asset ID, etc.) } -// Get currency codes from the wallets -// Note: EdgeSwapRequest doesn't have fromCurrencyCode/toCurrencyCode directly -const fromCurrencyCode = getCurrencyCode( - request.fromWallet, - request.fromTokenId -); -const toCurrencyCode = getCurrencyCode(request.toWallet, request.toTokenId); - -// Helper to get currency code from wallet and tokenId -function getCurrencyCode( +// Map Edge identifiers to exchange symbols +const fromSymbol = getExchangeSymbol(request.fromWallet, request.fromTokenId); +const toSymbol = getExchangeSymbol(request.toWallet, request.toTokenId); + +// Helper to map wallet/tokenId to exchange symbol +function getExchangeSymbol( wallet: EdgeCurrencyWallet, tokenId: EdgeTokenId ): string { - if (tokenId == null) { - // Native currency - return wallet.currencyInfo.currencyCode; - } - // Token - look it up in the wallet's token map - const token = wallet.currencyConfig.allTokens[tokenId]; - return token?.currencyCode ?? "UNKNOWN"; + const pluginId = wallet.currencyInfo.pluginId; + + // Your exchange-specific mapping logic + return mapToExchangeSymbol(pluginId, tokenId); } ``` @@ -251,15 +244,10 @@ return { networkFee: { tokenId: networkFee.tokenId, nativeAmount: networkFee.nativeAmount, - currencyCode: getCurrencyCode(fromWallet, networkFee.tokenId), // deprecated but still required + currencyCode: "", // deprecated field, but still required by type }, }; -// For chains with memo support -if (memo != null) { - spendInfo.memos = [{ type: "text", value: memo }]; -} - // Include in quote approval info quote.approveInfo = { ...approveInfo, From 9d78e15d0825786042a833c1d916dbad1462db79 Mon Sep 17 00:00:00 2001 From: Sam Holmes Date: Fri, 22 Aug 2025 14:58:06 -0700 Subject: [PATCH 11/11] fix(docs): replace hallucinated API response examples with real ones - Remove fictional apiResponse properties that don't exist - Add real examples from SideShift, ChangeNow, and LetsExchange - Emphasize that each exchange has unique API response formats - Show actual error handling patterns from existing plugins - Update testing guide to reflect API-specific validation Addresses PR feedback about 'hallucination' in documentation --- docs/guides/adding-new-exchange.md | 10 +- docs/patterns/swap-plugin-architecture.md | 118 ++++++++++++++++------ 2 files changed, 90 insertions(+), 38 deletions(-) diff --git a/docs/guides/adding-new-exchange.md b/docs/guides/adding-new-exchange.md index f5b3a273..9a2488f3 100644 --- a/docs/guides/adding-new-exchange.md +++ b/docs/guides/adding-new-exchange.md @@ -153,11 +153,11 @@ const plugins = { 1. Disable other exchanges in Settings > Exchange Settings 2. Test swaps with your plugin enabled -3. Verify error handling for: - - Unsupported assets (pluginId/tokenId combinations) - - Below/above limits from API - - Region restrictions - - Network errors +3. Verify error handling based on YOUR exchange's specific API: + - How does your API indicate unsupported pairs? + - What format does your API use for min/max limits? + - How does your API handle region restrictions (if any)? + - Test actual network failures and timeout scenarios ### Test Coverage diff --git a/docs/patterns/swap-plugin-architecture.md b/docs/patterns/swap-plugin-architecture.md index 4acf5cb3..d91f67aa 100644 --- a/docs/patterns/swap-plugin-architecture.md +++ b/docs/patterns/swap-plugin-architecture.md @@ -119,7 +119,7 @@ The `pluginId` in `swapInfo` must match the key used in `src/index.ts` for prope ### API-Driven Validation Principle -**Critical**: All validation must come from your exchange's API response, not hardcoded values: +**Critical**: All validation must come from your exchange's API response, not hardcoded values. Each exchange has its own API format: ```typescript // ❌ BAD - Hardcoded validation @@ -127,19 +127,30 @@ const MIN_BTC = "0.001"; const MAX_BTC = "10"; if (lt(amount, MIN_BTC)) throw new SwapBelowLimitError(swapInfo, MIN_BTC); -// ✅ GOOD - API-driven validation -const apiResponse = await fetchQuote(request); -if (apiResponse.belowMinimum) { - throw new SwapBelowLimitError(swapInfo, apiResponse.minAmount); +// ✅ GOOD - Real example from ChangeNow plugin +const marketRangeResponse = await fetch(`/market-info/range/${fromTo}`); +const { minAmount, maxAmount } = await marketRangeResponse.json(); + +if (lt(amount, minAmount)) { + const minNative = await denominationToNative(minAmount); + throw new SwapBelowLimitError(swapInfo, minNative); +} + +// ✅ GOOD - Real example from SideShift plugin +const quoteResponse = await fetch("/quotes", { body: quoteRequest }); +if (quoteResponse.error?.message) { + if (/below-min/i.test(quoteResponse.error.message)) { + throw new SwapBelowLimitError(swapInfo, quoteResponse.min); + } } ``` -Your exchange API should return: +Your exchange API should provide: -- Supported asset pairs (not hardcoded lists) -- Current min/max limits for the specific pair -- Region restrictions for the user's location -- Available liquidity and rates +- Supported asset pairs dynamically (not hardcoded lists) +- Current min/max limits for each specific pair +- Region restrictions or geo-blocking status +- Available liquidity and current rates ## Plugin Expectations and Requirements @@ -323,42 +334,66 @@ async function fetchSwapQuote( Always throw errors based on API response with proper priority: ```typescript -// Priority order for error handling -function validateApiResponse( - apiResponse: any, - request: EdgeSwapRequest, - swapInfo: EdgeSwapInfo +// Each exchange has unique API response formats +// Here are real examples from actual plugins: + +// Example: SideShift checks error message strings +async function handleSideshiftError( + response: { error?: { message: string } }, + swapInfo: EdgeSwapInfo, + request: EdgeSwapRequest ): void { + const errorMsg = response.error?.message || ""; + // 1. Region restrictions (highest priority) - if (apiResponse.geoRestricted || apiResponse.regionError) { + if (/country-blocked/i.test(errorMsg)) { throw new SwapPermissionError(swapInfo, "geoRestriction"); } // 2. Asset support - if (!apiResponse.pairSupported || apiResponse.assetError) { + if (/pair-not-active/i.test(errorMsg)) { + throw new SwapCurrencyError(swapInfo, request); + } + + // 3. Amount limits + if (/below-min/i.test(errorMsg)) { + // SideShift includes min in error response + throw new SwapBelowLimitError(swapInfo, response.min); + } +} + +// Example: ChangeNow uses separate API calls for limits +async function handleChangeNowQuote( + exchangeResponse: any, + swapInfo: EdgeSwapInfo, + request: EdgeSwapRequest +): void { + // Check general errors + if (exchangeResponse.error != null) { throw new SwapCurrencyError(swapInfo, request); } - // 3. Amount limits (use API values, not hardcoded) - if (apiResponse.amountTooLow) { - throw new SwapBelowLimitError( - swapInfo, - apiResponse.minAmount, // From API - apiResponse.limitSide // 'from' or 'to' - ); + // Separate API call for min/max limits + const marketRange = await fetchMarketRange(pair); + if (lt(amount, marketRange.minAmount)) { + const minNative = await denominationToNative(marketRange.minAmount); + throw new SwapBelowLimitError(swapInfo, minNative); } +} - if (apiResponse.amountTooHigh) { - throw new SwapAboveLimitError( - swapInfo, - apiResponse.maxAmount, // From API - apiResponse.limitSide - ); +// Example: LetsExchange uses HTTP status codes +async function handleLetsExchangeResponse( + response: Response, + swapInfo: EdgeSwapInfo, + request: EdgeSwapRequest +): void { + // HTTP 422 = validation error (unsupported pair or amount) + if (response.status === 422) { + throw new SwapCurrencyError(swapInfo, request); } - // 4. Other errors (liquidity, network issues, etc.) - if (apiResponse.insufficientLiquidity) { - throw new InsufficientFundsError(swapInfo, request); + if (!response.ok) { + throw new Error(`LetsExchange error ${response.status}`); } } @@ -371,6 +406,23 @@ try { } ``` +## Important: Exchange API Diversity + +**Every exchange has a unique API response format.** There is no standard structure for error messages, limits, or validation responses. When implementing a new plugin: + +1. **Study your exchange's actual API responses** - Don't assume any standard format +2. **Map exchange-specific responses to Edge error types** - Each exchange requires custom error parsing +3. **Test with real API calls** - Verify error handling with actual API responses +4. **Document exchange-specific quirks** - Help future maintainers understand the API's behavior + +Examples of API diversity: + +- **SideShift**: Returns errors in `error.message` field with descriptive strings +- **ChangeNow**: Uses separate endpoints for quotes vs min/max limits +- **LetsExchange**: Relies on HTTP status codes (422 for validation errors) +- **Godex**: Has its own unique error codes and response structure +- **Exolix**: Different field names and error formats entirely + ## Common Patterns ### Asset Validation