diff --git a/src/background/services/__tests__/paymentManager.test.ts b/src/background/services/__tests__/paymentManager.test.ts index 6623aadc2..a69cbdc68 100644 --- a/src/background/services/__tests__/paymentManager.test.ts +++ b/src/background/services/__tests__/paymentManager.test.ts @@ -1,4 +1,54 @@ -import { distributeAmount } from '../paymentManager'; +import { distributeAmount, calculateInterval } from '../paymentManager'; + +describe('continuous payments / calculateInterval', () => { + test('handles general rate - $0.60/hr', () => { + const result = calculateInterval(60n); + expect(result.units).toBe(1n); + expect(result.period).toBe(60_000); + }); + + test('handles slow-ish rate - $1.50/hr', () => { + const result = calculateInterval(150n); + expect(result.units).toBe(1n); + expect(result.period).toBe(24_000); + }); + + test('handles very slow rate - $0.01/hr', () => { + const result = calculateInterval(1n); + expect(result.units).toBe(1n); + expect(result.period).toBe(3_600_000); + }); + + test('handles exact floor rate - $1.80/hr', () => { + const result = calculateInterval(180n); + expect(result.units).toBe(1n); + expect(result.period).toBe(20_000); + }); + + test('scales up for fast rates - $18/hr', () => { + const result = calculateInterval(1800n); + expect(result.units).toBe(1n); + expect(result.period).toBe(2000); + }); + + test('scales up and adjusts period for very fast rates - $36/hr', () => { + const result = calculateInterval(3600n); + expect(result.units).toBe(2n); + expect(result.period).toBe(2000); + }); + + test('maintains precision with weird decimals - $1.70/hr', () => { + const result = calculateInterval(170n); + expect(result.units).toBe(1n); + expect(result.period).toBe(21_177); + }); + + test('handles extreme rates - $10,000/hr', () => { + const result = calculateInterval(10_00_000n); + expect(result.units).toBe(556n); + expect(result.period).toBe(2002); + }); +}); describe('one time payments / distributeAmount', () => { class PaymentSession { diff --git a/src/background/services/paymentManager.ts b/src/background/services/paymentManager.ts index 11c63bd25..900696b81 100644 --- a/src/background/services/paymentManager.ts +++ b/src/background/services/paymentManager.ts @@ -32,10 +32,10 @@ type Cradle = Pick< | 'PaymentSession' >; -/** Payable amount to increase by {@linkcode Interval.units} every {@linkcode Interval.duration}ms. */ -type Interval = { units: bigint; duration: number }; +/** Payable amount to increase by {@linkcode Interval.units} every {@linkcode Interval.period}ms. */ +type Interval = { units: bigint; period: number }; -const MS_IN_HOUR = 3600; +const MS_IN_HOUR = 60 * 60 * 1000; export class PaymentManager { private rootLogger: Cradle['rootLogger']; @@ -337,22 +337,14 @@ export class PaymentManager { setRate(hourlyRate: AmountValue) { this.hourlyRate = BigInt(hourlyRate); - const secondsPerUnit = MS_IN_HOUR / Number(this.hourlyRate); - const duration = Math.ceil(secondsPerUnit * 1000); - // The math below is equivalent to: - // interval = { units: 1n, duration }; - // while (interval.duration < MIN_PAYMENT_WAIT) { - // interval.units += 1n; - // interval.duration += duration; - // } - const units = BigInt(Math.ceil(MIN_PAYMENT_WAIT / duration)); - this.interval = { units, duration: Number(units) * duration }; + this.interval = calculateInterval(this.hourlyRate); + // TODO: Optimization opportunity above: see if we can set interval based on // some minSendAmount (HCF of all minSendAmount?). i.e. we will increment // amount by minSendAmount unit to make the interval longer. if (this.#state === 'active') { - this.timer.reset(this.interval.duration); + this.timer.reset(this.interval.period); } } @@ -391,7 +383,7 @@ export class PaymentManager { this.pendingAmount -= amount; this.checkAndPayContinuously(); - this.timer.reset(this.interval.duration); + this.timer.reset(this.interval.period); } pause(reason?: string) { @@ -430,7 +422,7 @@ export class PaymentManager { } const elapsed = Date.now() - lastPaymentInfo.ts.valueOf(); - const waitTime = this.interval.duration - elapsed; + const waitTime = this.interval.period - elapsed; if (waitTime > 0) { this.logger.log('[overpaying] Preventing overpaying:', { ...lastPaymentInfo, @@ -463,7 +455,7 @@ export class PaymentManager { } this.pendingAmount += this.interval.units; - this.timer.reset(this.interval.duration); // as if setInterval + this.timer.reset(this.interval.period); // as if setInterval const session = this.peekSessionToPay(); if (!session) { @@ -702,3 +694,17 @@ class PeekAbleIterator implements Iterator { return this; } } + +export function calculateInterval(hourlyRate: bigint): Interval { + const period = MS_IN_HOUR / Number(hourlyRate); + + // The math below is equivalent to: + // interval: Interval = { units: 1n, period }; + // while (interval.period < MIN_PAYMENT_WAIT) { + // interval.units += 1n; + // interval.period += period; + // } + + const units = Math.ceil(MIN_PAYMENT_WAIT / period); + return { units: BigInt(units), period: Math.ceil(units * period) }; +}