Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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(/=+$/, '');
}
152 changes: 152 additions & 0 deletions src/dex/renegade/api/renegade-client.ts
Original file line number Diff line number Diff line change
@@ -0,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,
};
}
}
Loading