Skip to content

Commit

Permalink
Merge pull request #275 from zallo-labs/Z-326-fee-estimation
Browse files Browse the repository at this point in the history
Z 326 fee estimation
  • Loading branch information
hbriese authored Jul 26, 2024
2 parents 3eae6fe + 9d14f66 commit b29798f
Show file tree
Hide file tree
Showing 33 changed files with 405 additions and 311 deletions.
28 changes: 28 additions & 0 deletions api/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ input CreateAccountInput {
chain: Chain! = "zksync-sepolia"
name: String!
policies: [PolicyInput!]!
salt: Bytes32
}

input CreatePolicyInput {
Expand Down Expand Up @@ -470,6 +471,15 @@ input PolicyUpdatedInput {
events: PolicyEvent
}

input PrepareTransactionInput {
account: UAddress!
feeToken: Address
gas: Uint256
operations: [OperationInput!]!
policy: PolicyKey
timestamp: DateTime
}

type Price implements CustomNode {
eth: Decimal!
ethEma: Decimal!
Expand Down Expand Up @@ -542,6 +552,7 @@ input ProposeCancelScheduledTransactionInput {
gas: Uint256
icon: URL
label: String
policy: PolicyKey
proposal: ID!

"""Approve the proposal"""
Expand Down Expand Up @@ -572,6 +583,7 @@ input ProposeTransactionInput {
icon: URL
label: String
operations: [OperationInput!]!
policy: PolicyKey

"""Approve the proposal"""
signature: Bytes
Expand All @@ -598,6 +610,7 @@ type Query {
nameAvailable(name: String!): Boolean!
node(id: ID!): Node
policy(input: UniquePolicyInput!): Policy
prepareTransaction(input: PrepareTransactionInput!): TransactionPreparation!
proposal(id: ID!): Proposal
requestableTokens(input: RequestTokensInput!): [Address!]!
token(address: UAddress!): Token
Expand Down Expand Up @@ -832,6 +845,21 @@ type Transaction implements Node & Proposal {
validationErrors: [ValidationError!]!
}

type TransactionPreparation implements CustomNode {
account: UAddress!
feeToken: Token!
gasLimit: BigInt!
hash: Bytes32!
id: ID!
maxAmount: Decimal!
maxNetworkFee: Decimal!
paymaster: Address!
paymasterEthFees: PaymasterFees!
policy: PolicyKey!
timestamp: DateTime!
totalEthFees: Decimal!
}

enum TransactionStatus {
Cancelled
Executing
Expand Down
2 changes: 1 addition & 1 deletion api/src/common/scalars/UAddress.scalar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const description = 'EIP-3770 address';

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

Expand Down
2 changes: 1 addition & 1 deletion api/src/core/database/database.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export class DatabaseService implements OnModuleInit {
.withConfig({
allow_user_specified_id: true /* Required for account insertion */,
})
.withRetryOptions({ attempts: 5 });
.withRetryOptions({ attempts: 7 });
this.DANGEROUS_superuserClient = this.__client.withConfig({ apply_access_policies: false });
}

Expand Down
6 changes: 5 additions & 1 deletion api/src/feat/accounts/accounts.input.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ArgsType, Field, InputType } from '@nestjs/graphql';
import { UAddress } from 'lib';
import { Hex, UAddress } from 'lib';
import { PolicyInput } from '../policies/policies.input';
import { Chain } from 'chains';
import {
Expand All @@ -8,6 +8,7 @@ import {
UAddressScalar,
UrlField,
minLengthMiddleware,
Bytes32Field,
} from '~/common/scalars';
import { AccountEvent } from './accounts.model';

Expand Down Expand Up @@ -51,6 +52,9 @@ export class CreateAccountInput {

@Field(() => [PolicyInput], { middleware: [minLengthMiddleware(1)] })
policies: PolicyInput[];

@Bytes32Field({ nullable: true })
salt?: Hex;
}

@InputType()
Expand Down
8 changes: 6 additions & 2 deletions api/src/feat/accounts/accounts.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,12 @@ export class AccountsService {
.run(this.db.DANGEROUS_superuserClient, { name });
}

async createAccount({ chain, name, policies: policyInputs }: CreateAccountInput) {
async createAccount({
chain,
name,
policies: policyInputs,
salt = randomDeploySalt(),
}: CreateAccountInput) {
const baseAutoKey = Math.max(MIN_AUTO_POLICY_KEY, ...policyInputs.map((p) => p.key ?? 0));
const policies = policyInputs.map((p, i) => ({
...p,
Expand All @@ -100,7 +105,6 @@ export class AccountsService {
throw new UserInputError('Duplicate policy keys');

const implementation = ACCOUNT_IMPLEMENTATION.address[chain];
const salt = randomDeploySalt();
const account = asUAddress(
getProxyAddress({
deployer: DEPLOYER.address[chain],
Expand Down
1 change: 1 addition & 0 deletions api/src/feat/policies/policies.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,7 @@ export class PoliciesService {
return {
policyId: p.id,
policy: e.assert_exists(e.select(e.Policy, () => ({ filter_single: { id: p.id } }))),
policyKey: p.key,
validationErrors: p.validationErrors,
};
}
Expand Down
37 changes: 27 additions & 10 deletions api/src/feat/transactions/executions.worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
UUID,
asAddress,
asApproval,
asFp,
asHex,
asScheduledSystemTransaction,
asSystemTransaction,
Expand All @@ -24,7 +25,6 @@ import { ProposalEvent } from '~/feat/proposals/proposals.input';
import { QueueReturnType, TypedJob, createQueue } from '~/core/bull/bull.util';
import { Worker } from '~/core/bull/Worker';
import { UnrecoverableError } from 'bullmq';
import { utils as zkUtils } from 'zksync-ethers';
import { TokensService } from '~/feat/tokens/tokens.service';
import { PricesService } from '~/feat/prices/prices.service';
import Decimal from 'decimal.js';
Expand Down Expand Up @@ -119,12 +119,12 @@ export class ExecutionsWorker extends Worker<ExecutionsQueue> {
paymasterEthFees: totalPaymasterEthFees(newPaymasterFees),
ignoreSimulation,
executeParams: {
...asSystemTransaction({ tx }),
customSignature: encodeTransactionSignature({
tx,
policy: policyStateAsPolicy(proposal.policy),
approvals,
}),
...asSystemTransaction({ tx }),
},
});
}
Expand Down Expand Up @@ -175,22 +175,36 @@ export class ExecutionsWorker extends Worker<ExecutionsQueue> {
.plus(paymasterEthFees ?? '0');
const amount = await this.tokens.asFp(feeToken, totalFeeTokenFees);
const maxAmount = await this.tokens.asFp(feeToken, new Decimal(proposal.maxAmount));
if (amount > maxAmount) throw new Error('Amount > maxAmount'); // TODO: handle
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)

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

const paymaster = asAddress(proposal.paymaster);
const paymasterInput = encodePaymasterInput({
token: asAddress(feeToken),
amount,
maxAmount,
});
const estimatedFee = await network.estimateFee({
type: 'eip712',
account: asAddress(account),
paymaster,
paymasterInput,
...executeParams,
});

// if (executeParams.gas && executeParams.gas < estimatedFee.gasLimit) throw new Error('gas less than estimated gasLimit');

const execution = await network.sendAccountTransaction({
from: asAddress(account),
paymaster: asAddress(proposal.paymaster),
paymasterInput: encodePaymasterInput({
token: asAddress(feeToken),
amount,
maxAmount,
}),
gasPerPubdata: BigInt(zkUtils.DEFAULT_GAS_PER_PUBDATA_LIMIT),
paymaster,
paymasterInput,
maxFeePerGas: asFp(maxFeePerGas, ETH),
maxPriorityFeePerGas: estimatedFee.maxPriorityFeePerGas,
gasPerPubdata: estimatedFee.gasPerPubdataLimit, // This should ideally be signed during proposal creation
...executeParams,
});

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

return hash;
} /* execution isErr */ else {

// TODO: adds failed submission result

// Validation failed
// const err = execution.error;
// await this.db.query(
Expand Down
24 changes: 24 additions & 0 deletions api/src/feat/transactions/transactions.input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,27 @@ export class OperationInput {
data?: Hex;
}

@InputType()
export class PrepareTransactionInput {
@UAddressField()
account: UAddress;

@Field(() => [OperationInput])
operations: OperationInput[];

@Field(() => Date, { nullable: true })
timestamp?: Date;

@Uint256Field({ nullable: true })
gas?: bigint;

@AddressField({ nullable: true })
feeToken?: Address;

@PolicyKeyField({ nullable: true })
policy?: PolicyKey;
}

@InputType()
export class ProposeTransactionInput {
@UAddressField()
Expand Down Expand Up @@ -49,6 +70,9 @@ export class ProposeTransactionInput {

@Field(() => BytesScalar, { nullable: true, description: 'Approve the proposal' })
signature?: Hex;

@PolicyKeyField({ nullable: true })
policy?: PolicyKey;
}

@InputType()
Expand Down
28 changes: 26 additions & 2 deletions api/src/feat/transactions/transactions.model.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import { Field, ObjectType, registerEnumType } from '@nestjs/graphql';
import { Field, ObjectType, PickType, registerEnumType } from '@nestjs/graphql';
import { GraphQLBigInt } from 'graphql-scalars';
import { SystemTx } from '../system-txs/system-tx.model';
import { Operation } from '../operations/operations.model';
import { Token } from '../tokens/tokens.model';
import { Simulation } from '../simulations/simulations.model';
import { Proposal } from '../proposals/proposals.model';
import { AddressField } from '~/common/scalars/Address.scalar';
import { Address } from 'lib';
import { Address, PolicyKey, UAddress } from 'lib';
import { DecimalField } from '~/common/scalars/Decimal.scalar';
import Decimal from 'decimal.js';
import { CustomNode, CustomNodeType } from '~/common/decorators/interface.decorator';
import { PaymasterFees } from '../paymasters/paymasters.model';
import { Result } from '../system-txs/results.model';
import { PolicyKeyField, UAddressField } from '~/common/scalars';

@ObjectType({ implements: () => Proposal })
export class Transaction extends Proposal {
Expand Down Expand Up @@ -55,6 +56,29 @@ export class Transaction extends Proposal {
status: TransactionStatus;
}

@CustomNodeType()
export class TransactionPreparation extends PickType(Transaction, [
'hash',
'timestamp',
'gasLimit',
'feeToken',
'maxAmount',
'paymaster',
'paymasterEthFees',
]) {
@UAddressField()
account: UAddress;

@PolicyKeyField()
policy: PolicyKey;

@DecimalField()
maxNetworkFee: Decimal;

@DecimalField()
totalEthFees: Decimal;
}

export enum TransactionStatus {
Pending = 'Pending',
Scheduled = 'Scheduled',
Expand Down
16 changes: 15 additions & 1 deletion api/src/feat/transactions/transactions.resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,17 @@ import { Args, ID, Info, Mutation, Parent, Query, Resolver } from '@nestjs/graph
import { GraphQLResolveInfo } from 'graphql';
import {
ExecuteTransactionInput,
PrepareTransactionInput,
ProposeCancelScheduledTransactionInput,
ProposeTransactionInput,
UpdateTransactionInput,
} from './transactions.input';
import { EstimatedTransactionFees, Transaction, TransactionStatus } from './transactions.model';
import {
EstimatedTransactionFees,
Transaction,
TransactionPreparation,
TransactionStatus,
} from './transactions.model';
import { EstimateFeesDeps, TransactionsService, estimateFeesDeps } from './transactions.service';
import { getShape } from '~/core/database';
import e from '~/edgeql-js';
Expand All @@ -25,6 +31,14 @@ export class TransactionsResolver {
return this.service.selectUnique(id, getShape(info));
}

@Query(() => TransactionPreparation)
async prepareTransaction(
@Input() input: PrepareTransactionInput,
@Info() info: GraphQLResolveInfo,
) {
return this.service.prepareTransaction(input, getShape(info));
}

@ComputedField<typeof e.Transaction>(() => Boolean, { status: true })
async updatable(@Parent() { status }: Transaction): Promise<boolean> {
return status === TransactionStatus.Pending;
Expand Down
3 changes: 2 additions & 1 deletion api/src/feat/transactions/transactions.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
randomUAddress,
randomUser,
} from '~/util/test';
import { randomDeploySalt, Hex, UAddress, ZERO_ADDR, asUUID } from 'lib';
import { randomDeploySalt, Hex, UAddress, ZERO_ADDR, asUUID, asPolicyKey } from 'lib';
import { Network, NetworksService } from '~/core/networks/networks.service';
import { ProposeTransactionInput } from './transactions.input';
import { DatabaseService } from '~/core/database';
Expand Down Expand Up @@ -145,6 +145,7 @@ describe(TransactionsService.name, () => {
policies.best.mockImplementation(async () => ({
policyId: await db.query(e.assert_exists(selectPolicy({ account, key: 0 })).id),
policy: selectPolicy({ account, key: 0 }) as any,
policyKey: asPolicyKey(0),
validationErrors: [],
}));

Expand Down
Loading

0 comments on commit b29798f

Please sign in to comment.