Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(aztec-js): Account class #1429

Merged
merged 5 commits into from
Aug 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions yarn-project/aztec-cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -354,10 +354,7 @@ async function main() {
accountCreationSalt,
);
const contract = await Contract.create(contractAddress, contractAbi, wallet);
const origin = (await wallet.getAccounts()).find(addr => addr.equals(wallet.getAddress()));
const tx = contract.methods[functionName](...functionArgs).send({
origin,
});
const tx = contract.methods[functionName](...functionArgs).send();
await tx.isMined();
log('\nTX has been mined');
const receipt = await tx.getReceipt();
Expand Down
12 changes: 9 additions & 3 deletions yarn-project/aztec.js/src/abis/ecdsa_account_contract.json

Large diffs are not rendered by default.

147 changes: 147 additions & 0 deletions yarn-project/aztec.js/src/abis/schnorr_multi_key_account_contract.json

Large diffs are not rendered by default.

Large diffs are not rendered by default.

112 changes: 112 additions & 0 deletions yarn-project/aztec.js/src/account/account.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { Fr, PublicKey, getContractDeploymentInfo } from '@aztec/circuits.js';
import { AztecRPC, PrivateKey } from '@aztec/types';

import { AccountWallet, ContractDeployer, WaitOpts, Wallet, generatePublicKey } from '../index.js';
import { CompleteAddress, isCompleteAddress } from './complete_address.js';
import { DeployAccountSentTx } from './deploy_account_sent_tx.js';
import { AccountContract, Salt } from './index.js';

/**
* Manages a user account. Provides methods for calculating the account's address, deploying the account contract,
* and creating and registering the user wallet in the RPC server.
*/
export class Account {
/** Deployment salt for the account contract. */
public readonly salt?: Fr;

private completeAddress?: CompleteAddress;
private encryptionPublicKey?: PublicKey;

constructor(
private rpc: AztecRPC,
private encryptionPrivateKey: PrivateKey,
private accountContract: AccountContract,
saltOrAddress?: Salt | CompleteAddress,
) {
if (isCompleteAddress(saltOrAddress)) {
this.completeAddress = saltOrAddress;
} else {
this.salt = saltOrAddress ? new Fr(saltOrAddress) : Fr.random();
}
}

protected async getEncryptionPublicKey() {
if (!this.encryptionPublicKey) {
this.encryptionPublicKey = await generatePublicKey(this.encryptionPrivateKey);
}
return this.encryptionPublicKey;
}

/**
* Gets the calculated complete address associated with this account.
* Does not require the account to be deployed or registered.
* @returns The address, partial address, and encryption public key.
*/
public async getCompleteAddress(): Promise<CompleteAddress> {
if (!this.completeAddress) {
const encryptionPublicKey = await generatePublicKey(this.encryptionPrivateKey);
this.completeAddress = await getContractDeploymentInfo(
this.accountContract.getContractAbi(),
await this.accountContract.getDeploymentArgs(),
this.salt!,
encryptionPublicKey,
);
}
return this.completeAddress;
}

/**
* Returns a Wallet instance associated with this account. Use it to create Contract
* instances to be interacted with from this account.
* @returns A Wallet instance.
*/
public async getWallet(): Promise<Wallet> {
const nodeInfo = await this.rpc.getNodeInfo();
const completeAddress = await this.getCompleteAddress();
const account = await this.accountContract.getEntrypoint(completeAddress, nodeInfo);
return new AccountWallet(this.rpc, account);
}

/**
* Registers this account in the RPC server and returns the associated wallet. Registering
* the account on the RPC server is required for managing private state associated with it.
* Use the returned wallet to create Contract instances to be interacted with from this account.
* @returns A Wallet instance.
*/
public async register(): Promise<Wallet> {
const { address, partialAddress } = await this.getCompleteAddress();
await this.rpc.addAccount(this.encryptionPrivateKey, address, partialAddress);
return this.getWallet();
}

/**
* Deploys the account contract that backs this account.
* Uses the salt provided in the constructor or a randomly generated one.
* Note that if the Account is constructed with an explicit complete address
* it is assumed that the account contract has already been deployed and this method will throw.
* Registers the account in the RPC server before deploying the contract.
* @returns A SentTx object that can be waited to get the associated Wallet.
*/
public async deploy(): Promise<DeployAccountSentTx> {
if (!this.salt) throw new Error(`Cannot deploy account contract without known salt.`);
const wallet = await this.register();
const encryptionPublicKey = await this.getEncryptionPublicKey();
const deployer = new ContractDeployer(this.accountContract.getContractAbi(), this.rpc, encryptionPublicKey);
const args = await this.accountContract.getDeploymentArgs();
const sentTx = deployer.deploy(...args).send({ contractAddressSalt: this.salt });
return new DeployAccountSentTx(wallet, sentTx.getTxHash());
}

/**
* Deploys the account contract that backs this account and awaits the tx to be mined.
* Uses the salt provided in the constructor or a randomly generated one.
* Note that if the Account is constructed with an explicit complete address
* it is assumed that the account contract has already been deployed and this method will throw.
* Registers the account in the RPC server before deploying the contract.
* @param opts - Options to wait for the tx to be mined.
* @returns A Wallet instance.
*/
public async waitDeploy(opts: WaitOpts): Promise<Wallet> {
return (await this.deploy()).getWallet(opts);
}
}
18 changes: 18 additions & 0 deletions yarn-project/aztec.js/src/account/complete_address.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { AztecAddress, PartialContractAddress, PublicKey } from '@aztec/circuits.js';

/** Address and preimages associated with an account. */
export type CompleteAddress = {
/** Address of an account. Derived from the partial address and public key. */
address: AztecAddress;
/** Partial address of the account. Required for deriving the address from the encryption public key. */
partialAddress: PartialContractAddress;
/** Encryption public key associated with this address. */
publicKey: PublicKey;
};

/** Returns whether the argument looks like a CompleteAddress. */
export function isCompleteAddress(obj: any): obj is CompleteAddress {
if (!obj) return false;
const maybe = obj as CompleteAddress;
return !!maybe.address && !!maybe.partialAddress && !!maybe.publicKey;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Ecdsa } from '@aztec/circuits.js/barretenberg';
import { ContractAbi } from '@aztec/foundation/abi';
import { NodeInfo, PrivateKey } from '@aztec/types';

import EcdsaAccountContractAbi from '../../abis/ecdsa_account_contract.json' assert { type: 'json' };
import { CompleteAddress } from '../complete_address.js';
import { StoredKeyAccountEntrypoint } from '../entrypoint/stored_key_account_entrypoint.js';
import { AccountContract } from './index.js';

/**
* Account contract that authenticates transactions using ECDSA signatures
* verified against a secp256k1 public key stored in an immutable encrypted note.
*/ export class EcdsaAccountContract implements AccountContract {
constructor(private signingPrivateKey: PrivateKey) {}

public async getDeploymentArgs() {
const signingPublicKey = await Ecdsa.new().then(e => e.computePublicKey(this.signingPrivateKey));
return [signingPublicKey.subarray(0, 32), signingPublicKey.subarray(32, 64)];
}

public async getEntrypoint({ address }: CompleteAddress, { chainId, version }: NodeInfo) {
return new StoredKeyAccountEntrypoint(address, this.signingPrivateKey, await Ecdsa.new(), chainId, version);
}

public getContractAbi(): ContractAbi {
return EcdsaAccountContractAbi as ContractAbi;
}
}
26 changes: 26 additions & 0 deletions yarn-project/aztec.js/src/account/contract/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { ContractAbi } from '@aztec/foundation/abi';
import { NodeInfo } from '@aztec/types';

import { Entrypoint } from '../index.js';
import { CompleteAddress } from './../complete_address.js';

export * from './ecdsa_account_contract.js';
export * from './schnorr_account_contract.js';
export * from './single_key_account_contract.js';

/**
* An account contract instance. Knows its ABI, deployment arguments, and to create transaction execution
* requests out of function calls through an entrypoint.
*/
export interface AccountContract {
/** Returns the ABI of this account contract. */
getContractAbi(): ContractAbi;
/** Returns the deployment arguments for this instance. */
getDeploymentArgs(): Promise<any[]>;
/**
* Creates an entrypoint for creating transaction execution requests for this account contract.
* @param address - Complete address of the deployed account contract.
* @param nodeInfo - Chain id and protocol version where the account contract is deployed.
*/
getEntrypoint(address: CompleteAddress, nodeInfo: NodeInfo): Promise<Entrypoint>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Schnorr } from '@aztec/circuits.js/barretenberg';
import { ContractAbi } from '@aztec/foundation/abi';
import { NodeInfo, PrivateKey } from '@aztec/types';

import SchnorrMultiKeyAccountContractAbi from '../../abis/schnorr_multi_key_account_contract.json' assert { type: 'json' };
import { CompleteAddress } from '../complete_address.js';
import { StoredKeyAccountEntrypoint } from '../entrypoint/stored_key_account_entrypoint.js';
import { AccountContract } from './index.js';

/**
* Account contract that authenticates transactions using Schnorr signatures
* verified against a Grumpkin public key stored in an immutable encrypted note.
*/
export class SchnorrAccountContract implements AccountContract {
constructor(private signingPrivateKey: PrivateKey) {}

public async getDeploymentArgs() {
const signingPublicKey = await Schnorr.new().then(e => e.computePublicKey(this.signingPrivateKey));
return [signingPublicKey.x, signingPublicKey.y];
}

public async getEntrypoint({ address }: CompleteAddress, { chainId, version }: NodeInfo) {
return new StoredKeyAccountEntrypoint(address, this.signingPrivateKey, await Schnorr.new(), chainId, version);
}

public getContractAbi(): ContractAbi {
return SchnorrMultiKeyAccountContractAbi as ContractAbi;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Schnorr } from '@aztec/circuits.js/barretenberg';
import { ContractAbi } from '@aztec/foundation/abi';
import { NodeInfo, PrivateKey } from '@aztec/types';

import SchnorrSingleKeyAccountContractAbi from '../../abis/schnorr_single_key_account_contract.json' assert { type: 'json' };
import { CompleteAddress } from '../complete_address.js';
import { SingleKeyAccountEntrypoint } from '../entrypoint/single_key_account_entrypoint.js';
import { AccountContract } from './index.js';

/**
* Account contract that authenticates transactions using Schnorr signatures verified against
* the note encryption key, relying on a single private key for both encryption and authentication.
*/
export class SingleKeyAccountContract implements AccountContract {
constructor(private encryptionPrivateKey: PrivateKey) {}

public getDeploymentArgs() {
return Promise.resolve([]);
}

public async getEntrypoint({ address, partialAddress }: CompleteAddress, { chainId, version }: NodeInfo) {
return new SingleKeyAccountEntrypoint(
address,
partialAddress,
this.encryptionPrivateKey,
await Schnorr.new(),
chainId,
version,
);
}

public getContractAbi(): ContractAbi {
return SchnorrSingleKeyAccountContractAbi as ContractAbi;
}
}
39 changes: 39 additions & 0 deletions yarn-project/aztec.js/src/account/deploy_account_sent_tx.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { FieldsOf } from '@aztec/circuits.js';
import { TxHash, TxReceipt } from '@aztec/types';

import { SentTx, WaitOpts, Wallet } from '../index.js';

/** Extends a transaction receipt with a wallet instance for the newly deployed contract. */
export type DeployAccountTxReceipt = FieldsOf<TxReceipt> & {
/** Wallet that corresponds to the newly deployed account contract. */
wallet: Wallet;
};

/**
* A deployment transaction for an account contract sent to the network, extending SentTx with methods to get the resulting wallet.
*/
export class DeployAccountSentTx extends SentTx {
constructor(private wallet: Wallet, txHashPromise: Promise<TxHash>) {
super(wallet, txHashPromise);
}

/**
* Awaits for the tx to be mined and returns the contract instance. Throws if tx is not mined.
* @param opts - Options for configuring the waiting for the tx to be mined.
* @returns The deployed contract instance.
*/
public async getWallet(opts?: WaitOpts): Promise<Wallet> {
const receipt = await this.wait(opts);
return receipt.wallet;
}

/**
* Awaits for the tx to be mined and returns the receipt along with a wallet instance. Throws if tx is not mined.
* @param opts - Options for configuring the waiting for the tx to be mined.
* @returns The transaction receipt with the wallet for the deployed account contract.
*/
public async wait(opts?: WaitOpts): Promise<DeployAccountTxReceipt> {
const receipt = await super.wait(opts);
return { ...receipt, wallet: this.wallet };
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { AztecAddress } from '@aztec/circuits.js';
import { FunctionCall, TxExecutionRequest } from '@aztec/types';

import { CreateTxRequestOpts, Entrypoint } from './index.js';

/**
* An entrypoint that groups together multiple concrete entrypoints.
* Delegates to the registered entrypoints based on the requested origin.
*/
export class EntrypointCollection implements Entrypoint {
private entrypoints: Map<string, Entrypoint> = new Map();

/**
* Registers an entrypoint against an aztec address
* @param addr - The aztec address agianst which to register the implementation.
* @param impl - The entrypoint to be registered.
*/
public registerAccount(addr: AztecAddress, impl: Entrypoint) {
this.entrypoints.set(addr.toString(), impl);
}

public createTxExecutionRequest(
executions: FunctionCall[],
opts: CreateTxRequestOpts = {},
): Promise<TxExecutionRequest> {
const defaultAccount = this.entrypoints.values().next().value as Entrypoint;
const impl = opts.origin ? this.entrypoints.get(opts.origin.toString()) : defaultAccount;
if (!impl) throw new Error(`No entrypoint registered for ${opts.origin}`);
return impl.createTxExecutionRequest(executions, opts);
}
}
Original file line number Diff line number Diff line change
@@ -1,24 +1,21 @@
import { AztecAddress } from '@aztec/circuits.js';
import { FunctionCall, TxExecutionRequest } from '@aztec/types';

export * from './account_collection.js';
export * from './single_key_account_contract.js';
export * from './stored_key_account_contract.js';
export * from './entrypoint_collection.js';
export * from './single_key_account_entrypoint.js';
export * from './stored_key_account_entrypoint.js';

/** Options for creating a tx request out of a set of function calls. */
export type CreateTxRequestOpts = {
/** Origin of the tx. Needs to be an address managed by this account. */
origin?: AztecAddress;
};

/** Represents an implementation for a user account contract. Knows how to encode and sign a tx for that particular implementation. */
export interface AccountImplementation {
/**
* Returns the address for the account contract used by this implementation.
* @returns The address.
*/
getAddress(): AztecAddress;

/**
* Represents a transaction entrypoint in an account contract.
* Knows how to assemble a transaction execution request given a set of function calls.
*/
export interface Entrypoint {
/**
* Generates an authenticated request out of set of intents
* @param executions - The execution intents to be run.
Expand Down
Loading