Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,6 @@ API_KEY_BEBOP_AUTH_NAME=paraswap
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
API_KEY_DEXALOT_AUTH_TOKEN=test
API_KEY_RENEGADE_AUTH_API_SECRET=test
API_KEY_RENEGADE_AUTH_API_KEY=test
8 changes: 8 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ type BaseConfig = {
bebopAuthName?: string;
bebopAuthToken?: string;
nativeApiKey?: string;
renegadeAuthApiKey?: string;
renegadeAuthApiSecret?: string;
forceRpcFallbackDexs: string[];
};

Expand Down Expand Up @@ -299,6 +301,8 @@ const baseConfigs: { [network: number]: BaseConfig } = {
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',
Expand Down Expand Up @@ -411,6 +415,8 @@ const baseConfigs: { [network: number]: BaseConfig } = {
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: {
Expand Down Expand Up @@ -565,6 +571,8 @@ export function generateConfig(network: number): Config {
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 || '',
Expand Down
2 changes: 2 additions & 0 deletions src/dex/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ import { UsdcTransmuter } from './usdc-transmuter/usdc-transmuter';
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,
Expand Down Expand Up @@ -187,6 +188,7 @@ const Dexes = [
Blackhole,
BlackholeCL,
Cap,
Renegade,
];

export type LegacyDexConstructor = new (dexHelper: IDexHelper) => IDexTxBuilder<
Expand Down
101 changes: 101 additions & 0 deletions src/dex/renegade/api/auth.ts
Original file line number Diff line number Diff line change
@@ -0,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(/=+$/, '');
}
92 changes: 92 additions & 0 deletions src/dex/renegade/api/renegade-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
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_ASSEMBLE_ENDPOINT,
} from '../constants';
import {
AssembleExternalMatchRequest,
ExternalOrder,
SponsoredMatchResponse,
} from './types';

const DEFAULT_RENEGADE_HEADERS: Record<string, string> = {
'content-type': 'application/json',
accept: 'application/json',
};

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);
}

async requestExternalMatch(
externalOrder: ExternalOrder,
): Promise<SponsoredMatchResponse> {
const requestBody: AssembleExternalMatchRequest = {
do_gas_estimation: false,
order: {
type: 'direct-order',
external_order: externalOrder,
},
};

const { url: assembleUrl, path } = this.buildUrl(
RENEGADE_ASSEMBLE_ENDPOINT,
);

const headers = generateRenegadeAuthHeaders(
path,
JSON.stringify(requestBody),
DEFAULT_RENEGADE_HEADERS,
this.apiKey,
this.apiSecret,
);

try {
const response = await this.dexHelper.httpRequest.request({
url: assembleUrl,
method: 'POST',
data: requestBody,
timeout: RENEGADE_API_TIMEOUT_MS,
headers,
});

if (response.status === 204 || !response.data) {
const err: any = new Error('No match available');
err.isNoMatchError = true;
throw err;
}

return response.data;
} catch (e: any) {
if (e.isNoMatchError) throw e;
this.logger.error('Renegade direct assemble request failed', {
url: assembleUrl,
requestBody,
status: e?.response?.status,
responseData: e?.response?.data,
});
throw e;
}
}

private buildUrl(endpoint: string): { url: string; path: string } {
const url = new URL(endpoint, this.baseUrl);
return {
url: url.toString(),
path: url.pathname,
};
}
}
47 changes: 47 additions & 0 deletions src/dex/renegade/api/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
export type ExternalOrder = {
input_mint: string;
output_mint: string;
input_amount: string;
output_amount: string;
use_exact_output_amount: boolean;
min_fill_size: string;
};

type AssetTransfer = {
mint: string;
amount: string;
};

type DirectOrderAssembly = {
type: 'direct-order';
external_order: ExternalOrder;
};

export type AssembleExternalMatchRequest = {
do_gas_estimation?: boolean;
order: DirectOrderAssembly;
};

export type SponsoredMatchResponse = {
match_bundle: {
min_receive: AssetTransfer;
max_receive: AssetTransfer;
min_send: AssetTransfer;
max_send: AssetTransfer;
deadline: number | string;
settlement_tx: SettlementTxResponse;
};
input_amount?: string | null;
gas_sponsorship_info?: {
refund_amount: string;
refund_native_eth: boolean;
refund_address: string | null;
} | null;
};

type SettlementTxResponse = {
to: string;
data?: string;
input?: string;
value?: string;
};
15 changes: 15 additions & 0 deletions src/dex/renegade/config.ts
Original file line number Diff line number Diff line change
@@ -0,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',
},
},
};
55 changes: 55 additions & 0 deletions src/dex/renegade/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Constants for Renegade DEX integration
import { Network } from '../../constants';

export const RENEGADE_NAME = 'Renegade';

// Renegade API hosts.
export const RENEGADE_ARBITRUM_API_BASE_URL =
'https://arbitrum-one.v2.auth-server.renegade.fi';
export const RENEGADE_BASE_API_BASE_URL =
'https://base-mainnet.v2.auth-server.renegade.fi';

export const RENEGADE_MARKETS_DEPTH_ENDPOINT = '/v2/markets/depth';
export const RENEGADE_ASSEMBLE_ENDPOINT =
'/v2/external-matches/assemble-match-bundle';

export function buildRenegadeApiUrl(network: Network): string {
switch (network) {
case Network.ARBITRUM:
return RENEGADE_ARBITRUM_API_BASE_URL;
case Network.BASE:
return RENEGADE_BASE_API_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';

// API timeout settings
export const RENEGADE_API_TIMEOUT_MS = 10_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;

// Calldata / selector constants
export const RENEGADE_SETTLEMENT_BUNDLE_DATA_WORDS = 33;
export const RENEGADE_SETTLE_EXTERNAL_MATCH_AMOUNT_IN_POS = 4;

// Token metadata API constants
export const RENEGADE_TOKEN_MAPPINGS_BASE_URL =
'https://raw.githubusercontent.com/renegade-fi/token-mappings/main/';
Loading