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
13 changes: 7 additions & 6 deletions src/routes/account.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
const { server, fetchAccountCreation } = require("../config/stellar");
const { success } = require("../utils/response");
const { getAssetMetadataFromToml } = require("../utils/tomlResolver");
const { formatBalance } = require("../utils/formatBalance");
const { Asset } = require("@stellar/stellar-sdk");
const {
validateAccountId,
Expand All @@ -29,19 +30,19 @@
assetCode: b.asset_code,
assetIssuer: b.asset_issuer,
assetType: b.asset_type,
balance: b.balance,
balance: formatBalance(b.balance),
limit: b.limit,
buyingLiabilities: b.buying_liabilities,
sellingLiabilities: b.selling_liabilities,
buyingLiabilities: formatBalance(b.buying_liabilities),
sellingLiabilities: formatBalance(b.selling_liabilities),
isAuthorized: b.is_authorized,
isClawbackEnabled: b.is_clawback_enabled,
}));

return {
xlm: {
balance: xlmBalance ? xlmBalance.balance : "0.0000000",
buyingLiabilities: xlmBalance ? xlmBalance.buying_liabilities : "0",
sellingLiabilities: xlmBalance ? xlmBalance.selling_liabilities : "0",
balance: formatBalance(xlmBalance ? xlmBalance.balance : "0.0000000"),
buyingLiabilities: formatBalance(xlmBalance ? xlmBalance.buying_liabilities : "0"),
sellingLiabilities: formatBalance(xlmBalance ? xlmBalance.selling_liabilities : "0"),
},
assets,
};
Expand Down Expand Up @@ -149,7 +150,7 @@
.order("asc")
.call();
transactions = response.records || [];
} catch (_) {

Check warning on line 153 in src/routes/account.js

View workflow job for this annotation

GitHub Actions / Test (20.x)

'_' is defined but never used

Check warning on line 153 in src/routes/account.js

View workflow job for this annotation

GitHub Actions / Test (18.x)

'_' is defined but never used
transactions = [];
}

Expand Down Expand Up @@ -848,7 +849,7 @@
trustline.assetCode
);
}
} catch (_) {

Check warning on line 852 in src/routes/account.js

View workflow job for this annotation

GitHub Actions / Test (20.x)

'_' is defined but never used

Check warning on line 852 in src/routes/account.js

View workflow job for this annotation

GitHub Actions / Test (18.x)

'_' is defined but never used
// Issuer account info and TOML are optional; continue if unreachable
}

Expand Down Expand Up @@ -1367,7 +1368,7 @@
for (const signerKey of signers) {
try {
validateAccountId(signerKey);
} catch (e) {

Check warning on line 1371 in src/routes/account.js

View workflow job for this annotation

GitHub Actions / Test (20.x)

'e' is defined but never used

Check warning on line 1371 in src/routes/account.js

View workflow job for this annotation

GitHub Actions / Test (18.x)

'e' is defined but never used
const err = new Error(`Invalid signer key: "${signerKey}".`);
err.status = 400;
return next(err);
Expand Down Expand Up @@ -1440,7 +1441,7 @@
for (const signerKey of availableSigners) {
try {
validateAccountId(signerKey);
} catch (e) {

Check warning on line 1444 in src/routes/account.js

View workflow job for this annotation

GitHub Actions / Test (20.x)

'e' is defined but never used

Check warning on line 1444 in src/routes/account.js

View workflow job for this annotation

GitHub Actions / Test (18.x)

'e' is defined but never used
const err = new Error(`Invalid signer key: "${signerKey}".`);
err.status = 400;
return next(err);
Expand Down Expand Up @@ -1683,7 +1684,7 @@
let decodedValue = null;
try {
decodedValue = Buffer.from(rawValue, "base64").toString("utf8");
} catch (e) {

Check warning on line 1687 in src/routes/account.js

View workflow job for this annotation

GitHub Actions / Test (20.x)

'e' is defined but never used

Check warning on line 1687 in src/routes/account.js

View workflow job for this annotation

GitHub Actions / Test (18.x)

'e' is defined but never used
// Not decodable as UTF-8
}

Expand Down Expand Up @@ -1727,7 +1728,7 @@
let decodedValue = null;
try {
decodedValue = Buffer.from(rawValue, "base64").toString("utf8");
} catch (e) {

Check warning on line 1731 in src/routes/account.js

View workflow job for this annotation

GitHub Actions / Test (20.x)

'e' is defined but never used

Check warning on line 1731 in src/routes/account.js

View workflow job for this annotation

GitHub Actions / Test (18.x)

'e' is defined but never used
// Not decodable as UTF-8
}

Expand Down Expand Up @@ -2084,7 +2085,7 @@
}

return result;
} catch (err) {

Check warning on line 2088 in src/routes/account.js

View workflow job for this annotation

GitHub Actions / Test (20.x)

'err' is defined but never used

Check warning on line 2088 in src/routes/account.js

View workflow job for this annotation

GitHub Actions / Test (18.x)

'err' is defined but never used
// Path finding might fail if no market exists or 400 from Horizon
return result;
}
Expand Down Expand Up @@ -2429,7 +2430,7 @@
validateAccountId(id);

const LIMIT = 100;
const opResponse = await server

Check warning on line 2433 in src/routes/account.js

View workflow job for this annotation

GitHub Actions / Test (20.x)

'opResponse' is assigned a value but never used

Check warning on line 2433 in src/routes/account.js

View workflow job for this annotation

GitHub Actions / Test (18.x)

'opResponse' is assigned a value but never used
.operations()
.forAccount(id)
.limit(LIMIT)
Expand Down
7 changes: 4 additions & 3 deletions src/routes/asset.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
const { Asset } = require("@stellar/stellar-sdk");
const { server } = require("../config/stellar");
const { success } = require("../utils/response");
const { formatBalance } = require("../utils/formatBalance");
const { assetHoldersRateLimiter } = require("../middleware/rateLimiter");
const {
validateAccountId,
Expand All @@ -22,10 +23,10 @@

return {
accountId: account.id || account.account_id,
balance: balance ? balance.balance : "0.0000000",
balance: formatBalance(balance ? balance.balance : "0.0000000"),
limit: balance ? balance.limit : null,
buyingLiabilities: balance ? balance.buying_liabilities : "0.0000000",
sellingLiabilities: balance ? balance.selling_liabilities : "0.0000000",
buyingLiabilities: formatBalance(balance ? balance.buying_liabilities : "0.0000000"),
sellingLiabilities: formatBalance(balance ? balance.selling_liabilities : "0.0000000"),
isAuthorized: balance ? balance.is_authorized : null,
isAuthorizedToMaintainLiabilities: balance
? balance.is_authorized_to_maintain_liabilities
Expand Down Expand Up @@ -427,7 +428,7 @@
try {
issuerAccount = await server.loadAccount(issuer);
checks.accountExists = { passed: true, detail: "Issuer account exists on the Stellar network." };
} catch (err) {

Check warning on line 431 in src/routes/asset.js

View workflow job for this annotation

GitHub Actions / Test (20.x)

'err' is defined but never used

Check warning on line 431 in src/routes/asset.js

View workflow job for this annotation

GitHub Actions / Test (18.x)

'err' is defined but never used
// All subsequent checks depend on account existing
return success(res, { verified: false, checks });
}
Expand All @@ -446,7 +447,7 @@
const response = await axios.get(tomlUrl, { timeout: 5000 });
tomlText = response.data;
checks.tomlReachable = { passed: true, detail: `stellar.toml fetched from ${tomlUrl}.` };
} catch (err) {

Check warning on line 450 in src/routes/asset.js

View workflow job for this annotation

GitHub Actions / Test (20.x)

'err' is defined but never used

Check warning on line 450 in src/routes/asset.js

View workflow job for this annotation

GitHub Actions / Test (18.x)

'err' is defined but never used
return success(res, { verified: false, checks });
}

Expand Down
33 changes: 33 additions & 0 deletions src/utils/formatBalance.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/**
* Formats a raw Stellar balance string into a human-friendly display string
* with thousand separators (commas).
*
* @param {string} balance - The raw balance string (e.g., "10000.1234567")
* @returns {string} The formatted balance string with thousand separators (e.g., "10,000.1234567")
*
* @example
* formatBalance("10000.1234567") // "10,000.1234567"
* formatBalance("0.0000100") // "0.0000100"
* formatBalance("1234567.89") // "1,234,567.89"
* formatBalance("100") // "100"
*/
function formatBalance(balance) {
if (!balance || typeof balance !== "string") {
return balance;
}

// Split the balance into integer and decimal parts
const parts = balance.split(".");

// Add thousand separators to the integer part
const integerPart = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ",");

// Reconstruct: if there's a decimal part, add it back; otherwise just return the integer part
if (parts.length > 1) {
return integerPart + "." + parts[1];
}

return integerPart;
}

module.exports = { formatBalance };
108 changes: 108 additions & 0 deletions tests/utils.formatBalance.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
const { formatBalance } = require("../src/utils/formatBalance");

describe("formatBalance", () => {
describe("Basic formatting with thousand separators", () => {
it('should format "10000.1234567" as "10,000.1234567"', () => {
expect(formatBalance("10000.1234567")).toBe("10,000.1234567");
});

it('should format "1234567.89" as "1,234,567.89"', () => {
expect(formatBalance("1234567.89")).toBe("1,234,567.89");
});

it('should format "100" as "100" (no formatting needed)', () => {
expect(formatBalance("100")).toBe("100");
});

it('should format "999" as "999" (under 1000)', () => {
expect(formatBalance("999")).toBe("999");
});

it('should format "1000" as "1,000"', () => {
expect(formatBalance("1000")).toBe("1,000");
});
});

describe("Small balance handling (no trailing zero stripping)", () => {
it('should return "0.0000100" without stripping trailing zeros', () => {
expect(formatBalance("0.0000100")).toBe("0.0000100");
});

it('should return "0.0000001" without stripping trailing zeros', () => {
expect(formatBalance("0.0000001")).toBe("0.0000001");
});

it('should format "0.10" as "0.10"', () => {
expect(formatBalance("0.10")).toBe("0.10");
});

it('should return "0" as "0"', () => {
expect(formatBalance("0")).toBe("0");
});
});

describe("Large balance formatting", () => {
it('should format "1000000.5555" as "1,000,000.5555"', () => {
expect(formatBalance("1000000.5555")).toBe("1,000,000.5555");
});

it('should format "1000000000.1" as "1,000,000,000.1"', () => {
expect(formatBalance("1000000000.1")).toBe("1,000,000,000.1");
});

it('should format "999999999.999999" as "999,999,999.999999"', () => {
expect(formatBalance("999999999.999999")).toBe("999,999,999.999999");
});
});

describe("Edge cases", () => {
it("should handle null gracefully", () => {
expect(formatBalance(null)).toBe(null);
});

it("should handle undefined gracefully", () => {
expect(formatBalance(undefined)).toBe(undefined);
});

it("should handle empty string", () => {
expect(formatBalance("")).toBe("");
});

it("should handle non-string input", () => {
const num = 123;
expect(formatBalance(num)).toBe(num);
});
});

describe("Stellar-specific balance values", () => {
it('should format XLM default balance "0.0000000" as "0.0000000"', () => {
expect(formatBalance("0.0000000")).toBe("0.0000000");
});

it('should format typical XLM balance "50000.0000000" as "50,000.0000000"', () => {
expect(formatBalance("50000.0000000")).toBe("50,000.0000000");
});

it('should format typical asset balance "1234567.123456" as "1,234,567.123456"', () => {
expect(formatBalance("1234567.123456")).toBe("1,234,567.123456");
});

it('should format liability value "10000" as "10,000"', () => {
expect(formatBalance("10000")).toBe("10,000");
});
});

describe("Decimal precision preservation", () => {
it("should preserve all decimal places", () => {
expect(formatBalance("1000.123456789")).toBe("1,000.123456789");
});

it("should preserve many decimal places for small amounts", () => {
expect(formatBalance("0.0000000000000001")).toBe("0.0000000000000001");
});

it("should format integer without decimal point", () => {
expect(formatBalance("50000")).toBe("50,000");
});
});
});
49 changes: 49 additions & 0 deletions verify-formatBalance.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#!/usr/bin/env node
const { formatBalance } = require("./src/utils/formatBalance");

console.log("Testing formatBalance utility...\n");

const testCases = [
{ input: "10000.1234567", expected: "10,000.1234567", description: "Basic formatting with decimals" },
{ input: "0.0000100", expected: "0.0000100", description: "Small balance - no trailing zero stripping" },
{ input: "1234567.89", expected: "1,234,567.89", description: "Large balance" },
{ input: "100", expected: "100", description: "Small number under 1000" },
{ input: "1000", expected: "1,000", description: "Exactly 1000" },
{ input: "50000.0000000", expected: "50,000.0000000", description: "XLM balance" },
{ input: "0", expected: "0", description: "Zero" },
{ input: "999999999.999999", expected: "999,999,999.999999", description: "Large balance with decimals" },
{ input: null, expected: null, description: "Null input" },
{ input: undefined, expected: undefined, description: "Undefined input" },
];

let passed = 0;
let failed = 0;

testCases.forEach(({ input, expected, description }) => {
const result = formatBalance(input);
const isPass = result === expected;

if (isPass) {
console.log(`✓ ${description}`);
console.log(` Input: ${JSON.stringify(input)} → Output: ${JSON.stringify(result)}`);
passed++;
} else {
console.log(`✗ ${description}`);
console.log(` Input: ${JSON.stringify(input)}`);
console.log(` Expected: ${JSON.stringify(expected)}`);
console.log(` Got: ${JSON.stringify(result)}`);
failed++;
}
console.log();
});

console.log(`\n========================================`);
console.log(`Results: ${passed} passed, ${failed} failed`);
console.log(`========================================\n`);

if (failed > 0) {
process.exit(1);
}

console.log("✅ All tests passed!");
process.exit(0);
Loading