From cc8bd80730f7ec269be9282d0e90fc2b6dc6561a Mon Sep 17 00:00:00 2001 From: Gregorio Juliana Date: Mon, 9 Dec 2024 14:35:55 +0100 Subject: [PATCH] feat: cli wallet improvements (#10425) `cli-wallet` now supports an integrated PXE, so it can connect to a real-er network directly. A new command was also added to register a recipient so the new note discovery and syncing process is supported. Also found a bug in the synchronizer that prevented PXE from recovering when starting from a pre-seeded db (it always set its own header to the one of the genesis block, regardless of stored state) --- yarn-project/aztec.js/src/index.ts | 1 + yarn-project/aztec.js/src/utils/index.ts | 1 + yarn-project/aztec.js/src/utils/node.ts | 17 ++ yarn-project/cli-wallet/package.json | 1 + yarn-project/cli-wallet/src/bin/index.ts | 34 +++- .../cli-wallet/src/cmds/bridge_fee_juice.ts | 13 +- .../cli-wallet/src/cmds/create_account.ts | 2 +- yarn-project/cli-wallet/src/cmds/index.ts | 159 +++++++++++++----- .../cli-wallet/src/cmds/register_contact.ts | 7 + .../cli-wallet/src/cmds/register_contract.ts | 38 +++++ .../cli-wallet/src/storage/wallet_db.ts | 5 + yarn-project/cli-wallet/src/utils/accounts.ts | 2 +- .../cli-wallet/src/utils/pxe_wrapper.ts | 21 +++ yarn-project/cli-wallet/test/test.sh | 9 + yarn-project/cli-wallet/tsconfig.json | 3 + yarn-project/cli/src/utils/commands.ts | 1 + .../end-to-end/scripts/docker-compose.yml | 1 + .../src/guides/up_quick_start.test.ts | 8 +- .../pxe/src/synchronizer/synchronizer.ts | 14 +- yarn-project/yarn.lock | 1 + 20 files changed, 277 insertions(+), 61 deletions(-) create mode 100644 yarn-project/aztec.js/src/utils/node.ts create mode 100644 yarn-project/cli-wallet/src/cmds/register_contact.ts create mode 100644 yarn-project/cli-wallet/src/cmds/register_contract.ts create mode 100644 yarn-project/cli-wallet/src/utils/pxe_wrapper.ts diff --git a/yarn-project/aztec.js/src/index.ts b/yarn-project/aztec.js/src/index.ts index 31cd10e926f..b1b58e52998 100644 --- a/yarn-project/aztec.js/src/index.ts +++ b/yarn-project/aztec.js/src/index.ts @@ -53,6 +53,7 @@ export { generatePublicKey, readFieldCompressedString, waitForPXE, + waitForNode, type AztecAddressLike, type EthAddressLike, type EventSelectorLike, diff --git a/yarn-project/aztec.js/src/utils/index.ts b/yarn-project/aztec.js/src/utils/index.ts index 7a980b6ca68..68a1b4d12fa 100644 --- a/yarn-project/aztec.js/src/utils/index.ts +++ b/yarn-project/aztec.js/src/utils/index.ts @@ -4,6 +4,7 @@ export * from './abi_types.js'; export * from './cheat_codes.js'; export * from './authwit.js'; export * from './pxe.js'; +export * from './node.js'; export * from './anvil_test_watcher.js'; export * from './field_compressed_string.js'; export * from './portal_manager.js'; diff --git a/yarn-project/aztec.js/src/utils/node.ts b/yarn-project/aztec.js/src/utils/node.ts new file mode 100644 index 00000000000..51c815aa5fe --- /dev/null +++ b/yarn-project/aztec.js/src/utils/node.ts @@ -0,0 +1,17 @@ +import { type AztecNode } from '@aztec/circuit-types'; +import { type DebugLogger } from '@aztec/foundation/log'; +import { retryUntil } from '@aztec/foundation/retry'; + +export const waitForNode = async (node: AztecNode, logger?: DebugLogger) => { + await retryUntil(async () => { + try { + logger?.verbose('Attempting to contact Aztec node...'); + await node.getNodeInfo(); + logger?.verbose('Contacted Aztec node'); + return true; + } catch (error) { + logger?.verbose('Failed to contact Aztec Node'); + } + return undefined; + }, 'RPC Get Node Info'); +}; diff --git a/yarn-project/cli-wallet/package.json b/yarn-project/cli-wallet/package.json index 7973038a447..6706f3273b3 100644 --- a/yarn-project/cli-wallet/package.json +++ b/yarn-project/cli-wallet/package.json @@ -75,6 +75,7 @@ "@aztec/foundation": "workspace:^", "@aztec/kv-store": "workspace:^", "@aztec/noir-contracts.js": "workspace:^", + "@aztec/pxe": "workspace:^", "commander": "^12.1.0", "inquirer": "^10.1.8", "source-map-support": "^0.5.21", diff --git a/yarn-project/cli-wallet/src/bin/index.ts b/yarn-project/cli-wallet/src/bin/index.ts index 638c800d190..8d70d048bf9 100644 --- a/yarn-project/cli-wallet/src/bin/index.ts +++ b/yarn-project/cli-wallet/src/bin/index.ts @@ -1,14 +1,17 @@ import { Fr, computeSecretHash, fileURLToPath } from '@aztec/aztec.js'; +import { LOCALHOST } from '@aztec/cli/cli-utils'; import { type LogFn, createConsoleLogger, createDebugLogger } from '@aztec/foundation/log'; import { AztecLmdbStore } from '@aztec/kv-store/lmdb'; +import { type PXEService } from '@aztec/pxe'; -import { Argument, Command } from 'commander'; +import { Argument, Command, Option } from 'commander'; import { readFileSync } from 'fs'; -import { dirname, resolve } from 'path'; +import { dirname, join, resolve } from 'path'; import { injectCommands } from '../cmds/index.js'; import { Aliases, WalletDB } from '../storage/wallet_db.js'; import { createAliasOption } from '../utils/options/index.js'; +import { PXEWrapper } from '../utils/pxe_wrapper.js'; const userLog = createConsoleLogger(); const debugLogger = createDebugLogger('aztec:wallet'); @@ -66,18 +69,39 @@ async function main() { const walletVersion: string = JSON.parse(readFileSync(packageJsonPath).toString()).version; const db = WalletDB.getInstance(); + const pxeWrapper = new PXEWrapper(); const program = new Command('wallet'); program .description('Aztec wallet') .version(walletVersion) .option('-d, --data-dir ', 'Storage directory for wallet data', WALLET_DATA_DIRECTORY) - .hook('preSubcommand', command => { - const dataDir = command.optsWithGlobals().dataDir; + .addOption( + new Option('--remote-pxe', 'Connect to an external PXE RPC server, instead of the local one') + .env('REMOTE_PXE') + .default(false) + .conflicts('rpc-url'), + ) + .addOption( + new Option('-n, --node-url ', 'URL of the Aztec node to connect to') + .env('AZTEC_NODE_URL') + .default(`http://${LOCALHOST}:8080`), + ) + .hook('preSubcommand', async command => { + const { dataDir, remotePxe, nodeUrl } = command.optsWithGlobals(); + if (!remotePxe) { + debugLogger.info('Using local PXE service'); + await pxeWrapper.init(nodeUrl, join(dataDir, 'pxe')); + } db.init(AztecLmdbStore.open(dataDir)); + }) + .hook('postAction', async () => { + if (pxeWrapper.getPXE()) { + await (pxeWrapper.getPXE() as PXEService).stop(); + } }); - injectCommands(program, userLog, debugLogger, db); + injectCommands(program, userLog, debugLogger, db, pxeWrapper); injectInternalCommands(program, userLog, db); await program.parseAsync(process.argv); } diff --git a/yarn-project/cli-wallet/src/cmds/bridge_fee_juice.ts b/yarn-project/cli-wallet/src/cmds/bridge_fee_juice.ts index 12daf7172c3..a9558b14281 100644 --- a/yarn-project/cli-wallet/src/cmds/bridge_fee_juice.ts +++ b/yarn-project/cli-wallet/src/cmds/bridge_fee_juice.ts @@ -1,4 +1,4 @@ -import { L1FeeJuicePortalManager, createCompatibleClient } from '@aztec/aztec.js'; +import { L1FeeJuicePortalManager, type PXE } from '@aztec/aztec.js'; import { prettyPrintJSON } from '@aztec/cli/utils'; import { createEthereumChain, createL1Clients } from '@aztec/ethereum'; import { type AztecAddress } from '@aztec/foundation/aztec-address'; @@ -8,7 +8,7 @@ import { type DebugLogger, type LogFn } from '@aztec/foundation/log'; export async function bridgeL1FeeJuice( amount: bigint, recipient: AztecAddress, - rpcUrl: string, + pxe: PXE, l1RpcUrl: string, chainId: number, privateKey: string | undefined, @@ -24,15 +24,12 @@ export async function bridgeL1FeeJuice( const chain = createEthereumChain(l1RpcUrl, chainId); const { publicClient, walletClient } = createL1Clients(chain.rpcUrl, privateKey ?? mnemonic, chain.chainInfo); - // Prepare L2 client - const client = await createCompatibleClient(rpcUrl, debugLogger); - const { protocolContractAddresses: { feeJuice: feeJuiceAddress }, - } = await client.getPXEInfo(); + } = await pxe.getPXEInfo(); // Setup portal manager - const portal = await L1FeeJuicePortalManager.new(client, publicClient, walletClient, debugLogger); + const portal = await L1FeeJuicePortalManager.new(pxe, publicClient, walletClient, debugLogger); const { claimAmount, claimSecret, messageHash, messageLeafIndex } = await portal.bridgeTokensPublic( recipient, amount, @@ -69,7 +66,7 @@ export async function bridgeL1FeeJuice( const delayedCheck = (delay: number) => { return new Promise(resolve => { setTimeout(async () => { - const witness = await client.getL1ToL2MembershipWitness( + const witness = await pxe.getL1ToL2MembershipWitness( feeJuiceAddress, Fr.fromString(messageHash), claimSecret, diff --git a/yarn-project/cli-wallet/src/cmds/create_account.ts b/yarn-project/cli-wallet/src/cmds/create_account.ts index 4d21262c97f..7381e61e6e3 100644 --- a/yarn-project/cli-wallet/src/cmds/create_account.ts +++ b/yarn-project/cli-wallet/src/cmds/create_account.ts @@ -27,8 +27,8 @@ export async function createAccount( client, undefined /* address, we don't have it yet */, undefined /* db, as we want to create from scratch */, - accountType, secretKey, + accountType, Fr.ZERO, publicKey, ); diff --git a/yarn-project/cli-wallet/src/cmds/index.ts b/yarn-project/cli-wallet/src/cmds/index.ts index a286ad58603..a836081825d 100644 --- a/yarn-project/cli-wallet/src/cmds/index.ts +++ b/yarn-project/cli-wallet/src/cmds/index.ts @@ -1,6 +1,6 @@ import { getIdentities } from '@aztec/accounts/utils'; import { TxHash, createCompatibleClient } from '@aztec/aztec.js'; -import { Fr, PublicKeys } from '@aztec/circuits.js'; +import { PublicKeys } from '@aztec/circuits.js'; import { ETHEREUM_HOST, PRIVATE_KEY, @@ -39,8 +39,15 @@ import { integerArgParser, parsePaymentMethod, } from '../utils/options/index.js'; - -export function injectCommands(program: Command, log: LogFn, debugLogger: DebugLogger, db?: WalletDB) { +import { type PXEWrapper } from '../utils/pxe_wrapper.js'; + +export function injectCommands( + program: Command, + log: LogFn, + debugLogger: DebugLogger, + db?: WalletDB, + pxeWrapper?: PXEWrapper, +) { const createAccountCommand = program .command('create-account') .description( @@ -91,7 +98,7 @@ export function injectCommands(program: Command, log: LogFn, debugLogger: DebugL ]); publicKey = answers.identity.split(' ')[1]; } - const client = await createCompatibleClient(rpcUrl, debugLogger); + const client = pxeWrapper?.getPXE() ?? (await createCompatibleClient(rpcUrl, debugLogger)); const accountCreationResult = await createAccount( client, type, @@ -128,7 +135,7 @@ export function injectCommands(program: Command, log: LogFn, debugLogger: DebugL const options = command.optsWithGlobals(); const { rpcUrl, wait, from: parsedFromAddress, json } = options; - const client = await createCompatibleClient(rpcUrl, debugLogger); + const client = pxeWrapper?.getPXE() ?? (await createCompatibleClient(rpcUrl, debugLogger)); const account = await createOrRetrieveAccount(client, parsedFromAddress, db); await deployAccount(account, wait, await FeeOpts.fromCli(options, client, log, db), json, debugLogger, log); @@ -158,7 +165,6 @@ export function injectCommands(program: Command, log: LogFn, debugLogger: DebugL ) .addOption(createAccountOption('Alias or address of the account to deploy from', !db, db)) .addOption(createAliasOption('Alias for the contract. Used for easy reference subsequent commands.', !db)) - .addOption(createTypeOption(false)) .option('--json', 'Emit output as json') // `options.wait` is default true. Passing `--no-wait` will set it to false. // https://github.com/tj/commander.js#other-option-types-negatable-boolean-and-booleanvalue @@ -183,10 +189,9 @@ export function injectCommands(program: Command, log: LogFn, debugLogger: DebugL rpcUrl, from: parsedFromAddress, alias, - type, } = options; - const client = await createCompatibleClient(rpcUrl, debugLogger); - const account = await createOrRetrieveAccount(client, parsedFromAddress, db, type, secretKey, Fr.ZERO, publicKey); + const client = pxeWrapper?.getPXE() ?? (await createCompatibleClient(rpcUrl, debugLogger)); + const account = await createOrRetrieveAccount(client, parsedFromAddress, db, secretKey); const wallet = await getWalletWithScopes(account, db); const artifactPath = await artifactPathPromise; @@ -231,7 +236,6 @@ export function injectCommands(program: Command, log: LogFn, debugLogger: DebugL createSecretKeyOption("The sender's secret key", !db, sk => aliasedSecretKeyParser(sk, db)).conflicts('account'), ) .addOption(createAccountOption('Alias or address of the account to send the transaction from', !db, db)) - .addOption(createTypeOption(false)) .option('--no-wait', 'Print transaction hash without waiting for it to be mined') .option('--no-cancel', 'Do not allow the transaction to be cancelled. This makes for cheaper transactions.'); @@ -245,14 +249,12 @@ export function injectCommands(program: Command, log: LogFn, debugLogger: DebugL from: parsedFromAddress, wait, rpcUrl, - type, secretKey, - publicKey, alias, cancel, } = options; - const client = await createCompatibleClient(rpcUrl, debugLogger); - const account = await createOrRetrieveAccount(client, parsedFromAddress, db, type, secretKey, Fr.ZERO, publicKey); + const client = pxeWrapper?.getPXE() ?? (await createCompatibleClient(rpcUrl, debugLogger)); + const account = await createOrRetrieveAccount(client, parsedFromAddress, db, secretKey); const wallet = await getWalletWithScopes(account, db); const artifactPath = await artifactPathFromPromiseOrAlias(artifactPathPromise, contractAddress, db); @@ -287,7 +289,6 @@ export function injectCommands(program: Command, log: LogFn, debugLogger: DebugL createSecretKeyOption("The sender's secret key", !db, sk => aliasedSecretKeyParser(sk, db)).conflicts('account'), ) .addOption(createAccountOption('Alias or address of the account to simulate from', !db, db)) - .addOption(createTypeOption(false)) .addOption(createProfileOption()) .action(async (functionName, _options, command) => { const { simulate } = await import('./simulate.js'); @@ -298,14 +299,12 @@ export function injectCommands(program: Command, log: LogFn, debugLogger: DebugL contractAddress, from: parsedFromAddress, rpcUrl, - type, secretKey, - publicKey, profile, } = options; - const client = await createCompatibleClient(rpcUrl, debugLogger); - const account = await createOrRetrieveAccount(client, parsedFromAddress, db, type, secretKey, Fr.ZERO, publicKey); + const client = pxeWrapper?.getPXE() ?? (await createCompatibleClient(rpcUrl, debugLogger)); + const account = await createOrRetrieveAccount(client, parsedFromAddress, db, secretKey); const wallet = await getWalletWithScopes(account, db); const artifactPath = await artifactPathFromPromiseOrAlias(artifactPathPromise, contractAddress, db); await simulate(wallet, functionName, args, artifactPath, contractAddress, profile, log); @@ -344,10 +343,12 @@ export function injectCommands(program: Command, log: LogFn, debugLogger: DebugL .action(async (amount, recipient, options) => { const { bridgeL1FeeJuice } = await import('./bridge_fee_juice.js'); const { rpcUrl, l1RpcUrl, l1ChainId, l1PrivateKey, mnemonic, mint, json, wait, interval: intervalS } = options; + const client = pxeWrapper?.getPXE() ?? (await createCompatibleClient(rpcUrl, debugLogger)); + const [secret, messageLeafIndex] = await bridgeL1FeeJuice( amount, recipient, - rpcUrl, + client, l1RpcUrl, l1ChainId, l1PrivateKey, @@ -404,7 +405,7 @@ export function injectCommands(program: Command, log: LogFn, debugLogger: DebugL transactionHash, } = options; const artifactPath = await artifactPathFromPromiseOrAlias(artifactPathPromise, contractAddress, db); - const client = await createCompatibleClient(rpcUrl, debugLogger); + const client = pxeWrapper?.getPXE() ?? (await createCompatibleClient(rpcUrl, debugLogger)); const account = await createOrRetrieveAccount(client, address, db, undefined, secretKey); const wallet = await getWalletWithScopes(account, db); @@ -438,7 +439,6 @@ export function injectCommands(program: Command, log: LogFn, debugLogger: DebugL createSecretKeyOption("The sender's secret key", !db, sk => aliasedSecretKeyParser(sk, db)).conflicts('account'), ) .addOption(createAccountOption('Alias or address of the account to simulate from', !db, db)) - .addOption(createTypeOption(false)) .addOption( createAliasOption('Alias for the authorization witness. Used for easy reference in subsequent commands.', !db), ) @@ -451,14 +451,12 @@ export function injectCommands(program: Command, log: LogFn, debugLogger: DebugL contractAddress, from: parsedFromAddress, rpcUrl, - type, secretKey, - publicKey, alias, } = options; - const client = await createCompatibleClient(rpcUrl, debugLogger); - const account = await createOrRetrieveAccount(client, parsedFromAddress, db, type, secretKey, Fr.ZERO, publicKey); + const client = pxeWrapper?.getPXE() ?? (await createCompatibleClient(rpcUrl, debugLogger)); + const account = await createOrRetrieveAccount(client, parsedFromAddress, db, secretKey); const wallet = await getWalletWithScopes(account, db); const artifactPath = await artifactPathFromPromiseOrAlias(artifactPathPromise, contractAddress, db); const witness = await createAuthwit(wallet, functionName, caller, args, artifactPath, contractAddress, log); @@ -485,7 +483,6 @@ export function injectCommands(program: Command, log: LogFn, debugLogger: DebugL createSecretKeyOption("The sender's secret key", !db, sk => aliasedSecretKeyParser(sk, db)).conflicts('account'), ) .addOption(createAccountOption('Alias or address of the account to simulate from', !db, db)) - .addOption(createTypeOption(false)) .action(async (functionName, caller, _options, command) => { const { authorizeAction } = await import('./authorize_action.js'); const options = command.optsWithGlobals(); @@ -495,13 +492,11 @@ export function injectCommands(program: Command, log: LogFn, debugLogger: DebugL contractAddress, from: parsedFromAddress, rpcUrl, - type, secretKey, - publicKey, } = options; - const client = await createCompatibleClient(rpcUrl, debugLogger); - const account = await createOrRetrieveAccount(client, parsedFromAddress, db, type, secretKey, Fr.ZERO, publicKey); + const client = pxeWrapper?.getPXE() ?? (await createCompatibleClient(rpcUrl, debugLogger)); + const account = await createOrRetrieveAccount(client, parsedFromAddress, db, secretKey); const wallet = await getWalletWithScopes(account, db); const artifactPath = await artifactPathFromPromiseOrAlias(artifactPathPromise, contractAddress, db); await authorizeAction(wallet, functionName, caller, args, artifactPath, contractAddress, log); @@ -521,17 +516,16 @@ export function injectCommands(program: Command, log: LogFn, debugLogger: DebugL createSecretKeyOption("The sender's secret key", !db, sk => aliasedSecretKeyParser(sk, db)).conflicts('account'), ) .addOption(createAccountOption('Alias or address of the account to simulate from', !db, db)) - .addOption(createTypeOption(false)) .addOption( createAliasOption('Alias for the authorization witness. Used for easy reference in subsequent commands.', !db), ) .action(async (authwit, authorizer, _options, command) => { const { addAuthwit } = await import('./add_authwit.js'); const options = command.optsWithGlobals(); - const { from: parsedFromAddress, rpcUrl, type, secretKey, publicKey } = options; + const { from: parsedFromAddress, rpcUrl, secretKey } = options; - const client = await createCompatibleClient(rpcUrl, debugLogger); - const account = await createOrRetrieveAccount(client, parsedFromAddress, db, type, secretKey, Fr.ZERO, publicKey); + const client = pxeWrapper?.getPXE() ?? (await createCompatibleClient(rpcUrl, debugLogger)); + const account = await createOrRetrieveAccount(client, parsedFromAddress, db, secretKey); const wallet = await getWalletWithScopes(account, db); await addAuthwit(wallet, authwit, authorizer, log); await addScopeToWallet(wallet, authorizer, db); @@ -553,7 +547,7 @@ export function injectCommands(program: Command, log: LogFn, debugLogger: DebugL const { checkTx } = await import('./check_tx.js'); const { rpcUrl, pageSize } = options; let { page } = options; - const client = await createCompatibleClient(rpcUrl, debugLogger); + const client = pxeWrapper?.getPXE() ?? (await createCompatibleClient(rpcUrl, debugLogger)); if (txHash) { await checkTx(client, txHash, false, log); @@ -592,13 +586,12 @@ export function injectCommands(program: Command, log: LogFn, debugLogger: DebugL createSecretKeyOption("The sender's secret key", !db, sk => aliasedSecretKeyParser(sk, db)).conflicts('account'), ) .addOption(createAccountOption('Alias or address of the account to simulate from', !db, db)) - .addOption(createTypeOption(false)) .addOption(FeeOpts.paymentMethodOption().default('method=none')) .action(async (txHash, options) => { const { cancelTx } = await import('./cancel_tx.js'); - const { from: parsedFromAddress, rpcUrl, type, secretKey, publicKey, payment } = options; - const client = await createCompatibleClient(rpcUrl, debugLogger); - const account = await createOrRetrieveAccount(client, parsedFromAddress, db, type, secretKey, Fr.ZERO, publicKey); + const { from: parsedFromAddress, rpcUrl, secretKey, payment } = options; + const client = pxeWrapper?.getPXE() ?? (await createCompatibleClient(rpcUrl, debugLogger)); + const account = await createOrRetrieveAccount(client, parsedFromAddress, db, secretKey); const wallet = await getWalletWithScopes(account, db); const txData = db?.retrieveTxData(txHash); @@ -611,5 +604,91 @@ export function injectCommands(program: Command, log: LogFn, debugLogger: DebugL await cancelTx(wallet, txData, paymentMethod, log); }); + program + .command('register-contact') + .description( + "Registers a contact's address in the wallet, so the note synching process will look for notes sent by them", + ) + .argument('[address]', 'The address of the contact to register', address => + aliasedAddressParser('accounts', address, db), + ) + .addOption(pxeOption) + .addOption(createAccountOption('Alias or address of the account to simulate from', !db, db)) + .addOption(createAliasOption('Alias for the contact. Used for easy reference in subsequent commands.', !db)) + .action(async (address, options) => { + const { registerContact } = await import('./register_contact.js'); + const { from: parsedFromAddress, rpcUrl, secretKey, alias } = options; + const client = pxeWrapper?.getPXE() ?? (await createCompatibleClient(rpcUrl, debugLogger)); + const account = await createOrRetrieveAccount(client, parsedFromAddress, db, secretKey); + const wallet = await getWalletWithScopes(account, db); + + await registerContact(wallet, address, log); + + if (db && alias) { + await db.storeContact(address, alias, log); + } + }); + + program + .command('register-contract') + .description("Registers a contract in this wallet's PXE") + .argument('[address]', 'The address of the contract to register', address => + aliasedAddressParser('accounts', address, db), + ) + .argument('[artifact]', ARTIFACT_DESCRIPTION, artifactPathParser) + .option('--init ', 'The contract initializer function to call', 'constructor') + .option( + '-k, --public-key ', + 'Optional encryption public key for this address. Set this value only if this contract is expected to receive private notes, which will be encrypted using this public key.', + parsePublicKey, + ) + .option( + '-s, --salt ', + 'Optional deployment salt as a hex string for generating the deployment address.', + parseFieldFromHexString, + ) + .option('--deployer ', 'The address of the account that deployed the contract', address => + aliasedAddressParser('accounts', address, db), + ) + .addOption(createArgsOption(true, db)) + .addOption(pxeOption) + .addOption(createAccountOption('Alias or address of the account to simulate from', !db, db)) + .addOption(createAliasOption('Alias for the contact. Used for easy reference in subsequent commands.', !db)) + .action(async (address, artifactPathPromise, _options, command) => { + const { registerContract } = await import('./register_contract.js'); + const { + from: parsedFromAddress, + rpcUrl, + secretKey, + alias, + init, + publicKey, + salt, + deployer, + args, + } = command.optsWithGlobals(); + const client = pxeWrapper?.getPXE() ?? (await createCompatibleClient(rpcUrl, debugLogger)); + const account = await createOrRetrieveAccount(client, parsedFromAddress, db, secretKey); + const wallet = await getWalletWithScopes(account, db); + + const artifactPath = await artifactPathPromise; + + const instance = await registerContract( + wallet, + address, + artifactPath, + init, + publicKey ? PublicKeys.fromString(publicKey) : undefined, + args, + salt, + deployer, + log, + ); + + if (db && alias) { + await db.storeContract(instance.address, artifactPath, log, alias); + } + }); + return program; } diff --git a/yarn-project/cli-wallet/src/cmds/register_contact.ts b/yarn-project/cli-wallet/src/cmds/register_contact.ts new file mode 100644 index 00000000000..8a3c9ea9d6f --- /dev/null +++ b/yarn-project/cli-wallet/src/cmds/register_contact.ts @@ -0,0 +1,7 @@ +import { type AccountWalletWithSecretKey, type AztecAddress } from '@aztec/aztec.js'; +import { type LogFn } from '@aztec/foundation/log'; + +export async function registerContact(wallet: AccountWalletWithSecretKey, address: AztecAddress, log: LogFn) { + await wallet.registerContact(address); + log(`Contact registered: ${address}`); +} diff --git a/yarn-project/cli-wallet/src/cmds/register_contract.ts b/yarn-project/cli-wallet/src/cmds/register_contract.ts new file mode 100644 index 00000000000..530d8f238f5 --- /dev/null +++ b/yarn-project/cli-wallet/src/cmds/register_contract.ts @@ -0,0 +1,38 @@ +import { + type AccountWalletWithSecretKey, + type AztecAddress, + type Fr, + PublicKeys, + getContractInstanceFromDeployParams, +} from '@aztec/aztec.js'; +import { getContractArtifact } from '@aztec/cli/cli-utils'; +import { getInitializer } from '@aztec/foundation/abi'; +import { type LogFn } from '@aztec/foundation/log'; + +export async function registerContract( + wallet: AccountWalletWithSecretKey, + address: AztecAddress, + artifactPath: string, + initializer: string, + publicKeys: PublicKeys | undefined, + rawArgs: any[], + salt: Fr, + deployer: AztecAddress | undefined, + log: LogFn, +) { + const contractArtifact = await getContractArtifact(artifactPath, log); + const constructorArtifact = getInitializer(contractArtifact, initializer); + const contractInstance = getContractInstanceFromDeployParams(contractArtifact, { + constructorArtifact, + publicKeys: publicKeys ?? PublicKeys.default(), + constructorArgs: rawArgs, + salt, + deployer, + }); + if (!contractInstance.address.equals(address)) { + throw new Error(`Contract address mismatch: expected ${address}, got ${contractInstance.address}`); + } + await wallet.registerContract({ instance: contractInstance, artifact: contractArtifact }); + log(`Contract registered: at ${contractInstance.address}`); + return contractInstance; +} diff --git a/yarn-project/cli-wallet/src/storage/wallet_db.ts b/yarn-project/cli-wallet/src/storage/wallet_db.ts index aeaf2f40cf4..3bef8ed0153 100644 --- a/yarn-project/cli-wallet/src/storage/wallet_db.ts +++ b/yarn-project/cli-wallet/src/storage/wallet_db.ts @@ -82,6 +82,11 @@ export class WalletDB { log(`Account stored in database with alias${alias ? `es last & ${alias}` : ' last'}`); } + async storeContact(address: AztecAddress, alias: string, log: LogFn) { + await this.#aliases.set(`accounts:${alias}`, Buffer.from(address.toString())); + log(`Account stored in database with alias ${alias} as a contact`); + } + async storeContract(address: AztecAddress, artifactPath: string, log: LogFn, alias?: string) { if (alias) { await this.#aliases.set(`contracts:${alias}`, Buffer.from(address.toString())); diff --git a/yarn-project/cli-wallet/src/utils/accounts.ts b/yarn-project/cli-wallet/src/utils/accounts.ts index c0b9bd910e4..8dbca3df63b 100644 --- a/yarn-project/cli-wallet/src/utils/accounts.ts +++ b/yarn-project/cli-wallet/src/utils/accounts.ts @@ -15,8 +15,8 @@ export async function createOrRetrieveAccount( pxe: PXE, address?: AztecAddress, db?: WalletDB, - type: AccountType = 'schnorr', secretKey?: Fr, + type: AccountType = 'schnorr', salt?: Fr, publicKey?: string | undefined, ) { diff --git a/yarn-project/cli-wallet/src/utils/pxe_wrapper.ts b/yarn-project/cli-wallet/src/utils/pxe_wrapper.ts new file mode 100644 index 00000000000..4fcf026373a --- /dev/null +++ b/yarn-project/cli-wallet/src/utils/pxe_wrapper.ts @@ -0,0 +1,21 @@ +import { type PXE, createAztecNodeClient } from '@aztec/circuit-types'; +import { createPXEService, getPXEServiceConfig } from '@aztec/pxe'; + +/* + * Wrapper class for PXE service, avoids initialization issues due to + * closures when providing PXE service to injected commander.js commands + */ +export class PXEWrapper { + private static pxe: PXE | undefined; + + getPXE(): PXE | undefined { + return PXEWrapper.pxe; + } + + async init(nodeUrl: string, dataDir: string) { + const aztecNode = createAztecNodeClient(nodeUrl); + const pxeConfig = getPXEServiceConfig(); + pxeConfig.dataDirectory = dataDir; + PXEWrapper.pxe = await createPXEService(aztecNode, pxeConfig); + } +} diff --git a/yarn-project/cli-wallet/test/test.sh b/yarn-project/cli-wallet/test/test.sh index 58e68ad2925..c0c7255782b 100755 --- a/yarn-project/cli-wallet/test/test.sh +++ b/yarn-project/cli-wallet/test/test.sh @@ -17,6 +17,10 @@ while [[ $# -gt 0 ]]; do FILTER="$2" shift 2 ;; + -r|--remote-pxe) + REMOTE_PXE="1" + shift 3 + ;; -*|--*) echo "Unknown option $1" exit 1 @@ -37,6 +41,11 @@ mkdir -p $WALLET_DATA_DIRECTORY COMMAND="node --no-warnings $(realpath ../dest/bin/index.js)" +if [ "${REMOTE_PXE:-}" = "1" ]; then + echo "Using remote PXE" + export REMOTE_PXE="1" +fi + if [ "${USE_DOCKER:-}" = "1" ]; then echo "Using docker" COMMAND="aztec-wallet" diff --git a/yarn-project/cli-wallet/tsconfig.json b/yarn-project/cli-wallet/tsconfig.json index 66251395644..8d99fdc9b02 100644 --- a/yarn-project/cli-wallet/tsconfig.json +++ b/yarn-project/cli-wallet/tsconfig.json @@ -32,6 +32,9 @@ }, { "path": "../noir-contracts.js" + }, + { + "path": "../pxe" } ], "include": ["src"] diff --git a/yarn-project/cli/src/utils/commands.ts b/yarn-project/cli/src/utils/commands.ts index 5bc66a8a5c0..6df225aed63 100644 --- a/yarn-project/cli/src/utils/commands.ts +++ b/yarn-project/cli/src/utils/commands.ts @@ -33,6 +33,7 @@ export const makePxeOption = (mandatory: boolean) => new Option('-u, --rpc-url ', 'URL of the PXE') .env('PXE_URL') .default(`http://${LOCALHOST}:8080`) + .conflicts('remote-pxe') .makeOptionMandatory(mandatory); export const pxeOption = makePxeOption(true); diff --git a/yarn-project/end-to-end/scripts/docker-compose.yml b/yarn-project/end-to-end/scripts/docker-compose.yml index f1aa66cc7ff..4a96de1088d 100644 --- a/yarn-project/end-to-end/scripts/docker-compose.yml +++ b/yarn-project/end-to-end/scripts/docker-compose.yml @@ -33,6 +33,7 @@ services: ETHEREUM_HOST: http://fork:8545 L1_CHAIN_ID: 31337 PXE_URL: http://sandbox:8080 + AZTEC_NODE_URL: http://sandbox:8080 entrypoint: > sh -c ' while ! nc -z sandbox 8080; do sleep 1; done; diff --git a/yarn-project/end-to-end/src/guides/up_quick_start.test.ts b/yarn-project/end-to-end/src/guides/up_quick_start.test.ts index 635334127c3..1fc6920a1f9 100644 --- a/yarn-project/end-to-end/src/guides/up_quick_start.test.ts +++ b/yarn-project/end-to-end/src/guides/up_quick_start.test.ts @@ -1,16 +1,16 @@ -import { createPXEClient, waitForPXE } from '@aztec/aztec.js'; +import { createAztecNodeClient, waitForNode } from '@aztec/aztec.js'; import { execSync } from 'child_process'; -const { PXE_URL = '' } = process.env; +const { AZTEC_NODE_URL = '' } = process.env; // Entrypoint for running the up-quick-start script on the CI describe('guides/up_quick_start', () => { // TODO: update to not use CLI it('works', async () => { - await waitForPXE(createPXEClient(PXE_URL)); + await waitForNode(createAztecNodeClient(AZTEC_NODE_URL)); execSync( - `LOG_LEVEL=\${LOG_LEVEL:-verbose} PXE_URL=\${PXE_URL:-http://localhost:8080} ./src/guides/up_quick_start.sh`, + `LOG_LEVEL=\${LOG_LEVEL:-verbose} AZTEC_NODE_URL=\${AZTEC_NODE_URL:-http://localhost:8080} ./src/guides/up_quick_start.sh`, { shell: '/bin/bash', stdio: 'inherit', diff --git a/yarn-project/pxe/src/synchronizer/synchronizer.ts b/yarn-project/pxe/src/synchronizer/synchronizer.ts index e855810912e..ee8c22a06d2 100644 --- a/yarn-project/pxe/src/synchronizer/synchronizer.ts +++ b/yarn-project/pxe/src/synchronizer/synchronizer.ts @@ -79,8 +79,18 @@ export class Synchronizer implements L2BlockStreamEventHandler { } this.running = true; - // REFACTOR: We should know the header of the genesis block without having to request it from the node. - await this.db.setHeader(await this.node.getBlockHeader(0)); + let currentHeader; + + try { + currentHeader = this.db.getBlockHeader(); + } catch (e) { + this.log.debug('Header is not set, requesting from the node'); + } + if (!currentHeader) { + // REFACTOR: We should know the header of the genesis block without having to request it from the node. + const storedBlockNumber = this.db.getBlockNumber(); + await this.db.setHeader(await this.node.getBlockHeader(storedBlockNumber ?? 0)); + } await this.trigger(); this.log.info('Initial sync complete'); diff --git a/yarn-project/yarn.lock b/yarn-project/yarn.lock index df1ce964b9f..df1012b9498 100644 --- a/yarn-project/yarn.lock +++ b/yarn-project/yarn.lock @@ -437,6 +437,7 @@ __metadata: "@aztec/foundation": "workspace:^" "@aztec/kv-store": "workspace:^" "@aztec/noir-contracts.js": "workspace:^" + "@aztec/pxe": "workspace:^" "@jest/globals": ^29.5.0 "@types/jest": ^29.5.0 "@types/node": ^18.7.23