Skip to content

Commit 4e2bd10

Browse files
authored
Merge pull request #29 from usherlabs/develop
include FetchFees for deposit/withdraw by calling FetchFees grpc with…
2 parents 53ed54f + 3d57170 commit 4e2bd10

File tree

4 files changed

+169
-9
lines changed

4 files changed

+169
-9
lines changed

.sandbox/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ RUN apt-get update -y \
66
&& apt-get install -y --no-install-recommends ca-certificates curl \
77
&& rm -rf /var/lib/apt/lists/*
88

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

1111
COPY --chmod=0755 ./.sandbox/entrypoint.sh /entrypoint.sh
1212
ENTRYPOINT ["/entrypoint.sh"]

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@usherlabs/cex-broker",
3-
"version": "0.2.4",
3+
"version": "0.2.5",
44
"description": "Unified gRPC API to CEXs by Usher Labs.",
55
"repository": {
66
"type": "git",

src/schemas/action-payloads.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,25 @@ export const CancelOrderPayloadSchema = z.object({
6565
params: z.preprocess(parseJsonString, stringNumberRecordSchema).default({}),
6666
});
6767

68+
const booleanLikeSchema = z.preprocess((value: unknown) => {
69+
if (typeof value !== "string") {
70+
return value;
71+
}
72+
const normalized = value.trim().toLowerCase();
73+
if (["true", "1", "yes"].includes(normalized)) {
74+
return true;
75+
}
76+
if (["false", "0", "no"].includes(normalized)) {
77+
return false;
78+
}
79+
return value;
80+
}, z.boolean());
81+
82+
export const FetchFeesPayloadSchema = z.object({
83+
includeAllFees: booleanLikeSchema.optional().default(false),
84+
includeFundingFees: booleanLikeSchema.optional(),
85+
});
86+
6887
export type DepositPayload = z.infer<typeof DepositPayloadSchema>;
6988
export type CallPayload = z.infer<typeof CallPayloadSchema>;
7089
export type FetchDepositAddressesPayload = z.infer<
@@ -76,3 +95,4 @@ export type GetOrderDetailsPayload = z.infer<
7695
typeof GetOrderDetailsPayloadSchema
7796
>;
7897
export type CancelOrderPayload = z.infer<typeof CancelOrderPayloadSchema>;
98+
export type FetchFeesPayload = z.infer<typeof FetchFeesPayloadSchema>;

src/server.ts

Lines changed: 147 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
CreateOrderPayloadSchema,
2828
DepositPayloadSchema,
2929
FetchDepositAddressesPayloadSchema,
30+
FetchFeesPayloadSchema,
3031
GetOrderDetailsPayloadSchema,
3132
WithdrawPayloadSchema,
3233
} from "./schemas/action-payloads";
@@ -338,21 +339,160 @@ export function getServer(
338339
null,
339340
);
340341
}
342+
const parsedPayload = parsePayload(
343+
FetchFeesPayloadSchema,
344+
call.request.payload,
345+
);
346+
if (!parsedPayload.success) {
347+
return wrappedCallback(
348+
{
349+
code: grpc.status.INVALID_ARGUMENT,
350+
message: parsedPayload.message,
351+
},
352+
null,
353+
);
354+
}
355+
const includeAllFees =
356+
parsedPayload.data.includeAllFees ||
357+
parsedPayload.data.includeFundingFees === true;
341358
try {
342359
await broker.loadMarkets();
343-
const market = await broker.market(symbol);
360+
const fetchFundingFees = async (currencyCodes: string[]) => {
361+
let fundingFeeSource:
362+
| "fetchDepositWithdrawFees"
363+
| "currencies"
364+
| "unavailable" = "unavailable";
365+
const fundingFeesByCurrency: Record<string, unknown> = {};
366+
367+
if (broker.has.fetchDepositWithdrawFees) {
368+
try {
369+
const feeMap = (await broker.fetchDepositWithdrawFees(
370+
currencyCodes,
371+
)) as unknown as Record<
372+
string,
373+
{
374+
deposit?: unknown;
375+
withdraw?: unknown;
376+
networks?: unknown;
377+
fee?: number;
378+
percentage?: boolean;
379+
}
380+
>;
381+
for (const code of currencyCodes) {
382+
const feeInfo = feeMap[code];
383+
if (!feeInfo) {
384+
continue;
385+
}
386+
const fallbackFee =
387+
feeInfo.fee !== undefined ||
388+
feeInfo.percentage !== undefined
389+
? {
390+
fee: feeInfo.fee ?? null,
391+
percentage: feeInfo.percentage ?? null,
392+
}
393+
: null;
394+
fundingFeesByCurrency[code] = {
395+
deposit: feeInfo.deposit ?? fallbackFee,
396+
withdraw: feeInfo.withdraw ?? fallbackFee,
397+
networks: feeInfo.networks ?? {},
398+
};
399+
}
400+
if (Object.keys(fundingFeesByCurrency).length > 0) {
401+
fundingFeeSource = "fetchDepositWithdrawFees";
402+
}
403+
} catch (error) {
404+
safeLogError(
405+
`Error fetching deposit/withdraw fee map for ${symbol} from ${cex}`,
406+
error,
407+
);
408+
}
409+
}
344410

345-
// Address CodeRabbit's concern: explicit handling for missing fees
346-
const generalFee = broker.fees ?? null;
347-
const feeStatus = broker.fees ? "available" : "unknown";
411+
if (fundingFeeSource === "unavailable") {
412+
try {
413+
const currencies = await broker.fetchCurrencies();
414+
for (const code of currencyCodes) {
415+
const currency = currencies[code];
416+
if (!currency) {
417+
continue;
418+
}
419+
fundingFeesByCurrency[code] = {
420+
deposit: {
421+
enabled: currency.deposit ?? null,
422+
},
423+
withdraw: {
424+
enabled: currency.withdraw ?? null,
425+
fee: currency.fee ?? null,
426+
limits: currency.limits?.withdraw ?? null,
427+
},
428+
networks: currency.networks ?? {},
429+
};
430+
}
431+
if (Object.keys(fundingFeesByCurrency).length > 0) {
432+
fundingFeeSource = "currencies";
433+
}
434+
} catch (error) {
435+
safeLogError(
436+
`Error fetching currency metadata for fees for ${symbol} from ${cex}`,
437+
error,
438+
);
439+
}
440+
}
441+
442+
return { fundingFeeSource, fundingFeesByCurrency };
443+
};
444+
445+
const isMarketSymbol = symbol.includes("/");
446+
if (isMarketSymbol) {
447+
const market = await broker.market(symbol);
448+
const generalFee = broker.fees ?? null;
449+
const feeStatus = broker.fees ? "available" : "unknown";
450+
451+
if (!broker.fees) {
452+
log.warn(`Fee metadata unavailable for ${cex}`, { symbol });
453+
}
454+
455+
if (!includeAllFees) {
456+
return wrappedCallback(null, {
457+
proof: verityProof,
458+
result: JSON.stringify({
459+
feeScope: "market",
460+
generalFee,
461+
feeStatus,
462+
market,
463+
}),
464+
});
465+
}
348466

349-
if (!broker.fees) {
350-
log.warn(`Fee metadata unavailable for ${cex}`, { symbol });
467+
const currencyCodes = Array.from(
468+
new Set([market.base, market.quote]),
469+
);
470+
const { fundingFeeSource, fundingFeesByCurrency } =
471+
await fetchFundingFees(currencyCodes);
472+
return wrappedCallback(null, {
473+
proof: verityProof,
474+
result: JSON.stringify({
475+
feeScope: "market+funding",
476+
generalFee,
477+
feeStatus,
478+
market,
479+
fundingFeeSource,
480+
fundingFeesByCurrency,
481+
}),
482+
});
351483
}
352484

485+
const tokenCode = symbol.toUpperCase();
486+
const { fundingFeeSource, fundingFeesByCurrency } =
487+
await fetchFundingFees([tokenCode]);
353488
return wrappedCallback(null, {
354489
proof: verityProof,
355-
result: JSON.stringify({ generalFee, feeStatus, market }),
490+
result: JSON.stringify({
491+
feeScope: "token",
492+
symbol: tokenCode,
493+
fundingFeeSource,
494+
fundingFeesByCurrency,
495+
}),
356496
});
357497
} catch (error) {
358498
safeLogError(

0 commit comments

Comments
 (0)