Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { describe, expect, it, beforeEach, vi, type Mock } from 'vitest';
import { TOKENS } from './constants.js';
import { getSubscriptionStatus } from './getSubscriptionStatus.js';

vi.mock('../public-utilities/spend-permission/index.js', () => ({
fetchPermission: vi.fn(),
getPermissionStatus: vi.fn(),
}));

vi.mock('../../store/chain-clients/utils.js', () => ({
createClients: vi.fn(),
getClient: vi.fn(),
FALLBACK_CHAINS: [
{
id: 8453,
rpcUrl: 'https://example-base.test',
nativeCurrency: { name: 'Base', symbol: 'ETH', decimal: 18 },
},
{
id: 84532,
rpcUrl: 'https://example-base-sepolia.test',
nativeCurrency: { name: 'Base Sepolia', symbol: 'ETH', decimal: 18 },
},
],
}));

vi.mock('viem/actions', () => ({
readContract: vi.fn(),
}));

vi.mock('../public-utilities/spend-permission/utils.js', () => ({
calculateCurrentPeriod: vi.fn(),
timestampInSecondsToDate: vi.fn((timestamp: number) => new Date(timestamp * 1000)),
toSpendPermissionArgs: vi.fn(),
}));

describe('getSubscriptionStatus', () => {
let fetchPermission: Mock;
let getPermissionStatus: Mock;
let getClient: Mock;
let createClients: Mock;
let calculateCurrentPeriod: Mock;

beforeEach(async () => {
vi.clearAllMocks();

const spendPermissionModule = await import('../public-utilities/spend-permission/index.js');
fetchPermission = vi.mocked(spendPermissionModule.fetchPermission);
getPermissionStatus = vi.mocked(spendPermissionModule.getPermissionStatus);

const chainClientsModule = await import('../../store/chain-clients/utils.js');
getClient = vi.mocked(chainClientsModule.getClient);
createClients = vi.mocked(chainClientsModule.createClients);

const utilsModule = await import('../public-utilities/spend-permission/utils.js');
calculateCurrentPeriod = vi.mocked(utilsModule.calculateCurrentPeriod);
vi.mocked(utilsModule.timestampInSecondsToDate).mockImplementation(
(timestamp: number) => new Date(timestamp * 1000)
);
});

const basePermission = {
createdAt: 0,
permissionHash: '0xhash',
signature: '0xsig',
chainId: 8453,
permission: {
account: '0x0000000000000000000000000000000000000001',
spender: '0x0000000000000000000000000000000000000002',
token: TOKENS.USDC.addresses.base,
allowance: '1000000',
period: 86400,
start: 1_699_999_000,
end: 1_700_086_400,
salt: '1',
extraData: '0x',
},
} as const;

it('treats non-revoked subscriptions with no on-chain state as active', async () => {
const nowSeconds = 1_700_000_000;
const nowSpy = vi.spyOn(Date, 'now').mockReturnValue(nowSeconds * 1000);

fetchPermission.mockResolvedValue(basePermission as any);
getPermissionStatus.mockResolvedValue({
remainingSpend: BigInt(1_000_000),
nextPeriodStart: new Date((nowSeconds + 86400) * 1000),
isActive: false,
isRevoked: false,
});
getClient.mockReturnValue(undefined);
createClients.mockReturnValue(undefined);
calculateCurrentPeriod.mockReturnValue({
start: basePermission.permission.start,
end: basePermission.permission.end,
spend: BigInt(0),
});

const status = await getSubscriptionStatus({ id: basePermission.permissionHash });

expect(status.isSubscribed).toBe(true);
expect(status.remainingChargeInPeriod).toBe('1');
expect(status.recurringCharge).toBe('1');

nowSpy.mockRestore();
});

it('returns inactive status when permission is revoked before any spend', async () => {
const nowSeconds = 1_700_000_000;
const nowSpy = vi.spyOn(Date, 'now').mockReturnValue(nowSeconds * 1000);

fetchPermission.mockResolvedValue(basePermission as any);
getPermissionStatus.mockResolvedValue({
remainingSpend: BigInt(1_000_000),
nextPeriodStart: new Date((nowSeconds + 86400) * 1000),
isActive: false,
isRevoked: true,
});
getClient.mockReturnValue(undefined);
createClients.mockReturnValue(undefined);
calculateCurrentPeriod.mockReturnValue({
start: basePermission.permission.start,
end: basePermission.permission.end,
spend: BigInt(0),
});

const status = await getSubscriptionStatus({ id: basePermission.permissionHash });

expect(status.isSubscribed).toBe(false);

nowSpy.mockRestore();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,8 @@ export async function getSubscriptionStatus(
// A subscription is considered active if we're within the valid time bounds
// and the permission hasn't been revoked.
const hasNoOnChainState = currentPeriod.spend === BigInt(0);
const isSubscribed = hasNotExpired && (status.isActive || hasNoOnChainState);
const isSubscribed =
hasNotExpired && !status.isRevoked && (status.isActive || hasNoOnChainState);

// Build the result with data from getCurrentPeriod and other on-chain functions
const result: SubscriptionStatus = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ describe('getPermissionStatus - browser + node', () => {
remainingSpend: BigInt('500000000000000000'), // 1 ETH - 0.5 ETH = 0.5 ETH remaining
nextPeriodStart: new Date(1641081601 * 1000), // end + 1 converted to Date
isActive: true, // not revoked and valid
isRevoked: false,
});

expect(getClient).toHaveBeenCalledWith(8453);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export type GetPermissionStatusResponseType = {
remainingSpend: bigint;
nextPeriodStart: Date;
isActive: boolean;
isRevoked: boolean;
};

/**
Expand Down Expand Up @@ -108,6 +109,7 @@ const getPermissionStatusFn = async (
remainingSpend,
nextPeriodStart: timestampInSecondsToDate(Number(nextPeriodStart)),
isActive,
isRevoked,
};
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ describe('prepareSpendCallData', () => {
remainingSpend: BigInt('500000000000000000'), // 0.5 ETH remaining
nextPeriodStart: new Date('2024-01-01T00:00:00Z'),
isActive: true,
isRevoked: false,
};

beforeEach(() => {
Expand Down Expand Up @@ -276,6 +277,7 @@ describe('prepareSpendCallData', () => {
remainingSpend: BigInt('500000000000000000'),
nextPeriodStart: new Date('2024-01-01T00:00:00Z'),
isActive: false,
isRevoked: false,
});

const result = await prepareSpendCallData(mockSpendPermission, 'max-remaining-allowance');
Expand All @@ -289,6 +291,7 @@ describe('prepareSpendCallData', () => {
remainingSpend: BigInt('500000000000000000'),
nextPeriodStart: new Date('2024-01-01T00:00:00Z'),
isActive: true,
isRevoked: false,
};
mockGetPermissionStatus.mockResolvedValue(status);

Expand Down Expand Up @@ -393,6 +396,7 @@ describe('prepareSpendCallData', () => {
remainingSpend: BigInt('500000000000000000'),
nextPeriodStart: new Date('2024-01-01T00:00:00Z'),
isActive: false,
isRevoked: false,
};
mockGetPermissionStatus.mockResolvedValue(status);

Expand All @@ -413,6 +417,7 @@ describe('prepareSpendCallData', () => {
remainingSpend: BigInt('0'),
nextPeriodStart: new Date('2024-01-01T00:00:00Z'),
isActive: true,
isRevoked: false,
};
mockGetPermissionStatus.mockResolvedValue(status);

Expand All @@ -426,6 +431,7 @@ describe('prepareSpendCallData', () => {
remainingSpend: BigInt('500000000000000000'),
nextPeriodStart: new Date('2024-01-01T00:00:00Z'),
isActive: false,
isRevoked: false,
};
mockGetPermissionStatus.mockResolvedValue(status);

Expand Down