Conversation
| assert( | ||
| apiSecret !== undefined, | ||
| 'Renegade API secret is not specified with env variable API_KEY_RENEGADE_AUTH_API_SECRET', | ||
| ); |
There was a problem hiding this comment.
Bug: Silent Empty Keys Break Authentication Initialization
The Renegade DEX constructor's API key validation checks for undefined, but the configuration defaults missing keys to empty strings. This allows initialization with empty keys, causing authentication failures instead of a clear error about missing credentials.
There was a problem hiding this comment.
Copied from other integrations e.g. bebop.ts, swaap-v2.ts, happy to change if needed
6b5c8c9 to
06e6095
Compare
* renegade: api: add hmac http client * renegade: add rate fetcher * renegade: add levels response class * renegade: add integration * renegade: add renegade definition * renegade: add integration tests * renegade: add e2e tests * renegade: fix getTopPoolsPerToken
…e, protect division by zero
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Autofix Details
Bugbot Autofix prepared fixes for both issues found in the latest run.
- ✅ Fixed: Missing
limitPoolsfiltering ingetPricesVolume- Added limitPools validation to return null when the pool identifier is not in the allowed set.
- ✅ Fixed: Quote cache key lacks ParaSwap side distinction
- Modified getQuoteCacheKey to include ParaSwap side parameter, preventing SELL/BUY quote cache collisions.
Or push these changes by commenting:
@cursor push e9cc5d6156
Preview (e9cc5d6156)
diff --git a/.env.example b/.env.example
--- a/.env.example
+++ b/.env.example
@@ -11,4 +11,6 @@
API_KEY_BEBOP_AUTH_TOKEN=test
API_KEY_HASHFLOW_AUTH_TOKEN=test
API_KEY_SWAAP_V2_AUTH_TOKEN=test
-API_KEY_DEXALOT_AUTH_TOKEN=test
\ No newline at end of file
+API_KEY_DEXALOT_AUTH_TOKEN=test
+API_KEY_RENEGADE_AUTH_API_SECRET=test
+API_KEY_RENEGADE_AUTH_API_KEY=test
\ No newline at end of file
diff --git a/src/config.ts b/src/config.ts
--- a/src/config.ts
+++ b/src/config.ts
@@ -36,6 +36,8 @@
bebopAuthName?: string;
bebopAuthToken?: string;
nativeApiKey?: string;
+ renegadeAuthApiKey?: string;
+ renegadeAuthApiSecret?: string;
forceRpcFallbackDexs: string[];
};
@@ -299,6 +301,8 @@
hashFlowAuthToken: process.env.API_KEY_HASHFLOW_AUTH_TOKEN || '',
swaapV2AuthToken: process.env.API_KEY_SWAAP_V2_AUTH_TOKEN || '',
nativeApiKey: process.env.API_KEY_NATIVE || '',
+ renegadeAuthApiKey: process.env.API_KEY_RENEGADE_AUTH_API_KEY || '',
+ renegadeAuthApiSecret: process.env.API_KEY_RENEGADE_AUTH_API_SECRET || '',
hashFlowDisabledMMs:
process.env[`HASHFLOW_DISABLED_MMS_42161`]?.split(',') || [],
augustusV6Address: '0x6a000f20005980200259b80c5102003040001068',
@@ -411,6 +415,8 @@
hashFlowAuthToken: process.env.API_KEY_HASHFLOW_AUTH_TOKEN || '',
swaapV2AuthToken: process.env.API_KEY_SWAAP_V2_AUTH_TOKEN || '',
nativeApiKey: process.env.API_KEY_NATIVE || '',
+ renegadeAuthApiKey: process.env.API_KEY_RENEGADE_AUTH_API_KEY || '',
+ renegadeAuthApiSecret: process.env.API_KEY_RENEGADE_AUTH_API_SECRET || '',
hashFlowDisabledMMs: [],
augustusV6Address: '0x6a000f20005980200259b80c5102003040001068',
executorsAddresses: {
@@ -565,6 +571,8 @@
bebopAuthName: baseConfig.bebopAuthName,
bebopAuthToken: baseConfig.bebopAuthToken,
nativeApiKey: baseConfig.nativeApiKey,
+ renegadeAuthApiKey: baseConfig.renegadeAuthApiKey,
+ renegadeAuthApiSecret: baseConfig.renegadeAuthApiSecret,
hashFlowDisabledMMs: baseConfig.hashFlowDisabledMMs,
forceRpcFallbackDexs: baseConfig.forceRpcFallbackDexs,
apiKeyTheGraph: process.env.API_KEY_THE_GRAPH || '',
diff --git a/src/dex/index.ts b/src/dex/index.ts
--- a/src/dex/index.ts
+++ b/src/dex/index.ts
@@ -97,6 +97,7 @@
import { Blackhole } from './solidly/forks-override/blackhole';
import { BlackholeCL } from './algebra-integral/forks/blackhole-cl';
import { Cap } from './cap/cap';
+import { Renegade } from './renegade/renegade';
const LegacyDexes = [
CurveV2,
@@ -187,6 +188,7 @@
Blackhole,
BlackholeCL,
Cap,
+ Renegade,
];
export type LegacyDexConstructor = new (dexHelper: IDexHelper) => IDexTxBuilder<
diff --git a/src/dex/renegade/api/auth.ts b/src/dex/renegade/api/auth.ts
new file mode 100644
--- /dev/null
+++ b/src/dex/renegade/api/auth.ts
@@ -1,0 +1,101 @@
+// Authentication helper for Renegade DEX integration.
+// Implements HMAC-SHA256 authentication signing requests over path, sorted x-renegade-* headers, and body.
+
+import { hmac } from '@noble/hashes/hmac';
+import { sha256 } from '@noble/hashes/sha256';
+import { Buffer } from 'buffer';
+import {
+ RENEGADE_HEADER_PREFIX,
+ RENEGADE_API_KEY_HEADER,
+ RENEGADE_AUTH_HEADER,
+ RENEGADE_AUTH_EXPIRATION_HEADER,
+ REQUEST_SIGNATURE_DURATION_MS,
+} from '../constants';
+
+// Generate authentication headers for Renegade API requests.
+// Adds API key header, generates HMAC-SHA256 signature over path + headers + body, and sets expiration timestamp.
+export function generateRenegadeAuthHeaders(
+ path: string,
+ body: string,
+ existingHeaders: Record<string, string>,
+ apiKey: string,
+ apiSecret: string,
+): Record<string, string> {
+ // Clone existing headers to avoid mutation
+ const signedHeaders: Record<string, string> = { ...existingHeaders };
+
+ // Add timestamp and expiry
+ const now = Date.now();
+ const expiry = now + REQUEST_SIGNATURE_DURATION_MS;
+ signedHeaders[RENEGADE_AUTH_EXPIRATION_HEADER] = expiry.toString();
+
+ // Add API key
+ signedHeaders[RENEGADE_API_KEY_HEADER] = apiKey;
+
+ // Decode API secret from base64 to Uint8Array
+ const apiSecretBytes = decodeBase64(apiSecret);
+
+ // Compute the MAC signature using the headers with expiry
+ const signature = computeHmacSignature(
+ path,
+ signedHeaders,
+ body,
+ apiSecretBytes,
+ );
+ signedHeaders[RENEGADE_AUTH_HEADER] = signature;
+
+ // Return new headers object with both auth headers
+ return signedHeaders;
+}
+
+// Compute the HMAC-SHA256 signature for a Renegade API request.
+// Returns base64-encoded HMAC signature without padding.
+function computeHmacSignature(
+ path: string,
+ headers: Record<string, string>,
+ body: string,
+ apiSecret: Uint8Array,
+): string {
+ // Filter and sort x-renegade-* headers (excluding auth header itself)
+ const candidateHeaderEntries = Object.entries(headers);
+ const renegadeHeaderEntries = candidateHeaderEntries
+ .filter(([key]) => key.toLowerCase().startsWith(RENEGADE_HEADER_PREFIX))
+ .filter(
+ ([key]) => key.toLowerCase() !== RENEGADE_AUTH_HEADER.toLowerCase(),
+ );
+
+ // Canonicalize header order (lexicographic by header name, case-insensitive)
+ const canonicalHeaderEntries = renegadeHeaderEntries.sort(([a], [b]) =>
+ a.toLowerCase().localeCompare(b.toLowerCase()),
+ );
+
+ // Prepare crypto primitives
+ const encoder = new TextEncoder();
+ const mac = hmac.create(sha256, apiSecret);
+
+ // Add path to signature
+ mac.update(encoder.encode(path));
+
+ // Add Renegade headers to signature
+ for (const [key, value] of canonicalHeaderEntries) {
+ mac.update(encoder.encode(key));
+ mac.update(encoder.encode(value.toString()));
+ }
+
+ // Add stringified body to signature
+ mac.update(encoder.encode(body));
+
+ // Generate signature and return as base64 without padding
+ const digest = mac.digest();
+ return encodeBase64(digest);
+}
+
+// Decode a base64 string to a Uint8Array.
+function decodeBase64(base64: string): Uint8Array {
+ return new Uint8Array(Buffer.from(base64, 'base64'));
+}
+
+// Encode a Uint8Array to a base64 string without trailing '=' padding.
+function encodeBase64(data: Uint8Array): string {
+ return Buffer.from(data).toString('base64').replace(/=+$/, '');
+}
diff --git a/src/dex/renegade/api/renegade-client.ts b/src/dex/renegade/api/renegade-client.ts
new file mode 100644
--- /dev/null
+++ b/src/dex/renegade/api/renegade-client.ts
@@ -1,0 +1,152 @@
+// RenegadeClient - Client for Renegade match endpoint API
+// Handles authentication and HTTP requests to Renegade's external match endpoint.
+
+import { Network } from '../../../constants';
+import { IDexHelper } from '../../../dex-helper/idex-helper';
+import { Logger } from '../../../types';
+import { generateRenegadeAuthHeaders } from './auth';
+import {
+ buildRenegadeApiUrl,
+ RENEGADE_API_TIMEOUT_MS,
+ RENEGADE_QUOTE_ENDPOINT,
+ RENEGADE_ASSEMBLE_ENDPOINT,
+} from '../constants';
+import {
+ AssembleExternalMatchRequest,
+ ExternalOrder,
+ ExternalQuoteRequest,
+ QuoteQueryParams,
+ SignedExternalQuote,
+ SponsoredMatchResponse,
+ SponsoredQuoteResponse,
+} from './types';
+
+// Default headers for Renegade API requests
+const DEFAULT_RENEGADE_HEADERS: Record<string, string> = {
+ 'content-type': 'application/json',
+ accept: 'application/json;number=string',
+};
+
+// Client for interacting with Renegade match endpoint API
+export class RenegadeClient {
+ private readonly baseUrl: string;
+
+ constructor(
+ private readonly dexHelper: IDexHelper,
+ private readonly network: Network,
+ private readonly apiKey: string,
+ private readonly apiSecret: string,
+ private readonly logger: Logger,
+ ) {
+ this.baseUrl = buildRenegadeApiUrl(this.network);
+ }
+
+ // Request a quote from Renegade API
+ async requestQuote(
+ externalOrder: ExternalOrder,
+ queryParams?: QuoteQueryParams,
+ ): Promise<SponsoredQuoteResponse> {
+ const requestBody: ExternalQuoteRequest = {
+ external_order: externalOrder,
+ };
+
+ const { url: quoteUrl, pathWithQuery } = this.buildUrlWithQueryParams(
+ RENEGADE_QUOTE_ENDPOINT,
+ queryParams,
+ );
+
+ const headers = generateRenegadeAuthHeaders(
+ pathWithQuery,
+ JSON.stringify(requestBody),
+ DEFAULT_RENEGADE_HEADERS,
+ this.apiKey,
+ this.apiSecret,
+ );
+
+ this.logger.debug('Requesting quote from Renegade API', {
+ url: quoteUrl,
+ order: externalOrder,
+ queryParams,
+ });
+
+ const response: SponsoredQuoteResponse =
+ await this.dexHelper.httpRequest.post(
+ quoteUrl,
+ requestBody,
+ RENEGADE_API_TIMEOUT_MS,
+ headers,
+ );
+
+ return response;
+ }
+
+ // Assemble an external match from a signed quote
+ async assembleExternalMatch(
+ signedQuote: SignedExternalQuote,
+ queryParams?: QuoteQueryParams & {
+ updated_order?: ExternalOrder | null;
+ },
+ ): Promise<SponsoredMatchResponse> {
+ const requestBody: AssembleExternalMatchRequest = {
+ signed_quote: signedQuote,
+ };
+
+ if (queryParams?.updated_order !== undefined) {
+ requestBody.updated_order = queryParams.updated_order;
+ }
+
+ const { updated_order, ...urlQueryParams } = queryParams || {};
+ const { url: assembleUrl, pathWithQuery } = this.buildUrlWithQueryParams(
+ RENEGADE_ASSEMBLE_ENDPOINT,
+ urlQueryParams,
+ );
+
+ const headers = generateRenegadeAuthHeaders(
+ pathWithQuery,
+ JSON.stringify(requestBody),
+ DEFAULT_RENEGADE_HEADERS,
+ this.apiKey,
+ this.apiSecret,
+ );
+
+ this.logger.debug('Assembling external match from Renegade API', {
+ url: assembleUrl,
+ queryParams,
+ });
+
+ const response: SponsoredMatchResponse =
+ await this.dexHelper.httpRequest.post(
+ assembleUrl,
+ requestBody,
+ RENEGADE_API_TIMEOUT_MS,
+ headers,
+ );
+
+ this.logger.debug(
+ 'Assembled external match from Renegade API',
+ JSON.stringify(response, null, 2),
+ );
+
+ return response;
+ }
+ // Build URL with query parameters from endpoint path and optional query params
+ private buildUrlWithQueryParams(
+ endpoint: string,
+ queryParams?: QuoteQueryParams,
+ ): { url: string; pathWithQuery: string } {
+ const url = new URL(endpoint, this.baseUrl);
+ if (queryParams) {
+ const searchParams = new URLSearchParams();
+ for (const [key, value] of Object.entries(queryParams)) {
+ if (value !== undefined) {
+ searchParams.append(key, value.toString());
+ }
+ }
+ url.search = searchParams.toString();
+ }
+ return {
+ url: url.toString(),
+ pathWithQuery: url.pathname + url.search,
+ };
+ }
+}
diff --git a/src/dex/renegade/api/types.ts b/src/dex/renegade/api/types.ts
new file mode 100644
--- /dev/null
+++ b/src/dex/renegade/api/types.ts
@@ -1,0 +1,140 @@
+// Request body for assemble external match endpoint
+export type AssembleExternalMatchRequest = {
+ signed_quote: SignedExternalQuote;
+ do_gas_estimation?: boolean;
+ allow_shared?: boolean;
+ matching_pool?: string;
+ relayer_fee_rate?: number;
+ receiver_address?: string | null;
+ updated_order?: ExternalOrder | null;
+};
+
+// External order structure for Renegade match requests
+export type ExternalOrder = {
+ quote_mint: string;
+ base_mint: string;
+ side: 'Buy' | 'Sell';
+ base_amount: string;
+ quote_amount: string;
+ exact_base_output: string;
+ exact_quote_output: string;
+ min_fill_size: string;
+};
+
+// Request body for quote endpoint
+export type ExternalQuoteRequest = {
+ external_order: ExternalOrder;
+ matching_pool?: string;
+ relayer_fee_rate?: number;
+};
+
+// Quote with signature binding it to the relayer
+export type SignedExternalQuote = {
+ quote: ApiExternalQuote;
+ signature: string;
+};
+
+// Response from assemble endpoint
+export type SponsoredMatchResponse = {
+ match_bundle: AtomicMatchApiBundle;
+ is_sponsored: boolean;
+ gas_sponsorship_info?: GasSponsorshipInfo | null;
+};
+
+// Response from quote endpoint with signed quote and optional sponsorship
+export type SponsoredQuoteResponse = {
+ signed_quote: SignedExternalQuote;
+ gas_sponsorship_info?: SignedGasSponsorshipInfo | null;
+};
+
+// Optional query parameters for quote and assembly endpoints
+export type QuoteQueryParams = {
+ disable_gas_sponsorship?: boolean;
+ refund_address?: string;
+ refund_native_eth?: boolean;
+ use_gas_sponsorship?: boolean;
+};
+
+// Represents a token transfer with mint address and amount
+type ApiExternalAssetTransfer = {
+ mint: string;
+ amount: string;
+};
+
+// Match result with quote/base mints, amounts, and direction
+type ApiExternalMatchResult = {
+ quote_mint: string;
+ base_mint: string;
+ quote_amount: string;
+ base_amount: string;
+ direction: 'Buy' | 'Sell';
+};
+
+// Price with timestamp
+type ApiTimestampedPrice = {
+ price: string;
+ timestamp: number;
+};
+
+// Complete match bundle with settlement transaction
+type AtomicMatchApiBundle = {
+ match_result: ApiExternalMatchResult;
+ fees: FeeTake;
+ receive: ApiExternalAssetTransfer;
+ send: ApiExternalAssetTransfer;
+ settlement_tx: TransactionRequest;
+};
+
+// Complete quote structure with order, match result, fees, and pricing
+type ApiExternalQuote = {
+ order: ExternalOrder;
+ match_result: ApiExternalMatchResult;
+ fees: FeeTake;
+ send: ApiExternalAssetTransfer;
+ receive: ApiExternalAssetTransfer;
+ price: ApiTimestampedPrice;
+ timestamp: number;
+};
+
+// Relayer and protocol fees
+type FeeTake = {
+ relayer_fee: string;
+ protocol_fee: string;
+};
+
+// Gas refund details
+type GasSponsorshipInfo = {
+ refund_amount: string;
+ refund_native_eth: boolean;
+ refund_address: string | null;
+};
+
+// Signed gas sponsorship info (deprecated signature field)
+type SignedGasSponsorshipInfo = {
+ gas_sponsorship_info: GasSponsorshipInfo;
+ signature: string; // deprecated
+};
+
+// EIP-1559 / EIP-4844 aware transaction request
+type TransactionRequest = {
+ from?: string | null;
+ to?: string | null;
+ gasPrice?: string;
+ maxFeePerGas?: string;
+ maxPriorityFeePerGas?: string;
+ maxFeePerBlobGas?: string;
+ gas?: string;
+ value?: string;
+ input?: string;
+ data?: string;
+ nonce?: string;
+ chainId?: string;
+ accessList?: Array<{
+ address: string;
+ storageKeys: string[];
+ }>;
+ type?: string;
+ blobVersionedHashes?: string[];
+ sidecar?: Record<string, any>;
+ authorizationList?: Array<Record<string, any>>;
+};
diff --git a/src/dex/renegade/config.ts b/src/dex/renegade/config.ts
new file mode 100644
--- /dev/null
+++ b/src/dex/renegade/config.ts
@@ -1,0 +1,15 @@
+import { DexParams } from './types';
+import { DexConfigMap } from '../../types';
+import { Network } from '../../constants';
+import { RENEGADE_NAME } from './constants';
+
+export const RenegadeConfig: DexConfigMap<DexParams> = {
+ [RENEGADE_NAME]: {
+ [Network.ARBITRUM]: {
+ usdcAddress: '0xaf88d065e77c8cc2239327c5edb3a432268e5831',
+ },
+ [Network.BASE]: {
+ usdcAddress: '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913',
+ },
+ },
+};
diff --git a/src/dex/renegade/constants.ts b/src/dex/renegade/constants.ts
new file mode 100644
--- /dev/null
+++ b/src/dex/renegade/constants.ts
@@ -1,0 +1,57 @@
+// Constants for Renegade DEX integration
+import { Network } from '../../constants';
+
+export const RENEGADE_NAME = 'Renegade';
+
+// Base API URLs for each network
+export const RENEGADE_ARBITRUM_BASE_URL =
+ 'https://arbitrum-one.auth-server.renegade.fi';
+export const RENEGADE_BASE_BASE_URL =
+ 'https://base-mainnet.auth-server.renegade.fi';
+
+export const RENEGADE_LEVELS_ENDPOINT = '/rfqt/v3/levels';
+export const RENEGADE_QUOTE_ENDPOINT = '/v0/matching-engine/quote';
+export const RENEGADE_ASSEMBLE_ENDPOINT =
+ '/v0/matching-engine/assemble-external-match';
+
+// Get Renegade API base URL for a specific network.
+export function buildRenegadeApiUrl(network: Network): string {
+ switch (network) {
+ case Network.ARBITRUM:
+ return RENEGADE_ARBITRUM_BASE_URL;
+ case Network.BASE:
+ return RENEGADE_BASE_BASE_URL;
+ default:
+ throw new Error(`Network ${network} is not supported by Renegade`);
+ }
+}
+
+// Caching constants
+export const RENEGADE_LEVELS_CACHE_TTL_SECONDS = 30;
+export const RENEGADE_LEVELS_POLLING_INTERVAL = 15_000;
+export const RENEGADE_LEVELS_CACHE_KEY = 'renegade_levels';
+
+export const RENEGADE_TOKEN_METADATA_CACHE_TTL_SECONDS = 24 * 60 * 60; // 24 hours
+export const RENEGADE_TOKEN_METADATA_POLLING_INTERVAL = 24 * 60 * 60 * 1000; // 24 hours
+export const RENEGADE_TOKEN_METADATA_CACHE_KEY = 'renegade_token_metadata';
+
+export const RENEGADE_QUOTE_CACHE_TTL_SECONDS = 5;
+export const RENEGADE_QUOTE_CACHE_KEY = 'renegade_quote';
+
+// API timeout settings
+export const RENEGADE_API_TIMEOUT_MS = 10_000;
+export const RENEGADE_INIT_TIMEOUT_MS = 5_000;
+
+// Authentication constants
+export const RENEGADE_HEADER_PREFIX = 'x-renegade';
+export const RENEGADE_API_KEY_HEADER = 'x-renegade-api-key';
+export const RENEGADE_AUTH_HEADER = 'x-renegade-auth';
+export const RENEGADE_AUTH_EXPIRATION_HEADER = 'x-renegade-auth-expiration';
+export const REQUEST_SIGNATURE_DURATION_MS = 10_000;
+
+// Gas cost estimation
+export const RENEGADE_GAS_COST = 3_000_000;
+
+// Token metadata API constants
+export const RENEGADE_TOKEN_MAPPINGS_BASE_URL =
+ 'https://raw.githubusercontent.com/renegade-fi/token-mappings/main/';
diff --git a/src/dex/renegade/rate-fetcher.ts b/src/dex/renegade/rate-fetcher.ts
new file mode 100644
--- /dev/null
+++ b/src/dex/renegade/rate-fetcher.ts
@@ -1,0 +1,190 @@
+import { Network } from '../../constants';
+import { IDexHelper } from '../../dex-helper';
+import { RequestConfig } from '../../dex-helper/irequest-wrapper';
+import { Fetcher, RequestInfo } from '../../lib/fetcher/fetcher';
+import { Logger, Token } from '../../types';
+import { generateRenegadeAuthHeaders } from './api/auth';
+import {
+ buildRenegadeApiUrl,
+ RENEGADE_LEVELS_ENDPOINT,
+ RENEGADE_LEVELS_POLLING_INTERVAL,
+ RENEGADE_TOKEN_MAPPINGS_BASE_URL,
+ RENEGADE_TOKEN_METADATA_POLLING_INTERVAL,
+} from './constants';
+import { RenegadeLevelsResponse } from './renegade-levels-response';
+import {
+ RenegadeDepth,
+ RenegadeRateFetcherConfig,
+ RenegadeTokenRemap,
+} from './types';
+
+// RateFetcher for Renegade DEX integration using the Fetcher class.
+// This implementation uses the standard Fetcher class with HMAC-SHA256 authentication
+// and includes polling functionality for real-time price level updates.
+export class RateFetcher {
+ private levelsFetcher: Fetcher<RenegadeLevelsResponse>;
+ private levelsCacheKey: string;
+ private levelsCacheTTL: number;
+
+ private tokenMetadataFetcher!: Fetcher<RenegadeTokenRemap>;
+ private tokenMetadataCacheKey: string;
+ private tokenMetadataCacheTTL: number;
+
+ constructor(
+ private dexHelper: IDexHelper,
+ private dexKey: string,
+ private network: Network,
+ private logger: Logger,
+ private config: RenegadeRateFetcherConfig,
+ ) {
+ this.levelsCacheKey = config.levelsCacheKey;
+ this.levelsCacheTTL = config.levelsCacheTTL;
+ this.tokenMetadataCacheKey = config.tokenMetadataCacheKey;
+ this.tokenMetadataCacheTTL = config.tokenMetadataCacheTTL;
+ // Build network-specific API URL
+ const baseUrl = buildRenegadeApiUrl(this.network);
+ const url = `${baseUrl}${RENEGADE_LEVELS_ENDPOINT}`;
+
+ // Create authentication function for Renegade API
+ const authenticate = (options: RequestConfig): RequestConfig => {
+ // Convert headers to string format for auth function
+ const stringHeaders: Record<string, string> = {};
+ for (const [key, value] of Object.entries(options.headers ?? {})) {
+ stringHeaders[key] = String(value);
+ }
+
+ // Generate authenticated headers using HMAC-SHA256
+ const authenticatedHeaders = generateRenegadeAuthHeaders(
+ RENEGADE_LEVELS_ENDPOINT,
+ options.data ? JSON.stringify(options.data) : '', // Convert body to string
+ stringHeaders,
+ this.config.apiKey,
+ this.config.apiSecret,
+ );
+
+ options.headers = authenticatedHeaders;
+
+ return options;
+ };
+
+ // Create caster function to validate and transform response
+ const caster = (data: unknown): RenegadeLevelsResponse => {
+ if (typeof data !== 'object' || data === null) {
+ throw new Error('Invalid response format from Renegade API');
+ }
+
+ const response = data as { [pairIdentifier: string]: RenegadeDepth };
+
+ return new RenegadeLevelsResponse(response);
+ };
+
+ // Create request info for the Fetcher
+ const requestInfo: RequestInfo<RenegadeLevelsResponse> = {
+ requestOptions: {
+ url,
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ },
+ caster,
+ authenticate,
+ };
+
+ // Initialize the Fetcher
+ this.levelsFetcher = new Fetcher<RenegadeLevelsResponse>(
+ this.dexHelper.httpRequest,
+ {
+ info: requestInfo,
+ handler: this.handleLevelsResponse.bind(this),
+ },
+ RENEGADE_LEVELS_POLLING_INTERVAL,
+ this.logger,
+ );
+
+ const chainName = this.getChainName();
+ const tokenUrl = `${RENEGADE_TOKEN_MAPPINGS_BASE_URL}${chainName}.json`;
+ const tokenCaster = (data: unknown): RenegadeTokenRemap => {
+ if (typeof data !== 'object' || data === null) {
+ throw new Error('Invalid token metadata response format');
+ }
+ return data as RenegadeTokenRemap;
+ };
+ const tokenRequestInfo: RequestInfo<RenegadeTokenRemap> = {
+ requestOptions: {
+ url: tokenUrl,
+ method: 'GET',
+ },
+ caster: tokenCaster,
+ };
+ this.tokenMetadataFetcher = new Fetcher<RenegadeTokenRemap>(
+ this.dexHelper.httpRequest,
+ {
+ info: tokenRequestInfo,
+ handler: this.handleTokenMetadataResponse.bind(this),
+ },
+ RENEGADE_TOKEN_METADATA_POLLING_INTERVAL,
+ this.logger,
+ );
+ }
+
+ // Get chain name for token mappings URL.
+ private getChainName(): string {
+ switch (this.network) {
+ case Network.ARBITRUM:
+ return 'arbitrum-one';
+ case Network.BASE:
+ return 'base-mainnet';
+ default:
+ throw new Error(
+ `Network ${this.network} is not supported for token metadata`,
+ );
+ }
+ }
+
+ // Handle successful levels response from the Fetcher.
+ private handleLevelsResponse(levelsResponse: RenegadeLevelsResponse): void {
+ const rawData = levelsResponse.getRawData();
+ this.logger.info(`Renegade levels response: ${JSON.stringify(rawData)}`);
+
+ this.dexHelper.cache.setex(
+ this.dexKey,
+ this.network,
+ this.levelsCacheKey,
+ this.levelsCacheTTL,
+ JSON.stringify(rawData),
+ );
+ }
+
+ // Handle successful token metadata response from the Fetcher.
+ private handleTokenMetadataResponse(tokenRemap: RenegadeTokenRemap): void {
+ const tokensMap: Record<string, Token> = {};
+
+ for (const tokenInfo of tokenRemap.tokens) {
+ const address = tokenInfo.address.toLowerCase();
+ tokensMap[address] = {
+ address: tokenInfo.address,
+ decimals: tokenInfo.decimals,
+ symbol: tokenInfo.ticker,
+ };
+ }
+
+ this.dexHelper.cache.setex(
+ this.dexKey,
+ this.network,
+ this.tokenMetadataCacheKey,
+ this.tokenMetadataCacheTTL,
+ JSON.stringify(tokensMap),
+ );
+ }
+
+ start(): void {
+ this.levelsFetcher.startPolling();
+ this.tokenMetadataFetcher.startPolling();
+ }
+
+ stop(): void {
+ this.levelsFetcher.stopPolling();
+ this.tokenMetadataFetcher.stopPolling();
+ }
+}
diff --git a/src/dex/renegade/renegade-e2e.test.ts b/src/dex/renegade/renegade-e2e.test.ts
new file mode 100644
--- /dev/null
+++ b/src/dex/renegade/renegade-e2e.test.ts
@@ -1,0 +1,144 @@
+import dotenv from 'dotenv';
+dotenv.config();
+
+import { testE2E } from '../../../tests/utils-e2e';
+import {
+ Tokens,
+ Holders,
+ NativeTokenSymbols,
+} from '../../../tests/constants-e2e';
+import { Network, ContractMethod, SwapSide } from '../../constants';
+import { StaticJsonRpcProvider } from '@ethersproject/providers';
+import { generateConfig } from '../../config';
+import { RENEGADE_NAME } from './constants';
+
+/** Pause test after `initializePricing` */
+const SLEEP_MS = 1000;
+
+/** Slippage in BPS */
+const SLIPPAGE = 1;
+
+function testForNetwork(
+ network: Network,
+ dexKey: string,
+ quoteSymbol: string,
+ baseSymbol: string,
+ quoteAmount: string,
+ baseAmount: string,
+ nativeTokenAmount: string,
+) {
+ const provider = new StaticJsonRpcProvider(
... diff truncated: showing 800 of 2335 linesRe-throw the original error instead of wrapping in a plain Error, so upstream handlers can still read isBlacklistError, isNoMatchError, and isSlippageError.
Compare lowercased addresses in _sortTokens so mixed-case inputs produce the same canonical pool identifier.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Transient failures (timeouts, 5xx) no longer restrict valid trading pairs. Only 4xx responses — indicating the server understood and rejected the request — trigger pair restriction.

DEX Background
Renegade is the first on-chain, decentralized darkpool. We run a decentralized, private order book off-chain (guaranteeing pre-trade privacy), and settle on-chain via zero-knowledge powered smart contracts (guaranteeing post-trade privacy).
Renegade's external match feature allows users to opt-in to crossing their darkpool orders against external orders -- effectively foregoing post-trade privacy to tap into broader liquidity. This feature allows routers, solvers, etc (in this case ParaSwap) to tap into dark liquidity.
Pricing Logic
getPricesVolumemakes no external API calls. Prices are computed locally from cached market depth levels, polled every 15s byRateFetchervia/v2/markets/depth. The only external call (/v2/external-matches/assemble-match-bundle) occurs inpreProcessTransaction, following the standard RFQ flow.Existing Documentation
Important Contract Addresses
The Renegade API will route orders through one of two contracts:
For the gas sponsorship contract, the relevant method is:
sponsorExternalMatch: executes the swap and refunds the gas cost. Returns the total amount received by the external party.You can find the code here.
For the darkpool contract, the relevant method is:
settleExternalMatch: Settles an external match and handles ERC20 transfers for the swap. Returns the amount received by the external party.With code here.
Test Results
Integration Tests
Other Requirements
Note
Medium Risk
Adds a new exchange path that affects quote generation and transaction preprocessing, plus introduces new authenticated external API calls; issues could surface as bad pricing/call data or increased failure rates for Renegade routes.
Overview
Adds a new Renegade DEX integration (Arbitrum + Base) and registers it in
src/dex/index.ts, enabling quoting from cached Renegade market depth and building on-chain settlement transactions via Renegade’s external-match API.Introduces Renegade-specific API auth (HMAC-signed
x-renegade-*headers), a pollingRateFetcherthat caches market depth + token metadata, and apreProcessTransactionflow that calls/v2/external-matches/assemble-match-bundleto obtain executable calldata (with BUY-path handling to preserve assembledamountIn). Adds new config/env fields for Renegade API credentials and includes integration + E2E tests.Written by Cursor Bugbot for commit 93badbf. This will update automatically on new commits. Configure here.