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