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.
- Using the Eth Validator
Note: This section assumes the
SmartrAccount
class has been instantiated in thesmartrAccount
variable as shown in Using the modular account from the SDK. It also assumes theCounter
contract that comes with the project has been deploys to thecounterAddress
and theCounterABI
class is available. The05-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 is
EthValidator
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,
classNames as moduleClassNames,
} 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,
"1",
"0x3"
);
const { classHash: ethValidatorClassHash } = await declareClass(
account,
moduleClassNames.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,
classNames as moduleClassNames,
} from "@0xknwn/starknet-module";
console.log(
"Computed EthValidator class hash:",
classHash(moduleClassNames.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,
classNames as moduleClassNames,
} 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,
undefined,
"1",
"0x3"
);
const { transaction_hash } = await account.addModule(
classHash(moduleClassNames.EthValidator)
);
const receipt = await account.waitForTransaction(transaction_hash);
console.log("transaction succeeded", receipt.isSuccess());
const isInstalled = await account.isModule(
classHash(moduleClassNames.EthValidator)
);
console.log(
"module",
classHash(moduleClassNames.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,
classNames as moduleClassNames,
} 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,
undefined,
"1",
"0x3"
);
const module_class_hash = ethClassHash(moduleClassNames.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,
classNames as moduleClassNames,
} 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,
undefined,
"1",
"0x3"
);
const moduleCallData = new CallData(EthValidatorABI);
const calldata = moduleCallData.compile("get_public_key", {});
const public_keys = await account.callOnModule(
ethClassHash(moduleClassNames.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 } = await init();
console.log("accountAddress", accountAddress);
const signer = new EthSigner(ethPrivateKey);
const ethModule = new EthModule(accountAddress);
const account = new SmartrAccount(
provider,
accountAddress,
signer,
ethModule,
"1",
"0x3"
);
console.log("counterAddress", counterAddress);
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, {
version: "0x3",
resourceBounds: {
l2_gas: {
max_amount: "0x0",
max_price_per_unit: "0x0",
},
l1_gas: {
max_amount: "0x2f10",
max_price_per_unit: "0x22ecb25c00",
},
},
});
console.log("transaction_hash", transaction_hash);
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,
classNames as moduleClassNames,
} 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,
undefined,
"1",
"0x3"
);
const { transaction_hash } = await account.removeModule(
classHash(moduleClassNames.EthValidator)
);
const receipt = await account.waitForTransaction(transaction_hash);
console.log("transaction succeeded", receipt.isSuccess());
const isInstalled = await account.isModule(
classHash(moduleClassNames.EthValidator)
);
console.log(
"module",
classHash(moduleClassNames.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,
Account,
} from "starknet";
import {
accountAddress,
deployAccount,
SmartrAccount,
classNames as accountClassName,
} from "@0xknwn/starknet-modular-account";
import {
classHash as ethClassHash,
classNames as moduleClassNames,
} from "@0xknwn/starknet-module";
import { init } from "./04-init";
import { ABI as ERC20ABI } from "./abi/ERC20";
const strkAddress =
"0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d";
// 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 providerURL = "http://127.0.0.1:5050/rpc";
const ethPrivateKey =
"0xb28ebb20fb1015da6e6367d1b5dba9b52862a06dbb3a4022e4749b6987ac1bd2";
const main = async () => {
const provider = new RpcProvider({ nodeUrl: providerURL });
const { ozAccountAddress, ozAccountPrivateKey } = 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(
accountClassName.SmartrAccount,
publicKeyHash,
[ethClassHash(moduleClassNames.EthValidator), "0x4", ...publicKeyArray]
);
// Step 3 - Send STRK to the computed account address
const account = new Account(
provider,
ozAccountAddress,
ozAccountPrivateKey,
"1",
"0x3"
);
const STRK = new Contract(ERC20ABI, strkAddress, account);
const initial_strkTransfer = cairo.uint256(50000n * 10n ** 15n);
const { transaction_hash } = await STRK.transfer(
computedAccountAddress,
initial_strkTransfer
);
const output = await account.waitForTransaction(transaction_hash);
if (!output.isSuccess()) {
throw new Error("Could not send STRK to the expected address");
}
// Step 4 - Deploy the account with the EthValidator as Core Validator
const ethAccount = new SmartrAccount(
provider,
computedAccountAddress,
ethSmartrSigner,
undefined,
"1",
"0x3"
);
const address = await deployAccount(
ethAccount,
accountClassName.SmartrAccount,
publicKeyHash,
[ethClassHash(moduleClassNames.EthValidator), "0x4", ...publicKeyArray],
{
version: "0x3",
resourceBounds: {
l2_gas: {
max_amount: "0x0",
max_price_per_unit: "0x0",
},
l1_gas: {
max_amount: "0x2f10",
max_price_per_unit: "0x22ecb25c00",
},
},
}
);
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,
classNames as accountClassNames,
} from "@0xknwn/starknet-modular-account";
import { init, CounterABI } from "./04-init";
import { RpcProvider, Contract, EthSigner, cairo, hash } from "starknet";
import {
classHash as ethClassHash,
classNames as moduleClassNames,
} 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(
accountClassNames.SmartrAccount,
publicKeyHash,
[ethClassHash(moduleClassNames.EthValidator), "0x4", ...publicKeyArray]
);
// execute the transaction
const provider = new RpcProvider({ nodeUrl: providerURL });
const { counterAddress } = await init();
const account = new SmartrAccount(
provider,
computedAccountAddress,
signer,
undefined,
"1",
"0x3"
);
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, {
version: "0x3",
resourceBounds: {
l2_gas: {
max_amount: "0x0",
max_price_per_unit: "0x0",
},
l1_gas: {
max_amount: "0x2f10",
max_price_per_unit: "0x22ecb25c00",
},
},
});
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.