This commit is contained in:
2026-08-28 17:31:02 +02:00
commit 2b30e8bd39
694 changed files with 49243 additions and 0 deletions
@@ -0,0 +1,11 @@
import { convertFiatToXmr } from './convertFiatToXmr';
describe('convertFiatToXmr', () => {
it('formats small amounts without scientific notation', () => {
expect(convertFiatToXmr(0.003, 300_000)).toBe('0.00000001');
});
it('converts fiat to XMR at the shop rate', () => {
expect(convertFiatToXmr(150, 300)).toBe('0.50000000');
});
});
@@ -0,0 +1,5 @@
import Decimal from 'decimal.js';
export const convertFiatToXmr = (fiatAmount: number, fiatPerXmr: number): string => {
return new Decimal(fiatAmount).div(fiatPerXmr).toDecimalPlaces(8, Decimal.ROUND_HALF_UP).toFixed(8);
};
@@ -0,0 +1,16 @@
import Decimal from 'decimal.js';
import { convertXmrAtomicToXmr } from './convertXmrAtomicToXmr';
describe('convertXmrAtomicToXmr', () => {
it('formats small amounts without scientific notation', () => {
expect(convertXmrAtomicToXmr('10000', Decimal.ROUND_CEIL)).toBe('0.00000001');
});
it('formats one XMR', () => {
expect(convertXmrAtomicToXmr('1000000000000')).toBe('1.00000000');
});
it('rounds up remaining amounts when requested', () => {
expect(convertXmrAtomicToXmr('9070001', Decimal.ROUND_CEIL)).toBe('0.00000908');
});
});
@@ -0,0 +1,9 @@
import Decimal from 'decimal.js';
import { XMR_ATOMIC_PER_XMR } from '../../consts/xmrAtomicPerXmr';
export const convertXmrAtomicToXmr = (
amountAtomic: string,
rounding: Decimal.Rounding = Decimal.ROUND_HALF_UP
): string => {
return new Decimal(amountAtomic).div(XMR_ATOMIC_PER_XMR).toDecimalPlaces(8, rounding).toFixed(8);
};
@@ -0,0 +1,20 @@
import { convertXmrToXmrAtomic } from './convertXmrToXmrAtomic';
describe('convertXmrToXmrAtomic', () => {
it('converts one XMR to atomic units', () => {
expect(convertXmrToXmrAtomic('1')).toBe('1000000000000');
});
it('converts the smallest display unit to atomic units', () => {
expect(convertXmrToXmrAtomic('0.00000001')).toBe('10000');
});
it('accepts scientific notation input', () => {
expect(convertXmrToXmrAtomic('1e-8')).toBe('10000');
});
it('returns an integer string without scientific notation', () => {
expect(convertXmrToXmrAtomic('0.00025907')).toBe('259070000');
expect(convertXmrToXmrAtomic('0.00025907')).not.toMatch(/e/i);
});
});
@@ -0,0 +1,6 @@
import Decimal from 'decimal.js';
import { XMR_ATOMIC_PER_XMR } from '../../consts/xmrAtomicPerXmr';
export const convertXmrToXmrAtomic = (amountXmr: string): string => {
return new Decimal(amountXmr).mul(XMR_ATOMIC_PER_XMR).toDecimalPlaces(0, Decimal.ROUND_HALF_UP).toString();
};
@@ -0,0 +1,16 @@
import { deduplicateIncomingMoneroTransfers } from './deduplicateIncomingMoneroTransfers';
describe('deduplicateIncomingMoneroTransfers', () => {
it('keeps the transfer with the highest confirmations for each tx hash', () => {
const result = deduplicateIncomingMoneroTransfers([
{ txHash: 'tx-a', amountAtomic: '100', confirmations: 1, subaddrIndex: 3 },
{ txHash: 'tx-a', amountAtomic: '100', confirmations: 4, subaddrIndex: 3 },
{ txHash: 'tx-b', amountAtomic: '200', confirmations: 2, subaddrIndex: 7 }
]);
expect(result).toEqual([
{ txHash: 'tx-a', amountAtomic: '100', confirmations: 4, subaddrIndex: 3 },
{ txHash: 'tx-b', amountAtomic: '200', confirmations: 2, subaddrIndex: 7 }
]);
});
});
@@ -0,0 +1,17 @@
import type { MoneroWalletRpcIncomingTransfer } from '../../modules/moneroWallet/types/MoneroWalletRpcIncomingTransfer';
export const deduplicateIncomingMoneroTransfers = (
transfers: MoneroWalletRpcIncomingTransfer[]
): MoneroWalletRpcIncomingTransfer[] => {
const byTxHash = new Map<string, MoneroWalletRpcIncomingTransfer>();
for (const transfer of transfers) {
const existing = byTxHash.get(transfer.txHash);
if (!existing || transfer.confirmations > existing.confirmations) {
byTxHash.set(transfer.txHash, transfer);
}
}
return [...byTxHash.values()];
};
@@ -0,0 +1,18 @@
import { groupIncomingMoneroTransfersBySubaddrIndex } from './groupIncomingMoneroTransfersBySubaddrIndex';
describe('groupIncomingMoneroTransfersBySubaddrIndex', () => {
it('deduplicates and groups transfers by subaddress index', () => {
const grouped = groupIncomingMoneroTransfersBySubaddrIndex([
{ txHash: 'tx-a', amountAtomic: '100', confirmations: 1, subaddrIndex: 3 },
{ txHash: 'tx-a', amountAtomic: '100', confirmations: 4, subaddrIndex: 3 },
{ txHash: 'tx-b', amountAtomic: '200', confirmations: 2, subaddrIndex: 7 }
]);
expect(grouped.get(3)).toEqual([
{ txHash: 'tx-a', amountAtomic: '100', confirmations: 4, subaddrIndex: 3 }
]);
expect(grouped.get(7)).toEqual([
{ txHash: 'tx-b', amountAtomic: '200', confirmations: 2, subaddrIndex: 7 }
]);
});
});
@@ -0,0 +1,19 @@
import type { MoneroWalletRpcIncomingTransfer } from '../../modules/moneroWallet/types/MoneroWalletRpcIncomingTransfer';
import { deduplicateIncomingMoneroTransfers } from './deduplicateIncomingMoneroTransfers';
export const groupIncomingMoneroTransfersBySubaddrIndex = (
transfers: MoneroWalletRpcIncomingTransfer[]
): Map<number, MoneroWalletRpcIncomingTransfer[]> => {
const deduped = deduplicateIncomingMoneroTransfers(transfers);
const grouped = new Map<number, MoneroWalletRpcIncomingTransfer[]>();
for (const transfer of deduped) {
const existing = grouped.get(transfer.subaddrIndex) ?? [];
existing.push(transfer);
grouped.set(transfer.subaddrIndex, existing);
}
return grouped;
};
@@ -0,0 +1,76 @@
import { XMR_ATOMIC_PER_XMR } from '../../consts/xmrAtomicPerXmr';
import type { MoneroWalletRpcIncomingTransfer } from '../../modules/moneroWallet/types/MoneroWalletRpcIncomingTransfer';
import { deduplicateIncomingMoneroTransfers } from './deduplicateIncomingMoneroTransfers';
import { groupIncomingMoneroTransfersBySubaddrIndex } from './groupIncomingMoneroTransfersBySubaddrIndex';
const oneXmrAtomic = XMR_ATOMIC_PER_XMR.toString();
const transfer = (
overrides: Partial<MoneroWalletRpcIncomingTransfer> & Pick<MoneroWalletRpcIncomingTransfer, 'txHash'>
): MoneroWalletRpcIncomingTransfer => ({
amountAtomic: oneXmrAtomic,
confirmations: 1,
subaddrIndex: 1,
...overrides
});
describe('deduplicateIncomingMoneroTransfers', () => {
it('keeps the entry with more confirmations for the same tx hash', () => {
const pending = transfer({ txHash: 'abc', confirmations: 0, amountAtomic: '100' });
const confirmed = transfer({ txHash: 'abc', confirmations: 3, amountAtomic: '100' });
const deduped = deduplicateIncomingMoneroTransfers([pending, confirmed]);
expect(deduped).toEqual([confirmed]);
});
it('keeps unrelated transfers', () => {
const first = transfer({ txHash: 'abc', subaddrIndex: 1 });
const second = transfer({ txHash: 'def', subaddrIndex: 2 });
const deduped = deduplicateIncomingMoneroTransfers([first, second]);
expect(deduped).toEqual([first, second]);
});
it('keeps one transfer when the same tx hash appears under different subaddrs', () => {
const first = transfer({ txHash: 'abc', subaddrIndex: 1, confirmations: 1 });
const second = transfer({ txHash: 'abc', subaddrIndex: 2, confirmations: 2 });
const deduped = deduplicateIncomingMoneroTransfers([first, second]);
expect(deduped).toEqual([second]);
});
});
describe('groupIncomingMoneroTransfersBySubaddrIndex', () => {
it('groups transfers by subaddr index', () => {
const first = transfer({ txHash: 'abc', subaddrIndex: 1 });
const second = transfer({ txHash: 'def', subaddrIndex: 2 });
const third = transfer({ txHash: 'ghi', subaddrIndex: 1 });
const grouped = groupIncomingMoneroTransfersBySubaddrIndex([first, second, third]);
expect(grouped).toEqual(
new Map([
[1, [first, third]],
[2, [second]]
])
);
});
it('deduplicates before grouping', () => {
const pending = transfer({ txHash: 'abc', subaddrIndex: 5, confirmations: 0 });
const confirmed = transfer({ txHash: 'abc', subaddrIndex: 5, confirmations: 2 });
const grouped = groupIncomingMoneroTransfersBySubaddrIndex([pending, confirmed]);
expect(grouped).toEqual(new Map([[5, [confirmed]]]));
});
it('returns an empty map for no transfers', () => {
const grouped = groupIncomingMoneroTransfersBySubaddrIndex([]);
expect(grouped).toEqual(new Map());
});
});
@@ -0,0 +1,21 @@
import { resolveMinConfirmations } from './resolveMinConfirmations';
const tiers = [
{ upToTotalFiat: '25', minConfirmations: 0 },
{ upToTotalFiat: '250', minConfirmations: 5 },
{ minConfirmations: 10 }
] as const;
describe('resolveMinConfirmations', () => {
it('returns 0 for small orders (tx-detected tier)', () => {
expect(resolveMinConfirmations(10, [...tiers])).toBe(0);
});
it('returns the middle tier for medium orders', () => {
expect(resolveMinConfirmations(100, [...tiers])).toBe(5);
});
it('returns the catch-all tier for large orders', () => {
expect(resolveMinConfirmations(500, [...tiers])).toBe(10);
});
});
@@ -0,0 +1,16 @@
import Decimal from 'decimal.js';
import type { MoneroConfirmationTier } from '../../types/MoneroConfirmationTier';
export const resolveMinConfirmations = (totalFiat: number, tiers: MoneroConfirmationTier[]): number => {
for (const tier of tiers) {
if (tier.upToTotalFiat === undefined) {
return tier.minConfirmations;
}
if (new Decimal(totalFiat).lte(tier.upToTotalFiat)) {
return tier.minConfirmations;
}
}
return tiers[tiers.length - 1].minConfirmations;
};