@@ -12,7 +12,7 @@ This is a refactor of the API for interacting with contracts to improve the user
12
12
13
13
The refactored approach mimics Viem's API, with some enhancements and modifications to fit our needs.
14
14
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.
16
16
17
17
Key changes:
18
18
- the wallet is the central point of interaction to simulate/prove/send transactions instead of ` BaseContractInteraction `
@@ -67,10 +67,13 @@ const paymentMethod = new SomeFeePaymentMethod(
67
67
68
68
// Changes to the PXE (e.g. notes, nullifiers, auth wits, contract deployments, capsules) are not persisted.
69
69
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
+ }],
74
77
paymentMethod ,
75
78
// gasSettings: undefined => automatic gas estimation. the returned `request` will have the gasSettings set.
76
79
});
@@ -112,14 +115,16 @@ const bananaCoinInstance = getContractInstanceFromDeployParams(
112
115
);
113
116
114
117
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
+ }],
123
128
paymentMethod
124
129
})
125
130
@@ -133,9 +138,11 @@ const receipt = await sentTx.wait()
133
138
134
139
``` ts
135
140
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
+ }]
139
146
});
140
147
141
148
@@ -276,16 +283,20 @@ export interface DeploymentOptions {
276
283
publicDeploy? : boolean ;
277
284
}
278
285
279
- // new
280
- export interface UserRequest {
286
+ export interface UserFunctionCall {
281
287
contractInstance: ContractInstanceWithAddress ;
282
288
functionName: string ;
283
289
args: any ;
284
290
deploymentOptions? : DeploymentOptions ;
285
- gasSettings? : GasSettings ;
286
- paymentMethod? : FeePaymentMethod ;
287
291
contractArtifact? : ContractArtifact ;
288
292
functionAbi? : FunctionAbi ;
293
+ }
294
+
295
+ // new
296
+ export interface UserRequest {
297
+ calls: UserFunctionCall [];
298
+ gasSettings? : GasSettings ;
299
+ paymentMethod? : FeePaymentMethod ;
289
300
from? : AztecAddress ;
290
301
simulatePublicFunctions? : boolean ;
291
302
executionResult? : ExecutionResult ; // the raw output of a simulation that can be proven
@@ -371,14 +382,16 @@ Consider that we have, e.g.:
371
382
372
383
``` ts
373
384
{
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
+ }],
382
395
paymentMethod
383
396
}
384
397
```
@@ -417,40 +430,60 @@ function makeFunctionCall(
417
430
418
431
```
419
432
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
+
420
453
#### class registration
421
454
422
455
Define a helper somewhere as:
423
456
424
457
``` ts
425
- export const addClassRegistration: TxExecutionRequestAdapter = (
426
- builder : TxExecutionRequestBuilder , request : UserRequest
458
+ const addClassRegistration = (
459
+ builder : TxExecutionRequestBuilder , call : UserFunctionCall
427
460
) => {
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
+ }
431
464
432
- const contractClass = getContractClassFromArtifact (request .contractArtifact );
465
+ const contractClass = getContractClassFromArtifact (call .contractArtifact );
433
466
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
+ ));
439
472
440
- const { artifact, instance } = getCanonicalClassRegisterer ();
473
+ const { artifact, instance } = getCanonicalClassRegisterer ();
441
474
442
- const registerFnAbi = findFunctionAbi (artifact , ' register' );
475
+ const registerFnAbi = findFunctionAbi (artifact , ' register' );
443
476
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
+ ));
454
487
}
455
488
```
456
489
@@ -460,8 +493,8 @@ Define a helper somewhere as
460
493
461
494
``` ts
462
495
463
- export const addPublicDeployment: TxExecutionRequestAdapter = (
464
- builder : TxExecutionRequestBuilder , request : UserRequest
496
+ const addPublicDeployment = (
497
+ builder : TxExecutionRequestBuilder , call : UserFunctionCall
465
498
) => {
466
499
const { artifact, instance } = getCanonicalInstanceDeployer ();
467
500
const deployFnAbi = findFunctionAbi (artifact , ' deploy' );
@@ -478,6 +511,7 @@ export const addPublicDeployment: TxExecutionRequestAdapter = (
478
511
}
479
512
));
480
513
}
514
+
481
515
```
482
516
483
517
#### Entrypoints implement ` TxExecutionRequestComponent `
@@ -544,9 +578,6 @@ The abstract `BaseWallet` can implement:
544
578
545
579
``` ts
546
580
async getTxExecutionRequest (userRequest : UserRequest ): Promise < TxExecutionRequest > {
547
- if (!userRequest.functionAbi) {
548
- throw new Error (' Function ABI must be provided' );
549
- }
550
581
if (!userRequest.gasSettings) {
551
582
throw new Error (' Gas settings must be provided' );
552
583
}
@@ -556,34 +587,27 @@ async getTxExecutionRequest(userRequest: UserRequest): Promise<TxExecutionReques
556
587
557
588
const builder = new TxExecutionRequestBuilder ();
558
589
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
+ }
566
607
567
608
// Add stuff needed for setup, e.g. function calls, auth witnesses, etc.
568
609
await userRequest.paymentMethod.adaptTxExecutionRequest(builder , userRequest);
569
610
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
-
587
611
// Adapt the request to the entrypoint in use.
588
612
// Since BaseWallet is abstract, this will be implemented by the concrete class.
589
613
this.adaptTxExecutionRequest(builder , userRequest);
@@ -597,8 +621,8 @@ async getTxExecutionRequest(userRequest: UserRequest): Promise<TxExecutionReques
597
621
598
622
``` ts
599
623
// 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 );
602
626
const simulatedTx = await this .simulateTx (txExecutionRequest , builder .simulatePublicFunctions , builder .from );
603
627
const decodedReturn = decodeSimulatedTx (simulatedTx , builder .functionAbi );
604
628
return {
@@ -607,7 +631,7 @@ async #simulateInner(userRequest: UserRequest): ReturnType<Wallet['simulate']> {
607
631
privateOutput : simulatedTx .privateReturnValues ,
608
632
executionResult : simulatedTx .executionResult ,
609
633
result : decodedReturn ,
610
- request : initRequest ,
634
+ request : userRequest ,
611
635
};
612
636
}
613
637
```
@@ -632,7 +656,7 @@ async simulate(userRequest: UserRequest): {
632
656
633
657
const builder = new UserRequestBuilder (userRequest );
634
658
635
- await this .#ensureFunctionAbi (builder );
659
+ await this .#ensureFunctionAbis (builder );
636
660
637
661
if (builder .gasSettings ) {
638
662
return this .#simulateInner (builder .build ());
@@ -653,16 +677,18 @@ async simulate(userRequest: UserRequest): {
653
677
return result ;
654
678
}
655
679
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 );
663
691
}
664
- const functionAbi = findFunctionAbi (builder .contractArtifact , builder .functionName );
665
- builder .setFunctionAbi (functionAbi );
666
692
}
667
693
}
668
694
@@ -695,7 +721,7 @@ async read(userRequest: UserRequest): DecodedReturn | [] {
695
721
builder .setGasSettings (GasSettings .default ());
696
722
}
697
723
698
- await this .#ensureFunctionAbi (builder );
724
+ await this .#ensureFunctionAbis (builder );
699
725
700
726
return this .#simulateInner (builder .build ());
701
727
}
@@ -710,7 +736,7 @@ async prove(request: UserRequest): Promise<UserRequest> {
710
736
throw new Error (' Execution result must be set before proving' );
711
737
}
712
738
const builder = new UserRequestBuilder (request );
713
- await this.#ensureFunctionAbi (builder );
739
+ await this.#ensureFunctionAbis (builder );
714
740
const initRequest = builder .build ();
715
741
const txExecutionRequest = await this .getTxExecutionRequest (initRequest );
716
742
const provenTx = await this .proveTx (txExecutionRequest , request .executionResult );
@@ -730,7 +756,7 @@ async send(request: UserRequest): Promise<UserRequest> {
730
756
throw new Error (' Tx must be proven before sending' );
731
757
}
732
758
const builder = new UserRequestBuilder (request );
733
- await this.#ensureFunctionAbi (builder );
759
+ await this.#ensureFunctionAbis (builder );
734
760
const initRequest = builder .build ();
735
761
const txExecutionRequest = await this .getTxExecutionRequest ();
736
762
const txHash = await this .sendTx (txExecutionRequest , request .tx );
@@ -744,6 +770,8 @@ async send(request: UserRequest): Promise<UserRequest> {
744
770
745
771
The ` UserRequest ` object is a bit of a kitchen sink. It might be better to have a ` DeployRequest ` , ` CallRequest ` , etc. that extends ` UserRequest ` .
746
772
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
+
747
775
#### Just shifting the mutable subclass problem
748
776
749
777
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