diff --git a/.changeset/fix-suggested-fees-pct-bigint.md b/.changeset/fix-suggested-fees-pct-bigint.md new file mode 100644 index 0000000..8f7e6ef --- /dev/null +++ b/.changeset/fix-suggested-fees-pct-bigint.md @@ -0,0 +1,5 @@ +--- +"@across-protocol/app-sdk": patch +--- + +Tighten percentageString validator to integer strings and protect BigInt pct conversions in parseSuggestedFees diff --git a/packages/sdk/src/actions/getSuggestedFees.ts b/packages/sdk/src/actions/getSuggestedFees.ts index e14814d..f5fbef8 100644 --- a/packages/sdk/src/actions/getSuggestedFees.ts +++ b/packages/sdk/src/actions/getSuggestedFees.ts @@ -185,6 +185,10 @@ export async function getSuggestedFees({ return parseSuggestedFees(data); } +function toBigIntPct(value: string): bigint { + return BigInt(value.includes(".") ? value.split(".")[0] || "0" : value); +} + export function parseSuggestedFees( raw: SuggestedFeesApiResponse, ): GetSuggestedFeesReturnType { @@ -209,19 +213,19 @@ export function parseSuggestedFees( destinationSpokePoolAddress: raw.destinationSpokePoolAddress as Address, fillDeadline: Number(raw.fillDeadline), totalRelayFee: { - pct: BigInt(raw.totalRelayFee.pct), + pct: toBigIntPct(raw.totalRelayFee.pct), total: BigInt(raw.totalRelayFee.total), }, relayerCapitalFee: { - pct: BigInt(raw.relayerCapitalFee.pct), + pct: toBigIntPct(raw.relayerCapitalFee.pct), total: BigInt(raw.relayerCapitalFee.total), }, relayerGasFee: { - pct: BigInt(raw.relayerGasFee.pct), + pct: toBigIntPct(raw.relayerGasFee.pct), total: BigInt(raw.relayerGasFee.total), }, lpFee: { - pct: BigInt(raw.lpFee.pct), + pct: toBigIntPct(raw.lpFee.pct), total: BigInt(raw.lpFee.total), }, limits: { diff --git a/packages/sdk/src/api/validators.ts b/packages/sdk/src/api/validators.ts index dc185e2..8b2276c 100644 --- a/packages/sdk/src/api/validators.ts +++ b/packages/sdk/src/api/validators.ts @@ -8,11 +8,9 @@ export const ethereumAddress = z.string().regex(/^0x[a-fA-F0-9]{40}$/, { }); // Solana base58 address (32-44 chars) -export const solanaAddress = z - .string() - .regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/, { - message: "Invalid Solana address format", - }); +export const solanaAddress = z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/, { + message: "Invalid Solana address format", +}); // EVM or Solana address export const anyChainAddress = z.union([ethereumAddress, solanaAddress]); @@ -21,7 +19,7 @@ export const bigNumberString = z.string().regex(/^-?\d+$/, { message: "Invalid BigNumber string format", }); -export const percentageString = z.string().regex(/^\d+(\.\d+)?$/, { +export const percentageString = z.string().regex(/^\d+$/, { message: "Invalid percentage string format", }); @@ -33,11 +31,15 @@ export const positiveIntString = z.string().regex(/^\d+$/, { message: "Invalid positive integer string format", }); -export const positiveFloatString = (max: number) => z.string().regex(/^\d+(\.\d+)?$/, { - message: "Invalid positive float string format", -}).refine(val => parseFloat(val) <= max, { - message: `Value must be <= ${max}`, -}); +export const positiveFloatString = (max: number) => + z + .string() + .regex(/^\d+(\.\d+)?$/, { + message: "Invalid positive float string format", + }) + .refine((val) => parseFloat(val) <= max, { + message: `Value must be <= ${max}`, + }); export const booleanString = z.string().regex(/^(true|false)$/, { message: "Invalid boolean string format", diff --git a/packages/sdk/test/unit/api/suggested-fees.test.ts b/packages/sdk/test/unit/api/suggested-fees.test.ts index e1a4404..d826b29 100644 --- a/packages/sdk/test/unit/api/suggested-fees.test.ts +++ b/packages/sdk/test/unit/api/suggested-fees.test.ts @@ -1,6 +1,8 @@ +import { percentageString } from "../../../src/api/validators.js"; import { getSuggestedFees, GetSuggestedFeesParams, + parseSuggestedFees, } from "../../../src/actions/getSuggestedFees.js"; import { suggestedFeesResponseJsonSchema } from "../../../src/api/suggested-fees.js"; import { MAINNET_API_URL } from "../../../src/constants/index.js"; @@ -49,4 +51,60 @@ describe("suggested-fees", async () => { const compareRawWithParsed = checkFields(raw, parsed); expect(compareRawWithParsed).toBe(true); }); + test("percentageString rejects decimal strings and accepts integer strings", () => { + expect(percentageString.safeParse("0.5").success).toBe(false); + expect(percentageString.safeParse("0.0005").success).toBe(false); + expect(percentageString.safeParse("76950972772560").success).toBe(true); + expect(percentageString.safeParse("0").success).toBe(true); + }); + + test("parseSuggestedFees safely converts decimal and integer pct fields to bigint", () => { + const mockRaw: any = { + outputAmount: "1999846098054454880", + estimatedFillTimeSec: 3, + capitalFeePct: "76875000000000", + capitalFeeTotal: "153750000000000", + relayGasFeePct: "75972772560", + relayGasFeeTotal: "151945545120", + relayFeePct: "76950972772560", + relayFeeTotal: "153901945545120", + lpFeePct: "0", + timestamp: "1789091423", + isAmountTooLow: false, + quoteBlock: "25950980", + exclusiveRelayer: "0xaD03abC6223e9DEf79B3Feb624F54303A372f0c7", + exclusivityDeadline: 1789091551, + spokePoolAddress: "0x5c7BCd6E7De5423a257D81B442095A1a6ced35C5", + destinationSpokePoolAddress: "0x6f26Bf09B1C792e3228e5467807a900A503c0281", + totalRelayFee: { pct: "0.5", total: "153901945545120" }, + relayerCapitalFee: { pct: "76875000000000", total: "153750000000000" }, + relayerGasFee: { pct: "75972772560", total: "151945545120" }, + lpFee: { pct: "0", total: "0" }, + limits: { + minDeposit: "203996703413272", + maxDeposit: "1143902177198408031000", + maxDepositInstant: "41759844987637500165", + maxDepositShortDelay: "1143902177198408031000", + recommendedDepositInstant: "41759844987637500165", + }, + fillDeadline: "1789098623", + inputToken: { + address: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + symbol: "WETH", + decimals: 18, + chainId: 1, + }, + outputToken: { + address: "0x4200000000000000000000000000000000000006", + symbol: "WETH", + decimals: 18, + chainId: 10, + }, + }; + + expect(() => parseSuggestedFees(mockRaw)).not.toThrow(); + const parsed = parseSuggestedFees(mockRaw); + expect(parsed.totalRelayFee.pct).toBe(0n); + expect(parsed.relayerCapitalFee.pct).toBe(76875000000000n); + }); });