Skip to content
15 changes: 15 additions & 0 deletions api/_cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,21 @@
}
await this.client.del(key);
}

async getAll<T>(prefix: string): Promise<T[] | null> {
if (!this.client) {
return null;
}

const [_, values] = await this.client.scan(0, {

Check warning on line 82 in api/_cache.ts

View workflow job for this annotation

GitHub Actions / format-and-lint

'_' is assigned a value but never used
match: prefix,
});
if (values.length === 0 || values == null) {
return null;
}

return (await this.client.mget(values)) as T[];
}
}

export const redisCache = new RedisCache();
Expand Down
32 changes: 32 additions & 0 deletions api/relayer-config-list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { VercelResponse } from "@vercel/node";
import { getLogger, handleErrorCondition } from "./_utils";
import { TypedVercelRequest } from "./_types";
import { RelayerConfigCacheEntry } from "./relayer/_types";
import { redisCache } from "./_cache";

const handler = async (
request: TypedVercelRequest<Record<string, never>>,
response: VercelResponse
) => {
const logger = getLogger();
logger.debug({
at: "RelayerConfig",
message: "Body data",
body: request.body,
});
try {
const relayerConfigs =
await redisCache.getAll<RelayerConfigCacheEntry>("relayer-config*");

logger.debug({
at: "RelayerConfigList",
message: "Response data",
responseJson: relayerConfigs,
});
response.status(200).json(relayerConfigs);
} catch (error: unknown) {
return handleErrorCondition("relayer-config", response, logger, error);
}
};

export default handler;
55 changes: 55 additions & 0 deletions api/relayer-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { VercelResponse } from "@vercel/node";
import { assert } from "superstruct";
import { getLogger, handleErrorCondition } from "./_utils";
import { TypedVercelRequest } from "./_types";
import { RelayerConfig, RelayerConfigSchema } from "./relayer/_types";
import { buildCacheKey, redisCache } from "./_cache";

const handler = async (
request: TypedVercelRequest<RelayerConfig>,
response: VercelResponse
) => {
const logger = getLogger();
logger.debug({
at: "RelayerConfig",
message: "Body data",
body: request.body,
});
try {
const { body, method } = request;

if (method !== "POST") {
return handleErrorCondition(
"relayer-config",
response,
logger,
new Error("Method not allowed")
);
}

assert(body, RelayerConfigSchema);

// TODO: validate authentication

const relayerConfig: any = body;
relayerConfig.updatedAt = new Date().getTime();

const cacheKey = buildCacheKey(
"relayer-config",
body.authentication.address
);
await redisCache.set(cacheKey, relayerConfig, 60 * 60 * 24 * 2);
const storedConfig = await redisCache.get(cacheKey);

logger.debug({
at: "RelayerConfig",
message: "Response data",
responseJson: storedConfig,
});
response.status(201).json(storedConfig);
} catch (error: unknown) {
return handleErrorCondition("relayer-config", response, logger, error);
}
};

export default handler;
77 changes: 77 additions & 0 deletions api/relayer/_types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import {
Infer,
assign,
number,
object,
type,
record,
string,
boolean,
optional,
unknown,
array,
} from "superstruct";
import { validAddress, positiveIntStr } from "../_utils";

export const OrderbookQueryParamsSchema = type({
originChainId: positiveIntStr(),
destinationChainId: positiveIntStr(),
originToken: validAddress(),
destinationToken: validAddress(),
});

export const OrderbookResponseSchema = record(
validAddress(),
array(
object({
amount: number(),
spread: number(),
})
)
);

export const RelayerConfigSchema = type({
prices: record(
string(),
object({
origin: record(
string(),
record(validAddress(), record(positiveIntStr(), number()))
),
destination: record(
string(),
record(validAddress(), record(positiveIntStr(), number()))
),
messageExecution: boolean(),
})
),
minExclusivityPeriods: object({
default: number(),
routes: optional(record(string(), record(string(), number()))),
origin: optional(record(string(), record(string(), number()))),
destination: optional(record(string(), record(string(), number()))),
sizes: optional(record(string(), number())),
}),
authentication: object({
address: validAddress(),
method: optional(string()),
payload: optional(record(string(), unknown())),
}),
});

export const RelayerConfigCacheEntrySchema = assign(
RelayerConfigSchema,
object({
updatedAt: number(),
})
);

export type OrderbookQueryParams = Infer<typeof OrderbookQueryParamsSchema>;

export type OrderbookResponse = Infer<typeof OrderbookResponseSchema>;

export type RelayerConfig = Infer<typeof RelayerConfigSchema>;

export type RelayerConfigCacheEntry = Infer<
typeof RelayerConfigCacheEntrySchema
>;
8 changes: 8 additions & 0 deletions api/relayer/_utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export function getBaseCurrency(token: string): string | null {
if (token === "USDC" || token === "USDT") {
return "usd";
} else if (token === "WETH") {
return "eth";
}
return null;
}
148 changes: 148 additions & 0 deletions api/relayer/orderbook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import { getTokenByAddress } from "../_utils";
import { ApiHandler } from "../_base/api-handler";
import { VercelAdapter } from "../_adapters/vercel-adapter";
import { redisCache } from "../_cache";
import {
OrderbookQueryParams,
OrderbookResponse,
OrderbookQueryParamsSchema,
OrderbookResponseSchema,
RelayerConfig,
} from "./_types";
import { getBaseCurrency } from "./_utils";

class OrderbookHandler extends ApiHandler<
OrderbookQueryParams,
OrderbookResponse
> {
constructor() {
super({
name: "Orderbook",
requestSchema: OrderbookQueryParamsSchema,
responseSchema: OrderbookResponseSchema,
headers: {
"Cache-Control": "s-maxage=1, stale-while-revalidate=1",
},
});
}

protected async process(
request: OrderbookQueryParams
): Promise<OrderbookResponse> {
const {
originChainId: _originChainId,
destinationChainId: _destinationChainId,
originToken,
destinationToken,
} = request;

const originChainId = Number(_originChainId);
const destinationChainId = Number(_destinationChainId);

const relayerConfigs =
await redisCache.getAll<RelayerConfig>("relayer-config*");

if (!relayerConfigs) {
throw new Error("No relayer configs found");
}

const originTokenInfo = getTokenByAddress(originToken, originChainId);
const destinationTokenInfo = getTokenByAddress(
destinationToken,
destinationChainId
);

if (!originTokenInfo || !destinationTokenInfo) {
throw new Error("Token information not found for provided addresses");
}

const originBaseCurrency = getBaseCurrency(originTokenInfo.symbol);
const destinationBaseCurrency = getBaseCurrency(
destinationTokenInfo.symbol
);

if (!originBaseCurrency || !destinationBaseCurrency) {
throw new Error("Base currency not found for provided tokens");
}

const orderbooks: OrderbookResponse = {};

for (const relayerConfig of relayerConfigs) {
const originChainPrices = relayerConfig.prices[originChainId.toString()];
const destinationChainPrices =
relayerConfig.prices[destinationChainId.toString()];

if (!originChainPrices || !destinationChainPrices) {
continue;
}

const originTokenPrices =
originChainPrices.origin?.[originBaseCurrency]?.[
originTokenInfo.addresses[originChainId]
];
const destinationTokenPrices =
destinationChainPrices.destination?.[destinationBaseCurrency]?.[
destinationTokenInfo.addresses[destinationChainId]
];

if (!originTokenPrices || !destinationTokenPrices) {
continue;
}

const originAmounts = Object.entries(originTokenPrices).sort(
([, a], [, b]) => b - a
);
const destinationAmounts = Object.entries(destinationTokenPrices).sort(
([, a], [, b]) => a - b
);

const orderbook = [];
let destinationIndex = 0;
let remainingDestinationAmount = 0;

for (const [originAmount, originPrice] of originAmounts) {
let remainingOriginAmount = Number(originAmount);

while (
remainingOriginAmount > 0 &&
destinationIndex < destinationAmounts.length
) {
const [destAmount, destPrice] = destinationAmounts[destinationIndex];
const availableDestAmount =
Number(destAmount) - remainingDestinationAmount;

if (availableDestAmount > 0) {
const matchAmount = Math.min(
remainingOriginAmount,
availableDestAmount
);
orderbook.push({
amount: matchAmount,
spread: destPrice - originPrice,
});

remainingOriginAmount -= matchAmount;
remainingDestinationAmount += matchAmount;

if (remainingDestinationAmount >= Number(destAmount)) {
destinationIndex++;
remainingDestinationAmount = 0;
}
} else {
destinationIndex++;
remainingDestinationAmount = 0;
}
}
}

orderbook.sort((a, b) => a.spread - b.spread);
orderbooks[relayerConfig.authentication.address] = orderbook;
}

return orderbooks;
}
}

const handler = new OrderbookHandler();
const adapter = new VercelAdapter<OrderbookQueryParams, OrderbookResponse>();
export default adapter.adaptHandler(handler);
5 changes: 5 additions & 0 deletions src/Routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ const Staking = lazyWithRetry(
() => import(/* webpackChunkName: "RewardStaking" */ "./views/Staking")
);
const DepositStatus = lazyWithRetry(() => import("./views/DepositStatus"));
const RelayerConfigs = lazyWithRetry(
() =>
import(/* webpackChunkName: "RelayerConfigs" */ "./views/RelayerConfigs")
);

const warningMessage = `
We noticed that you have connected from a contract address.
Expand Down Expand Up @@ -201,6 +205,7 @@ const Routes: React.FC = () => {
)),
]
)}
<Route exact path="/relayer-configs" component={RelayerConfigs} />
<Route
path="*"
render={() => <NotFound custom404Message="page not found" />}
Expand Down
2 changes: 2 additions & 0 deletions src/components/Text/Text.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,11 @@ type TextProps = {
weight?: number;
color?: TextColor;
casing?: TextCasing;
monospace?: boolean;
};

export const Text = styled.div<TextProps>`
font-family: ${({ monospace }) => (monospace ? "monospace" : "inherit")};
font-style: normal;
font-weight: ${({ weight = 400 }) => weight};
color: ${({ color = "white-88" }) => COLORS[color]};
Expand Down
4 changes: 4 additions & 0 deletions src/utils/serverless-api/mocked/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { poolsApiCall } from "./pools.mocked";
import { swapQuoteApiCall } from "./swap-quote";
import { poolsUserApiCall } from "./pools-user.mocked";
import { swapApprovalApiCall } from "../prod/swap-approval";
import { orderBookApiCall } from "../prod/order-book";
import { relayerConfigsApiCall } from "../prod/relayer-configs";

export const mockedEndpoints: ServerlessAPIEndpoints = {
coingecko: coingeckoMockedApiCall,
Expand All @@ -29,4 +31,6 @@ export const mockedEndpoints: ServerlessAPIEndpoints = {
poolsUser: poolsUserApiCall,
swapQuote: swapQuoteApiCall,
swapApproval: swapApprovalApiCall,
orderBook: orderBookApiCall,
relayerConfigs: relayerConfigsApiCall,
};
Loading
Loading