Skip to content

Commit 0f74acd

Browse files
committed
multicall support
1 parent f3699a0 commit 0f74acd

File tree

1 file changed

+124
-96
lines changed

1 file changed

+124
-96
lines changed

in-progress/6973-refactor-base-contract-interaction.md

Lines changed: 124 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ This is a refactor of the API for interacting with contracts to improve the user
1212

1313
The refactored approach mimics Viem's API, with some enhancements and modifications to fit our needs.
1414

15-
In a nutshell, by being more verbose in the API, we can remove a lot of complexity and make the code easier to understand and maintain; this also affords greater understanding and control over the lifecycle of their contracts and transactions.
15+
In a nutshell, by being more verbose in the API, we can remove a lot of complexity and make the code easier to understand and maintain; this also affords greater understanding and control over the lifecycle of contracts and transactions.
1616

1717
Key changes:
1818
- the wallet is the central point of interaction to simulate/prove/send transactions instead of `BaseContractInteraction`
@@ -67,10 +67,13 @@ const paymentMethod = new SomeFeePaymentMethod(
6767

6868
// Changes to the PXE (e.g. notes, nullifiers, auth wits, contract deployments, capsules) are not persisted.
6969
const { request: deployAliceAccountRequest } = await aliceWallet.simulate({
70-
artifact: SchnorrAccountContract.artifact,
71-
instance: aliceContractInstance,
72-
functionName: deploymentArgs.constructorName,
73-
args: deploymentArgs.constructorArgs,
70+
// easy multicall support
71+
calls: [{
72+
artifact: SchnorrAccountContract.artifact,
73+
instance: aliceContractInstance,
74+
functionName: deploymentArgs.constructorName,
75+
args: deploymentArgs.constructorArgs,
76+
}],
7477
paymentMethod,
7578
// gasSettings: undefined => automatic gas estimation. the returned `request` will have the gasSettings set.
7679
});
@@ -112,14 +115,16 @@ const bananaCoinInstance = getContractInstanceFromDeployParams(
112115
);
113116

114117
const { request: deployTokenRequest } = await aliceWallet.simulate({
115-
artifact: TokenContract.artifact,
116-
instance: bananaCoinInstance,
117-
functionName: bananaCoinDeploymentArgs.constructorName,
118-
args: bananaCoinDeploymentArgs.constructorArgs,
119-
deploymentOptions: {
120-
registerClass: true,
121-
publicDeploy: true,
122-
},
118+
calls: [{
119+
artifact: TokenContract.artifact,
120+
instance: bananaCoinInstance,
121+
functionName: bananaCoinDeploymentArgs.constructorName,
122+
args: bananaCoinDeploymentArgs.constructorArgs,
123+
deploymentOptions: {
124+
registerClass: true,
125+
publicDeploy: true,
126+
},
127+
}],
123128
paymentMethod
124129
})
125130

@@ -133,9 +138,11 @@ const receipt = await sentTx.wait()
133138

134139
```ts
135140
const { result: privateBalance } = await aliceWallet.read({
136-
contractInstance: bananaCoinInstance,
137-
functionName: 'balance_of_private'
138-
args: {owner: aliceWallet.getAddress()},
141+
calls: [{
142+
contractInstance: bananaCoinInstance,
143+
functionName: 'balance_of_private'
144+
args: {owner: aliceWallet.getAddress()}
145+
}]
139146
});
140147

141148

@@ -276,16 +283,20 @@ export interface DeploymentOptions {
276283
publicDeploy?: boolean;
277284
}
278285

279-
// new
280-
export interface UserRequest {
286+
export interface UserFunctionCall {
281287
contractInstance: ContractInstanceWithAddress;
282288
functionName: string;
283289
args: any;
284290
deploymentOptions?: DeploymentOptions;
285-
gasSettings?: GasSettings;
286-
paymentMethod?: FeePaymentMethod;
287291
contractArtifact?: ContractArtifact;
288292
functionAbi?: FunctionAbi;
293+
}
294+
295+
// new
296+
export interface UserRequest {
297+
calls: UserFunctionCall[];
298+
gasSettings?: GasSettings;
299+
paymentMethod?: FeePaymentMethod;
289300
from?: AztecAddress;
290301
simulatePublicFunctions?: boolean;
291302
executionResult?: ExecutionResult; // the raw output of a simulation that can be proven
@@ -371,14 +382,16 @@ Consider that we have, e.g.:
371382

372383
```ts
373384
{
374-
contractInstance: bananaCoinInstance,
375-
functionName: 'transfer',
376-
args: {
377-
from: aliceAddress,
378-
to: bobAddress,
379-
value: privateBalance,
380-
nonce: 0n
381-
},
385+
calls: [{
386+
contractInstance: bananaCoinInstance,
387+
functionName: 'transfer',
388+
args: {
389+
from: aliceAddress,
390+
to: bobAddress,
391+
value: privateBalance,
392+
nonce: 0n
393+
},
394+
}],
382395
paymentMethod
383396
}
384397
```
@@ -417,40 +430,60 @@ function makeFunctionCall(
417430

418431
```
419432

433+
#### main function calls
434+
435+
Define a helper somewhere as:
436+
437+
```ts
438+
const addMainFunctionCall: TxExecutionRequestAdapter = (
439+
builder: TxExecutionRequestBuilder, call: UserFunctionCall
440+
) => {
441+
if (!call.functionAbi) {
442+
throw new Error('Function ABI must be provided');
443+
}
444+
builder.addAppFunctionCall(
445+
makeFunctionCall(
446+
call.functionAbi,
447+
call.contractInstance.address,
448+
call.args
449+
));
450+
}
451+
```
452+
420453
#### class registration
421454

422455
Define a helper somewhere as:
423456

424457
```ts
425-
export const addClassRegistration: TxExecutionRequestAdapter = (
426-
builder: TxExecutionRequestBuilder, request: UserRequest
458+
const addClassRegistration = (
459+
builder: TxExecutionRequestBuilder, call: UserFunctionCall
427460
) => {
428-
if (!request.contractArtifact) {
429-
throw new Error('Contract artifact must be provided to register class');
430-
}
461+
if (!call.contractArtifact) {
462+
throw new Error('Contract artifact must be provided to register class');
463+
}
431464

432-
const contractClass = getContractClassFromArtifact(request.contractArtifact);
465+
const contractClass = getContractClassFromArtifact(call.contractArtifact);
433466

434-
builder.addCapsule(
435-
bufferAsFields(
436-
contractClass.packedBytecode,
437-
MAX_PACKED_PUBLIC_BYTECODE_SIZE_IN_FIELDS
438-
));
467+
builder.addCapsule(
468+
bufferAsFields(
469+
contractClass.packedBytecode,
470+
MAX_PACKED_PUBLIC_BYTECODE_SIZE_IN_FIELDS
471+
));
439472

440-
const { artifact, instance } = getCanonicalClassRegisterer();
473+
const { artifact, instance } = getCanonicalClassRegisterer();
441474

442-
const registerFnAbi = findFunctionAbi(artifact, 'register');
475+
const registerFnAbi = findFunctionAbi(artifact, 'register');
443476

444-
builder.addAppFunctionCall(
445-
makeFunctionCall(
446-
registerFnAbi,
447-
instance.address,
448-
{
449-
artifact_hash: contractClass.artifactHash,
450-
private_functions_root: contractClass.privateFunctionsRoot,
451-
public_bytecode_commitment: contractClass.publicBytecodeCommitment
452-
}
453-
));
477+
builder.addAppFunctionCall(
478+
makeFunctionCall(
479+
registerFnAbi,
480+
instance.address,
481+
{
482+
artifact_hash: contractClass.artifactHash,
483+
private_functions_root: contractClass.privateFunctionsRoot,
484+
public_bytecode_commitment: contractClass.publicBytecodeCommitment
485+
}
486+
));
454487
}
455488
```
456489

@@ -460,8 +493,8 @@ Define a helper somewhere as
460493

461494
```ts
462495

463-
export const addPublicDeployment: TxExecutionRequestAdapter = (
464-
builder: TxExecutionRequestBuilder, request: UserRequest
496+
const addPublicDeployment = (
497+
builder: TxExecutionRequestBuilder, call: UserFunctionCall
465498
) => {
466499
const { artifact, instance } = getCanonicalInstanceDeployer();
467500
const deployFnAbi = findFunctionAbi(artifact, 'deploy');
@@ -478,6 +511,7 @@ export const addPublicDeployment: TxExecutionRequestAdapter = (
478511
}
479512
));
480513
}
514+
481515
```
482516

483517
#### Entrypoints implement `TxExecutionRequestComponent`
@@ -544,9 +578,6 @@ The abstract `BaseWallet` can implement:
544578

545579
```ts
546580
async getTxExecutionRequest(userRequest: UserRequest): Promise<TxExecutionRequest> {
547-
if (!userRequest.functionAbi) {
548-
throw new Error('Function ABI must be provided');
549-
}
550581
if (!userRequest.gasSettings) {
551582
throw new Error('Gas settings must be provided');
552583
}
@@ -556,34 +587,27 @@ async getTxExecutionRequest(userRequest: UserRequest): Promise<TxExecutionReques
556587

557588
const builder = new TxExecutionRequestBuilder();
558589

559-
// Add the "main" function call
560-
builder.addAppFunctionCall(
561-
makeFunctionCall(
562-
userRequest.functionAbi,
563-
userRequest.contractInstance.address,
564-
userRequest.args
565-
));
590+
for (const call of request.calls) {
591+
addMainFunctionCall(builder, call);
592+
if (call.deploymentOptions?.registerClass) {
593+
addClassRegistration(builder, call);
594+
}
595+
if (call.deploymentOptions?.publicDeploy) {
596+
addPublicDeployment(builder, call);
597+
}
598+
// if the user is giving us an artifact,
599+
// allow the PXE to access it
600+
if (call.contractArtifact) {
601+
builder.addTransientContract({
602+
artifact: call.contractArtifact,
603+
instance: call.contractInstance,
604+
});
605+
}
606+
}
566607

567608
// Add stuff needed for setup, e.g. function calls, auth witnesses, etc.
568609
await userRequest.paymentMethod.adaptTxExecutionRequest(builder, userRequest);
569610

570-
if (userRequest.deploymentOptions?.registerClass) {
571-
addClassRegistration(builder, userRequest);
572-
}
573-
574-
if (userRequest.deploymentOptions?.publicDeploy) {
575-
addPublicDeployment(builder, userRequest);
576-
}
577-
578-
// if the user is giving us an artifact,
579-
// allow the PXE to access it
580-
if (userRequest.contractArtifact) {
581-
builder.addTransientContract({
582-
artifact: userRequest.contractArtifact,
583-
instance: userRequest.contractInstance,
584-
});
585-
}
586-
587611
// Adapt the request to the entrypoint in use.
588612
// Since BaseWallet is abstract, this will be implemented by the concrete class.
589613
this.adaptTxExecutionRequest(builder, userRequest);
@@ -597,8 +621,8 @@ async getTxExecutionRequest(userRequest: UserRequest): Promise<TxExecutionReques
597621

598622
```ts
599623
// Used by simulate and read
600-
async #simulateInner(userRequest: UserRequest): ReturnType<Wallet['simulate']> {
601-
const txExecutionRequest = await this.getTxExecutionRequest(initRequest);
624+
async #simulateInner(userRequest: UserRequest): ReturnType<BaseWallet['simulate']> {
625+
const txExecutionRequest = await this.getTxExecutionRequest(userRequest);
602626
const simulatedTx = await this.simulateTx(txExecutionRequest, builder.simulatePublicFunctions, builder.from);
603627
const decodedReturn = decodeSimulatedTx(simulatedTx, builder.functionAbi);
604628
return {
@@ -607,7 +631,7 @@ async #simulateInner(userRequest: UserRequest): ReturnType<Wallet['simulate']> {
607631
privateOutput: simulatedTx.privateReturnValues,
608632
executionResult: simulatedTx.executionResult,
609633
result: decodedReturn,
610-
request: initRequest,
634+
request: userRequest,
611635
};
612636
}
613637
```
@@ -632,7 +656,7 @@ async simulate(userRequest: UserRequest): {
632656

633657
const builder = new UserRequestBuilder(userRequest);
634658

635-
await this.#ensureFunctionAbi(builder);
659+
await this.#ensureFunctionAbis(builder);
636660

637661
if (builder.gasSettings) {
638662
return this.#simulateInner(builder.build());
@@ -653,16 +677,18 @@ async simulate(userRequest: UserRequest): {
653677
return result;
654678
}
655679

656-
async #ensureFunctionAbi(builder: UserRequestBuilder): void {
657-
// User can call simulate without the artifact if they have the function ABI
658-
if (!builder.functionAbi) {
659-
// If the user provides the contract artifact, we don't need to ask the PXE
660-
if (!builder.contractArtifact) {
661-
const contractArtifact = await this.getContractArtifact(builder.contractInstance.contractClassId);
662-
builder.setContractArtifact(contractArtifact);
680+
async #ensureFunctionAbis(builder: UserRequestBuilder): void {
681+
for (const call of builder.calls) {
682+
// User can call simulate without the artifact if they have the function ABI
683+
if (!call.functionAbi) {
684+
// If the user provides the contract artifact, we don't need to ask the PXE
685+
if (!call.contractArtifact) {
686+
const contractArtifact = await this.getContractArtifact(call.contractInstance.contractClassId);
687+
call.setContractArtifact(contractArtifact);
688+
}
689+
const functionAbi = findFunctionAbi(call.contractArtifact, call.functionName);
690+
call.setFunctionAbi(functionAbi);
663691
}
664-
const functionAbi = findFunctionAbi(builder.contractArtifact, builder.functionName);
665-
builder.setFunctionAbi(functionAbi);
666692
}
667693
}
668694

@@ -695,7 +721,7 @@ async read(userRequest: UserRequest): DecodedReturn | [] {
695721
builder.setGasSettings(GasSettings.default());
696722
}
697723

698-
await this.#ensureFunctionAbi(builder);
724+
await this.#ensureFunctionAbis(builder);
699725

700726
return this.#simulateInner(builder.build());
701727
}
@@ -710,7 +736,7 @@ async prove(request: UserRequest): Promise<UserRequest> {
710736
throw new Error('Execution result must be set before proving');
711737
}
712738
const builder = new UserRequestBuilder(request);
713-
await this.#ensureFunctionAbi(builder);
739+
await this.#ensureFunctionAbis(builder);
714740
const initRequest = builder.build();
715741
const txExecutionRequest = await this.getTxExecutionRequest(initRequest);
716742
const provenTx = await this.proveTx(txExecutionRequest, request.executionResult);
@@ -730,7 +756,7 @@ async send(request: UserRequest): Promise<UserRequest> {
730756
throw new Error('Tx must be proven before sending');
731757
}
732758
const builder = new UserRequestBuilder(request);
733-
await this.#ensureFunctionAbi(builder);
759+
await this.#ensureFunctionAbis(builder);
734760
const initRequest = builder.build();
735761
const txExecutionRequest = await this.getTxExecutionRequest();
736762
const txHash = await this.sendTx(txExecutionRequest, request.tx);
@@ -744,6 +770,8 @@ async send(request: UserRequest): Promise<UserRequest> {
744770

745771
The `UserRequest` object is a bit of a kitchen sink. It might be better to have a `DeployRequest`, `CallRequest`, etc. that extends `UserRequest`.
746772

773+
Downside here is that the "pipeline" it goes through would be less clear, and components would have to be more aware of the type of request they are dealing with.
774+
747775
#### Just shifting the mutable subclass problem
748776

749777
Arguably the builder + adapter pattern just shifts the "mutable subclass" problem around. I think that since the entire lifecycle of the builder is contained to the `getTxExecutionRequest` method within a single abstract class, it's not nearly as bad as the current situation.

0 commit comments

Comments
 (0)