From 66a17b826700c210eebeff861e315a5065fb8b8c Mon Sep 17 00:00:00 2001 From: Alejo Amiras Date: Mon, 2 Mar 2026 21:21:49 +0000 Subject: [PATCH] fix: always register contracts with wallet PXE instead of conditional check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The old code called getContractMetadata before registration and skipped contracts where metadata.result.instance was truthy. This caused "Unknown contract" errors during simulation because: 1. getContractMetadata.instance comes from pxe.getContractInstance() (LMDB local read). A wallet with persistent storage from a prior session returns truthy instance data even when the artifact is missing (e.g. after a wallet update or SDK version change). 2. The wallet SDK's registerContract (base_wallet.ts:257-293) treats matching class IDs as a no-op—it does NOT verify the artifact is still present. So skipping registration left the PXE with instance data but no artifact, causing getContract() to return undefined and simulation to throw "Unknown contract". 3. gregoCoinPremium was registered with undefined artifact, relying on gregoCoin's class being registered first in the same sequential batch. This was fragile and would break if gregoCoin was skipped. The fix: - Remove the getContractMetadata check entirely (also saves a batch round-trip through the wallet SDK communication layer) - Always construct instances and register all contracts with their artifacts in a single batch - registerContract is idempotent by design (base_wallet.ts checks pxe.getContractInstance internally), so re-registering is safe Applied the same fix to registerDripContracts for consistency. Co-Authored-By: Claude Opus 4.6 --- src/services/contractService.ts | 105 ++++++++++---------------------- 1 file changed, 33 insertions(+), 72 deletions(-) diff --git a/src/services/contractService.ts b/src/services/contractService.ts index 5f98ce7..83e2ef2 100644 --- a/src/services/contractService.ts +++ b/src/services/contractService.ts @@ -49,7 +49,6 @@ export async function getSponsoredFPCData() { /** * Registers contracts needed for the swap flow * Returns the contract instances after registration - * Skips registration for contracts that are already registered */ export async function registerSwapContracts( wallet: Wallet, @@ -67,56 +66,33 @@ export async function registerSwapContracts( const { TokenContract, TokenContractArtifact } = await import('@aztec/noir-contracts.js/Token'); const { AMMContract, AMMContractArtifact } = await import('@aztec/noir-contracts.js/AMM'); - // Check which contracts are already registered - const [ammMetadata, gregoCoinMetadata, gregoCoinPremiumMetadata] = await wallet.batch([ - { name: 'getContractMetadata', args: [ammAddress] }, - { name: 'getContractMetadata', args: [gregoCoinAddress] }, - { name: 'getContractMetadata', args: [gregoCoinPremiumAddress] }, - ]); - - // Reconstruct contract instances for unregistered contracts + // Reconstruct contract instances for registration const [ammInstance, gregoCoinInstance, gregoCoinPremiumInstance] = await Promise.all([ - !ammMetadata.result.instance - ? getContractInstanceFromInstantiationParams(AMMContractArtifact, { - salt: contractSalt, - deployer: deployerAddress, - constructorArgs: [gregoCoinAddress, gregoCoinPremiumAddress, liquidityTokenAddress], - }) - : null, - !gregoCoinMetadata.result.instance - ? getContractInstanceFromInstantiationParams(TokenContractArtifact, { - salt: contractSalt, - deployer: deployerAddress, - constructorArgs: [deployerAddress, 'GregoCoin', 'GRG', 18], - }) - : null, - !gregoCoinPremiumMetadata.result.instance - ? getContractInstanceFromInstantiationParams(TokenContractArtifact, { - salt: contractSalt, - deployer: deployerAddress, - constructorArgs: [deployerAddress, 'GregoCoinPremium', 'GRGP', 18], - }) - : null, + getContractInstanceFromInstantiationParams(AMMContractArtifact, { + salt: contractSalt, + deployer: deployerAddress, + constructorArgs: [gregoCoinAddress, gregoCoinPremiumAddress, liquidityTokenAddress], + }), + getContractInstanceFromInstantiationParams(TokenContractArtifact, { + salt: contractSalt, + deployer: deployerAddress, + constructorArgs: [deployerAddress, 'GregoCoin', 'GRG', 18], + }), + getContractInstanceFromInstantiationParams(TokenContractArtifact, { + salt: contractSalt, + deployer: deployerAddress, + constructorArgs: [deployerAddress, 'GregoCoinPremium', 'GRGP', 18], + }), ]); - // Build registration batch for unregistered contracts only - const registrationBatch: { name: 'registerContract'; args: [any, any, any] }[] = []; - - if (ammInstance) { - registrationBatch.push({ name: 'registerContract', args: [ammInstance, AMMContractArtifact, undefined] }); - } - if (gregoCoinInstance) { - registrationBatch.push({ name: 'registerContract', args: [gregoCoinInstance, TokenContractArtifact, undefined] }); - } - if (gregoCoinPremiumInstance) { - // gregoCoinPremium shares the same artifact as gregoCoin, so we can omit it - registrationBatch.push({ name: 'registerContract', args: [gregoCoinPremiumInstance, undefined, undefined] }); - } - - // Only call batch if there are contracts to register - if (registrationBatch.length > 0) { - await wallet.batch(registrationBatch); - } + // Register all contracts with the wallet's PXE. + // Always register even if contracts exist on-chain, since the wallet's PXE + // may be freshly initialized and needs them registered locally for simulation. + await wallet.batch([ + { name: 'registerContract', args: [ammInstance, AMMContractArtifact, undefined] }, + { name: 'registerContract', args: [gregoCoinInstance, TokenContractArtifact, undefined] }, + { name: 'registerContract', args: [gregoCoinPremiumInstance, TokenContractArtifact, undefined] }, + ]); // Instantiate the contracts const gregoCoin = TokenContract.at(gregoCoinAddress, wallet); @@ -129,7 +105,6 @@ export async function registerSwapContracts( /** * Registers contracts needed for the drip flow * Returns the contract instances after registration - * Skips registration for contracts that are already registered */ export async function registerDripContracts( wallet: Wallet, @@ -144,30 +119,16 @@ export async function registerDripContracts( const { instance: sponsoredFPCInstance, artifact: SponsoredFPCContractArtifact } = await getSponsoredFPCData(); - // Check which contracts are already registered - const [popMetadata, sponsoredFPCMetadata] = await wallet.batch([ - { name: 'getContractMetadata', args: [popAddress] }, - { name: 'getContractMetadata', args: [sponsoredFPCInstance.address] }, - ]); - - // Build registration batch for unregistered contracts only - const registrationBatch: { name: 'registerContract'; args: [any, any, any] }[] = []; - - if (!popMetadata.result.instance) { - const instance = await node.getContract(popAddress); - registrationBatch.push({ name: 'registerContract', args: [instance, ProofOfPasswordContractArtifact, undefined] }); - } - if (!sponsoredFPCMetadata.result.instance) { - registrationBatch.push({ - name: 'registerContract', - args: [sponsoredFPCInstance, SponsoredFPCContractArtifact, undefined], - }); - } + // Fetch the PoP contract instance from the node + const popInstance = await node.getContract(popAddress); - // Only call batch if there are contracts to register - if (registrationBatch.length > 0) { - await wallet.batch(registrationBatch); - } + // Register all contracts with the wallet's PXE. + // Always register even if contracts exist on-chain, since the wallet's PXE + // may be freshly initialized and needs them registered locally for simulation. + await wallet.batch([ + { name: 'registerContract', args: [popInstance, ProofOfPasswordContractArtifact, undefined] }, + { name: 'registerContract', args: [sponsoredFPCInstance, SponsoredFPCContractArtifact, undefined] }, + ]); // Instantiate the ProofOfPassword contract const pop = ProofOfPasswordContract.at(popAddress, wallet);