-
-
{dateFormatter.format(donation.date)}
-
- {currencyFormatter(donation.currency).format(
- donation.amount,
- )}
+ {donations.map(
+ ({ date, amount, currency, currencyDecimals, note }, i) => (
+ <>
+
+
+
{dateFormatter.format(date)}
+
+ {CurrencyUtils.format(
+ i18n.language,
+ amount,
+ currency,
+ currencyDecimals,
+ )}
+
+
{note}
-
{donation.note}
-
- {i != donations.length - 1 && (
-
- )}
- >
- ))}
+ {i != donations.length - 1 && (
+
+ )}
+ >
+ ),
+ )}
diff --git a/apps/website/src/app/donors/Donors.tsx b/apps/website/src/app/donors/Donors.tsx
index 6593de3fa..d1bb0bf01 100644
--- a/apps/website/src/app/donors/Donors.tsx
+++ b/apps/website/src/app/donors/Donors.tsx
@@ -40,8 +40,8 @@ export function Donors() {
return donors.sort(
(a, b) =>
- b.donor.donations.reduce((a, c) => c.amount + a, 0) -
- a.donor.donations.reduce((a, c) => c.amount + a, 0),
+ b.donor.donations.reduce((a, c) => Number(c.value) + a, 0) -
+ a.donor.donations.reduce((a, c) => Number(c.value) + a, 0),
);
}, [donorsQuery.data]);
@@ -49,7 +49,7 @@ export function Donors() {
() =>
donors.reduce(
(sum, { donor }) =>
- sum + donor.donations.reduce((a, c) => c.amount + a, 0),
+ sum + donor.donations.reduce((a, c) => Number(c.value) + a, 0),
0,
),
[donors],
@@ -57,8 +57,8 @@ export function Donors() {
const processedDonors = useMemo(() => {
const processedDonors = structuredClone(donors);
- const donationBubbleRadius = (amount: number) => {
- const radius = (amount / totalDonated) * (totalDonated * 4);
+ const donationBubbleRadius = (value: bigint) => {
+ const radius = (Number(value) / totalDonated) * (totalDonated * 4);
if (radius > 0.15 * screenSize[0]) {
return screenSize[0] * 0.15;
}
@@ -71,7 +71,7 @@ export function Donors() {
processedDonors.forEach((bubble) => {
bubble.radius = donationBubbleRadius(
- bubble.donor.donations.reduce((a, c) => c.amount + a, 0),
+ bubble.donor.donations.reduce((a, c) => c.value + a, 0n),
);
});
diff --git a/apps/website/src/i18n/locales/en-US/main.json b/apps/website/src/i18n/locales/en-US/main.json
index 2b1e3d0fd..bba402913 100644
--- a/apps/website/src/i18n/locales/en-US/main.json
+++ b/apps/website/src/i18n/locales/en-US/main.json
@@ -550,6 +550,7 @@
"userId": "Discord user ID",
"amount": "Amount",
"currency": "Currency",
+ "currencyDecimals": "Currency decimals",
"date": "Date",
"note": "Note",
"anonymous": "Anonymous",
diff --git a/apps/website/src/server/api/routers/donor.ts b/apps/website/src/server/api/routers/donor.ts
index 1d82d1856..25a31efb2 100644
--- a/apps/website/src/server/api/routers/donor.ts
+++ b/apps/website/src/server/api/routers/donor.ts
@@ -91,8 +91,9 @@ export const donorRouter = createTRPCRouter({
note: z.string(),
anonymous: z.boolean(),
date: z.date(),
- amount: z.number(),
+ amount: z.bigint(),
currency: z.string(),
+ currencyDecimals: z.number(),
}),
)
.mutation(async ({ ctx: { authUser }, input }) => {
@@ -139,8 +140,9 @@ export const donorRouter = createTRPCRouter({
note: z.string().optional(),
anonymous: z.boolean().optional(),
date: z.date().optional(),
- amount: z.number().optional(),
+ amount: z.bigint().optional(),
currency: z.string().optional(),
+ currencyDecimals: z.number().optional(),
}),
)
.mutation(async ({ ctx: { authUser }, input }) => {
diff --git a/packages/common/src/currencyUtils.ts b/packages/common/src/currencyUtils.ts
new file mode 100644
index 000000000..6c0b81d12
--- /dev/null
+++ b/packages/common/src/currencyUtils.ts
@@ -0,0 +1,71 @@
+import assert from "assert";
+
+export class CurrencyUtils {
+ /**
+ * Converts a string (e.g., "10.44") or BigInt to a Number.
+ * @param {string | bigint} value - The monetary value as a string or BigInt.
+ * @param {number} decimals - Currency decimals.
+ * @returns {number} - The monetary value as a Number.
+ */
+ static toNumber(value: string | bigint, decimals = 2): number {
+ if (typeof value === "string") {
+ return parseFloat(value);
+ }
+ if (typeof value === "bigint") {
+ return Number(value) / Math.pow(10, decimals);
+ }
+ throw new TypeError("Invalid type for conversion to Number");
+ }
+
+ /**
+ * Converts a string (e.g., "10.44") to a BigInt representing the smallest unit.
+ * @param {string} value - The monetary value as a string.
+ * @param {number} decimals - Currency decimals.
+ * @returns {bigint} - The amount in the smallest unit as a BigInt.
+ */
+ static toBigInt(value: string, decimals = 2): bigint {
+ const [whole, fraction = "0"] = value.split(".");
+ assert(whole);
+ const fractionPadded = fraction.padEnd(decimals, "0"); // Ensure required decimals
+ return (
+ BigInt(whole) * BigInt(Math.pow(10, decimals)) +
+ BigInt(fractionPadded.slice(0, decimals))
+ );
+ }
+
+ /**
+ * Converts a BigInt to a string with the appropriate decimal places.
+ * @param {bigint} value - The amount in the smallest unit as a BigInt.
+ * @param {number} decimals - Currency decimals.
+ * @returns {string} - The monetary value as a string.
+ */
+ static toString(value: bigint, decimals = 2): string {
+ const isNegative = value < 0n;
+ const absoluteValue = isNegative ? -value : value;
+
+ const divisor = BigInt(Math.pow(10, decimals));
+ const whole = absoluteValue / divisor;
+ const fraction = absoluteValue % divisor;
+
+ const result = `${whole}.${fraction.toString().padStart(decimals, "0")}`;
+ return isNegative ? `-${result}` : result;
+ }
+
+ static format(
+ locale: string,
+ amount: bigint,
+ currency: string,
+ currencyDecimals: number,
+ ) {
+ // Convert the amount to a floating-point number with appropriate precision
+ const amountInUnits = Number(amount) / Math.pow(10, currencyDecimals);
+
+ // Handle the case where the value has many decimal places for small units (e.g., BTC, ETH)
+ return Intl.NumberFormat(locale, {
+ currency,
+ style: "currency",
+ minimumFractionDigits: Math.min(2, currencyDecimals), // Ensure minimum fraction digits
+ maximumFractionDigits: currencyDecimals, // Format using the given currency decimals
+ }).format(amountInUnits);
+ }
+}
diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma
index 8ecf480c5..b22c2d9b1 100644
--- a/packages/db/prisma/schema.prisma
+++ b/packages/db/prisma/schema.prisma
@@ -73,12 +73,13 @@ type DemoLink {
}
model Donation {
- id String @id @default(auto()) @map("_id") @db.ObjectId
- note String
- anonymous Boolean
- date DateTime
- amount Float
- currency String
- user User? @relation(fields: [userId], references: [discordUserId], onDelete: Cascade)
- userId String
+ id String @id @default(auto()) @map("_id") @db.ObjectId
+ note String
+ anonymous Boolean
+ date DateTime
+ amount BigInt
+ currency String
+ currencyDecimals Int
+ user User? @relation(fields: [userId], references: [discordUserId], onDelete: Cascade)
+ userId String
}