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
52 changes: 51 additions & 1 deletion src/background/services/__tests__/paymentManager.test.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down
40 changes: 23 additions & 17 deletions src/background/services/paymentManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'];
Expand Down Expand Up @@ -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);
}
}

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -702,3 +694,17 @@ class PeekAbleIterator<T> implements Iterator<T, never, never> {
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) };
}
Loading