Skip to content

Commit 6da241d

Browse files
authored
Merge pull request #2358 from AmbireTech/feature/add-gas-field-to-advanced-form
Feature/add gas field to advanced form
2 parents 320e989 + 76264b6 commit 6da241d

13 files changed

Lines changed: 302 additions & 65 deletions

File tree

src/controllers/signAccountOp/signAccountOp.test.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ import { ProvidersController } from '../providers/providers'
5555
import { SafeController } from '../safe/safe'
5656
import { SelectedAccountController } from '../selectedAccount/selectedAccount'
5757
import { StorageController } from '../storage/storage'
58+
import { SurveyController } from '../survey/survey'
5859
import { UiController } from '../ui/ui'
5960
import { getFeeSpeedIdentifier } from './helper'
6061
import { FeeSpeed, SigningStatus } from './signAccountOp'
@@ -426,10 +427,24 @@ const init = async (
426427
{},
427428
new InviteController({ relayerUrl, fetch, storage: storageCtrl })
428429
)
430+
const surveyCtrl = new SurveyController({
431+
fetch,
432+
relayerUrl,
433+
storage: storageCtrl,
434+
ui: uiCtrl,
435+
dismissBanner: () => {}
436+
})
437+
const bannerCtrl = new BannerController(
438+
storageCtrl,
439+
() => ({ status: 'no-selected-account' }),
440+
surveyCtrl,
441+
'test'
442+
)
429443
const selectedAccountCtrl = new SelectedAccountController({
430444
storage: storageCtrl,
431445
accounts: accountsCtrl,
432-
autoLogin: autoLoginCtrl
446+
autoLogin: autoLoginCtrl,
447+
banner: bannerCtrl
433448
})
434449
const addressBookCtrl = new AddressBookController(storageCtrl, accountsCtrl, selectedAccountCtrl)
435450
await accountsCtrl.initialLoadPromise
@@ -447,7 +462,7 @@ const init = async (
447462
keystore,
448463
'https://staging-relayer.ambire.com',
449464
velcroUrl,
450-
new BannerController(storageCtrl),
465+
bannerCtrl,
451466
featureFlagsCtrl
452467
)
453468
const phishing = new PhishingController({
@@ -871,6 +886,7 @@ describe('SignAccountOp Controller ', () => {
871886
paidBy: eoaAccount.addr,
872887
broadcastOption: BROADCAST_OPTIONS.bySelf,
873888
paidByKeyType: 'internal',
889+
isCustomGasLimit: false,
874890
isGasTank: false,
875891
inToken: '0x0000000000000000000000000000000000000000',
876892
feeTokenChainId: 1n,

src/controllers/signAccountOp/signAccountOp.ts

Lines changed: 40 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ import {
9696
FeePaymentOption,
9797
FullEstimationSummary
9898
} from '../../libs/estimate/interfaces'
99+
import { calculateFeeAmount } from '../../libs/fees/fees'
99100
import { humanizeAccountOp } from '../../libs/humanizer'
100101
import { HumanizerWarning, IrCall } from '../../libs/humanizer/interfaces'
101102
import { hasRelayerSupport, relayerAdditionalNetworks } from '../../libs/networks/networks'
@@ -201,6 +202,7 @@ export const noStateUpdateStatuses = [
201202
export type SignAccountOpUpdateProps = {
202203
gasPrices?: GasSpeeds
203204
customGasPrices?: GasSpeeds
205+
customGasLimit?: bigint
204206
feeToken?: TokenResult
205207
paidBy?: string
206208
paidByKeyType?: Key['type']
@@ -260,6 +262,8 @@ export class SignAccountOpController extends EventEmitter implements ISignAccoun
260262

261263
hasCustomGasPrices: boolean = false
262264

265+
customGasLimit?: bigint
266+
263267
feeSpeeds: {
264268
[identifier: string]: SpeedCalc[]
265269
} = {}
@@ -1234,6 +1238,7 @@ export class SignAccountOpController extends EventEmitter implements ISignAccoun
12341238
update({
12351239
gasPrices,
12361240
customGasPrices,
1241+
customGasLimit,
12371242
feeToken,
12381243
paidBy,
12391244
speed,
@@ -1390,6 +1395,10 @@ export class SignAccountOpController extends EventEmitter implements ISignAccoun
13901395
this.gasPrices = gasPrices
13911396
}
13921397

1398+
if (typeof customGasLimit !== 'undefined') {
1399+
this.customGasLimit = customGasLimit
1400+
}
1401+
13931402
this.#syncSpeedUpFeeSelectionFromEstimation()
13941403

13951404
if (feeToken && paidBy && !isSpeedUpTransaction) {
@@ -1461,6 +1470,7 @@ export class SignAccountOpController extends EventEmitter implements ISignAccoun
14611470
Array.isArray(accountOpData?.calls) ||
14621471
gasPrices ||
14631472
customGasPrices ||
1473+
typeof customGasLimit !== 'undefined' ||
14641474
this.#paidBy ||
14651475
this.feeTokenResult ||
14661476
hasNewEstimation
@@ -1754,38 +1764,6 @@ export class SignAccountOpController extends EventEmitter implements ISignAccoun
17541764
return BigInt(toBigInt)
17551765
}
17561766

1757-
static getAmountAfterFeeTokenConvert(
1758-
simulatedGasLimit: bigint,
1759-
gasPrice: bigint,
1760-
nativeRatio: bigint,
1761-
feeTokenDecimals: number,
1762-
addedNative: bigint
1763-
) {
1764-
const amountInWei = simulatedGasLimit * gasPrice + addedNative
1765-
1766-
// Let's break down the process of converting the amount into FeeToken:
1767-
// 1. Initially, we multiply the amount in wei by the native to fee token ratio.
1768-
// 2. Next, we address the decimal places:
1769-
// 2.1. First, we convert wei to native by dividing by 10^18 (representing the decimals).
1770-
// 2.2. Now, with the amount in the native token, we incorporate nativeRatio decimals into the calculation (18 + 18) to standardize the amount.
1771-
// 2.3. At this point, we precisely determine the number of fee tokens. For instance, if the amount is 3 USDC, we must convert it to a BigInt value, while also considering feeToken.decimals.
1772-
const extraDecimals = BigInt(10 ** 18)
1773-
const feeTokenExtraDecimals = BigInt(10 ** (18 - feeTokenDecimals))
1774-
const pow = extraDecimals * feeTokenExtraDecimals
1775-
const result = (amountInWei * nativeRatio) / pow
1776-
1777-
// Fixes the edge case where the fee in wei is not zero
1778-
// but the decimals of the token we are converting to
1779-
// cannot represent the amount in wei. Example: 0.(6zeros)1 USDC
1780-
// We are returning 1n which is the smallest possible amount
1781-
// to be represented in USDC
1782-
if (result === 0n && amountInWei !== 0n) {
1783-
return 1n
1784-
}
1785-
1786-
return result
1787-
}
1788-
17891767
async #traceCall() {
17901768
// `traceCall` should not be invoked too frequently. However, if there is a pending timeout,
17911769
// it should be cleared to prevent the previous interval from changing the status
@@ -1880,15 +1858,6 @@ export class SignAccountOpController extends EventEmitter implements ISignAccoun
18801858
clearTimeout(timeoutId)
18811859
}
18821860

1883-
/**
1884-
* Increase the paymaster fee by 10%, the relayer by 5%.
1885-
* This is required because even now, we are broadcasting at a loss
1886-
*/
1887-
#increaseFee(amount: bigint, broadcaster: string = 'relayer'): bigint {
1888-
if (broadcaster === 'paymaster') return amount + amount / 10n
1889-
return amount + amount / 20n
1890-
}
1891-
18921861
#addExtra(gasInWei: bigint, percentageIncrease: bigint): Hex {
18931862
const percent = 100n / percentageIncrease
18941863
return toBeHex(gasInWei + gasInWei / percent) as Hex
@@ -2009,23 +1978,18 @@ export class SignAccountOpController extends EventEmitter implements ISignAccoun
20091978
let simulatedGasLimit: bigint
20101979
let gasPrice
20111980
let maxPriorityFeePerGas
1981+
let amountGasPrice: bigint
1982+
let usesPaymaster = false
20121983

20131984
if (broadcastOption === BROADCAST_OPTIONS.byBundler) {
20141985
if (!estimation.bundlerEstimation) return
20151986

2016-
const usesPaymaster = estimation.bundlerEstimation?.paymaster.isUsable()
1987+
usesPaymaster = !!estimation.bundlerEstimation?.paymaster.isUsable()
20171988
simulatedGasLimit =
20181989
BigInt(gasUsed) +
20191990
BigInt(estimation.bundlerEstimation.preVerificationGas) +
20201991
BigInt(option.gasUsed)
2021-
amount = SignAccountOpController.getAmountAfterFeeTokenConvert(
2022-
simulatedGasLimit,
2023-
BigInt(increasedPrices.maxFeePerGas),
2024-
nativeRatio,
2025-
option.token.decimals,
2026-
0n
2027-
)
2028-
if (usesPaymaster) amount = this.#increaseFee(amount, 'paymaster')
1992+
amountGasPrice = BigInt(increasedPrices.maxFeePerGas)
20291993
gasPrice = BigInt(receivedPrices.maxFeePerGas)
20301994
maxPriorityFeePerGas = BigInt(receivedPrices.maxPriorityFeePerGas)
20311995
} else if (
@@ -2036,36 +2000,42 @@ export class SignAccountOpController extends EventEmitter implements ISignAccoun
20362000
simulatedGasLimit = gasUsed
20372001
gasPrice = BigInt(increasedPrices.maxFeePerGas)
20382002
maxPriorityFeePerGas = BigInt(increasedPrices.maxPriorityFeePerGas)
2003+
amountGasPrice = BigInt(receivedPrices.maxFeePerGas)
20392004

20402005
this.accountOp.calls.forEach((call) => {
20412006
if (call.to && getAddress(call.to) === SINGLETON) {
20422007
simulatedGasLimit = getGasUsed(simulatedGasLimit)
20432008
}
20442009
})
2045-
2046-
amount = simulatedGasLimit * BigInt(receivedPrices.maxFeePerGas) + option.addedNative
20472010
} else if (broadcastOption === BROADCAST_OPTIONS.byOtherEOA) {
20482011
// Smart account, but EOA pays the fee
20492012
// 7702, and it pays for the fee by itself
20502013
simulatedGasLimit = gasUsed
2051-
amount = simulatedGasLimit * BigInt(receivedPrices.maxFeePerGas) + option.addedNative
20522014
gasPrice = BigInt(increasedPrices.maxFeePerGas)
20532015
maxPriorityFeePerGas = BigInt(increasedPrices.maxPriorityFeePerGas)
2016+
amountGasPrice = BigInt(receivedPrices.maxFeePerGas)
20542017
} else {
20552018
// Relayer
20562019
simulatedGasLimit = gasUsed + option.gasUsed
2057-
amount = SignAccountOpController.getAmountAfterFeeTokenConvert(
2058-
simulatedGasLimit,
2059-
BigInt(increasedPrices.maxFeePerGas),
2060-
nativeRatio,
2061-
option.token.decimals,
2062-
option.addedNative
2063-
)
2064-
amount = this.#increaseFee(amount)
2020+
amountGasPrice = BigInt(increasedPrices.maxFeePerGas)
20652021
gasPrice = BigInt(increasedPrices.maxFeePerGas)
20662022
maxPriorityFeePerGas = BigInt(increasedPrices.maxPriorityFeePerGas)
20672023
}
20682024

2025+
if (typeof this.customGasLimit !== 'undefined') {
2026+
simulatedGasLimit = this.customGasLimit
2027+
}
2028+
2029+
amount = calculateFeeAmount({
2030+
broadcastOption,
2031+
simulatedGasLimit,
2032+
gasPrice: amountGasPrice,
2033+
nativeRatio,
2034+
feeTokenDecimals: option.token.decimals,
2035+
addedNative: broadcastOption === BROADCAST_OPTIONS.byBundler ? 0n : option.addedNative,
2036+
usesPaymaster
2037+
})
2038+
20692039
const feeSpeed: SpeedCalc = {
20702040
type: speed,
20712041
simulatedGasLimit,
@@ -2184,10 +2154,11 @@ export class SignAccountOpController extends EventEmitter implements ISignAccoun
21842154
inToken: this.feeTokenResult.address,
21852155
feeTokenChainId: this.feeTokenResult.chainId,
21862156
amount: chosenSpeed.amount,
2187-
simulatedGasLimit: chosenSpeed.simulatedGasLimit,
2157+
simulatedGasLimit: this.customGasLimit ?? chosenSpeed.simulatedGasLimit,
21882158
gasPrice: chosenSpeed.gasPrice,
21892159
maxPriorityFeePerGas:
21902160
'maxPriorityFeePerGas' in chosenSpeed ? chosenSpeed.maxPriorityFeePerGas : undefined,
2161+
isCustomGasLimit: typeof this.customGasLimit !== 'undefined',
21912162
broadcastOption: this.baseAccount.getBroadcastOption(this.selectedOption, {
21922163
op: this.accountOp,
21932164
isSponsored: this.isSponsored
@@ -3488,6 +3459,12 @@ export class SignAccountOpController extends EventEmitter implements ISignAccoun
34883459
return this.baseAccount.canSetCustomGasPrices(this.selectedOption)
34893460
}
34903461

3462+
get canSetCustomGas(): boolean {
3463+
if (!this.selectedOption) return false
3464+
3465+
return this.baseAccount.canSetCustomGas(this.selectedOption, this.accountOp)
3466+
}
3467+
34913468
get threshold(): number {
34923469
const accountState =
34933470
this.#accounts.accountStates[this.account.addr]![this.#network.chainId.toString()]
@@ -3528,6 +3505,7 @@ export class SignAccountOpController extends EventEmitter implements ISignAccoun
35283505
banners: this.banners,
35293506
canAccountBroadcastByItself: this.canAccountBroadcastByItself,
35303507
canSetCustomGasPrices: this.canSetCustomGasPrices,
3508+
canSetCustomGas: this.canSetCustomGas,
35313509
threshold: this.threshold,
35323510
canBroadcast: this.canBroadcast,
35333511
hasSafeApiFailed: this.hasSafeApiFailed

src/libs/account/BaseAccount.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@ export abstract class BaseAccount {
9494

9595
abstract canSetCustomGasPrices(feeOption: FeePaymentOption): boolean
9696

97+
abstract canSetCustomGas(feeOption: FeePaymentOption, accountOp?: AccountOp): boolean
98+
9799
// this is specific for v2 accounts, hardcoding a false for all else
98100
shouldIncludeActivatorCall(paidBy?: string) {
99101
return false

src/libs/account/EOA.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,4 +135,12 @@ export class EOA extends BaseAccount {
135135
canSetCustomGasPrices(): boolean {
136136
return true
137137
}
138+
139+
canSetCustomGas(_feeOption?: FeePaymentOption, accountOp?: AccountOp): boolean {
140+
// we do not allow custom gas for a bundle as we can estimate
141+
// the gas for the next transaction after the first one has completed
142+
if (accountOp && accountOp.calls.length > 1) return false
143+
144+
return this.canSetCustomGasPrices()
145+
}
138146
}

src/libs/account/EOA7702.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,4 +221,8 @@ export class EOA7702 extends BaseAccount {
221221
canSetCustomGasPrices(feeOption: FeePaymentOption): boolean {
222222
return feeOption.token.address === ZeroAddress
223223
}
224+
225+
canSetCustomGas(feeOption: FeePaymentOption): boolean {
226+
return this.canSetCustomGasPrices(feeOption)
227+
}
224228
}

src/libs/account/Safe.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,4 +202,8 @@ export class Safe extends BaseAccount {
202202
canSetCustomGasPrices(): boolean {
203203
return true
204204
}
205+
206+
canSetCustomGas(): boolean {
207+
return this.canSetCustomGasPrices()
208+
}
205209
}

src/libs/account/V1.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,4 +136,8 @@ export class V1 extends BaseAccount {
136136
canSetCustomGasPrices(): boolean {
137137
return true
138138
}
139+
140+
canSetCustomGas(): boolean {
141+
return this.canSetCustomGasPrices()
142+
}
139143
}

src/libs/account/V2.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,4 +205,8 @@ export class V2 extends BaseAccount {
205205
feeOption.paidBy.toLowerCase() !== this.account.addr.toLowerCase()
206206
)
207207
}
208+
209+
canSetCustomGas(feeOption: FeePaymentOption): boolean {
210+
return this.canSetCustomGasPrices(feeOption)
211+
}
208212
}

src/libs/account/customGasPrices.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { describe, expect, test } from '@jest/globals'
44

55
import { Account, AccountOnchainState } from '../../interfaces/account'
66
import { Network } from '../../interfaces/network'
7+
import { AccountOp } from '../accountOp/accountOp'
78
import { FeePaymentOption } from '../estimate/interfaces'
89
import { EOA } from './EOA'
910
import { EOA7702 } from './EOA7702'
@@ -66,6 +67,14 @@ const tokenFeeOption = {
6667
}
6768
} as FeePaymentOption
6869

70+
const accountOp = {
71+
calls: [{}]
72+
} as AccountOp
73+
74+
const batchAccountOp = {
75+
calls: [{}, {}]
76+
} as AccountOp
77+
6978
describe('custom gas price support', () => {
7079
test('EOA, V1 and Safe always allow custom gas prices', () => {
7180
expect(new EOA(account, network, accountState).canSetCustomGasPrices()).toBe(true)
@@ -87,4 +96,18 @@ describe('custom gas price support', () => {
8796
expect(v2.canSetCustomGasPrices(eoaNativeFeeOption)).toBe(true)
8897
expect(v2.canSetCustomGasPrices(tokenFeeOption)).toBe(false)
8998
})
99+
100+
test('EOA allows custom gas only for single-call account ops', () => {
101+
const eoa = new EOA(account, network, accountState)
102+
103+
expect(eoa.canSetCustomGas(nativeFeeOption, accountOp)).toBe(true)
104+
expect(eoa.canSetCustomGas(nativeFeeOption, batchAccountOp)).toBe(false)
105+
})
106+
107+
test('non-EOA custom gas support follows custom gas price support', () => {
108+
expect(new V1(account, network, accountState).canSetCustomGas(nativeFeeOption)).toBe(true)
109+
expect(new Safe(account, network, accountState).canSetCustomGas(nativeFeeOption)).toBe(true)
110+
expect(new EOA7702(account, network, accountState).canSetCustomGas(tokenFeeOption)).toBe(false)
111+
expect(new V2(account, network, accountState).canSetCustomGas(eoaNativeFeeOption)).toBe(true)
112+
})
90113
})

src/libs/accountOp/accountOp.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export interface GasFeePayment {
3030
feeTokenChainId?: bigint
3131
amount: bigint
3232
simulatedGasLimit: bigint
33+
isCustomGasLimit?: boolean
3334
gasPrice: bigint
3435
broadcastOption: string
3536
maxPriorityFeePerGas?: bigint

0 commit comments

Comments
 (0)