Skip to content
Open
Show file tree
Hide file tree
Changes from 7 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