Skip to content

Commit f34bfde

Browse files
feat: Implement SNIP29
1 parent af4af75 commit f34bfde

25 files changed

+1214
-5
lines changed

__tests__/accountPaymaster.test.ts

+116
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import { Account, Signature, Call, PaymasterDetails } from '../src';
2+
import { PaymasterRpc } from '../src/paymaster/rpc';
3+
4+
jest.mock('../src/paymaster/rpc');
5+
6+
describe('Account - Paymaster integration', () => {
7+
const mockBuildTypedData = jest.fn();
8+
const mockExecute = jest.fn();
9+
const mockSignMessage = jest.fn();
10+
11+
const fakeTypedData = {
12+
types: {},
13+
domain: {},
14+
primaryType: '',
15+
message: {
16+
caller: '0xcaller',
17+
nonce: '0xnonce',
18+
execute_after: '0x1',
19+
execute_before: '0x2',
20+
calls_len: '0x0',
21+
calls: [],
22+
},
23+
};
24+
25+
const fakeSignature: Signature = ['0x1', '0x2'];
26+
const calls: Call[] = [{ contractAddress: '0x123', entrypoint: 'transfer', calldata: [] }];
27+
28+
const paymasterResponse = {
29+
typedData: fakeTypedData,
30+
tokenAmountAndPrice: {
31+
estimatedAmount: 1000n,
32+
priceInStrk: 200n,
33+
},
34+
};
35+
36+
const mockPaymaster = () =>
37+
({
38+
buildTypedData: mockBuildTypedData,
39+
execute: mockExecute,
40+
}) as any;
41+
42+
const setupAccount = () =>
43+
new Account(
44+
{},
45+
'0xabc',
46+
{ signMessage: mockSignMessage.mockResolvedValue(fakeSignature) } as any,
47+
undefined,
48+
undefined,
49+
mockPaymaster()
50+
);
51+
52+
beforeEach(() => {
53+
jest.clearAllMocks();
54+
(PaymasterRpc as jest.Mock).mockImplementation(mockPaymaster);
55+
mockBuildTypedData.mockResolvedValue(paymasterResponse);
56+
mockExecute.mockResolvedValue({ transaction_hash: '0x123' });
57+
});
58+
59+
describe('buildPaymasterTypedData', () => {
60+
it('should return typed data and token prices from paymaster', async () => {
61+
// Given
62+
const account = setupAccount();
63+
64+
// When
65+
const result = await account.buildPaymasterTypedData(calls, { gasToken: '0x456' });
66+
67+
// Then
68+
expect(mockBuildTypedData).toHaveBeenCalledWith(
69+
'0xabc',
70+
calls,
71+
'0x456',
72+
undefined,
73+
undefined
74+
);
75+
expect(result).toEqual(paymasterResponse);
76+
});
77+
});
78+
79+
describe('executePaymaster', () => {
80+
it('should sign and execute transaction via paymaster', async () => {
81+
// Given
82+
const account = setupAccount();
83+
84+
// When
85+
const result = await account.executePaymaster(calls);
86+
87+
// Then
88+
expect(mockBuildTypedData).toHaveBeenCalledTimes(1);
89+
expect(mockSignMessage).toHaveBeenCalledWith(fakeTypedData, '0xabc');
90+
expect(mockExecute).toHaveBeenCalledWith('0xabc', fakeTypedData, fakeSignature, undefined);
91+
expect(result).toEqual({ transaction_hash: '0x123' });
92+
});
93+
94+
it('should throw if estimated fee exceeds maxEstimatedFee', async () => {
95+
// Given
96+
const account = setupAccount();
97+
const details: PaymasterDetails = { maxEstimatedFee: 500n };
98+
99+
// When / Then
100+
await expect(account.executePaymaster(calls, details)).rejects.toThrow(
101+
'Estimated max fee too high'
102+
);
103+
});
104+
105+
it('should throw if token price exceeds maxPriceInStrk', async () => {
106+
// Given
107+
const account = setupAccount();
108+
const details: PaymasterDetails = { maxPriceInStrk: 100n };
109+
110+
// When / Then
111+
await expect(account.executePaymaster(calls, details)).rejects.toThrow(
112+
'Gas token price is too high'
113+
);
114+
});
115+
});
116+
});

__tests__/config/fixtures.ts

-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import { ETransactionVersion } from '../../src/types/api';
1212
import { toHex } from '../../src/utils/num';
1313
import { wait } from '../../src/utils/provider';
1414
import { isString } from '../../src/utils/typed';
15-
import './customMatchers'; // ensures TS traversal
1615

1716
const readFile = (subpath: string) => fs.readFileSync(path.resolve(__dirname, subpath));
1817

__tests__/defaultPaymaster.test.ts

+175
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
import { RpcError } from '../src';
2+
import { PaymasterRpc } from '../src/paymaster/rpc';
3+
import fetchMock from '../src/utils/fetchPonyfill';
4+
import { signatureToHexArray } from '../src/utils/stark';
5+
import { OutsideExecutionTypedData } from '../src/types/api/paymaster-rpc-spec/nonspec';
6+
7+
jest.mock('../src/utils/fetchPonyfill');
8+
jest.mock('../src/utils/stark', () => ({
9+
signatureToHexArray: jest.fn(() => ['0x1', '0x2']),
10+
}));
11+
jest.mock('../src/utils/paymaster', () => ({
12+
getDefaultPaymasterNodeUrl: jest.fn(() => 'https://mock-node-url'),
13+
}));
14+
15+
describe('PaymasterRpc', () => {
16+
const mockFetch = fetchMock as jest.Mock;
17+
18+
beforeEach(() => {
19+
jest.clearAllMocks();
20+
});
21+
22+
describe('constructor', () => {
23+
it('should initialize with default values', () => {
24+
// When
25+
const client = new PaymasterRpc();
26+
27+
// Then
28+
expect(client.nodeUrl).toBe('https://mock-node-url');
29+
expect(client.requestId).toBe(0);
30+
});
31+
});
32+
33+
describe('isAvailable', () => {
34+
it('should return true when paymaster is available', async () => {
35+
// Given
36+
const client = new PaymasterRpc();
37+
mockFetch.mockResolvedValueOnce({
38+
json: async () => ({ result: true }),
39+
});
40+
41+
// When
42+
const result = await client.isAvailable();
43+
44+
// Then
45+
expect(result).toBe(true);
46+
expect(mockFetch).toHaveBeenCalledWith(
47+
expect.any(String),
48+
expect.objectContaining({
49+
body: expect.stringContaining('"method":"paymaster_isAvailable"'),
50+
})
51+
);
52+
});
53+
54+
it('should return false when paymaster is not available', async () => {
55+
// Given
56+
const client = new PaymasterRpc();
57+
mockFetch.mockResolvedValueOnce({
58+
json: async () => ({ result: false }),
59+
});
60+
61+
// When
62+
const result = await client.isAvailable();
63+
64+
// Then
65+
expect(result).toBe(false);
66+
});
67+
68+
it('should throw RpcError when RPC returns error', async () => {
69+
// Given
70+
const client = new PaymasterRpc();
71+
mockFetch.mockResolvedValueOnce({
72+
json: async () => ({ error: { code: -32000, message: 'RPC failure' } }),
73+
});
74+
75+
// When / Then
76+
await expect(client.isAvailable()).rejects.toThrow(RpcError);
77+
});
78+
79+
it('should throw on network error', async () => {
80+
// Given
81+
const client = new PaymasterRpc();
82+
mockFetch.mockRejectedValueOnce(new Error('Network down'));
83+
84+
// When / Then
85+
await expect(client.isAvailable()).rejects.toThrow('Network down');
86+
});
87+
});
88+
89+
describe('buildTypedData', () => {
90+
it('should return typedData and parsed tokenAmountAndPrice', async () => {
91+
// Given
92+
const client = new PaymasterRpc();
93+
const mockCall = {
94+
contractAddress: '0xabc',
95+
entrypoint: 'transfer',
96+
calldata: ['0x1', '0x2'],
97+
};
98+
99+
mockFetch.mockResolvedValueOnce({
100+
json: async () => ({
101+
result: {
102+
typed_data: { domain: {}, message: {}, types: {} },
103+
token_amount_and_price: {
104+
estimated_amount: '0x1234',
105+
price_in_strk: '0x5678',
106+
},
107+
},
108+
}),
109+
});
110+
111+
// When
112+
const result = await client.buildTypedData('0xuser', [mockCall]);
113+
114+
// Then
115+
expect(result.tokenAmountAndPrice.estimatedAmount).toBe(BigInt(0x1234));
116+
expect(result.tokenAmountAndPrice.priceInStrk).toBe(BigInt(0x5678));
117+
expect(result.typedData).toBeDefined();
118+
});
119+
});
120+
121+
describe('execute', () => {
122+
it('should send execution request and return transaction hash', async () => {
123+
// Given
124+
const client = new PaymasterRpc();
125+
const mockSignature = ['0x1', '0x2'];
126+
const mockTypedData: OutsideExecutionTypedData = {
127+
domain: {},
128+
types: {},
129+
primaryType: '',
130+
message: {
131+
caller: '0xcaller',
132+
nonce: '0xnonce',
133+
execute_after: '0x1',
134+
execute_before: '0x2',
135+
calls_len: '0x0',
136+
calls: [],
137+
},
138+
};
139+
140+
mockFetch.mockResolvedValueOnce({
141+
json: async () => ({
142+
result: {
143+
transaction_hash: '0xaaa',
144+
execution_result: 'ok',
145+
},
146+
}),
147+
});
148+
149+
// When
150+
const result = await client.execute('0xuser', mockTypedData, mockSignature);
151+
152+
// Then
153+
expect(signatureToHexArray).toHaveBeenCalledWith(mockSignature);
154+
expect(result.transaction_hash).toBe('0xaaa');
155+
});
156+
});
157+
158+
describe('getSupportedTokensAndPrices', () => {
159+
it('should return supported tokens and prices', async () => {
160+
// Given
161+
const client = new PaymasterRpc();
162+
const expected = { tokens: [], prices: {} };
163+
164+
mockFetch.mockResolvedValueOnce({
165+
json: async () => ({ result: expected }),
166+
});
167+
168+
// When
169+
const result = await client.getSupportedTokensAndPrices();
170+
171+
// Then
172+
expect(result).toEqual(expected);
173+
});
174+
});
175+
});

0 commit comments

Comments
 (0)