1
- import { Account , Signature , Call , PaymasterDetails , PaymasterRpc } from '../src' ;
1
+ import { OutsideCallV2 , OutsideExecutionTypedDataV2 } from 'starknet-types-08' ;
2
+ import { Account , Signature , Call , PaymasterDetails , OutsideExecutionVersion } from '../src' ;
3
+ import { getSelectorFromName } from '../src/utils/hash' ;
2
4
3
5
jest . mock ( '../src/paymaster/rpc' ) ;
4
6
5
7
describe ( 'Account - Paymaster integration' , ( ) => {
8
+ let account : Account | null = null ;
6
9
const mockBuildTransaction = jest . fn ( ) ;
10
+ const mockMaliciousBuildTransactionChangeToken = jest . fn ( ) ;
11
+ const mockMaliciousBuildTransactionChangeFees = jest . fn ( ) ;
12
+ const mockMaliciousBuildTransactionAddedCalls = jest . fn ( ) ;
7
13
const mockExecuteTransaction = jest . fn ( ) ;
14
+ const mockGetSnip9Version = jest . fn ( ) ;
8
15
const mockSignMessage = jest . fn ( ) ;
9
16
10
- const fakeTypedData = {
17
+ const fakeSignature : Signature = [ '0x1' , '0x2' ] ;
18
+ const originalCalls : Call [ ] = [
19
+ { contractAddress : '0x123' , entrypoint : 'transfer' , calldata : [ ] } ,
20
+ ] ;
21
+
22
+ const originalCallsAsOutsideCalls : OutsideCallV2 [ ] = [
23
+ {
24
+ To : '0x123' ,
25
+ Selector : getSelectorFromName ( 'transfer' ) ,
26
+ Calldata : [ ] ,
27
+ } ,
28
+ ] ;
29
+
30
+ const typedData : OutsideExecutionTypedDataV2 = {
11
31
types : { } ,
12
32
domain : { } ,
13
33
primaryType : '' ,
14
34
message : {
15
- caller : '0xcaller' ,
16
- nonce : '0xnonce' ,
17
- execute_after : '0x1' ,
18
- execute_before : '0x2' ,
19
- calls_len : '0x0' ,
20
- calls : [ ] ,
35
+ Caller : '0xcaller' ,
36
+ Nonce : '0xnonce' ,
37
+ 'Execute After' : '0x1' ,
38
+ 'Execute Before' : '0x2' ,
39
+ Calls : [
40
+ ...originalCallsAsOutsideCalls ,
41
+ {
42
+ To : '0x456' ,
43
+ Selector : getSelectorFromName ( 'transfer' ) ,
44
+ Calldata : [ '0xcaller' , '1200' , '0' ] ,
45
+ } ,
46
+ ] ,
21
47
} ,
22
48
} ;
23
49
24
- const fakeSignature : Signature = [ '0x1' , '0x2' ] ;
25
- const calls : Call [ ] = [ { contractAddress : '0x123' , entrypoint : 'transfer' , calldata : [ ] } ] ;
26
-
27
50
const paymasterResponse = {
28
51
type : 'invoke' ,
29
- typed_data : fakeTypedData ,
52
+ typed_data : typedData ,
30
53
parameters : {
31
54
version : '0x1' ,
32
55
feeMode : { mode : 'default' , gasToken : '0x456' } ,
@@ -40,41 +63,109 @@ describe('Account - Paymaster integration', () => {
40
63
} ,
41
64
} ;
42
65
43
- const mockPaymaster = ( ) =>
44
- ( {
45
- buildTransaction : mockBuildTransaction ,
46
- executeTransaction : mockExecuteTransaction ,
47
- } ) as unknown as PaymasterRpc ;
48
-
49
- const setupAccount = ( ) =>
50
- new Account (
51
- { } ,
52
- '0xabc' ,
53
- { signMessage : mockSignMessage . mockResolvedValue ( fakeSignature ) } as any ,
54
- undefined ,
55
- undefined ,
56
- mockPaymaster ( )
57
- ) ;
66
+ const maliciousTypedDataChangeToken : OutsideExecutionTypedDataV2 = {
67
+ ...typedData ,
68
+ message : {
69
+ ...typedData . message ,
70
+ Calls : [
71
+ ...originalCallsAsOutsideCalls ,
72
+ {
73
+ To : '0x4567' ,
74
+ Selector : getSelectorFromName ( 'transfer' ) ,
75
+ Calldata : [ '0xcaller' , '1200' , '0' ] ,
76
+ } ,
77
+ ] ,
78
+ } ,
79
+ } ;
80
+
81
+ const maliciousTypedDataChangeFees : OutsideExecutionTypedDataV2 = {
82
+ ...typedData ,
83
+ message : {
84
+ ...typedData . message ,
85
+ Calls : [
86
+ ...originalCallsAsOutsideCalls ,
87
+ {
88
+ To : '0x456' ,
89
+ Selector : getSelectorFromName ( 'transfer' ) ,
90
+ Calldata : [ '0xcaller' , '13000' , '0' ] ,
91
+ } ,
92
+ ] ,
93
+ } ,
94
+ } ;
95
+
96
+ const maliciousTypedDataAddedCalls : OutsideExecutionTypedDataV2 = {
97
+ ...typedData ,
98
+ message : {
99
+ ...typedData . message ,
100
+ Calls : [
101
+ ...originalCallsAsOutsideCalls ,
102
+ {
103
+ To : '0x456' ,
104
+ Selector : getSelectorFromName ( 'transfer' ) ,
105
+ Calldata : [ '0xcaller' , '13000' , '0' ] ,
106
+ } ,
107
+ {
108
+ To : '0x456' ,
109
+ Selector : getSelectorFromName ( 'transfer' ) ,
110
+ Calldata : [ '0xcaller' , '13000' , '0' ] ,
111
+ } ,
112
+ ] ,
113
+ } ,
114
+ } ;
115
+
116
+ const maliciousPaymasterResponseChangeToken = {
117
+ ...paymasterResponse ,
118
+ typed_data : maliciousTypedDataChangeToken ,
119
+ } ;
120
+ const maliciousPaymasterResponseChangeFees = {
121
+ ...paymasterResponse ,
122
+ typed_data : maliciousTypedDataChangeFees ,
123
+ } ;
124
+ const maliciousPaymasterResponseAddedCalls = {
125
+ ...paymasterResponse ,
126
+ typed_data : maliciousTypedDataAddedCalls ,
127
+ } ;
128
+
129
+ const getAccount = ( ) => {
130
+ if ( ! account ) {
131
+ account = new Account (
132
+ { } ,
133
+ '0xabc' ,
134
+ { signMessage : mockSignMessage . mockResolvedValue ( fakeSignature ) } as any ,
135
+ undefined ,
136
+ undefined ,
137
+ undefined
138
+ ) ;
139
+ // account object is instanciate in the constructor, we need to mock the paymaster methods after paymaster object is instanciate
140
+ account . paymaster . buildTransaction = mockBuildTransaction ;
141
+ account . paymaster . executeTransaction = mockExecuteTransaction ;
142
+ }
143
+ return account ;
144
+ } ;
58
145
59
146
beforeEach ( ( ) => {
60
147
jest . clearAllMocks ( ) ;
61
- ( PaymasterRpc as jest . Mock ) . mockImplementation ( mockPaymaster ) ;
148
+ jest . spyOn ( getAccount ( ) , 'getSnip9Version' ) . mockImplementation ( mockGetSnip9Version ) ;
62
149
mockBuildTransaction . mockResolvedValue ( paymasterResponse ) ;
150
+ mockMaliciousBuildTransactionChangeToken . mockResolvedValue (
151
+ maliciousPaymasterResponseChangeToken
152
+ ) ;
153
+ mockMaliciousBuildTransactionChangeFees . mockResolvedValue ( maliciousPaymasterResponseChangeFees ) ;
154
+ mockMaliciousBuildTransactionAddedCalls . mockResolvedValue ( maliciousPaymasterResponseAddedCalls ) ;
63
155
mockExecuteTransaction . mockResolvedValue ( { transaction_hash : '0x123' } ) ;
156
+ mockGetSnip9Version . mockResolvedValue ( OutsideExecutionVersion . V2 ) ;
64
157
} ) ;
65
158
66
159
describe ( 'estimatePaymasterTransactionFee' , ( ) => {
67
160
it ( 'should return estimated transaction fee from paymaster' , async ( ) => {
68
- const account = setupAccount ( ) ;
69
-
70
- const result = await account . estimatePaymasterTransactionFee ( calls , {
161
+ const result = await getAccount ( ) . estimatePaymasterTransactionFee ( originalCalls , {
71
162
feeMode : { mode : 'default' , gasToken : '0x456' } ,
72
163
} ) ;
73
164
74
165
expect ( mockBuildTransaction ) . toHaveBeenCalledWith (
75
166
{
76
167
type : 'invoke' ,
77
- invoke : { userAddress : '0xabc' , calls } ,
168
+ invoke : { userAddress : '0xabc' , calls : originalCalls } ,
78
169
} ,
79
170
{
80
171
version : '0x1' ,
@@ -88,33 +179,21 @@ describe('Account - Paymaster integration', () => {
88
179
} ) ;
89
180
90
181
describe ( 'executePaymasterTransaction' , ( ) => {
91
- it ( 'should throw if token price exceeds maxPriceInStrk' , async ( ) => {
92
- const account = setupAccount ( ) ;
93
- const details : PaymasterDetails = {
94
- feeMode : { mode : 'default' , gasToken : '0x456' } ,
95
- } ;
96
-
97
- await expect ( account . executePaymasterTransaction ( calls , details , 100n ) ) . rejects . toThrow (
98
- 'Gas token price is too high'
99
- ) ;
100
- } ) ;
101
-
102
- it ( 'should sign and execute transaction via paymaster' , async ( ) => {
103
- const account = setupAccount ( ) ;
182
+ it ( 'should sign and execute transaction via paymaster without checking gas fees' , async ( ) => {
104
183
const details : PaymasterDetails = {
105
184
feeMode : { mode : 'default' , gasToken : '0x456' } ,
106
185
} ;
107
186
108
- const result = await account . executePaymasterTransaction ( calls , details ) ;
187
+ const result = await getAccount ( ) . executePaymasterTransaction ( originalCalls , details ) ;
109
188
110
189
expect ( mockBuildTransaction ) . toHaveBeenCalledTimes ( 1 ) ;
111
- expect ( mockSignMessage ) . toHaveBeenCalledWith ( fakeTypedData , '0xabc' ) ;
190
+ expect ( mockSignMessage ) . toHaveBeenCalledWith ( typedData , '0xabc' ) ;
112
191
expect ( mockExecuteTransaction ) . toHaveBeenCalledWith (
113
192
{
114
193
type : 'invoke' ,
115
194
invoke : {
116
195
userAddress : '0xabc' ,
117
- typedData : fakeTypedData ,
196
+ typedData,
118
197
signature : [ '0x1' , '0x2' ] ,
119
198
} ,
120
199
} ,
@@ -126,5 +205,73 @@ describe('Account - Paymaster integration', () => {
126
205
) ;
127
206
expect ( result ) . toEqual ( { transaction_hash : '0x123' } ) ;
128
207
} ) ;
208
+
209
+ it ( 'should sign and execute transaction via paymaster' , async ( ) => {
210
+ const details : PaymasterDetails = {
211
+ feeMode : { mode : 'default' , gasToken : '0x456' } ,
212
+ } ;
213
+
214
+ const result = await getAccount ( ) . executePaymasterTransaction (
215
+ originalCalls ,
216
+ details ,
217
+ '0x123456'
218
+ ) ;
219
+ expect ( result ) . toEqual ( { transaction_hash : '0x123' } ) ;
220
+ } ) ;
221
+
222
+ it ( 'should not throw if token price exceeds maxPriceInGasToken but transaction is sponsored' , async ( ) => {
223
+ const details : PaymasterDetails = {
224
+ feeMode : { mode : 'sponsored' } ,
225
+ } ;
226
+ const result = await getAccount ( ) . executePaymasterTransaction (
227
+ originalCalls ,
228
+ details ,
229
+ '0x123'
230
+ ) ;
231
+ expect ( result ) . toEqual ( {
232
+ transaction_hash : '0x123' ,
233
+ } ) ;
234
+ } ) ;
235
+
236
+ it ( 'should throw if token price exceeds maxPriceInGasToken' , async ( ) => {
237
+ const details : PaymasterDetails = {
238
+ feeMode : { mode : 'default' , gasToken : '0x456' } ,
239
+ } ;
240
+
241
+ await expect (
242
+ getAccount ( ) . executePaymasterTransaction ( originalCalls , details , '0x123' )
243
+ ) . rejects . toThrow ( 'Gas token price is too high' ) ;
244
+ } ) ;
245
+
246
+ it ( 'should throw if Gas token value is not equal to the provided gas fees' , async ( ) => {
247
+ const details : PaymasterDetails = {
248
+ feeMode : { mode : 'default' , gasToken : '0x456' } ,
249
+ } ;
250
+ getAccount ( ) . paymaster . buildTransaction = mockMaliciousBuildTransactionChangeFees ;
251
+ await expect (
252
+ getAccount ( ) . executePaymasterTransaction ( originalCalls , details , '0x123456' )
253
+ ) . rejects . toThrow ( 'Gas token value is not equal to the provided gas fees' ) ;
254
+ } ) ;
255
+
256
+ it ( 'should throw if Gas token address is not equal to the provided gas token' , async ( ) => {
257
+ const details : PaymasterDetails = {
258
+ feeMode : { mode : 'default' , gasToken : '0x456' } ,
259
+ } ;
260
+ getAccount ( ) . paymaster . buildTransaction = mockMaliciousBuildTransactionChangeToken ;
261
+ await expect (
262
+ getAccount ( ) . executePaymasterTransaction ( originalCalls , details , '0x123456' )
263
+ ) . rejects . toThrow ( 'Gas token address is not equal to the provided gas token' ) ;
264
+ } ) ;
265
+
266
+ it ( 'should throw if provided calls are not strictly equal to the returned calls' , async ( ) => {
267
+ const details : PaymasterDetails = {
268
+ feeMode : { mode : 'default' , gasToken : '0x456' } ,
269
+ } ;
270
+ getAccount ( ) . paymaster . buildTransaction = mockMaliciousBuildTransactionAddedCalls ;
271
+
272
+ await expect (
273
+ getAccount ( ) . executePaymasterTransaction ( originalCalls , details , '0x123456' )
274
+ ) . rejects . toThrow ( 'Provided calls are not strictly equal to the returned calls' ) ;
275
+ } ) ;
129
276
} ) ;
130
277
} ) ;
0 commit comments