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..8716c62a --- /dev/null +++ b/docs/conventions/typescript-style-guide.md @@ -0,0 +1,143 @@ +# 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, request); +throw new SwapAboveLimitError(swapInfo, max, "from"); +throw new SwapBelowLimitError(swapInfo, min, "to"); +throw new SwapPermissionError(swapInfo, "geoRestriction"); + +// ❌ 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..9a2488f3 --- /dev/null +++ b/docs/guides/adding-new-exchange.md @@ -0,0 +1,306 @@ +# 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 + +### Option 1: Debug Server (Recommended for Development) + +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. 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. + +## 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. 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, fromSymbol, toSymbol) + + // 3. Call your exchange API for validation and quote + const quote = await fetchQuoteFromApi(apiRequest) + + // 4. Validate based on API response (not hardcoded values) + // Throw errors in priority order based on API response + validateQuoteResponse(quote, request, swapInfo) + + // 5. Return EdgeSwapQuote + return makeSwapPluginQuote({ + request, + swapInfo, + // Your quote details from API + }) +} +``` + +### 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 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 + +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 + +### PluginId/TokenId to Symbol Mapping + +Map Edge's pluginId and tokenId to your exchange's symbols: + +```typescript +// 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 +}; +``` + +### 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: + +```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. **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 + +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..d91f67aa --- /dev/null +++ b/docs/patterns/swap-plugin-architecture.md @@ -0,0 +1,570 @@ +# 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 and how they integrate with edge-core-js. + +## Plugin System Fundamentals + +### 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 +// 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, // 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 that identifies the plugin: + +```typescript +export const swapInfo: EdgeSwapInfo = { + 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/`) + +- 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 + +## Plugin Expectations and Requirements + +### API-Driven Validation Principle + +**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 +const MIN_BTC = "0.001"; +const MAX_BTC = "10"; +if (lt(amount, MIN_BTC)) throw new SwapBelowLimitError(swapInfo, MIN_BTC); + +// ✅ 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 provide: + +- 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 + +### Chain Support Mapping + +Plugins must properly map Edge pluginIds to exchange-specific identifiers: + +```typescript +// Map Edge pluginId to your exchange's chain identifiers +const PLUGIN_ID_MAP: Record = { + bitcoin: "btc", + ethereum: "eth", + binancesmartchain: "bsc", + avalanche: "avax", + // Add all supported chains +}; + +// 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 + +The `EdgeSwapRequest` provides all necessary information: + +```typescript +interface EdgeSwapRequest { + fromWallet: EdgeCurrencyWallet; // Source wallet with currencyInfo and currencyConfig + toWallet: EdgeCurrencyWallet; // Destination wallet + 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 +} + +// 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; + +// Token handling +const fromTokenId = request.fromTokenId; // null for native/mainnet currency (ETH, BTC, etc.) +const toTokenId = request.toTokenId; // string identifier for tokens + +// 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.) +} + +// 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 { + const pluginId = wallet.currencyInfo.pluginId; + + // Your exchange-specific mapping logic + return mapToExchangeSymbol(pluginId, tokenId); +} +``` + +### 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.networkFees[0]; // networkFees is an array of EdgeTxAmount + +// 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.networkFees[0]; // networkFees is an array of EdgeTxAmount + +// Return fee in the quote +return { + ...quote, + networkFee: { + tokenId: networkFee.tokenId, + nativeAmount: networkFee.nativeAmount, + currencyCode: "", // deprecated field, but still required by type + }, +}; + +// Include in quote approval info +quote.approveInfo = { + ...approveInfo, + customFee: { + nativeAmount: networkFee, + }, +}; +``` + +### Quote Types: Fixed vs Variable + +Specify quote behavior clearly: + +```typescript +// EdgeSwapQuote properties for quote types +interface EdgeSwapQuote { + isEstimate: boolean; // true for variable rates, false for fixed + expirationDate?: Date; // When this quote expires + canBePartial?: boolean; // Can fulfill partially + maxFulfillmentSeconds?: number; // Max time to complete + minReceiveAmount?: string; // Worst-case receive amount + + fromNativeAmount: string; // Input amount + toNativeAmount: string; // Output amount (estimated or guaranteed) +} + +// 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 + +Always throw errors based on API response with proper priority: + +```typescript +// 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 (/country-blocked/i.test(errorMsg)) { + throw new SwapPermissionError(swapInfo, "geoRestriction"); + } + + // 2. Asset support + 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); + } + + // 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); + } +} + +// 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); + } + + if (!response.ok) { + throw new Error(`LetsExchange error ${response.status}`); + } +} + +// Network errors +try { + const response = await fetch(url); +} catch (error) { + console.error(`${pluginId} network error:`, error); + throw error; +} +``` + +## 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 + +```typescript +// Map pluginId/tokenId to exchange symbols +const fromSymbol = mapToExchangeSymbol(request.fromWallet, request.fromTokenId); +const toSymbol = mapToExchangeSymbol(request.toWallet, request.toTokenId); + +// 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. Map Edge identifiers to exchange symbols + const fromSymbol = mapToExchangeSymbol( + request.fromWallet, + request.fromTokenId + ); + const toSymbol = mapToExchangeSymbol(request.toWallet, request.toTokenId); + + // 2. Convert request to exchange API format + const convertedRequest = await convertRequest(request, fromSymbol, toSymbol); + + // 3. Fetch quote from API (includes validation data) + const apiQuote = await fetchQuoteFromApi(convertedRequest); + + // 4. Validate based on API response with proper priority + validateApiResponse(apiQuote, request, swapInfo); + + // 5. Create and return EdgeSwapQuote + return makeSwapPluginQuote({ + request, + swapInfo, + // Quote details from API + }); +} +``` + +### 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. **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 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