@@ -43,7 +43,9 @@ describe("Safe API Action Provider", () => {
43
43
let mockSafeApiKit : jest . Mocked < SafeApiKit > ;
44
44
45
45
const MOCK_SAFE_ADDRESS = "0xe6b2af36b3bb8d47206a129ff11d5a2de2a63c83" ;
46
- const MOCK_NETWORK = "base-sepolia" ;
46
+ const MOCK_DELEGATE_ADDRESS = "0x1234567890123456789012345678901234567890" ;
47
+ const MOCK_TOKEN_ADDRESS = "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd" ;
48
+ const MOCK_NETWORK = "ethereum-sepolia" ;
47
49
const MOCK_BALANCE = BigInt ( 1000000000000000000 ) ; // 1 ETH in wei
48
50
49
51
beforeEach ( ( ) => {
@@ -144,4 +146,295 @@ describe("Safe API Action Provider", () => {
144
146
expect ( result ) . toBe ( false ) ;
145
147
} ) ;
146
148
} ) ;
149
+
150
+ describe ( "getAllowanceInfo" , ( ) => {
151
+ beforeEach ( ( ) => {
152
+ // Mock additional wallet methods needed for getAllowanceInfo
153
+ mockWallet . readContract = jest . fn ( ) ;
154
+
155
+ // Mock the getAllowanceModuleDeployment function
156
+ jest . mock ( "@safe-global/safe-modules-deployments" , ( ) => ( {
157
+ getAllowanceModuleDeployment : jest . fn ( ) . mockReturnValue ( {
158
+ networkAddresses : { "421614" : "0xallowanceModuleAddress" } ,
159
+ abi : [
160
+ {
161
+ name : "getTokens" ,
162
+ type : "function" ,
163
+ inputs : [ { type : "address" } , { type : "address" } ] ,
164
+ outputs : [ { type : "address[]" } ] ,
165
+ } ,
166
+ {
167
+ name : "getTokenAllowance" ,
168
+ type : "function" ,
169
+ inputs : [ { type : "address" } , { type : "address" } , { type : "address" } ] ,
170
+ outputs : [
171
+ { type : "uint256" } , // amount
172
+ { type : "uint256" } , // spent
173
+ { type : "uint256" } , // resetTimeMin
174
+ { type : "uint256" } , // lastResetMin
175
+ { type : "uint256" } , // nonce
176
+ ] ,
177
+ } ,
178
+ ] ,
179
+ } ) ,
180
+ } ) ) ;
181
+ } ) ;
182
+
183
+ it ( "should successfully get allowance info" , async ( ) => {
184
+ // Mock token list response
185
+ ( mockWallet . readContract as jest . Mock ) . mockImplementation ( params => {
186
+ if ( params . functionName === "getTokens" ) {
187
+ return [ MOCK_TOKEN_ADDRESS ] ;
188
+ } else if ( params . functionName === "getTokenAllowance" ) {
189
+ return [
190
+ BigInt ( 1000000000000000000 ) , // amount: 1 token
191
+ BigInt ( 300000000000000000 ) , // spent: 0.3 token
192
+ BigInt ( 1440 ) , // resetTimeMin: 24 hours
193
+ BigInt ( Math . floor ( Date . now ( ) / ( 60 * 1000 ) ) - 720 ) , // lastResetMin: 12 hours ago
194
+ BigInt ( 1 ) , // nonce
195
+ ] ;
196
+ } else if ( params . functionName === "symbol" ) {
197
+ return "TEST" ;
198
+ } else if ( params . functionName === "decimals" ) {
199
+ return 18 ;
200
+ } else if ( params . functionName === "balanceOf" ) {
201
+ return BigInt ( 5000000000000000000 ) ; // 5 tokens
202
+ }
203
+ } ) ;
204
+
205
+ const args = {
206
+ safeAddress : MOCK_SAFE_ADDRESS ,
207
+ delegateAddress : MOCK_DELEGATE_ADDRESS ,
208
+ } ;
209
+
210
+ const response = await actionProvider . getAllowanceInfo ( mockWallet , args ) ;
211
+
212
+ // Verify response contains expected information
213
+ expect ( response ) . toContain ( `Delegate ${ MOCK_DELEGATE_ADDRESS } has the following allowances` ) ;
214
+ expect ( response ) . toContain ( `TEST (${ MOCK_TOKEN_ADDRESS } )` ) ;
215
+ expect ( response ) . toContain ( "Current Safe balance: 5 TEST" ) ;
216
+ expect ( response ) . toContain ( "Allowance: 0.7 available of 1 total (0.3 spent)" ) ;
217
+ expect ( response ) . toContain ( "resets every 1440 minutes" ) ;
218
+ } ) ;
219
+
220
+ it ( "should handle case with no allowances" , async ( ) => {
221
+ // Mock empty token list response
222
+ ( mockWallet . readContract as jest . Mock ) . mockImplementation ( params => {
223
+ if ( params . functionName === "getTokens" ) {
224
+ return [ ] ; // No tokens with allowances
225
+ }
226
+ } ) ;
227
+
228
+ const args = {
229
+ safeAddress : MOCK_SAFE_ADDRESS ,
230
+ delegateAddress : MOCK_DELEGATE_ADDRESS ,
231
+ } ;
232
+
233
+ const response = await actionProvider . getAllowanceInfo ( mockWallet , args ) ;
234
+
235
+ // Verify response indicates no allowances
236
+ expect ( response ) . toBe (
237
+ `Get allowance: Delegate ${ MOCK_DELEGATE_ADDRESS } has no token allowances from Safe ${ MOCK_SAFE_ADDRESS } ` ,
238
+ ) ;
239
+ } ) ;
240
+
241
+ it ( "should handle errors when getting allowance info" , async ( ) => {
242
+ // Mock error when reading contract
243
+ const error = new Error ( "Failed to get allowance info" ) ;
244
+ ( mockWallet . readContract as jest . Mock ) . mockRejectedValue ( error ) ;
245
+
246
+ const args = {
247
+ safeAddress : MOCK_SAFE_ADDRESS ,
248
+ delegateAddress : MOCK_DELEGATE_ADDRESS ,
249
+ } ;
250
+
251
+ const response = await actionProvider . getAllowanceInfo ( mockWallet , args ) ;
252
+ expect ( response ) . toBe ( `Get allowance: Error getting allowance: ${ error . message } ` ) ;
253
+ } ) ;
254
+ } ) ;
255
+
256
+ describe ( "withdrawAllowance" , ( ) => {
257
+ beforeEach ( ( ) => {
258
+ // Mock wallet methods needed for withdrawAllowance
259
+ mockWallet . readContract = jest . fn ( ) ;
260
+ mockWallet . signHash = jest . fn ( ) ;
261
+ mockWallet . sendTransaction = jest . fn ( ) ;
262
+ mockWallet . waitForTransactionReceipt = jest . fn ( ) ;
263
+
264
+ // Mock the getAllowanceModuleDeployment function
265
+ jest . mock ( "@safe-global/safe-modules-deployments" , ( ) => ( {
266
+ getAllowanceModuleDeployment : jest . fn ( ) . mockReturnValue ( {
267
+ networkAddresses : { "11155111" : "0xallowanceModuleAddress" } , // Sepolia chain ID
268
+ abi : [
269
+ {
270
+ name : "getTokenAllowance" ,
271
+ type : "function" ,
272
+ inputs : [ { type : "address" } , { type : "address" } , { type : "address" } ] ,
273
+ outputs : [
274
+ { type : "uint256" } , // amount
275
+ { type : "uint256" } , // spent
276
+ { type : "uint256" } , // resetTimeMin
277
+ { type : "uint256" } , // lastResetMin
278
+ { type : "uint256" } , // nonce
279
+ ] ,
280
+ } ,
281
+ {
282
+ name : "generateTransferHash" ,
283
+ type : "function" ,
284
+ inputs : [
285
+ { type : "address" } , // safe
286
+ { type : "address" } , // token
287
+ { type : "address" } , // to
288
+ { type : "uint256" } , // amount
289
+ { type : "address" } , // paymentToken
290
+ { type : "uint256" } , // payment
291
+ { type : "uint256" } , // nonce
292
+ ] ,
293
+ outputs : [ { type : "bytes32" } ] ,
294
+ } ,
295
+ {
296
+ name : "executeAllowanceTransfer" ,
297
+ type : "function" ,
298
+ inputs : [
299
+ { type : "address" } , // safe
300
+ { type : "address" } , // token
301
+ { type : "address" } , // to
302
+ { type : "uint256" } , // amount
303
+ { type : "address" } , // paymentToken
304
+ { type : "uint256" } , // payment
305
+ { type : "address" } , // delegate
306
+ { type : "bytes" } , // signature
307
+ ] ,
308
+ outputs : [ ] ,
309
+ } ,
310
+ ] ,
311
+ } ) ,
312
+ } ) ) ;
313
+ } ) ;
314
+
315
+ it ( "should successfully withdraw tokens using allowance" , async ( ) => {
316
+ // Mock contract read responses
317
+ ( mockWallet . readContract as jest . Mock ) . mockImplementation ( params => {
318
+ if ( params . functionName === "getTokenAllowance" ) {
319
+ return [
320
+ BigInt ( 5000000000000000000 ) , // amount: 5 tokens
321
+ BigInt ( 1000000000000000000 ) , // spent: 1 token
322
+ BigInt ( 0 ) , // resetTimeMin: no reset
323
+ BigInt ( 0 ) , // lastResetMin: no reset
324
+ BigInt ( 3 ) , // nonce: 3
325
+ ] ;
326
+ } else if ( params . functionName === "generateTransferHash" ) {
327
+ return "0xmockhash123456789" ;
328
+ } else if ( params . functionName === "decimals" ) {
329
+ return 18 ;
330
+ } else if ( params . functionName === "symbol" ) {
331
+ return "TEST" ;
332
+ }
333
+ } ) ;
334
+
335
+ // Mock signature
336
+ ( mockWallet . signHash as jest . Mock ) . mockResolvedValue ( "0xmocksignature" ) ;
337
+
338
+ // Mock transaction sending
339
+ const mockTxHash = "0xmocktxhash123456789" ;
340
+ ( mockWallet . sendTransaction as jest . Mock ) . mockResolvedValue ( mockTxHash ) ;
341
+
342
+ // Mock transaction receipt
343
+ ( mockWallet . waitForTransactionReceipt as jest . Mock ) . mockResolvedValue ( {
344
+ transactionHash : mockTxHash ,
345
+ status : "success" ,
346
+ } ) ;
347
+
348
+ const args = {
349
+ safeAddress : MOCK_SAFE_ADDRESS ,
350
+ delegateAddress : MOCK_DELEGATE_ADDRESS ,
351
+ tokenAddress : MOCK_TOKEN_ADDRESS ,
352
+ amount : "2.5" , // 2.5 tokens
353
+ } ;
354
+
355
+ const response = await actionProvider . withdrawAllowance ( mockWallet , args ) ;
356
+
357
+ // Verify the response contains expected information
358
+ expect ( response ) . toContain ( `Successfully withdrew 2.5 TEST from Safe ${ MOCK_SAFE_ADDRESS } ` ) ;
359
+ expect ( response ) . toContain ( `Transaction hash: ${ mockTxHash } ` ) ;
360
+
361
+ // Verify the correct contract methods were called
362
+ expect ( mockWallet . readContract ) . toHaveBeenCalledWith (
363
+ expect . objectContaining ( {
364
+ functionName : "getTokenAllowance" ,
365
+ args : [ MOCK_SAFE_ADDRESS , MOCK_DELEGATE_ADDRESS , MOCK_TOKEN_ADDRESS ] ,
366
+ } ) ,
367
+ ) ;
368
+
369
+ expect ( mockWallet . readContract ) . toHaveBeenCalledWith (
370
+ expect . objectContaining ( {
371
+ functionName : "generateTransferHash" ,
372
+ } ) ,
373
+ ) ;
374
+
375
+ expect ( mockWallet . signHash ) . toHaveBeenCalledWith ( "0xmockhash123456789" ) ;
376
+
377
+ expect ( mockWallet . sendTransaction ) . toHaveBeenCalledWith (
378
+ expect . objectContaining ( {
379
+ to : expect . any ( String ) ,
380
+ data : expect . any ( String ) ,
381
+ value : BigInt ( 0 ) ,
382
+ } ) ,
383
+ ) ;
384
+ } ) ;
385
+
386
+ it ( "should handle errors when withdrawing allowance" , async ( ) => {
387
+ // Mock error when reading contract
388
+ const error = new Error ( "Insufficient allowance" ) ;
389
+ ( mockWallet . readContract as jest . Mock ) . mockRejectedValue ( error ) ;
390
+
391
+ const args = {
392
+ safeAddress : MOCK_SAFE_ADDRESS ,
393
+ delegateAddress : MOCK_DELEGATE_ADDRESS ,
394
+ tokenAddress : MOCK_TOKEN_ADDRESS ,
395
+ amount : "10" , // 10 tokens
396
+ } ;
397
+
398
+ const response = await actionProvider . withdrawAllowance ( mockWallet , args ) ;
399
+ expect ( response ) . toBe ( `Withdraw allowance: Error withdrawing allowance: ${ error . message } ` ) ;
400
+ } ) ;
401
+
402
+ it ( "should handle transaction failure" , async ( ) => {
403
+ // Mock successful contract reads
404
+ ( mockWallet . readContract as jest . Mock ) . mockImplementation ( params => {
405
+ if ( params . functionName === "getTokenAllowance" ) {
406
+ return [
407
+ BigInt ( 5000000000000000000 ) , // amount: 5 tokens
408
+ BigInt ( 1000000000000000000 ) , // spent: 1 token
409
+ BigInt ( 0 ) , // resetTimeMin: no reset
410
+ BigInt ( 0 ) , // lastResetMin: no reset
411
+ BigInt ( 3 ) , // nonce: 3
412
+ ] ;
413
+ } else if ( params . functionName === "generateTransferHash" ) {
414
+ return "0xmockhash123456789" ;
415
+ } else if ( params . functionName === "decimals" ) {
416
+ return 18 ;
417
+ } else if ( params . functionName === "symbol" ) {
418
+ return "TEST" ;
419
+ }
420
+ } ) ;
421
+
422
+ // Mock signature
423
+ ( mockWallet . signHash as jest . Mock ) . mockResolvedValue ( "0xmocksignature" ) ;
424
+
425
+ // Mock transaction sending failure
426
+ const txError = new Error ( "Transaction reverted" ) ;
427
+ ( mockWallet . sendTransaction as jest . Mock ) . mockRejectedValue ( txError ) ;
428
+
429
+ const args = {
430
+ safeAddress : MOCK_SAFE_ADDRESS ,
431
+ delegateAddress : MOCK_DELEGATE_ADDRESS ,
432
+ tokenAddress : MOCK_TOKEN_ADDRESS ,
433
+ amount : "2.5" , // 2.5 tokens
434
+ } ;
435
+
436
+ const response = await actionProvider . withdrawAllowance ( mockWallet , args ) ;
437
+ expect ( response ) . toBe ( `Withdraw allowance: Error withdrawing allowance: ${ txError . message } ` ) ;
438
+ } ) ;
439
+ } ) ;
147
440
} ) ;
0 commit comments