init
This commit is contained in:
@@ -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;
|
||||
};
|
||||
Reference in New Issue
Block a user