Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .sandbox/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ RUN apt-get update -y \
&& apt-get install -y --no-install-recommends ca-certificates curl \
&& rm -rf /var/lib/apt/lists/*

RUN bun install --global @usherlabs/cex-broker@0.2.4
RUN bun install --global @usherlabs/cex-broker@0.2.5

COPY --chmod=0755 ./.sandbox/entrypoint.sh /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@usherlabs/cex-broker",
"version": "0.2.4",
"version": "0.2.5",
"description": "Unified gRPC API to CEXs by Usher Labs.",
"repository": {
"type": "git",
Expand Down
20 changes: 20 additions & 0 deletions src/schemas/action-payloads.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,25 @@ export const CancelOrderPayloadSchema = z.object({
params: z.preprocess(parseJsonString, stringNumberRecordSchema).default({}),
});

const booleanLikeSchema = z.preprocess((value: unknown) => {
if (typeof value !== "string") {
return value;
}
const normalized = value.trim().toLowerCase();
if (["true", "1", "yes"].includes(normalized)) {
return true;
}
if (["false", "0", "no"].includes(normalized)) {
return false;
}
return value;
}, z.boolean());

export const FetchFeesPayloadSchema = z.object({
includeAllFees: booleanLikeSchema.optional().default(false),
includeFundingFees: booleanLikeSchema.optional(),
});

export type DepositPayload = z.infer<typeof DepositPayloadSchema>;
export type CallPayload = z.infer<typeof CallPayloadSchema>;
export type FetchDepositAddressesPayload = z.infer<
Expand All @@ -76,3 +95,4 @@ export type GetOrderDetailsPayload = z.infer<
typeof GetOrderDetailsPayloadSchema
>;
export type CancelOrderPayload = z.infer<typeof CancelOrderPayloadSchema>;
export type FetchFeesPayload = z.infer<typeof FetchFeesPayloadSchema>;
154 changes: 147 additions & 7 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
CreateOrderPayloadSchema,
DepositPayloadSchema,
FetchDepositAddressesPayloadSchema,
FetchFeesPayloadSchema,
GetOrderDetailsPayloadSchema,
WithdrawPayloadSchema,
} from "./schemas/action-payloads";
Expand Down Expand Up @@ -338,21 +339,160 @@ export function getServer(
null,
);
}
const parsedPayload = parsePayload(
FetchFeesPayloadSchema,
call.request.payload,
);
if (!parsedPayload.success) {
return wrappedCallback(
{
code: grpc.status.INVALID_ARGUMENT,
message: parsedPayload.message,
},
null,
);
}
const includeAllFees =
parsedPayload.data.includeAllFees ||
parsedPayload.data.includeFundingFees === true;
try {
await broker.loadMarkets();
const market = await broker.market(symbol);
const fetchFundingFees = async (currencyCodes: string[]) => {
let fundingFeeSource:
| "fetchDepositWithdrawFees"
| "currencies"
| "unavailable" = "unavailable";
const fundingFeesByCurrency: Record<string, unknown> = {};

if (broker.has.fetchDepositWithdrawFees) {
try {
const feeMap = (await broker.fetchDepositWithdrawFees(
currencyCodes,
)) as unknown as Record<
string,
{
deposit?: unknown;
withdraw?: unknown;
networks?: unknown;
fee?: number;
percentage?: boolean;
}
>;
for (const code of currencyCodes) {
const feeInfo = feeMap[code];
if (!feeInfo) {
continue;
}
const fallbackFee =
feeInfo.fee !== undefined ||
feeInfo.percentage !== undefined
? {
fee: feeInfo.fee ?? null,
percentage: feeInfo.percentage ?? null,
}
: null;
fundingFeesByCurrency[code] = {
deposit: feeInfo.deposit ?? fallbackFee,
withdraw: feeInfo.withdraw ?? fallbackFee,
networks: feeInfo.networks ?? {},
};
}
if (Object.keys(fundingFeesByCurrency).length > 0) {
fundingFeeSource = "fetchDepositWithdrawFees";
}
} catch (error) {
safeLogError(
`Error fetching deposit/withdraw fee map for ${symbol} from ${cex}`,
error,
);
}
}

// Address CodeRabbit's concern: explicit handling for missing fees
const generalFee = broker.fees ?? null;
const feeStatus = broker.fees ? "available" : "unknown";
if (fundingFeeSource === "unavailable") {
try {
const currencies = await broker.fetchCurrencies();
for (const code of currencyCodes) {
const currency = currencies[code];
if (!currency) {
continue;
}
fundingFeesByCurrency[code] = {
deposit: {
enabled: currency.deposit ?? null,
},
withdraw: {
enabled: currency.withdraw ?? null,
fee: currency.fee ?? null,
limits: currency.limits?.withdraw ?? null,
},
networks: currency.networks ?? {},
};
}
if (Object.keys(fundingFeesByCurrency).length > 0) {
fundingFeeSource = "currencies";
}
} catch (error) {
safeLogError(
`Error fetching currency metadata for fees for ${symbol} from ${cex}`,
error,
);
}
}

return { fundingFeeSource, fundingFeesByCurrency };
};

const isMarketSymbol = symbol.includes("/");
if (isMarketSymbol) {
const market = await broker.market(symbol);
const generalFee = broker.fees ?? null;
const feeStatus = broker.fees ? "available" : "unknown";

if (!broker.fees) {
log.warn(`Fee metadata unavailable for ${cex}`, { symbol });
}

if (!includeAllFees) {
return wrappedCallback(null, {
proof: verityProof,
result: JSON.stringify({
feeScope: "market",
generalFee,
feeStatus,
market,
}),
});
}

if (!broker.fees) {
log.warn(`Fee metadata unavailable for ${cex}`, { symbol });
const currencyCodes = Array.from(
new Set([market.base, market.quote]),
);
const { fundingFeeSource, fundingFeesByCurrency } =
await fetchFundingFees(currencyCodes);
return wrappedCallback(null, {
proof: verityProof,
result: JSON.stringify({
feeScope: "market+funding",
generalFee,
feeStatus,
market,
fundingFeeSource,
fundingFeesByCurrency,
}),
});
}

const tokenCode = symbol.toUpperCase();
const { fundingFeeSource, fundingFeesByCurrency } =
await fetchFundingFees([tokenCode]);
return wrappedCallback(null, {
proof: verityProof,
result: JSON.stringify({ generalFee, feeStatus, market }),
result: JSON.stringify({
feeScope: "token",
symbol: tokenCode,
fundingFeeSource,
fundingFeesByCurrency,
}),
});
} catch (error) {
safeLogError(
Expand Down
Loading