Skip to content
Open
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
49 changes: 48 additions & 1 deletion typescript/packages/core/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export function convertToTokenAmount(decimalAmount: string, decimals: number): s
`Invalid amount: ${decimalAmount} — use decimal notation, not scientific notation`,
);
}
if (!/^-?\d+\.?\d*$/.test(decimalAmount)) {
if (!isPlainDecimalAmount(decimalAmount)) {
throw new Error(`Invalid amount: ${decimalAmount}`);
}
const [intPart, decPart = ""] = decimalAmount.split(".");
Expand All @@ -61,6 +61,53 @@ export function convertToTokenAmount(decimalAmount: string, decimals: number): s
return tokenAmount;
}

/**
* Validates a plain decimal string with a linear scan to avoid regex backtracking.
*/
function isPlainDecimalAmount(decimalAmount: string): boolean {
if (decimalAmount.length === 0) {
return false;
}

let index = decimalAmount.startsWith("-") ? 1 : 0;
if (index === decimalAmount.length) {
return false;
}

let hasIntegerDigit = false;
while (index < decimalAmount.length) {
const charCode = decimalAmount.charCodeAt(index);
if (charCode < 48 || charCode > 57) {
break;
}
hasIntegerDigit = true;
index++;
}

if (!hasIntegerDigit) {
return false;
}

if (index === decimalAmount.length) {
return true;
}

if (decimalAmount[index] !== ".") {
return false;
}

index++;
while (index < decimalAmount.length) {
const charCode = decimalAmount.charCodeAt(index);
if (charCode < 48 || charCode > 57) {
return false;
}
index++;
}

return true;
}

/**
* Scheme data structure for facilitator storage
*/
Expand Down
21 changes: 21 additions & 0 deletions typescript/packages/core/test/unit/utils/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,11 @@ describe("Utils", () => {
expect(convertToTokenAmount("0.1000000", 7)).toBe("1000000");
});

it("should handle trailing decimal points", () => {
expect(convertToTokenAmount("1.", 6)).toBe("1000000");
expect(convertToTokenAmount("0.", 6)).toBe("0");
});

it("should handle negative numbers", () => {
expect(convertToTokenAmount("-1.5", 6)).toBe("-1500000");
});
Expand Down Expand Up @@ -420,6 +425,22 @@ describe("Utils", () => {
expect(() => convertToTokenAmount("", 6)).toThrow("Invalid amount");
expect(() => convertToTokenAmount("NaN", 6)).toThrow("Invalid amount");
});

it("should throw for malformed decimal strings", () => {
expect(() => convertToTokenAmount(".5", 6)).toThrow("Invalid amount");
expect(() => convertToTokenAmount("+1", 6)).toThrow("Invalid amount");
expect(() => convertToTokenAmount("-", 6)).toThrow("Invalid amount");
expect(() => convertToTokenAmount("1.2.3", 6)).toThrow("Invalid amount");
expect(() => convertToTokenAmount("1 ", 6)).toThrow("Invalid amount");
});

it("should reject long adversarial invalid strings", () => {
const longIntegerWithSuffix = `${"0".repeat(10000)}x`;
const longDecimalWithSuffix = `0.${"0".repeat(10000)}x`;

expect(() => convertToTokenAmount(longIntegerWithSuffix, 6)).toThrow("Invalid amount");
expect(() => convertToTokenAmount(longDecimalWithSuffix, 6)).toThrow("Invalid amount");
});
});
});

Expand Down
Loading