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
4 changes: 3 additions & 1 deletion src/adapters/handleGraphQL/resolvers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
ExploreStatsService,
TransactionStatsResponse,
} from "../../services/ExploreStatsService";
import { JuiceGatewayService } from "../../services/JuiceGatewayService";
import { getChainName } from "../../config/chains";

let quoteHandler: any;
Expand All @@ -20,8 +21,9 @@ export function initializeResolvers(
routerService: any,
logger: Logger,
exploreStats?: ExploreStatsService,
juiceGatewayService?: JuiceGatewayService,
) {
quoteHandler = createQuoteHandler(routerService, logger);
quoteHandler = createQuoteHandler(routerService, logger, juiceGatewayService);
swapsHandler = createSwapsHandler(routerService, logger);
exploreStatsService = exploreStats ?? null;
}
Expand Down
6 changes: 5 additions & 1 deletion src/cache/quoteCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,11 @@ export class QuoteCache {
constructor(logger?: Logger) {
this.logger = logger;
// Start periodic cleanup
setInterval(() => this.cleanup(), this.CLEANUP_INTERVAL);
const cleanupTimer = setInterval(
() => this.cleanup(),
this.CLEANUP_INTERVAL,
);
cleanupTimer.unref();
if (this.logger) {
this.logger.debug(
"[QuoteCache] Initialized with TTL: 30s default, 60s for Citrea",
Expand Down
243 changes: 243 additions & 0 deletions src/endpoints/__tests__/gatewayDepositDisabled.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
import { ChainId } from "@juiceswapxyz/sdk-core";
import Logger from "bunyan";
import { quoteCache } from "../../cache/quoteCache";
import { JuiceGatewayService } from "../../services/JuiceGatewayService";
import { getChainContracts } from "../../config/contracts";
import {
initializeResolvers,
resolvers,
} from "../../adapters/handleGraphQL/resolvers";
import { createLpCreateHandler } from "../lpCreate";
import { createLpIncreaseHandler } from "../lpIncrease";
import { createQuoteHandler, Routing } from "../quote";
import { createSwapHandler } from "../swap";

jest.mock("../../services/LaunchpadTokenService", () => ({
isGraduatedLaunchpadToken: jest.fn().mockResolvedValue(false),
}));
jest.mock("../../services/userTracking", () => ({
trackUser: jest.fn(),
}));

function createMockLogger(): Logger {
const logger = {
child: jest.fn(),
info: jest.fn(),
debug: jest.fn(),
warn: jest.fn(),
error: jest.fn(),
};
logger.child.mockReturnValue(logger);
return logger as unknown as Logger;
}

function createMockResponse() {
return {
status: jest.fn().mockReturnThis(),
json: jest.fn().mockReturnThis(),
setHeader: jest.fn().mockReturnThis(),
};
}

describe("Gateway deposit disabled endpoint gating", () => {
const chainId = ChainId.CITREA_MAINNET;
const contracts = getChainContracts(chainId)!;
const savingsAddress = "0x0000000000000000000000000000000000000001";
let logger: Logger;
let juiceGatewayService: JuiceGatewayService;

beforeEach(() => {
quoteCache.clear();
logger = createMockLogger();
juiceGatewayService = new JuiceGatewayService(new Map(), logger);
juiceGatewayService.setSavingsRateOverride(chainId, 0, savingsAddress);
});

afterEach(() => {
quoteCache.clear();
});

it("returns GATEWAY_DEPOSIT_DISABLED before serving a stale cached quote", async () => {
const body = {
tokenIn: contracts.JUSD,
tokenOut: contracts.WCBTC,
tokenInChainId: chainId,
tokenOutChainId: chainId,
tokenInDecimals: 18,
tokenOutDecimals: 18,
amount: "1000000000000000000",
type: "EXACT_INPUT",
swapper: "0x000000000000000000000000000000000000dead",
};
quoteCache.set(body, {
requestId: "cached",
routing: Routing.GATEWAY_JUSD,
permitData: null,
quote: { quote: "1" },
});
const routerService = {
isChainSupported: jest.fn().mockReturnValue(true),
getQuote: jest.fn(),
};
const handler = createQuoteHandler(
routerService as any,
logger,
juiceGatewayService,
);
const res = createMockResponse();

await handler({ body, headers: {} } as any, res as any);

expect(res.status).toHaveBeenCalledWith(404);
expect(res.json).toHaveBeenCalledWith(
expect.objectContaining({ error: "GATEWAY_DEPOSIT_DISABLED" }),
);
expect(routerService.getQuote).not.toHaveBeenCalled();
});

it("rejects JUICE input swaps before building the old multi-step calldata", async () => {
const body = {
tokenIn: contracts.JUICE,
tokenOut: contracts.WCBTC,
tokenInChainId: chainId,
tokenOutChainId: chainId,
tokenInDecimals: 18,
tokenOutDecimals: 18,
amount: "1000000000000000000",
type: "exactIn",
recipient: "0x000000000000000000000000000000000000dead",
from: "0x000000000000000000000000000000000000dead",
slippageTolerance: "0.5",
};
const routerService = {
getProvider: jest.fn(),
};
const handler = createSwapHandler(
routerService as any,
logger,
juiceGatewayService,
);
const res = createMockResponse();

await handler({ body, headers: {} } as any, res as any);

expect(res.status).toHaveBeenCalledWith(404);
expect(res.json).toHaveBeenCalledWith(
expect.objectContaining({ error: "GATEWAY_DEPOSIT_DISABLED" }),
);
expect(routerService.getProvider).not.toHaveBeenCalled();
});

it("applies the same stale-cache gate to the GraphQL quote resolver", async () => {
const input = {
tokenInAddress: contracts.JUSD,
tokenOutAddress: contracts.WCBTC,
tokenInChainId: chainId,
tokenOutChainId: chainId,
amount: "1000000000000000000",
type: "EXACT_INPUT",
swapper: "0x000000000000000000000000000000000000dead",
};
quoteCache.set(
{
...input,
tokenIn: undefined,
tokenOut: undefined,
},
{
requestId: "cached",
routing: Routing.GATEWAY_JUSD,
permitData: null,
quote: { quote: "1" },
},
);
const routerService = {
isChainSupported: jest.fn().mockReturnValue(true),
getQuote: jest.fn(),
};
initializeResolvers(
routerService as any,
logger,
undefined,
juiceGatewayService,
);

await expect(resolvers.Query.quote(null, { input })).rejects.toThrow(
"JUSD savings rate is zero",
);
expect(routerService.getQuote).not.toHaveBeenCalled();
});

it("rejects LP create with JUSD before provider or pool work", async () => {
const body = {
protocol: "V3",
walletAddress: "0x000000000000000000000000000000000000dead",
chainId,
independentAmount: "1000000000000000000",
independentToken: "TOKEN_0",
position: {
tickLower: -600,
tickUpper: 600,
pool: {
token0: contracts.JUSD,
token1: contracts.WCBTC,
fee: 3000,
},
},
};
const routerService = {
getProvider: jest.fn(),
};
const handler = createLpCreateHandler(
routerService as any,
logger,
juiceGatewayService,
);
const res = createMockResponse();

await handler({ body, headers: {} } as any, res as any);

expect(res.status).toHaveBeenCalledWith(404);
expect(res.json).toHaveBeenCalledWith(
expect.objectContaining({ error: "GATEWAY_DEPOSIT_DISABLED" }),
);
expect(routerService.getProvider).not.toHaveBeenCalled();
});

it("rejects LP increase with bridged stable input before provider or Ponder work", async () => {
const body = {
protocol: "V3",
walletAddress: "0x000000000000000000000000000000000000dead",
chainId,
tokenId: "123",
independentAmount: "1000000",
independentToken: "TOKEN_0",
position: {
tickLower: -600,
tickUpper: 600,
pool: {
token0: contracts.USDC,
token1: contracts.WCBTC,
fee: 3000,
},
},
};
const routerService = {
getProvider: jest.fn(),
};
const handler = createLpIncreaseHandler(
routerService as any,
logger,
juiceGatewayService,
);
const res = createMockResponse();

await handler({ body, headers: {} } as any, res as any);

expect(res.status).toHaveBeenCalledWith(404);
expect(res.json).toHaveBeenCalledWith(
expect.objectContaining({ error: "GATEWAY_DEPOSIT_DISABLED" }),
);
expect(routerService.getProvider).not.toHaveBeenCalled();
});
});
95 changes: 95 additions & 0 deletions src/endpoints/__tests__/gatewayDepositGuard.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import Logger from "bunyan";
import {
isGatewayDepositDisabledError,
sendGatewayDepositDisabled,
} from "../gatewayDepositGuard";

function createMockLogger(): Logger {
const logger = {
child: jest.fn(),
info: jest.fn(),
debug: jest.fn(),
warn: jest.fn(),
error: jest.fn(),
};
logger.child.mockReturnValue(logger);
return logger as unknown as Logger;
}

function createMockResponse() {
return {
status: jest.fn().mockReturnThis(),
json: jest.fn().mockReturnThis(),
};
}

describe("gatewayDepositGuard", () => {
describe("isGatewayDepositDisabledError", () => {
it("recognises the GATEWAY_DEPOSIT_DISABLED error", () => {
expect(
isGatewayDepositDisabledError({ code: "GATEWAY_DEPOSIT_DISABLED" }),
).toBe(true);
});

it("rejects other errors and non-objects", () => {
expect(isGatewayDepositDisabledError(new Error("boom"))).toBe(false);
expect(isGatewayDepositDisabledError({ code: "SOMETHING_ELSE" })).toBe(
false,
);
expect(isGatewayDepositDisabledError(null)).toBe(false);
expect(isGatewayDepositDisabledError("nope")).toBe(false);
});
});

describe("sendGatewayDepositDisabled", () => {
it("uses the error message as the detail when present", () => {
const res = createMockResponse();
const log = createMockLogger();

sendGatewayDepositDisabled(
res as never,
log,
{
code: "GATEWAY_DEPOSIT_DISABLED",
message: "custom detail",
savingsRate: "0",
savingsAddress: "0xabc",
},
{ chainId: 4114 },
);

expect(res.status).toHaveBeenCalledWith(404);
expect(res.json).toHaveBeenCalledWith({
error: "GATEWAY_DEPOSIT_DISABLED",
detail: "custom detail",
});
expect(log.warn).toHaveBeenCalledWith(
expect.objectContaining({
chainId: 4114,
savingsRate: "0",
savingsAddress: "0xabc",
}),
"Gateway deposit disabled",
);
});

it("falls back to the canonical detail when no message is set", () => {
const res = createMockResponse();
const log = createMockLogger();

sendGatewayDepositDisabled(
res as never,
log,
{ code: "GATEWAY_DEPOSIT_DISABLED" },
{},
);

expect(res.status).toHaveBeenCalledWith(404);
expect(res.json).toHaveBeenCalledWith({
error: "GATEWAY_DEPOSIT_DISABLED",
detail:
"JUSD savings rate is zero; svJUSD deposits revert while Savings is disabled.",
});
});
});
});
Loading
Loading