Skip to content

Commit fdf1da4

Browse files
Thunkarbenesjan
andauthored
chore: fee cleanup (#12941)
- `SponsoredFeePaymentMethod` gets into `aztec.js` , but that's it (meaning the method can be used from aztec.js, but there's no concept of where's deployed/how to get the address/bytecode, etc). It's under `@aztec/aztec.js/fee/testing` to make abundantly clear that this is not a "production acceptable" approach. - Similarly to `setupCanonicalL2FeeJuice` there's a utility method that can be used from the CLI/during sandbox setup to deploy a `SponsoredFeePaymentContract`. It spits out the address where the contract is located, just like the test accounts. - You CAN programatically obtain the address of the "canonical" `SponsoredFeePaymentContract` via `@aztec/sandbox`, but you CANNOT via `@aztec/aztec.js`. This is because getting the address implies loading the contract bytecode and it's not a protocol contract, making imports messy and tripping the user into making poor decisions in their app design. If you want to use it in your app, obtain it from the sandbox output or from an announcement (just like a faucet address, for example) - This address is only canonical in the sense it's salt is hardcoded to 0 (this lives in `@aztec/constants` under `SPONSORED_FPC_SALT`. For testnet it should be prefunded! @PhilWindle @alexghr This PR also builds upon the work done in `aztec.js`, allowing us to finally get rid of the special handling of account contract deployments `deploy_account_method.ts` by creating a new `FeePaymentMethod` (that's not exposed externally!) that allows an account to pay for its own deployment 😁 --------- Co-authored-by: Jan Beneš <[email protected]>
1 parent 397144f commit fdf1da4

37 files changed

+456
-214
lines changed

yarn-project/aztec.js/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@
2323
"./tx_hash": "./dest/api/tx_hash.js",
2424
"./wallet": "./dest/api/wallet.js",
2525
"./utils": "./dest/api/utils.js",
26-
"./testing": "./dest/api/testing.js"
26+
"./testing": "./dest/api/testing.js",
27+
"./fee/testing": "./dest/api/fee_testing.js"
2728
},
2829
"typedocOptions": {
2930
"entryPoints": [

yarn-project/aztec.js/src/account_manager/account_manager.ts

Lines changed: 73 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
import { DefaultMultiCallEntrypoint } from '@aztec/entrypoints/multicall';
12
import { Fr } from '@aztec/foundation/fields';
23
import { CompleteAddress, type ContractInstanceWithAddress } from '@aztec/stdlib/contract';
34
import { getContractInstanceFromDeployParams } from '@aztec/stdlib/contract';
5+
import type { GasSettings } from '@aztec/stdlib/gas';
46
import type { PXE } from '@aztec/stdlib/interfaces/client';
57
import { deriveKeys } from '@aztec/stdlib/keys';
68

@@ -10,9 +12,9 @@ import type { AccountInterface } from '../account/interface.js';
1012
import { Contract } from '../contract/contract.js';
1113
import { DeployMethod, type DeployOptions } from '../contract/deploy_method.js';
1214
import { DefaultWaitOpts, type WaitOpts } from '../contract/sent_tx.js';
13-
import { DefaultMultiCallEntrypoint } from '../entrypoint/default_multi_call_entrypoint.js';
15+
import { AccountEntrypointMetaPaymentMethod } from '../fee/account_entrypoint_meta_payment_method.js';
16+
import { FeeJuicePaymentMethod, type FeePaymentMethod } from '../index.js';
1417
import { AccountWalletWithSecretKey, SignerlessWallet, type Wallet } from '../wallet/index.js';
15-
import { DeployAccountMethod } from './deploy_account_method.js';
1618
import { DeployAccountSentTx } from './deploy_account_sent_tx.js';
1719

1820
/**
@@ -139,12 +141,11 @@ export class AccountManager {
139141

140142
/**
141143
* Returns the pre-populated deployment method to deploy the account contract that backs this account.
142-
* Typically you will not need this method and can call `deploy` directly. Use this for having finer
143-
* grained control on when to create, simulate, and send the deployment tx.
144+
* If no wallet is provided, it uses a signerless wallet with the multi call entrypoint
144145
* @param deployWallet - Wallet used for deploying the account contract.
145-
* @returns A DeployMethod instance that deploys this account contract.
146+
* @returns A DeployMethod instance that deploys this account contract
146147
*/
147-
public async getDeployMethod(deployWallet?: Wallet) {
148+
async #getDeployMethod(deployWallet?: Wallet): Promise<DeployMethod> {
148149
const artifact = await this.accountContract.getContractArtifact();
149150

150151
if (!(await this.isDeployable())) {
@@ -179,14 +180,36 @@ export class AccountManager {
179180
// and it can't be used unless the contract is initialized.
180181
const wallet = new SignerlessWallet(this.pxe, new DefaultMultiCallEntrypoint(chainId, protocolVersion));
181182

182-
return new DeployAccountMethod(
183-
this.accountContract.getAuthWitnessProvider(completeAddress),
183+
return new DeployMethod(
184184
this.getPublicKeys(),
185185
wallet,
186186
artifact,
187+
address => Contract.at(address, artifact, wallet),
187188
constructorArgs,
188189
constructorName,
190+
);
191+
}
192+
193+
/**
194+
* Returns a FeePaymentMethod that routes the original one provided as an argument
195+
* through the account's entrypoint. This allows an account contract to pay
196+
* for its own deployment and initialization.
197+
*
198+
* For more details on how the fee payment routing works see documentation of AccountEntrypointMetaPaymentMethod class.
199+
*
200+
* @param originalPaymentMethod - originalPaymentMethod The original payment method to be wrapped.
201+
* @returns A FeePaymentMethod that routes the original one through the account's entrypoint (AccountEntrypointMetaPaymentMethod)
202+
*/
203+
async #getSelfPaymentMethod(originalPaymentMethod?: FeePaymentMethod) {
204+
const artifact = await this.accountContract.getContractArtifact();
205+
const wallet = await this.getWallet();
206+
const address = wallet.getAddress();
207+
return new AccountEntrypointMetaPaymentMethod(
208+
artifact,
209+
wallet,
189210
'entrypoint',
211+
address,
212+
originalPaymentMethod ?? new FeeJuicePaymentMethod(address),
190213
);
191214
}
192215

@@ -199,21 +222,57 @@ export class AccountManager {
199222
* @returns A SentTx object that can be waited to get the associated Wallet.
200223
*/
201224
public deploy(opts?: DeployAccountOptions): DeployAccountSentTx {
202-
const sentTx = this.getDeployMethod(opts?.deployWallet)
203-
.then(deployMethod =>
204-
deployMethod.send({
225+
let deployMethod: DeployMethod;
226+
const sentTx = this.#getDeployMethod(opts?.deployWallet)
227+
.then(method => {
228+
deployMethod = method;
229+
if (!opts?.deployWallet && opts?.fee) {
230+
return this.#getSelfPaymentMethod(opts?.fee?.paymentMethod);
231+
}
232+
})
233+
.then(maybeWrappedPaymentMethod => {
234+
let fee = opts?.fee;
235+
if (maybeWrappedPaymentMethod) {
236+
fee = { ...opts?.fee, paymentMethod: maybeWrappedPaymentMethod };
237+
}
238+
return deployMethod.send({
205239
contractAddressSalt: new Fr(this.salt),
206240
skipClassRegistration: opts?.skipClassRegistration ?? true,
207241
skipPublicDeployment: opts?.skipPublicDeployment ?? true,
208242
skipInitialization: opts?.skipInitialization ?? false,
209243
universalDeploy: true,
210-
fee: opts?.fee,
211-
}),
212-
)
244+
fee,
245+
});
246+
})
213247
.then(tx => tx.getTxHash());
214248
return new DeployAccountSentTx(this.pxe, sentTx, this.getWallet());
215249
}
216250

251+
/**
252+
* Estimates the gas needed to deploy the account contract that backs this account.
253+
* This method is here to ensure that the fee payment method is correctly set up in case
254+
* the account contract needs to pay for its own deployment.
255+
* @param opts - Fee options to be used for the deployment.
256+
* @returns The gas estimations for the account contract deployment and initialization.
257+
*/
258+
public async estimateDeploymentGas(
259+
opts?: DeployAccountOptions,
260+
): Promise<Pick<GasSettings, 'gasLimits' | 'teardownGasLimits'>> {
261+
const deployMethod = await this.#getDeployMethod(opts?.deployWallet);
262+
const fee =
263+
!opts?.deployWallet && opts?.fee
264+
? { ...opts.fee, paymentMethod: await this.#getSelfPaymentMethod(opts.fee.paymentMethod) }
265+
: opts?.fee;
266+
return deployMethod.estimateGas({
267+
contractAddressSalt: new Fr(this.salt),
268+
skipClassRegistration: opts?.skipClassRegistration ?? true,
269+
skipPublicDeployment: opts?.skipPublicDeployment ?? true,
270+
skipInitialization: opts?.skipInitialization ?? false,
271+
universalDeploy: true,
272+
fee,
273+
});
274+
}
275+
217276
/**
218277
* Deploys the account contract that backs this account if needed and awaits the tx to be mined.
219278
* Uses the salt provided in the constructor or a randomly generated one. If no initialization

yarn-project/aztec.js/src/account_manager/deploy_account_method.ts

Lines changed: 0 additions & 87 deletions
This file was deleted.
Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,2 @@
11
export { AccountManager, type DeployAccountOptions } from './account_manager.js';
2-
export { DeployAccountMethod } from './deploy_account_method.js';
32
export { type DeployAccountTxReceipt, DeployAccountSentTx } from './deploy_account_sent_tx.js';

yarn-project/aztec.js/src/api/fee.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ export { FeeJuicePaymentMethod } from '../fee/fee_juice_payment_method.js';
33
export { PrivateFeePaymentMethod } from '../fee/private_fee_payment_method.js';
44
export { PublicFeePaymentMethod } from '../fee/public_fee_payment_method.js';
55
export { FeeJuicePaymentMethodWithClaim } from '../fee/fee_juice_payment_method_with_claim.js';
6+
export { SponsoredFeePaymentMethod } from '../fee/sponsored_fee_payment.js';
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { SponsoredFeePaymentMethod } from '../fee/sponsored_fee_payment.js';
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import {
2+
EncodedAppEntrypointCalls,
3+
EncodedCallsForEntrypoint,
4+
computeCombinedPayloadHash,
5+
} from '@aztec/entrypoints/encoding';
6+
import type { AuthWitnessProvider, FeePaymentMethod } from '@aztec/entrypoints/interfaces';
7+
import { ExecutionPayload } from '@aztec/entrypoints/payload';
8+
import {
9+
type ContractArtifact,
10+
type FunctionArtifact,
11+
FunctionCall,
12+
FunctionSelector,
13+
encodeArguments,
14+
getFunctionArtifactByName,
15+
} from '@aztec/stdlib/abi';
16+
import { AztecAddress } from '@aztec/stdlib/aztec-address';
17+
import type { GasSettings } from '@aztec/stdlib/gas';
18+
19+
/**
20+
* Fee payment method that allows a contract to pay for its own deployment
21+
* It works by rerouting the provided fee payment method through the account's entrypoint,
22+
* which sets itself as fee payer.
23+
*
24+
* Usually, in order to pay fees it is necessary to obtain an ExecutionPayload that encodes the necessary information
25+
* that is sent to the user's account entrypoint, that has plumbing to handle a fee payload.
26+
* If there's no account contract yet (it's being deployed) a MultiCallContract is used, which doesn't have a concept of fees or
27+
* how to handle this payload.
28+
* HOWEVER, the account contract's entrypoint does, so this method reshapes that fee payload into a call to the account contract entrypoint
29+
* being deployed with the original fee payload.
30+
*
31+
* This class can be seen in action in AccountManager.ts#getSelfPaymentMethod
32+
*/
33+
export class AccountEntrypointMetaPaymentMethod implements FeePaymentMethod {
34+
constructor(
35+
private artifact: ContractArtifact,
36+
private authWitnessProvider: AuthWitnessProvider,
37+
private feePaymentNameOrArtifact: string | FunctionArtifact,
38+
private accountAddress: AztecAddress,
39+
private paymentMethod: FeePaymentMethod,
40+
) {}
41+
42+
getAsset(): Promise<AztecAddress> {
43+
return this.paymentMethod.getAsset();
44+
}
45+
46+
async getExecutionPayload(gasSettings: GasSettings): Promise<ExecutionPayload> {
47+
const emptyAppCalls = await EncodedAppEntrypointCalls.fromAppExecution([]);
48+
// Get the execution payload for the fee, it includes the calls and potentially authWitnesses
49+
const { calls: feeCalls, authWitnesses: feeAuthwitnesses } = await this.paymentMethod.getExecutionPayload(
50+
gasSettings,
51+
);
52+
// Encode the calls for the fee
53+
const feePayer = await this.paymentMethod.getFeePayer(gasSettings);
54+
const isFeePayer = feePayer.equals(this.accountAddress);
55+
const feeEncodedCalls = await EncodedCallsForEntrypoint.fromFeeCalls(feeCalls, isFeePayer);
56+
57+
// Get the entrypoint args
58+
const args = [emptyAppCalls, feeEncodedCalls, false];
59+
const feePaymentArtifact =
60+
typeof this.feePaymentNameOrArtifact === 'string'
61+
? getFunctionArtifactByName(this.artifact, this.feePaymentNameOrArtifact)
62+
: this.feePaymentNameOrArtifact;
63+
64+
const entrypointCall = new FunctionCall(
65+
feePaymentArtifact.name,
66+
this.accountAddress,
67+
await FunctionSelector.fromNameAndParameters(feePaymentArtifact.name, feePaymentArtifact.parameters),
68+
feePaymentArtifact.functionType,
69+
feePaymentArtifact.isStatic,
70+
encodeArguments(feePaymentArtifact, args),
71+
feePaymentArtifact.returnTypes,
72+
);
73+
74+
// Compute the authwitness required to verify the combined payload
75+
const combinedPayloadAuthWitness = await this.authWitnessProvider.createAuthWit(
76+
await computeCombinedPayloadHash(emptyAppCalls, feeEncodedCalls),
77+
);
78+
79+
return new ExecutionPayload(
80+
[entrypointCall],
81+
[combinedPayloadAuthWitness, ...feeAuthwitnesses],
82+
[],
83+
[...emptyAppCalls.hashedArguments, ...feeEncodedCalls.hashedArguments],
84+
);
85+
}
86+
87+
getFeePayer(gasSettings: GasSettings): Promise<AztecAddress> {
88+
return this.paymentMethod.getFeePayer(gasSettings);
89+
}
90+
}

yarn-project/cli-wallet/src/utils/sponsored_fee_payment.ts renamed to yarn-project/aztec.js/src/fee/sponsored_fee_payment.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ import { ExecutionPayload } from '@aztec/entrypoints/payload';
33
import { FunctionSelector, FunctionType } from '@aztec/stdlib/abi';
44
import { AztecAddress } from '@aztec/stdlib/aztec-address';
55

6+
/**
7+
* A fee payment method that uses a contract that blindly sponsors transactions.
8+
* This contract is expected to be prefunded in testing environments.
9+
*/
610
export class SponsoredFeePaymentMethod implements FeePaymentMethod {
711
constructor(private paymentContract: AztecAddress) {}
812

yarn-project/aztec/src/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,4 @@ export {
33
getDeployedBananaCoinAddress,
44
getDeployedBananaFPCAddress,
55
getDeployedSponsoredFPCAddress,
6-
SponsoredFeePaymentMethod,
76
} from './sandbox/index.js';

yarn-project/aztec/src/sandbox/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,3 @@ export * from './sandbox.js';
22

33
export { getDeployedBananaCoinAddress, getDeployedBananaFPCAddress } from './banana_fpc.js';
44
export { getDeployedSponsoredFPCAddress } from './sponsored_fpc.js';
5-
export { SponsoredFeePaymentMethod } from './sponsored_fee_payment_method.js';

0 commit comments

Comments
 (0)