Skip to content
This repository was archived by the owner on Feb 8, 2025. It is now read-only.

Commit b29798f

Browse files
authored
Merge pull request #275 from zallo-labs/Z-326-fee-estimation
Z 326 fee estimation
2 parents 3eae6fe + 9d14f66 commit b29798f

33 files changed

+405
-311
lines changed

api/schema.graphql

+28
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,7 @@ input CreateAccountInput {
204204
chain: Chain! = "zksync-sepolia"
205205
name: String!
206206
policies: [PolicyInput!]!
207+
salt: Bytes32
207208
}
208209

209210
input CreatePolicyInput {
@@ -470,6 +471,15 @@ input PolicyUpdatedInput {
470471
events: PolicyEvent
471472
}
472473

474+
input PrepareTransactionInput {
475+
account: UAddress!
476+
feeToken: Address
477+
gas: Uint256
478+
operations: [OperationInput!]!
479+
policy: PolicyKey
480+
timestamp: DateTime
481+
}
482+
473483
type Price implements CustomNode {
474484
eth: Decimal!
475485
ethEma: Decimal!
@@ -542,6 +552,7 @@ input ProposeCancelScheduledTransactionInput {
542552
gas: Uint256
543553
icon: URL
544554
label: String
555+
policy: PolicyKey
545556
proposal: ID!
546557

547558
"""Approve the proposal"""
@@ -572,6 +583,7 @@ input ProposeTransactionInput {
572583
icon: URL
573584
label: String
574585
operations: [OperationInput!]!
586+
policy: PolicyKey
575587

576588
"""Approve the proposal"""
577589
signature: Bytes
@@ -598,6 +610,7 @@ type Query {
598610
nameAvailable(name: String!): Boolean!
599611
node(id: ID!): Node
600612
policy(input: UniquePolicyInput!): Policy
613+
prepareTransaction(input: PrepareTransactionInput!): TransactionPreparation!
601614
proposal(id: ID!): Proposal
602615
requestableTokens(input: RequestTokensInput!): [Address!]!
603616
token(address: UAddress!): Token
@@ -832,6 +845,21 @@ type Transaction implements Node & Proposal {
832845
validationErrors: [ValidationError!]!
833846
}
834847

848+
type TransactionPreparation implements CustomNode {
849+
account: UAddress!
850+
feeToken: Token!
851+
gasLimit: BigInt!
852+
hash: Bytes32!
853+
id: ID!
854+
maxAmount: Decimal!
855+
maxNetworkFee: Decimal!
856+
paymaster: Address!
857+
paymasterEthFees: PaymasterFees!
858+
policy: PolicyKey!
859+
timestamp: DateTime!
860+
totalEthFees: Decimal!
861+
}
862+
835863
enum TransactionStatus {
836864
Cancelled
837865
Executing

api/src/common/scalars/UAddress.scalar.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ const description = 'EIP-3770 address';
77

88
const parseValue = (value: unknown): UAddress => {
99
const address = typeof value === 'string' && tryAsUAddress(value);
10-
if (!address) throw new UserInputError(`Provided value is not a ${description}`);
10+
if (!address) throw new UserInputError(`Value "${value}" is not a ${description}`);
1111
return asUAddress(value);
1212
};
1313

api/src/core/database/database.service.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export class DatabaseService implements OnModuleInit {
2626
.withConfig({
2727
allow_user_specified_id: true /* Required for account insertion */,
2828
})
29-
.withRetryOptions({ attempts: 5 });
29+
.withRetryOptions({ attempts: 7 });
3030
this.DANGEROUS_superuserClient = this.__client.withConfig({ apply_access_policies: false });
3131
}
3232

api/src/feat/accounts/accounts.input.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { ArgsType, Field, InputType } from '@nestjs/graphql';
2-
import { UAddress } from 'lib';
2+
import { Hex, UAddress } from 'lib';
33
import { PolicyInput } from '../policies/policies.input';
44
import { Chain } from 'chains';
55
import {
@@ -8,6 +8,7 @@ import {
88
UAddressScalar,
99
UrlField,
1010
minLengthMiddleware,
11+
Bytes32Field,
1112
} from '~/common/scalars';
1213
import { AccountEvent } from './accounts.model';
1314

@@ -51,6 +52,9 @@ export class CreateAccountInput {
5152

5253
@Field(() => [PolicyInput], { middleware: [minLengthMiddleware(1)] })
5354
policies: PolicyInput[];
55+
56+
@Bytes32Field({ nullable: true })
57+
salt?: Hex;
5458
}
5559

5660
@InputType()

api/src/feat/accounts/accounts.service.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,12 @@ export class AccountsService {
9090
.run(this.db.DANGEROUS_superuserClient, { name });
9191
}
9292

93-
async createAccount({ chain, name, policies: policyInputs }: CreateAccountInput) {
93+
async createAccount({
94+
chain,
95+
name,
96+
policies: policyInputs,
97+
salt = randomDeploySalt(),
98+
}: CreateAccountInput) {
9499
const baseAutoKey = Math.max(MIN_AUTO_POLICY_KEY, ...policyInputs.map((p) => p.key ?? 0));
95100
const policies = policyInputs.map((p, i) => ({
96101
...p,
@@ -100,7 +105,6 @@ export class AccountsService {
100105
throw new UserInputError('Duplicate policy keys');
101106

102107
const implementation = ACCOUNT_IMPLEMENTATION.address[chain];
103-
const salt = randomDeploySalt();
104108
const account = asUAddress(
105109
getProxyAddress({
106110
deployer: DEPLOYER.address[chain],

api/src/feat/policies/policies.service.ts

+1
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,7 @@ export class PoliciesService {
393393
return {
394394
policyId: p.id,
395395
policy: e.assert_exists(e.select(e.Policy, () => ({ filter_single: { id: p.id } }))),
396+
policyKey: p.key,
396397
validationErrors: p.validationErrors,
397398
};
398399
}

api/src/feat/transactions/executions.worker.ts

+27-10
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
UUID,
66
asAddress,
77
asApproval,
8+
asFp,
89
asHex,
910
asScheduledSystemTransaction,
1011
asSystemTransaction,
@@ -24,7 +25,6 @@ import { ProposalEvent } from '~/feat/proposals/proposals.input';
2425
import { QueueReturnType, TypedJob, createQueue } from '~/core/bull/bull.util';
2526
import { Worker } from '~/core/bull/Worker';
2627
import { UnrecoverableError } from 'bullmq';
27-
import { utils as zkUtils } from 'zksync-ethers';
2828
import { TokensService } from '~/feat/tokens/tokens.service';
2929
import { PricesService } from '~/feat/prices/prices.service';
3030
import Decimal from 'decimal.js';
@@ -119,12 +119,12 @@ export class ExecutionsWorker extends Worker<ExecutionsQueue> {
119119
paymasterEthFees: totalPaymasterEthFees(newPaymasterFees),
120120
ignoreSimulation,
121121
executeParams: {
122-
...asSystemTransaction({ tx }),
123122
customSignature: encodeTransactionSignature({
124123
tx,
125124
policy: policyStateAsPolicy(proposal.policy),
126125
approvals,
127126
}),
127+
...asSystemTransaction({ tx }),
128128
},
129129
});
130130
}
@@ -175,22 +175,36 @@ export class ExecutionsWorker extends Worker<ExecutionsQueue> {
175175
.plus(paymasterEthFees ?? '0');
176176
const amount = await this.tokens.asFp(feeToken, totalFeeTokenFees);
177177
const maxAmount = await this.tokens.asFp(feeToken, new Decimal(proposal.maxAmount));
178-
if (amount > maxAmount) throw new Error('Amount > maxAmount'); // TODO: handle
178+
if (amount > maxAmount) throw new Error('Amount > maxAmount'); // TODO: add to failed submission result TODO: fail due to insufficient funds -- re-submit for re-simulation (forced)
179179

180180
await this.prices.updatePriceFeedsIfNecessary(network.chain.key, [
181181
ETH.pythUsdPriceId,
182182
asHex(proposal.feeToken.pythUsdPriceId!),
183183
]);
184184

185+
const paymaster = asAddress(proposal.paymaster);
186+
const paymasterInput = encodePaymasterInput({
187+
token: asAddress(feeToken),
188+
amount,
189+
maxAmount,
190+
});
191+
const estimatedFee = await network.estimateFee({
192+
type: 'eip712',
193+
account: asAddress(account),
194+
paymaster,
195+
paymasterInput,
196+
...executeParams,
197+
});
198+
199+
// if (executeParams.gas && executeParams.gas < estimatedFee.gasLimit) throw new Error('gas less than estimated gasLimit');
200+
185201
const execution = await network.sendAccountTransaction({
186202
from: asAddress(account),
187-
paymaster: asAddress(proposal.paymaster),
188-
paymasterInput: encodePaymasterInput({
189-
token: asAddress(feeToken),
190-
amount,
191-
maxAmount,
192-
}),
193-
gasPerPubdata: BigInt(zkUtils.DEFAULT_GAS_PER_PUBDATA_LIMIT),
203+
paymaster,
204+
paymasterInput,
205+
maxFeePerGas: asFp(maxFeePerGas, ETH),
206+
maxPriorityFeePerGas: estimatedFee.maxPriorityFeePerGas,
207+
gasPerPubdata: estimatedFee.gasPerPubdataLimit, // This should ideally be signed during proposal creation
194208
...executeParams,
195209
});
196210

@@ -210,6 +224,9 @@ export class ExecutionsWorker extends Worker<ExecutionsQueue> {
210224

211225
return hash;
212226
} /* execution isErr */ else {
227+
228+
// TODO: adds failed submission result
229+
213230
// Validation failed
214231
// const err = execution.error;
215232
// await this.db.query(

api/src/feat/transactions/transactions.input.ts

+24
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,27 @@ export class OperationInput {
2121
data?: Hex;
2222
}
2323

24+
@InputType()
25+
export class PrepareTransactionInput {
26+
@UAddressField()
27+
account: UAddress;
28+
29+
@Field(() => [OperationInput])
30+
operations: OperationInput[];
31+
32+
@Field(() => Date, { nullable: true })
33+
timestamp?: Date;
34+
35+
@Uint256Field({ nullable: true })
36+
gas?: bigint;
37+
38+
@AddressField({ nullable: true })
39+
feeToken?: Address;
40+
41+
@PolicyKeyField({ nullable: true })
42+
policy?: PolicyKey;
43+
}
44+
2445
@InputType()
2546
export class ProposeTransactionInput {
2647
@UAddressField()
@@ -49,6 +70,9 @@ export class ProposeTransactionInput {
4970

5071
@Field(() => BytesScalar, { nullable: true, description: 'Approve the proposal' })
5172
signature?: Hex;
73+
74+
@PolicyKeyField({ nullable: true })
75+
policy?: PolicyKey;
5276
}
5377

5478
@InputType()

api/src/feat/transactions/transactions.model.ts

+26-2
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
1-
import { Field, ObjectType, registerEnumType } from '@nestjs/graphql';
1+
import { Field, ObjectType, PickType, registerEnumType } from '@nestjs/graphql';
22
import { GraphQLBigInt } from 'graphql-scalars';
33
import { SystemTx } from '../system-txs/system-tx.model';
44
import { Operation } from '../operations/operations.model';
55
import { Token } from '../tokens/tokens.model';
66
import { Simulation } from '../simulations/simulations.model';
77
import { Proposal } from '../proposals/proposals.model';
88
import { AddressField } from '~/common/scalars/Address.scalar';
9-
import { Address } from 'lib';
9+
import { Address, PolicyKey, UAddress } from 'lib';
1010
import { DecimalField } from '~/common/scalars/Decimal.scalar';
1111
import Decimal from 'decimal.js';
1212
import { CustomNode, CustomNodeType } from '~/common/decorators/interface.decorator';
1313
import { PaymasterFees } from '../paymasters/paymasters.model';
1414
import { Result } from '../system-txs/results.model';
15+
import { PolicyKeyField, UAddressField } from '~/common/scalars';
1516

1617
@ObjectType({ implements: () => Proposal })
1718
export class Transaction extends Proposal {
@@ -55,6 +56,29 @@ export class Transaction extends Proposal {
5556
status: TransactionStatus;
5657
}
5758

59+
@CustomNodeType()
60+
export class TransactionPreparation extends PickType(Transaction, [
61+
'hash',
62+
'timestamp',
63+
'gasLimit',
64+
'feeToken',
65+
'maxAmount',
66+
'paymaster',
67+
'paymasterEthFees',
68+
]) {
69+
@UAddressField()
70+
account: UAddress;
71+
72+
@PolicyKeyField()
73+
policy: PolicyKey;
74+
75+
@DecimalField()
76+
maxNetworkFee: Decimal;
77+
78+
@DecimalField()
79+
totalEthFees: Decimal;
80+
}
81+
5882
export enum TransactionStatus {
5983
Pending = 'Pending',
6084
Scheduled = 'Scheduled',

api/src/feat/transactions/transactions.resolver.ts

+15-1
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,17 @@ import { Args, ID, Info, Mutation, Parent, Query, Resolver } from '@nestjs/graph
22
import { GraphQLResolveInfo } from 'graphql';
33
import {
44
ExecuteTransactionInput,
5+
PrepareTransactionInput,
56
ProposeCancelScheduledTransactionInput,
67
ProposeTransactionInput,
78
UpdateTransactionInput,
89
} from './transactions.input';
9-
import { EstimatedTransactionFees, Transaction, TransactionStatus } from './transactions.model';
10+
import {
11+
EstimatedTransactionFees,
12+
Transaction,
13+
TransactionPreparation,
14+
TransactionStatus,
15+
} from './transactions.model';
1016
import { EstimateFeesDeps, TransactionsService, estimateFeesDeps } from './transactions.service';
1117
import { getShape } from '~/core/database';
1218
import e from '~/edgeql-js';
@@ -25,6 +31,14 @@ export class TransactionsResolver {
2531
return this.service.selectUnique(id, getShape(info));
2632
}
2733

34+
@Query(() => TransactionPreparation)
35+
async prepareTransaction(
36+
@Input() input: PrepareTransactionInput,
37+
@Info() info: GraphQLResolveInfo,
38+
) {
39+
return this.service.prepareTransaction(input, getShape(info));
40+
}
41+
2842
@ComputedField<typeof e.Transaction>(() => Boolean, { status: true })
2943
async updatable(@Parent() { status }: Transaction): Promise<boolean> {
3044
return status === TransactionStatus.Pending;

api/src/feat/transactions/transactions.service.spec.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
randomUAddress,
1010
randomUser,
1111
} from '~/util/test';
12-
import { randomDeploySalt, Hex, UAddress, ZERO_ADDR, asUUID } from 'lib';
12+
import { randomDeploySalt, Hex, UAddress, ZERO_ADDR, asUUID, asPolicyKey } from 'lib';
1313
import { Network, NetworksService } from '~/core/networks/networks.service';
1414
import { ProposeTransactionInput } from './transactions.input';
1515
import { DatabaseService } from '~/core/database';
@@ -145,6 +145,7 @@ describe(TransactionsService.name, () => {
145145
policies.best.mockImplementation(async () => ({
146146
policyId: await db.query(e.assert_exists(selectPolicy({ account, key: 0 })).id),
147147
policy: selectPolicy({ account, key: 0 }) as any,
148+
policyKey: asPolicyKey(0),
148149
validationErrors: [],
149150
}));
150151

0 commit comments

Comments
 (0)