Using the Eth Validator

The Eth Validator Module can both work as a Secondary or as the Core Validator for the account. It requires a separate SDK. In this section of the documentation, you will see how you can use the Moduler Account to interact with the Eth Validator Module.

Note: This section assumes the SmartrAccount class has been instantiated in the smartrAccount variable as shown in Using the modular account from the SDK. It also assumes the Counter contract that comes with the project has been deploys to the counterAddress and the CounterABI class is available. The 05-setup.ts script that comes with this project ensure those steps are executed.

Installing the Eth Validator SDK

If you plan to use the Eth Validatoi module, you might need the @0xknwn/starknet-module SDK in addition to the @0xknwn/starknet-modular-account SDK. To install it, run:

npm install --save \
  @0xknwn/starknet-module

Declaring the Eth Validator

If you are working on a network that does not have the eth validator class already declared, you will need to declare it. The Eth validator module SDK, aka @0xknwn/starknet-module contains a helper function named declareClass to declare the class to the network. To use it, you need to pass:

  • A starknet.js Account as a first parameter
  • The name of the class to declare as the 2nd parameter. For the Eth Validator, the name isEthValidator

Below is an example of a script that declares the new classes.

// file src/04-declare-eth-validator.ts
import { RpcProvider, Account } from "starknet";
import { declareClass } from "@0xknwn/starknet-module";

// these are the settings for the devnet with --seed=0
// change them to mee your requirements
const providerURL = "http://127.0.0.1:5050/rpc";
const ozAccountAddress =
  "0x64b48806902a367c8598f4f95c305e8c1a1acba5f082d294a43793113115691";
const ozPrivateKey = "0x71d7bb07b9a64f6f78ac4c816aff4da9";

const main = async () => {
  const provider = new RpcProvider({ nodeUrl: providerURL });
  const account = new Account(provider, ozAccountAddress, ozPrivateKey);

  const { classHash: ethValidatorClassHash } = await declareClass(
    account,
    "EthValidator"
  );
  console.log("EthValidator class hash:", ethValidatorClassHash);
};

main()
  .then(() => {})
  .catch((e) => {
    console.warn(e);
  });

Note: To declare the class, the account you use must be loaded with ETH.

Assuming you have named the script src/04-declare-eth-validator.ts, transpile and run it:

npx tsc --build

node dist/04-declare-eth-validator.js

The output should return the hash for the class.

Verify the Eth Validator class hash

The class hash does NOT depend on the deployment or the network. So you can find them at any time with the classHash helper that comes with the SDK. The script below shows how to use that function:

// file src/04-check-eth-validator.ts
import { classHash } from "@0xknwn/starknet-module";

console.log("Computed EthValidator class hash:", classHash("EthValidator"));

Transpile and run the script:

npx tsc --build

node dist/04-check-eth-validator.js

Using the Eth Validator as a Secondary Validator

The simplest way to use the Eth Validator is to add it as a module to an existing account and execute a transaction with the EthModule class from the @0xknwn/starknet-module.

Register the Eth Validator as a Module

The modular account SDK comes with the addModule, removeModule and isModule. You can use those 3 functions to manage the module in the account once it has been declared to the network. To register the module in the account, use addModule:

// file src/04-add-module.ts
import { SmartrAccount } from "@0xknwn/starknet-modular-account";
import { classHash } from "@0xknwn/starknet-module";
import { init } from "./04-init";
import { RpcProvider } from "starknet";

const providerURL = "http://127.0.0.1:5050/rpc";

const main = async () => {
  const provider = new RpcProvider({ nodeUrl: providerURL });
  const { accountAddress, smartrAccountPrivateKey } = await init();
  const account = new SmartrAccount(
    provider,
    accountAddress,
    smartrAccountPrivateKey
  );
  const { transaction_hash } = await account.addModule(
    classHash("EthValidator")
  );
  const receipt = await account.waitForTransaction(transaction_hash);
  console.log("transaction succeeded", receipt.isSuccess());

  const isInstalled = await account.isModule(classHash("EthValidator"));
  console.log("module", classHash("EthValidator"), "is installed", isInstalled);
};

main()
  .then(() => {})
  .catch((e) => {
    console.warn(e);
  });

Transpile and run the script:

npx tsc --build

node dist/04-add-module.js

Register the public key associated with your Eth Private Key

Every module comes with a set of Management API. In the case of the Eth Validator, the associated interfaces are the following:

#![allow(unused)]
fn main() {
#[starknet::interface]
pub trait IPublicKey<TState> {
    fn set_public_key(ref self: TState, new_public_key: EthPublicKey);
    fn get_public_key(self: @TState) -> EthPublicKey;
}
}

Now that you have installed the module, you can create an ETH private key and register the associated public key in the module. For the purpose of the demonstration, we will use an arbitrary (and now unsafe) private/public key pair:

  • private key: 0xb28ebb20fb1015da6e6367d1b5dba9b52862a06dbb3a4022e4749b6987ac1bd2
  • public key:
    • x: 0xd31cf702f5c89d49c567dcfd568bc4869e343506749f69d849eb408802cfa646
    • y: 0x348c7bbf341964c306669365292c0066c23a2fedd131907534677aa3e22db2fc

Because Starknet types can only manage felt252 that are smaller than uint256 the format used by EthPublicKey is actually an array<felt252> that is made of [x.low, x.high, y.low, y.high]. To register the public key, use the script below:

// file src/04-register-publickey.ts
import { SmartrAccount } from "@0xknwn/starknet-modular-account";
import { classHash as ethClassHash } from "@0xknwn/starknet-module";
import { EthSigner, cairo } from "starknet";
import { init } from "./04-init";
import { RpcProvider } from "starknet";

const providerURL = "http://127.0.0.1:5050/rpc";
const ethPrivateKey =
  "0xb28ebb20fb1015da6e6367d1b5dba9b52862a06dbb3a4022e4749b6987ac1bd2";

const main = async () => {
  const provider = new RpcProvider({ nodeUrl: providerURL });
  const { accountAddress, smartrAccountPrivateKey } = await init();
  const account = new SmartrAccount(
    provider,
    accountAddress,
    smartrAccountPrivateKey
  );
  const module_class_hash = ethClassHash("EthValidator");
  const signer = new EthSigner(ethPrivateKey);
  const publicKey = await signer.getPubKey();
  const coords = publicKey.slice(2, publicKey.length);
  const x = coords.slice(0, 64);
  const x_felts = cairo.uint256(`0x${x}`);
  const y = coords.slice(64, 128);
  const y_felts = cairo.uint256(`0x${y}`);
  console.log("x:", `0x${x}`);
  console.log("(x.low:", x_felts.low, ", x.high:", x_felts.high, ")");
  console.log("y:", `0x${y}`);
  console.log("(y.low:", y_felts.low, ", y.high:", y_felts.high, ")");
  const { transaction_hash } = await account.executeOnModule(
    module_class_hash,
    "set_public_key",
    [
      x_felts.low.toString(),
      x_felts.high.toString(),
      y_felts.low.toString(),
      y_felts.high.toString(),
    ]
  );
  const receipt = await account.waitForTransaction(transaction_hash);
  console.log("transaction succeeded", receipt.isSuccess());
};

main()
  .then(() => {})
  .catch((e) => {
    console.warn(e);
  });

Transpile and run the script:

npx tsc --build

node dist/04-register-publickey.js

You can check the public key is correctly registered with the script below:

// file src/04-get-publickey.ts
import { SmartrAccount } from "@0xknwn/starknet-modular-account";
import {
  EthValidatorABI,
  classHash as ethClassHash,
} from "@0xknwn/starknet-module";
import { init } from "./04-init";
import { CallData, RpcProvider } from "starknet";

const providerURL = "http://127.0.0.1:5050/rpc";

const main = async () => {
  const provider = new RpcProvider({ nodeUrl: providerURL });
  const { accountAddress, smartrAccountPrivateKey } = await init();
  const account = new SmartrAccount(
    provider,
    accountAddress,
    smartrAccountPrivateKey
  );
  const moduleCallData = new CallData(EthValidatorABI);
  const calldata = moduleCallData.compile("get_public_key", {});
  const public_keys = await account.callOnModule(
    ethClassHash("EthValidator"),
    "get_public_key",
    calldata
  );
  public_keys.forEach((public_key, idx) =>
    console.log(`public key (${idx}):`, public_key)
  );
};

main()
  .then(() => {})
  .catch((e) => {
    console.warn(e);
  });

Transpile and run the script:

npx tsc --build

node dist/04-get-publickey.js

Run a transaction with the EthModule

The EthModule is an implementation of a module that can be passed to the SmartrAccount and manages the decoration of the transaction under the hood. To fully instantiate that module, you will need:

  • to instantiate the EthModule module from the SDK
  • to use the EthSigner provided by Starknet.js with the Private Key
  • to instantiate the SmartrAccount with the 2 classes above

Then you can run a transaction, exactly as you would do with any Starknet.js account. The example below execute the increment entrypoint on the Counter contract:

// file src/04-execute-tx.ts
import { SmartrAccount } from "@0xknwn/starknet-modular-account";
import { init, CounterABI } from "./04-init";
import { RpcProvider, Contract, EthSigner } from "starknet";
import { EthModule } from "@0xknwn/starknet-module";

const providerURL = "http://127.0.0.1:5050/rpc";
const ethPrivateKey =
  "0xb28ebb20fb1015da6e6367d1b5dba9b52862a06dbb3a4022e4749b6987ac1bd2";

const main = async () => {
  const provider = new RpcProvider({ nodeUrl: providerURL });
  const { accountAddress, counterAddress, smartrAccountPrivateKey } =
    await init();
  const signer = new EthSigner(ethPrivateKey);
  const ethModule = new EthModule(accountAddress);
  const account = new SmartrAccount(
    provider,
    accountAddress,
    signer,
    ethModule
  );
  const counter = new Contract(CounterABI, counterAddress, account);
  let currentCounter = await counter.call("get");
  console.log("currentCounter", currentCounter);
  const call = counter.populate("increment");
  const { transaction_hash } = await account.execute(call);
  const receipt = await account.waitForTransaction(transaction_hash);
  console.log("transaction succeeded", receipt.isSuccess());
  currentCounter = await counter.call("get");
  console.log("currentCounter", currentCounter);
};

main()
  .then(() => {})
  .catch((e) => {
    console.warn(e);
  });

Transpile and run the script:

npx tsc --build

node dist/04-execute-tx.js

Remove the Eth Validator Module

You can use removeModule and isModule to remove the module from the account with the script below:

// file src/04-remove-module.ts
import { SmartrAccount } from "@0xknwn/starknet-modular-account";
import { classHash } from "@0xknwn/starknet-module";
import { init } from "./04-init";
import { RpcProvider } from "starknet";

const providerURL = "http://127.0.0.1:5050/rpc";

const main = async () => {
  const provider = new RpcProvider({ nodeUrl: providerURL });
  const { accountAddress, smartrAccountPrivateKey } = await init();
  const account = new SmartrAccount(
    provider,
    accountAddress,
    smartrAccountPrivateKey
  );
  const { transaction_hash } = await account.removeModule(
    classHash("EthValidator")
  );
  const receipt = await account.waitForTransaction(transaction_hash);
  console.log("transaction succeeded", receipt.isSuccess());

  const isInstalled = await account.isModule(classHash("EthValidator"));
  console.log(
    "module",
    classHash("EthValidator"),
    "has been removed",
    isInstalled
  );
};

main()
  .then(() => {})
  .catch((e) => {
    console.warn(e);
  });

Transpile and run the script:

npx tsc --build

node dist/04-remove-module.js

Using the Eth Validator as the Core Validator

You can also use the Eth Validator as a Core Validator for the account. For that purpose you will deploy a new account and use the EthSigner to validate the account deployment without registering the EthModule in the SmartrAccount. In order to proceed you need to:

  • Generate the public key as an Array of felt252
  • Compute the account address
  • Send ETH to the modular account address
  • Deploy the Account

Compute the Public Key as an Array of felt252

The EthSigner helps to generate the public key from the private key. Once you have the public key you should slice to get an array of 4 pieces like below:

const signer = new EthSigner(ethPrivateKey);
const publicKey = await signer.getPubKey();
const coords = publicKey.slice(2, publicKey.length);
const x = coords.slice(0, 64);
const x_felts = cairo.uint256(`0x${x}`);
const y = coords.slice(64, 128);
const y_felts = cairo.uint256(`0x${y}`);
const publicKeyArray = [
  x_felts.low.toString(),
  x_felts.high.toString(),
  y_felts.low.toString(),
  y_felts.high.toString(),
];

Compute the Account Address

Once you have the public key, you should use the accountAddress function from @0xknwn/starknet-modular-account to compute the address of the account you will install. As a Salt, we will use the hash.computeHashOnElements from the public key like below:

const publicKeyHash = hash.computeHashOnElements(publicKeyArray);
const computedAccountAddress = accountAddress(
  "SmartrAccount",
  publicKeyHash,
  [ethClassHash("EthValidator"), "0x4", ...publicKeyArray]
);

Note: The "0x4" that is inserted in the calldata is here to indicate there are 4 pieces to the publci key:

Send ETH to the SmartrAccount Address to deploy it

To deploy the account, you need to have ETH associated with the target account address. Assuming you have access to an account with ETH, this is how you send eth to the computedAccountAddress:

const account = new SmartrAccount(
  provider,
  ozAccountAddress,
  smartrAccountPrivateKey
);
const ETH = new Contract(ERC20ABI, ethAddress, account);
const initial_EthTransfer = cairo.uint256(5n * 10n ** 15n);
const call = ETH.populate("transfer", {
  recipient: computedAccountAddress,
  amount: initial_EthTransfer,
});
const { transaction_hash } = await account.execute(call);
const output = await account.waitForTransaction(transaction_hash);

Deploy the Account with the Eth Validator as Core

To deploy the account, you will need to use the deployAccount helper function from @0xknwn/starknet-modular-account with a SmartrAccount that has been instantiated with a EthSigner like below:

const ethSmartrSigner = new EthSigner(smartrAccountPrivateKey);
const ethAccount = new SmartrAccount(
  provider,
  computedAccountAddress,
  ethSmartrSigner
);
const address = await deployAccount(
  ethAccount,
  "SmartrAccount",
  publicKeyHash,
  [ethClassHash("EthValidator"), "0x4", ...publicKeyArray]
);

The Script Code

You will find below the whole script that does the account deployment:

// file src/04-deploy-account.ts
import { RpcProvider, EthSigner, Contract, cairo, hash } from "starknet";
import {
  accountAddress,
  deployAccount,
  SmartrAccount,
} from "@0xknwn/starknet-modular-account";
import { classHash as ethClassHash } from "@0xknwn/starknet-module";
import { init } from "./04-init";
import { ABI as ERC20ABI } from "./abi/ERC20";
const ethAddress =
  "0x49D36570D4E46F48E99674BD3FCC84644DDD6B96F7C741B1562B82F9E004DC7";

// these are the settings for the devnet with --seed=0
// change them to mee your requirements
const providerURL = "http://127.0.0.1:5050/rpc";
const ethPrivateKey =
  "0xb28ebb20fb1015da6e6367d1b5dba9b52862a06dbb3a4022e4749b6987ac1bd2";

const main = async () => {
  const provider = new RpcProvider({ nodeUrl: providerURL });
  const { accountAddress: ozAccountAddress, smartrAccountPrivateKey } =
    await init();

  // Step 1 - Get the public key from the Eth Signer
  const ethSmartrSigner = new EthSigner(ethPrivateKey);
  const publicKey = await ethSmartrSigner.getPubKey();
  const coords = publicKey.slice(2, publicKey.length);
  const x = coords.slice(0, 64);
  const x_felts = cairo.uint256(`0x${x}`);
  const y = coords.slice(64, 128);
  const y_felts = cairo.uint256(`0x${y}`);
  const publicKeyArray = [
    x_felts.low.toString(),
    x_felts.high.toString(),
    y_felts.low.toString(),
    y_felts.high.toString(),
  ];

  // Step 2 - Compute the account address
  const publicKeyHash = hash.computeHashOnElements(publicKeyArray);
  const computedAccountAddress = accountAddress(
    "SmartrAccount",
    publicKeyHash,
    [ethClassHash("EthValidator"), "0x4", ...publicKeyArray]
  );

  // Step 3 - Send ETH to the computed account address
  const account = new SmartrAccount(
    provider,
    ozAccountAddress,
    smartrAccountPrivateKey
  );
  const ETH = new Contract(ERC20ABI, ethAddress, account);
  const initial_EthTransfer = cairo.uint256(5n * 10n ** 15n);
  const call = ETH.populate("transfer", {
    recipient: computedAccountAddress,
    amount: initial_EthTransfer,
  });
  const { transaction_hash } = await account.execute(call);
  const output = await account.waitForTransaction(transaction_hash);
  if (!output.isSuccess()) {
    throw new Error("Could not send ETH to the expected address");
  }

  // Step 4 - Deploy the account with the EthValidator as Core Validator
  const ethAccount = new SmartrAccount(
    provider,
    computedAccountAddress,
    ethSmartrSigner
  );
  const address = await deployAccount(
    ethAccount,
    "SmartrAccount",
    publicKeyHash,
    [ethClassHash("EthValidator"), "0x4", ...publicKeyArray]
  );
  if (address !== computedAccountAddress) {
    throw new Error(
      `The account should have been deployed to ${computedAccountAddress}, instead ${address}`
    );
  }
  console.log("accountAddress", computedAccountAddress);
  console.log("public key", publicKeyArray);
};

main()
  .then(() => {})
  .catch((e) => {
    console.warn(e);
  });

Transpile and run it:

npx tsc --build

node dist/04-deploy-account.js

Running a transaction with the Eth Validator as Core

Running a transaction with the EthValidator as a Core is no more complex than running a transaction on a regular account. All you need to do is

  • get the account address that could have been saved from earlier
  • instantiate the SmartrAccount with the Starknet.js EthSigner
  • execute the transaction

Below is an example that assumes you have deployed the account with the 04-deploy-account.ts script earlier:

// file src/04-execute-tx-core.ts
import {
  SmartrAccount,
  accountAddress,
} from "@0xknwn/starknet-modular-account";
import { init, CounterABI } from "./04-init";
import { RpcProvider, Contract, EthSigner, cairo, hash } from "starknet";
import { classHash as ethClassHash } from "@0xknwn/starknet-module";
const providerURL = "http://127.0.0.1:5050/rpc";
const ethPrivateKey =
  "0xb28ebb20fb1015da6e6367d1b5dba9b52862a06dbb3a4022e4749b6987ac1bd2";

const main = async () => {
  // recompute the account address
  const signer = new EthSigner(ethPrivateKey);
  const publicKey = await signer.getPubKey();
  const coords = publicKey.slice(2, publicKey.length);
  const x = coords.slice(0, 64);
  const x_felts = cairo.uint256(`0x${x}`);
  const y = coords.slice(64, 128);
  const y_felts = cairo.uint256(`0x${y}`);
  const publicKeyArray = [
    x_felts.low.toString(),
    x_felts.high.toString(),
    y_felts.low.toString(),
    y_felts.high.toString(),
  ];

  const publicKeyHash = hash.computeHashOnElements(publicKeyArray);
  const computedAccountAddress = accountAddress(
    "SmartrAccount",
    publicKeyHash,
    [ethClassHash("EthValidator"), "0x4", ...publicKeyArray]
  );

  // execute the transaction
  const provider = new RpcProvider({ nodeUrl: providerURL });
  const { counterAddress } = await init();
  const account = new SmartrAccount(provider, computedAccountAddress, signer);
  const counter = new Contract(CounterABI, counterAddress, account);
  let currentCounter = await counter.call("get");
  console.log("currentCounter", currentCounter);
  const call = counter.populate("increment");
  const { transaction_hash } = await account.execute(call);
  const receipt = await account.waitForTransaction(transaction_hash);
  console.log("transaction succeeded", receipt.isSuccess());
  currentCounter = await counter.call("get");
  console.log("currentCounter", currentCounter);
};

main()
  .then(() => {})
  .catch((e) => {
    console.warn(e);
  });

Transpile and run it:

npx tsc --build

node dist/04-execute-tx-core.js

As you can see from the script:

  • You do not need the EthModule to interact with the account. That is because the validator is used as a Core Validator and, as such, the transaction does not required to be prefixed
  • Running transactions is the same as running a transaction with the Stark validator. Only the signature changes.