11import type { Provider , TransactionReceipt , TransactionRequest , TransactionResponse } from '@ethersproject/abstract-provider' ;
22import { Signer } from '@ethersproject/abstract-signer' ;
33import { BigNumber } from '@ethersproject/bignumber' ;
4- import type { Deferrable } from '@ethersproject/properties' ;
4+ import { hexlify } from '@ethersproject/bytes' ;
5+ import { resolveProperties , type Deferrable } from '@ethersproject/properties' ;
56import { StaticJsonRpcProvider } from '@ethersproject/providers' ;
67import { Wallet } from '@ethersproject/wallet' ;
78import { type Address } from 'viem' ;
@@ -15,9 +16,9 @@ import {
1516 RNBW_DECIMALS ,
1617 RNBW_TOKEN_ADDRESS ,
1718 RNBW_TOKEN_UNIQUE_ID ,
19+ STAKING_APPROVAL_GAS_LIMIT ,
1820 STAKING_CHAIN_ID ,
1921 STAKING_CONTRACT_ADDRESS ,
20- STAKING_GAS_LIMIT ,
2122} from '../constants' ;
2223import { executeStakeRnbw } from './executeStakeRnbw' ;
2324
@@ -76,18 +77,22 @@ jest.mock('./stakeRnbwCalls', () => ({
7677 buildStakeRnbwExecutionPlan : ( params : unknown ) => mockBuildStakeRnbwExecutionPlan ( params ) ,
7778} ) ) ;
7879
79- const ACCOUNT = '0x3333333333333333333333333333333333333333' satisfies Address ;
80+ const ACCOUNT : Address = '0x3333333333333333333333333333333333333333' ;
8081const PRIVATE_KEY = '0x0123456789012345678901234567890123456789012345678901234567890123' ;
8182const STAKE_AMOUNT_RAW = '1000000000000000000' ;
8283const APPROVAL_TX_HASH = '0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' ;
8384const TX_HASH = '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' ;
8485
86+ const APPROVAL_CALL : Call = { data : '0x095ea7b3' , to : RNBW_TOKEN_ADDRESS , value : 0n } ;
87+ const STAKE_CALL : Call = { data : '0xa694fc3a' , to : STAKING_CONTRACT_ADDRESS , value : 0n } ;
88+ const SPONSORED_REQUIREMENTS : CallsRequirements = { atomic : 'required' , fees : { payer : 'sponsor' } } ;
89+ const SPONSORED_PLAN = { calls : [ APPROVAL_CALL , STAKE_CALL ] , requirements : SPONSORED_REQUIREMENTS } ;
90+
91+ const GAS_PARAMS = { maxFeePerGas : '6000000' , maxPriorityFeePerGas : '1000000' } ;
92+ const ESTIMATED_STAKE_GAS_LIMIT = BigNumber . from ( 103_406 ) ;
93+
8594const provider = new StaticJsonRpcProvider ( 'http://127.0.0.1:8545' , STAKING_CHAIN_ID ) ;
8695const signer = new Wallet ( PRIVATE_KEY , provider ) ;
87- const APPROVAL_CALL = { data : '0x095ea7b3' , to : RNBW_TOKEN_ADDRESS , value : 0n } satisfies Call ;
88- const STAKE_CALL = { data : '0xa694fc3a' , to : STAKING_CONTRACT_ADDRESS , value : 0n } satisfies Call ;
89- const SPONSORED_REQUIREMENTS = { atomic : 'required' , fees : { payer : 'sponsor' } } satisfies CallsRequirements ;
90- const SPONSORED_PLAN = { calls : [ APPROVAL_CALL , STAKE_CALL ] , requirements : SPONSORED_REQUIREMENTS } ;
9196
9297class TestHardwareSigner extends Signer {
9398 readonly provider : Provider ;
@@ -165,24 +170,39 @@ function buildReceipt(transactionHash = TX_HASH): TransactionReceipt {
165170}
166171
167172function buildTransactionResponse ( {
168- call = STAKE_CALL ,
169173 hash = TX_HASH ,
174+ nonce = 1 ,
175+ transaction,
170176} : {
171- call ?: Call ;
172177 hash ?: string ;
173- } = { } ) : TransactionResponse {
174- return {
178+ nonce ?: number ;
179+ transaction : Deferrable < TransactionRequest > ;
180+ } ) : Promise < TransactionResponse > {
181+ return resolveProperties ( transaction ) . then ( request => ( {
175182 chainId : STAKING_CHAIN_ID ,
176183 confirmations : 0 ,
177- data : call . data ,
184+ data : request . data ? hexlify ( request . data ) : '0x' ,
178185 from : ACCOUNT ,
179- gasLimit : BigNumber . from ( 21_000 ) ,
186+ gasLimit : BigNumber . from ( request . gasLimit ?? 21_000 ) ,
187+ gasPrice : BigNumber . from ( 1 ) ,
180188 hash,
181- nonce : 1 ,
182- to : call . to ,
183- value : BigNumber . from ( 0 ) ,
189+ maxFeePerGas : request . maxFeePerGas == null ? undefined : BigNumber . from ( request . maxFeePerGas ) ,
190+ maxPriorityFeePerGas : request . maxPriorityFeePerGas == null ? undefined : BigNumber . from ( request . maxPriorityFeePerGas ) ,
191+ nonce,
192+ to : request . to ,
193+ value : BigNumber . from ( request . value ?? 0 ) ,
184194 wait : ( ) => Promise . resolve ( buildReceipt ( hash ) ) ,
185- } ;
195+ } ) ) ;
196+ }
197+
198+ function mockSubmittedTransactions ( signer : TestHardwareSigner , submissions : { hash : string ; nonce : number } [ ] ) : void {
199+ let index = 0 ;
200+ signer . sendTransactionMock . mockImplementation ( transaction => {
201+ const submission = submissions [ index ] ;
202+ if ( ! submission ) throw new Error ( `Unexpected staking transaction ${ index + 1 } ` ) ;
203+ index += 1 ;
204+ return buildTransactionResponse ( { ...submission , transaction } ) ;
205+ } ) ;
186206}
187207
188208async function prepareCalls ( plan : unknown ) : Promise < PreparedCallsExecution > {
@@ -210,8 +230,15 @@ async function waitForMockCalls(mock: { mock: { calls: unknown[][] } }, count: n
210230 if ( mock . mock . calls . length < count ) throw new Error ( `Expected mock to be called ${ count } times` ) ;
211231}
212232
233+ async function submittedRequest ( signer : TestHardwareSigner , transactionNumber : number ) : Promise < TransactionRequest > {
234+ const call = signer . sendTransactionMock . mock . calls [ transactionNumber - 1 ] ;
235+ if ( ! call ) throw new Error ( `Missing staking transaction ${ transactionNumber } ` ) ;
236+ return resolveProperties ( call [ 0 ] ) ;
237+ }
238+
213239describe ( 'executeStakeRnbw' , ( ) => {
214240 beforeEach ( ( ) => {
241+ jest . restoreAllMocks ( ) ;
215242 jest . clearAllMocks ( ) ;
216243 mockBuildStakeRnbwCalls . mockResolvedValue ( [ STAKE_CALL ] ) ;
217244 mockBuildStakeRnbwExecutionPlan . mockResolvedValue ( { calls : [ STAKE_CALL ] } ) ;
@@ -251,6 +278,7 @@ describe('executeStakeRnbw', () => {
251278 const result = await executeStakeRnbw ( {
252279 address : ACCOUNT ,
253280 asset : rnbwAsset ,
281+ gasParams : GAS_PARAMS ,
254282 preparedCalls,
255283 provider,
256284 signer,
@@ -292,6 +320,7 @@ describe('executeStakeRnbw', () => {
292320 const result = await executeStakeRnbw ( {
293321 address : ACCOUNT ,
294322 asset : rnbwAsset ,
323+ gasParams : GAS_PARAMS ,
295324 preparedCalls,
296325 provider,
297326 signer,
@@ -360,6 +389,7 @@ describe('executeStakeRnbw', () => {
360389 const result = await executeStakeRnbw ( {
361390 address : ACCOUNT ,
362391 asset : rnbwAsset ,
392+ gasParams : GAS_PARAMS ,
363393 preparedCalls : null ,
364394 provider,
365395 signer,
@@ -379,22 +409,28 @@ describe('executeStakeRnbw', () => {
379409 ) ;
380410 } ) ;
381411
382- it ( 'waits for hardware-wallet approval before submitting the stake transaction ' , async ( ) => {
412+ it ( 'uses selected gas params, estimates call gas, and waits for hardware-wallet approval before staking ' , async ( ) => {
383413 const hardwareSigner = new TestHardwareSigner ( provider ) ;
384414 const approvalConfirmation = defer < TransactionReceipt > ( ) ;
385415 const waitForTransaction = jest . spyOn ( provider , 'waitForTransaction' ) . mockImplementation ( hash => {
386416 if ( hash === APPROVAL_TX_HASH ) return approvalConfirmation . promise ;
387417 return Promise . resolve ( buildReceipt ( hash ) ) ;
388418 } ) ;
419+ jest
420+ . spyOn ( provider , 'estimateGas' )
421+ . mockRejectedValueOnce ( new Error ( 'approval estimate failed' ) )
422+ . mockResolvedValueOnce ( ESTIMATED_STAKE_GAS_LIMIT ) ;
389423
390424 mockBuildStakeRnbwCalls . mockResolvedValue ( [ APPROVAL_CALL , STAKE_CALL ] ) ;
391- hardwareSigner . sendTransactionMock
392- . mockResolvedValueOnce ( buildTransactionResponse ( { call : APPROVAL_CALL , hash : APPROVAL_TX_HASH } ) )
393- . mockResolvedValueOnce ( buildTransactionResponse ( { call : STAKE_CALL , hash : TX_HASH } ) ) ;
425+ mockSubmittedTransactions ( hardwareSigner , [
426+ { hash : APPROVAL_TX_HASH , nonce : 1 } ,
427+ { hash : TX_HASH , nonce : 2 } ,
428+ ] ) ;
394429
395430 const execution = executeStakeRnbw ( {
396431 address : ACCOUNT ,
397432 asset : rnbwAsset ,
433+ gasParams : GAS_PARAMS ,
398434 preparedCalls : null ,
399435 provider,
400436 signer : hardwareSigner ,
@@ -404,7 +440,12 @@ describe('executeStakeRnbw', () => {
404440 await waitForMockCalls ( waitForTransaction , 1 ) ;
405441
406442 expect ( hardwareSigner . sendTransactionMock ) . toHaveBeenCalledTimes ( 1 ) ;
407- expect ( hardwareSigner . sendTransactionMock ) . toHaveBeenNthCalledWith ( 1 , APPROVAL_CALL ) ;
443+ await expect ( submittedRequest ( hardwareSigner , 1 ) ) . resolves . toMatchObject ( {
444+ to : RNBW_TOKEN_ADDRESS ,
445+ value : 0n ,
446+ gasLimit : STAKING_APPROVAL_GAS_LIMIT ,
447+ ...GAS_PARAMS ,
448+ } ) ;
408449 expect ( waitForTransaction ) . toHaveBeenCalledWith ( APPROVAL_TX_HASH , 1 , time . minutes ( 2 ) ) ;
409450
410451 approvalConfirmation . resolve ( buildReceipt ( APPROVAL_TX_HASH ) ) ;
@@ -417,19 +458,53 @@ describe('executeStakeRnbw', () => {
417458 chainId : STAKING_CHAIN_ID ,
418459 transaction : expect . objectContaining ( {
419460 hash : TX_HASH ,
420- gasLimit : BigNumber . from ( 21_000 ) ,
421- nonce : 1 ,
461+ gasLimit : ESTIMATED_STAKE_GAS_LIMIT ,
462+ nonce : 2 ,
422463 type : 'stake' ,
423464 } ) ,
424465 } ) ;
425- expect ( hardwareSigner . sendTransactionMock ) . toHaveBeenNthCalledWith ( 2 , {
426- ...STAKE_CALL ,
427- gasLimit : STAKING_GAS_LIMIT ,
466+ await expect ( submittedRequest ( hardwareSigner , 2 ) ) . resolves . toMatchObject ( {
467+ to : STAKING_CONTRACT_ADDRESS ,
468+ value : 0n ,
469+ gasLimit : ESTIMATED_STAKE_GAS_LIMIT ,
470+ ...GAS_PARAMS ,
428471 } ) ;
429472
430473 await result . waitForConfirmation ( ) ;
431474
432475 expect ( waitForTransaction ) . toHaveBeenCalledTimes ( 2 ) ;
433476 expect ( waitForTransaction ) . toHaveBeenLastCalledWith ( TX_HASH , 1 , time . minutes ( 2 ) ) ;
434477 } ) ;
478+
479+ it ( 'submits one hardware-wallet stake transaction when approval is not required' , async ( ) => {
480+ const hardwareSigner = new TestHardwareSigner ( provider ) ;
481+ const waitForTransaction = jest . spyOn ( provider , 'waitForTransaction' ) . mockResolvedValue ( buildReceipt ( TX_HASH ) ) ;
482+ jest . spyOn ( provider , 'estimateGas' ) . mockResolvedValue ( ESTIMATED_STAKE_GAS_LIMIT ) ;
483+
484+ mockBuildStakeRnbwCalls . mockResolvedValue ( [ STAKE_CALL ] ) ;
485+ mockSubmittedTransactions ( hardwareSigner , [ { hash : TX_HASH , nonce : 2 } ] ) ;
486+
487+ const execution = await executeStakeRnbw ( {
488+ address : ACCOUNT ,
489+ asset : rnbwAsset ,
490+ gasParams : GAS_PARAMS ,
491+ preparedCalls : null ,
492+ provider,
493+ signer : hardwareSigner ,
494+ stakeAmountRaw : STAKE_AMOUNT_RAW ,
495+ } ) ;
496+
497+ expect ( hardwareSigner . sendTransactionMock ) . toHaveBeenCalledTimes ( 1 ) ;
498+ await expect ( submittedRequest ( hardwareSigner , 1 ) ) . resolves . toMatchObject ( {
499+ to : STAKING_CONTRACT_ADDRESS ,
500+ value : 0n ,
501+ gasLimit : ESTIMATED_STAKE_GAS_LIMIT ,
502+ ...GAS_PARAMS ,
503+ } ) ;
504+ expect ( waitForTransaction ) . not . toHaveBeenCalled ( ) ;
505+
506+ await execution . waitForConfirmation ( ) ;
507+
508+ expect ( waitForTransaction ) . toHaveBeenCalledWith ( TX_HASH , 1 , time . minutes ( 2 ) ) ;
509+ } ) ;
435510} ) ;
0 commit comments