Skip to content

Commit b1d175d

Browse files
Add managed exact-call execution tests
1 parent 9be55d8 commit b1d175d

4 files changed

Lines changed: 223 additions & 0 deletions

File tree

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { type Address } from 'viem';
2+
3+
import { TransactionStatus } from '@/entities/transactions';
4+
import { RelayExecutionStatus, type ExecuteCallsResult, type ExecutionResult } from '@rainbow-me/delegation';
5+
6+
import { trackCallsExecution } from './callsExecutionTracking';
7+
8+
jest.mock('@/resources/assets/assets', () => ({
9+
parseGoldskyAddressAsset: jest.fn(),
10+
parseGoldskyAsset: jest.fn(),
11+
}));
12+
13+
const mockAddPendingTransaction = jest.fn();
14+
const mockAddNewTransaction = jest.fn();
15+
16+
jest.mock('@/state/pendingTransactions', () => ({
17+
addNewTransaction: (...args: unknown[]) => mockAddNewTransaction(...args),
18+
pendingTransactionsActions: {
19+
addPendingTransaction: (...args: unknown[]) => mockAddPendingTransaction(...args),
20+
},
21+
}));
22+
23+
const ADDRESS: Address = '0x1111111111111111111111111111111111111111';
24+
const HASH = '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
25+
26+
describe('callsExecutionTracking', () => {
27+
beforeEach(() => {
28+
jest.clearAllMocks();
29+
});
30+
31+
it('registers managed exact-call execution under the relay execution id', () => {
32+
trackCallsExecution({
33+
address: ADDRESS,
34+
batch: true,
35+
chainId: 8453,
36+
execution: {
37+
executionId: 'execution-1',
38+
kind: 'calls.managed',
39+
status: RelayExecutionStatus.Pending,
40+
} satisfies ExecuteCallsResult,
41+
transaction: {
42+
asset: null,
43+
chainId: 8453,
44+
data: Uint8Array.from([0xab, 0xcd]),
45+
from: null,
46+
network: 'Base',
47+
nonce: 7,
48+
status: TransactionStatus.pending,
49+
to: null,
50+
type: 'swap',
51+
value: 5n,
52+
},
53+
});
54+
55+
const pendingTransaction = mockAddPendingTransaction.mock.calls[0]?.[0]?.pendingTransaction;
56+
57+
expect(mockAddPendingTransaction).toHaveBeenCalledWith({
58+
address: ADDRESS,
59+
pendingTransaction: expect.objectContaining({
60+
batch: true,
61+
data: '0xabcd',
62+
delegation: false,
63+
hash: 'execution-1',
64+
relayExecutionId: 'execution-1',
65+
title: 'swap.pending',
66+
value: '5',
67+
}),
68+
});
69+
expect(() => JSON.stringify(pendingTransaction)).not.toThrow();
70+
});
71+
72+
it('registers wallet exact-call execution under the submitted transaction hash', () => {
73+
trackCallsExecution({
74+
address: ADDRESS,
75+
batch: false,
76+
chainId: 8453,
77+
execution: {
78+
hash: HASH,
79+
transaction: {
80+
data: '0x1234',
81+
gas: 21000n,
82+
maxFeePerGas: 2n,
83+
maxPriorityFeePerGas: 1n,
84+
nonce: 9,
85+
to: ADDRESS,
86+
value: 0n,
87+
},
88+
type: 'eip1559',
89+
} satisfies ExecutionResult,
90+
transaction: {
91+
asset: null,
92+
chainId: 8453,
93+
from: ADDRESS,
94+
network: 'Base',
95+
nonce: -1,
96+
status: TransactionStatus.pending,
97+
to: null,
98+
type: 'stake',
99+
value: 0,
100+
},
101+
});
102+
103+
expect(mockAddNewTransaction).toHaveBeenCalledWith({
104+
address: ADDRESS,
105+
chainId: 8453,
106+
transaction: expect.objectContaining({
107+
data: '0x1234',
108+
gasLimit: '21000',
109+
hash: HASH,
110+
nonce: 9,
111+
to: ADDRESS,
112+
}),
113+
});
114+
});
115+
});
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { destroyStore } from '@storesjs/stores';
2+
3+
import { createPreparedCallsStore } from './preparedCallsStore';
4+
5+
type TestParams = {
6+
id: string;
7+
};
8+
9+
describe('createPreparedCallsStore', () => {
10+
it('uses the active cached preparation once, then refreshes repeated submissions for the same params', async () => {
11+
const fetcher = jest
12+
.fn<Promise<string>, [TestParams]>()
13+
.mockResolvedValueOnce('prepared:a:cached')
14+
.mockResolvedValueOnce('prepared:a:fresh');
15+
const store = createPreparedCallsStore<string, TestParams>(fetcher);
16+
17+
await store.getState().fetch({ id: 'a' });
18+
19+
await expect(store.getState().getPreparedCalls({ id: 'a' })).resolves.toBe('prepared:a:cached');
20+
await expect(store.getState().getPreparedCalls({ id: 'a' })).resolves.toBe('prepared:a:fresh');
21+
22+
expect(fetcher).toHaveBeenCalledTimes(2);
23+
24+
destroyStore(store, { clearQueryCache: true });
25+
});
26+
});

src/features/delegation/sponsoredCalls.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,30 @@ describe('sponsoredCalls', () => {
2626
mockIsSponsorshipEligible.mockReturnValue(true);
2727
});
2828

29+
it('requires a delegation-capable wallet', () => {
30+
mockCanUseDelegatedExecution.mockReturnValue(false);
31+
32+
expect(predictSponsoredCallsExecution({ address: ACCOUNT, chainId: ChainId.base })).toBe(false);
33+
34+
expect(mockIsSponsorshipEligible).not.toHaveBeenCalled();
35+
});
36+
37+
it('requires sponsorship eligibility when the chain is known', () => {
38+
mockIsSponsorshipEligible.mockReturnValue(false);
39+
40+
expect(predictSponsoredCallsExecution({ address: ACCOUNT, chainId: ChainId.mainnet })).toBe(false);
41+
});
42+
43+
it('allows sponsorship prediction when the known chain is eligible', () => {
44+
expect(predictSponsoredCallsExecution({ address: ACCOUNT, chainId: ChainId.base })).toBe(true);
45+
});
46+
47+
it('allows explicit prediction before the chain is known', () => {
48+
expect(predictSponsoredCallsExecution({ address: ACCOUNT, chainId: null })).toBe(true);
49+
50+
expect(mockIsSponsorshipEligible).not.toHaveBeenCalled();
51+
});
52+
2953
it('uses the provided sponsorship eligibility list when one is supplied', () => {
3054
expect(
3155
predictSponsoredCallsExecution({
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { RelayExecutionStatus, type RelayStatusSnapshot } from '@rainbow-me/delegation';
2+
3+
import { waitForManagedExecutionConfirmation } from './waitForManagedExecution';
4+
5+
const mockGetStatus = jest.fn<Promise<{ status: RelayStatusSnapshot }>, [string]>();
6+
7+
jest.mock('@/features/delegation/relayService', () => ({
8+
relayService: {
9+
getStatus: (executionId: string) => mockGetStatus(executionId),
10+
},
11+
}));
12+
13+
jest.mock('@/utils/delay', () => ({
14+
delay: () => Promise.resolve(),
15+
}));
16+
17+
function buildRelayUpdate(status: RelayExecutionStatus, errorMessage?: string): { status: RelayStatusSnapshot } {
18+
return {
19+
status: {
20+
status,
21+
updatedAtMs: 1,
22+
...(errorMessage ? { errorMessage } : {}),
23+
},
24+
};
25+
}
26+
27+
describe('waitForManagedExecutionConfirmation', () => {
28+
beforeEach(() => {
29+
jest.clearAllMocks();
30+
});
31+
32+
it('polls relay status until confirmation', async () => {
33+
mockGetStatus
34+
.mockResolvedValueOnce(buildRelayUpdate(RelayExecutionStatus.Pending))
35+
.mockResolvedValueOnce(buildRelayUpdate(RelayExecutionStatus.Confirmed));
36+
37+
await expect(waitForManagedExecutionConfirmation('execution-id')).resolves.toBeUndefined();
38+
39+
expect(mockGetStatus).toHaveBeenCalledTimes(2);
40+
expect(mockGetStatus).toHaveBeenCalledWith('execution-id');
41+
});
42+
43+
it('continues polling after transient relay status errors', async () => {
44+
mockGetStatus.mockRejectedValueOnce(new Error('network down')).mockResolvedValueOnce(buildRelayUpdate(RelayExecutionStatus.Confirmed));
45+
46+
await expect(waitForManagedExecutionConfirmation('execution-id')).resolves.toBeUndefined();
47+
48+
expect(mockGetStatus).toHaveBeenCalledTimes(2);
49+
});
50+
51+
it('surfaces terminal relay failure details', async () => {
52+
mockGetStatus.mockResolvedValueOnce(buildRelayUpdate(RelayExecutionStatus.Reverted, 'execution reverted'));
53+
54+
await expect(waitForManagedExecutionConfirmation('execution-id')).rejects.toThrow(
55+
'Managed relay execution reverted: execution reverted'
56+
);
57+
});
58+
});

0 commit comments

Comments
 (0)