账户
与 Ethereum 中账户从私钥派生不同,所有 Starknet 账户都是合约。这意味着 Starknet 上没有外部拥有账户 (EOA) 的概念。
相反,网络具有原生的账户抽象,签名验证发生在合约级别。
有关账户抽象的总体概述,请参阅 Starknet 的文档。 有关该主题的更详细讨论,请参见 Starknet Shaman 的论坛。
有关用法和实施的详细信息,请查看 API 参考部分。 |
什么是账户?
Starknet 中的账户是智能合约,因此可以像任何其他合约一样进行部署和交互,并且可以扩展以实现任何自定义逻辑。但是,账户是一种特殊类型的合约,用于验证和执行交易。因此,它必须实现一组协议用于此执行流程的入口点。https://github.com/starknet-io/SNIPs/blob/main/SNIPS/snip-6.md[SNIP-6] 提案定义了账户的标准接口,支持此执行流程以及与生态系统中 DApp 的互操作性。
ISRC6 接口
/// 表示对目标合约函数的调用。
struct Call {
to: ContractAddress,
selector: felt252,
calldata: Span<felt252>
}
/// 标准账户接口
#[starknet::interface]
pub trait ISRC6 {
/// 通过账户执行交易。
fn __execute__(calls: Array<Call>);
/// 断言交易是否有效以执行。
fn __validate__(calls: Array<Call>) -> felt252;
/// 断言给定哈希的给定签名是否有效。
fn is_valid_signature(hash: felt252, signature: Array<felt252>) -> felt252;
}
出于优化目的,账户中 Call 结构体的 calldata 成员已更新为 Span<felt252> ,但为了向后兼容,接口 ID 保持不变。此不一致将在未来的版本中得到修复。
|
SNIP-6 添加了 is_valid_signature
方法。协议不使用此方法,但它对于 DApp 验证签名的有效性、支持诸如使用 Starknet 登录之类的功能很有用。
SNIP-6 还定义了兼容账户必须遵循 SNIP-5 实现 SRC5 接口,作为通过自省检测合约是否为账户的机制。
ISRC5 接口
/// 标准接口检测
#[starknet::interface]
pub trait ISRC5 {
/// 查询合约是否实现了给定的接口。
fn supports_interface(interface_id: felt252) -> bool;
}
当查询 ISRC6 接口 ID 时,https://github.com/starknet-io/SNIPs/blob/main/SNIPS/snip-6.md[SNIP-6] 兼容账户必须返回 true
。
即使协议不强制执行这些接口,也建议实现它们以实现与生态系统的互操作性。
Starknet 账户
Starknet 原生账户抽象模式允许创建具有不同验证方案的自定义账户,但通常大多数账户实现都使用 Stark 曲线 验证交易,这是验证签名的最有效方式,因为它是 STARK 友好的曲线。
用于 Cairo 的 OpenZeppelin Contracts 提供了 AccountComponent 用于实现此验证方案。
用法
构造账户合约需要集成 AccountComponent 和 SRC5Component。该合约还应设置构造函数以初始化将用作账户签名者的公钥。这是一个基本合约的示例:
#[starknet::contract(account)]
mod MyAccount {
use openzeppelin_account::AccountComponent;
use openzeppelin_introspection::src5::SRC5Component;
component!(path: AccountComponent, storage: account, event: AccountEvent);
component!(path: SRC5Component, storage: src5, event: SRC5Event);
// Account Mixin
#[abi(embed_v0)]
impl AccountMixinImpl = AccountComponent::AccountMixinImpl<ContractState>;
impl AccountInternalImpl = AccountComponent::InternalImpl<ContractState>;
#[storage]
struct Storage {
#[substorage(v0)]
account: AccountComponent::Storage,
#[substorage(v0)]
src5: SRC5Component::Storage
}
#[event]
#[derive(Drop, starknet::Event)]
enum Event {
#[flat]
AccountEvent: AccountComponent::Event,
#[flat]
SRC5Event: SRC5Component::Event
}
#[constructor]
fn constructor(ref self: ContractState, public_key: felt252) {
self.account.initializer(public_key);
}
}
接口
这是 AccountMixinImpl
实现的完整接口:
#[starknet::interface]
pub trait AccountABI {
// ISRC6
fn __execute__(calls: Array<Call>);
fn __validate__(calls: Array<Call>) -> felt252;
fn is_valid_signature(hash: felt252, signature: Array<felt252>) -> felt252;
// ISRC5
fn supports_interface(interface_id: felt252) -> bool;
// IDeclarer
fn __validate_declare__(class_hash: felt252) -> felt252;
// IDeployable
fn __validate_deploy__(
class_hash: felt252, contract_address_salt: felt252, public_key: felt252
) -> felt252;
// IPublicKey
fn get_public_key() -> felt252;
fn set_public_key(new_public_key: felt252, signature: Span<felt252>);
// ISRC6CamelOnly
fn isValidSignature(hash: felt252, signature: Array<felt252>) -> felt252;
// IPublicKeyCamel
fn getPublicKey() -> felt252;
fn setPublicKey(newPublicKey: felt252, signature: Span<felt252>);
}
Ethereum 账户
除了 Stark 曲线账户之外,用于 Cairo 的 OpenZeppelin Contracts 还提供 Ethereum 风格的账户,这些账户使用 secp256k1 曲线进行签名验证。 为此,必须使用 EthAccountComponent。
用法
构造 secp256k1 账户合约还需要集成 EthAccountComponent 和 SRC5Component。 该合约还应设置构造函数以初始化将用作账户签名者的公钥。 这是一个基本合约的示例:
#[starknet::contract(account)]
mod MyEthAccount {
use openzeppelin_account::EthAccountComponent;
use openzeppelin_account::interface::EthPublicKey;
use openzeppelin_introspection::src5::SRC5Component;
use starknet::ClassHash;
component!(path: EthAccountComponent, storage: eth_account, event: EthAccountEvent);
component!(path: SRC5Component, storage: src5, event: SRC5Event);
// EthAccount Mixin
#[abi(embed_v0)]
impl EthAccountMixinImpl =
EthAccountComponent::EthAccountMixinImpl<ContractState>;
impl EthAccountInternalImpl = EthAccountComponent::InternalImpl<ContractState>;
#[storage]
struct Storage {
#[substorage(v0)]
eth_account: EthAccountComponent::Storage,
#[substorage(v0)]
src5: SRC5Component::Storage
}
#[event]
#[derive(Drop, starknet::Event)]
enum Event {
#[flat]
EthAccountEvent: EthAccountComponent::Event,
#[flat]
SRC5Event: SRC5Component::Event
}
#[constructor]
fn constructor(ref self: ContractState, public_key: EthPublicKey) {
self.eth_account.initializer(public_key);
}
}
接口
这是 EthAccountMixinImpl
实现的完整接口:
#[starknet::interface]
pub trait EthAccountABI {
// ISRC6
fn __execute__(calls: Array<Call>);
fn __validate__(calls: Array<Call>) -> felt252;
fn is_valid_signature(hash: felt252, signature: Array<felt252>) -> felt252;
// ISRC5
fn supports_interface(interface_id: felt252) -> bool;
// IDeclarer
fn __validate_declare__(class_hash: felt252) -> felt252;
// IEthDeployable
fn __validate_deploy__(
class_hash: felt252, contract_address_salt: felt252, public_key: EthPublicKey
) -> felt252;
// IEthPublicKey
fn get_public_key() -> EthPublicKey;
fn set_public_key(new_public_key: EthPublicKey, signature: Span<felt252>);
// ISRC6CamelOnly
fn isValidSignature(hash: felt252, signature: Array<felt252>) -> felt252;
// IEthPublicKeyCamel
fn getPublicKey() -> EthPublicKey;
fn setPublicKey(newPublicKey: EthPublicKey, signature: Span<felt252>);
}
部署账户
在 Starknet 中,有两种部署智能合约的方式:使用 deploy_syscall
和进行反事实部署。
前者可以使用 通用部署合约 (UDC) 轻松完成,该合约包装并公开 deploy_syscall
以通过常规合约调用提供任意部署。
但是,如果您没有要调用的帐户,您可能需要使用后者。
要进行反事实部署,您需要实现另一个协议级别的入口点,名为 __validate_deploy__
。查看 反事实部署 指南以了解如何操作。
发送交易
现在让我们探讨如何通过这些账户发送交易。
Starknet 账户
首先,让我们采用之前创建的示例账户并部署它:
#[starknet::contract(account)]
mod MyAccount {
use openzeppelin_account::AccountComponent;
use openzeppelin_introspection::src5::SRC5Component;
component!(path: AccountComponent, storage: account, event: AccountEvent);
component!(path: SRC5Component, storage: src5, event: SRC5Event);
// Account Mixin
#[abi(embed_v0)]
impl AccountMixinImpl = AccountComponent::AccountMixinImpl<ContractState>;
impl AccountInternalImpl = AccountComponent::InternalImpl<ContractState>;
#[storage]
struct Storage {
#[substorage(v0)]
account: AccountComponent::Storage,
#[substorage(v0)]
src5: SRC5Component::Storage
}
#[event]
#[derive(Drop, starknet::Event)]
enum Event {
#[flat]
AccountEvent: AccountComponent::Event,
#[flat]
SRC5Event: SRC5Component::Event
}
#[constructor]
fn constructor(ref self: ContractState, public_key: felt252) {
self.account.initializer(public_key);
}
}
要部署账户变体,请编译合约并声明类哈希,因为自定义账户可能尚未声明。 这意味着您需要一个已经部署的账户。
接下来,使用 Starknet Foundry 的 自定义账户设置 创建账户 JSON,并包含带有已声明类哈希的 --class-hash
标志。
该标志启用自定义账户变体。
以下示例使用 sncast v0.23.0。
|
$ sncast \
--url http://127.0.0.1:5050 \
account create \
--name my-custom-account \
--class-hash 0x123456...
此命令将输出预先计算的合约地址和建议的 max-fee
。
要反事实地部署账户,请将资金发送到该地址,然后部署自定义账户。
$ sncast \
--url http://127.0.0.1:5050 \
account deploy \
--name my-custom-account
部署账户后,使用自定义账户名称设置 --account
标志,以从该账户发送交易。
$ sncast \
--account my-custom-account \
--url http://127.0.0.1:5050 \
invoke \
--contract-address 0x123... \
--function "some_function" \
--calldata 1 2 3
Ethereum 账户
首先,让我们采用之前创建的示例账户并部署它:
#[starknet::contract(account)]
mod MyEthAccount {
use openzeppelin_account::EthAccountComponent;
use openzeppelin_account::interface::EthPublicKey;
use openzeppelin_introspection::src5::SRC5Component;
component!(path: EthAccountComponent, storage: eth_account, event: EthAccountEvent);
component!(path: SRC5Component, storage: src5, event: SRC5Event);
// EthAccount Mixin
#[abi(embed_v0)]
impl EthAccountMixinImpl =
EthAccountComponent::EthAccountMixinImpl<ContractState>;
impl EthAccountInternalImpl = EthAccountComponent::InternalImpl<ContractState>;
#[storage]
struct Storage {
#[substorage(v0)]
eth_account: EthAccountComponent::Storage,
#[substorage(v0)]
src5: SRC5Component::Storage
}
#[event]
#[derive(Drop, starknet::Event)]
enum Event {
#[flat]
EthAccountEvent: EthAccountComponent::Event,
#[flat]
SRC5Event: SRC5Component::Event
}
#[constructor]
fn constructor(ref self: ContractState, public_key: EthPublicKey) {
self.eth_account.initializer(public_key);
}
}
需要特殊的工具才能部署和发送带有 Ethereum 风格的账户合约的交易。 以下示例利用 StarknetJS 库。
编译并在目标网络上声明合约。 接下来,使用已声明的类哈希预先计算 EthAccount 合约地址。
以下示例使用来自提交 d002baea0abc1de3ac6e87a671f3dec3757437b3 的 StarknetJS (starknetjs@next ) 的未发布功能。
|
import * as dotenv from 'dotenv';
import { CallData, EthSigner, hash } from 'starknet';
import { ABI as ETH_ABI } from '../abis/eth_account.js';
dotenv.config();
// Calculate EthAccount address
const ethSigner = new EthSigner(process.env.ETH_PRIVATE_KEY);
const ethPubKey = await ethSigner.getPubKey();
const ethAccountClassHash = '<ETH_ACCOUNT_CLASS_HASH>';
const ethCallData = new CallData(ETH_ABI);
const ethAccountConstructorCalldata = ethCallData.compile('constructor', {
public_key: ethPubKey
})
const salt = '0x12345';
const deployerAddress = '0x0';
const ethContractAddress = hash.calculateContractAddressFromHash(
salt,
ethAccountClassHash,
ethAccountConstructorCalldata,
deployerAddress
);
console.log('Pre-calculated EthAccount address: ', ethContractAddress);
将资金发送到预先计算的 EthAccount 地址并部署合约。
import * as dotenv from 'dotenv';
import { Account, CallData, EthSigner, RpcProvider, stark } from 'starknet';
import { ABI as ETH_ABI } from '../abis/eth_account.js';
dotenv.config();
// Prepare EthAccount
const provider = new RpcProvider({ nodeUrl: process.env.API_URL });
const ethSigner = new EthSigner(process.env.ETH_PRIVATE_KEY);
const ethPubKey = await ethSigner.getPubKey();
const ethAccountAddress = '<ETH_ACCOUNT_ADDRESS>'
const ethAccount = new Account(provider, ethAccountAddress, ethSigner);
// Prepare payload
const ethAccountClassHash = '<ETH_ACCOUNT_CLASS_HASH>'
const ethCallData = new CallData(ETH_ABI);
const ethAccountConstructorCalldata = ethCallData.compile('constructor', {
public_key: ethPubKey
})
const salt = '0x12345';
const deployPayload = {
classHash: ethAccountClassHash,
constructorCalldata: ethAccountConstructorCalldata,
addressSalt: salt,
};
// Deploy
const { suggestedMaxFee: feeDeploy } = await ethAccount.estimateAccountDeployFee(deployPayload);
const { transaction_hash, contract_address } = await ethAccount.deployAccount(
deployPayload,
{ maxFee: stark.estimatedFeeToMaxFee(feeDeploy, 100) }
);
await provider.waitForTransaction(transaction_hash);
console.log('EthAccount deployed at: ', contract_address);
部署完成后,将 EthAccount 实例连接到目标合约,从而允许从 EthAccount 发起调用。 这是一个来自 EthAccount 的 ERC20 转账的示例。
import * as dotenv from 'dotenv';
import { Account, RpcProvider, Contract, EthSigner } from 'starknet';
dotenv.config();
// Prepare EthAccount
const provider = new RpcProvider({ nodeUrl: process.env.API_URL });
const ethSigner = new EthSigner(process.env.ETH_PRIVATE_KEY);
const ethAccountAddress = '<ETH_ACCOUNT_CONTRACT_ADDRESS>'
const ethAccount = new Account(provider, ethAccountAddress, ethSigner);
// Prepare target contract
const erc20 = new Contract(compiledErc20.abi, erc20Address, provider);
// Connect EthAccount with the target contract
erc20.connect(ethAccount);
// Execute ERC20 transfer
const transferCall = erc20.populate('transfer', {
recipient: recipient.address,
amount: 50n
});
const tx = await erc20.transfer(
transferCall.calldata, { maxFee: 900_000_000_000_000 }
);
await provider.waitForTransaction(tx.transaction_hash);