Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Z 326 fee estimation #275

Merged
merged 14 commits into from
Jul 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading