在Starknet上部署合约

本文详细介绍了在Starknet上部署ERC-20合约的完整流程,包括使用Starknet Foundry(sncast)和Starknet.js两种方法。内容涵盖账户设置、合约类声明、实例部署以及与部署后合约的交互。提供了清晰的代码示例和截图,适合希望学习Starknet开发的人员。

在上一篇文章中,我们介绍了 Starknet 的声明-部署模型,包括普通合约和账户合约的部署路径。本文将这些概念付诸实践。我们将以 ERC-20 章节中的 ERC-20 合约作为部署示例,使用 Starknet Foundry(sncast)和 Starknet.js 演示完整的部署工作流:设置用于签署交易的账户、声明 ERC-20 合约类、部署 ERC-20 合约实例,以及与已部署的合约进行交互。

合约设置

使用 scarb new erc20 创建一个新的 Scarb 项目,用 cd erc20 进入项目目录,然后将 src/lib.cairo 的内容替换为以下代码:

use starknet::ContractAddress;

#[starknet::interface]
pub trait IERC20<TContractState> {
    fn total_supply(self: @TContractState) -> u256;
    fn balance_of(self: @TContractState, account: ContractAddress) -> u256;
    fn allowance(self: @TContractState, owner: ContractAddress, spender: ContractAddress) -> u256;
    fn transfer(ref self: TContractState, recipient: ContractAddress, amount: u256) -> bool;
    fn transfer_from(
        ref self: TContractState, sender: ContractAddress, recipient: ContractAddress, amount: u256,
    ) -> bool;
    fn approve(ref self: TContractState, spender: ContractAddress, amount: u256) -> bool;

    fn name(self: @TContractState) -> ByteArray;
    fn symbol(self: @TContractState) -> ByteArray;
    fn decimals(self: @TContractState) -> u8;

    fn mint(ref self: TContractState, recipient: ContractAddress, amount: u256) -> bool; // 用于测试目的
}

#[starknet::contract]
pub mod ERC20 {
    use starknet::{ContractAddress, get_caller_address};
    use starknet::storage::{
        Map, StoragePointerWriteAccess, StoragePointerReadAccess, StoragePathEntry,
    };

    #[storage]
    pub struct Storage {
        balances: Map<ContractAddress, u256>,
        allowances: Map<
            (ContractAddress, ContractAddress), u256,
        >, //  (owner, spender) -> amount
        token_name: ByteArray,
        symbol: ByteArray,
        decimal: u8,
        total_supply: u256,
        owner: ContractAddress,
    }

    #[event]
    #[derive(Drop, starknet::Event)]
    pub enum Event {
        Transfer: Transfer,
        Approval: Approval,
    }

    #[derive(Drop, starknet::Event)]
    pub struct Transfer {
        #[key]
        from: ContractAddress,
        #[key]
        to: ContractAddress,
        amount: u256,
    }

    #[derive(Drop, starknet::Event)]
    pub struct Approval {
        #[key]
        owner: ContractAddress,
        #[key]
        spender: ContractAddress,
        value: u256,
    }

      #[constructor]
    fn constructor(ref self: ContractState, owner: ContractAddress) {
        self.token_name.write("Rare Token");
        self.symbol.write("RST");
        self.decimal.write(18);
        self.owner.write(owner);
    }

    #[abi(embed_v0)]
    impl ERC20Impl of super::IERC20<ContractState> {
        fn total_supply(self: @ContractState) -> u256 {
            self.total_supply.read()
        }
        fn balance_of(self: @ContractState, account: ContractAddress) -> u256 {
            let balance = self.balances.entry(account).read();
            balance
        }

        fn name(self: @ContractState) -> ByteArray {
            self.token_name.read()
        }

        fn symbol(self: @ContractState) -> ByteArray {
            self.symbol.read()
        }

        fn decimals(self: @ContractState) -> u8 {
            self.decimal.read()
        }

        fn allowance(
            self: @ContractState, owner: ContractAddress, spender: ContractAddress,
        ) -> u256 {
            let allowance = self.allowances.entry((owner, spender)).read();

            allowance
        }

        fn transfer(ref self: ContractState, recipient: ContractAddress, amount: u256) -> bool {
            let sender = get_caller_address();

            let sender_prev_balance = self.balances.entry(sender).read();
            let recipient_prev_balance = self.balances.entry(recipient).read();

            assert(sender_prev_balance >= amount, 'Insufficient amount');

            self.balances.entry(sender).write(sender_prev_balance - amount);
            self.balances.entry(recipient).write(recipient_prev_balance + amount);

            assert(
                self.balances.entry(recipient).read() > recipient_prev_balance,
                'Transaction failed',
            );
            self.emit(Transfer { from: sender, to: recipient, amount });

            true
        }

        fn transfer_from(
            ref self: ContractState,
            sender: ContractAddress,
            recipient: ContractAddress,
            amount: u256,
        ) -> bool {
            let spender = get_caller_address();

            let spender_allowance = self.allowances.entry((sender, spender)).read();
            let sender_balance = self.balances.entry(sender).read();
            let recipient_balance = self.balances.entry(recipient).read();

            assert(amount <= spender_allowance, 'amount exceeds allowance');
            assert(amount <= sender_balance, 'amount exceeds balance');

            self.allowances.entry((sender, spender)).write(spender_allowance - amount);
            self.balances.entry(sender).write(sender_balance - amount);
            self.balances.entry(recipient).write(recipient_balance + amount);

            self.emit(Transfer { from: sender, to: recipient, amount });

            true
        }

        fn approve(ref self: ContractState, spender: ContractAddress, amount: u256) -> bool {
            let caller = get_caller_address();

            self.allowances.entry((caller, spender)).write(amount);

            self.emit(Approval { owner: caller, spender, value: amount });

            true
        }

        fn mint(ref self: ContractState, recipient: ContractAddress, amount: u256) -> bool {
            let caller = get_caller_address();
            assert(caller == self.owner.read(), 'Call not owner');

            let previous_total_supply = self.total_supply.read();
            let previous_balance = self.balances.entry(recipient).read();

            self.total_supply.write(previous_total_supply + amount);
            self.balances.entry(recipient).write(previous_balance + amount);

            let zero_address: ContractAddress = 0.try_into().unwrap();

            self.emit(Transfer { from: zero_address, to: recipient, amount });

            true
        }
    }
}

我们将使用两种方法部署此合约:Starknet Foundry(sncast)和 Starknet.js。两种方法遵循相同的部署步骤:

  1. 设置一个账户来签署交易并支付 Gas 费用
  2. 声明一个 ERC-20 合约类,将代码注册到链上并获取类哈希
  3. 部署 ERC-20 合约实例,从该类哈希创建一个实时实例

使用 Starknet Foundry(sncast)部署

账户设置

我们需要一个账户合约来签署交易并支付部署费用。如果你已经配置了 sncast 账户,可以跳到声明步骤。否则,我们创建一个。

使用 sncast 创建账户的命令是:

sncast account create --network <NETWORK_NAME> --name <ACCOUNT_NAME>

其中:

  • <NETWORK_NAME> 是你要部署到的网络(例如 sepoliamainnet
  • <ACCOUNT_NAME> 是你选择的本地标识符,用于引用此账户(例如 my_accountdeployer_account

例如,要在 Sepolia 上创建账户:

sncast account create --network sepolia --name new_account_1

运行上述命令时,sncast 会:

  • 在本地生成一对公私钥
  • 根据公钥和 salt 确定性地计算账户地址
  • 将账户详细信息保存在本地 JSON 文件中(~/.starknet_accounts/starknet_open_zeppelin_accounts.json)。使用以下命令查看文件:
cat ~/.starknet_accounts/starknet_open_zeppelin_accounts.json
  • 地址对网络(此处为 sepolia)已知,但尚未部署账户合约。

注意:此处我们使用 account create(而不是 declare),因为账户合约类通常由提供者预先声明。在本例中,OpenZeppelin 已经声明了我们使用的账户合约类,因此我们只是创建它的一个实例,而不是声明一个新的。

运行 create 命令后,你的输出应如下所示:

终端输出:sncast 在 Sepolia 网络上创建账户的命令,显示创建的账户地址和部署费用估算

如果你在 Voyager 上搜索该地址,会注意到账户地址存在,但状态为“未初始化”。这表明账户合约尚未部署。

区块浏览器显示未初始化合约,带有类状态指示器和合约未部署警告

要部署新创建的账户,从输出中复制地址,并从 Starknet 水龙头 向其注入 STRK 代币。这就是我们在上一篇文章中讨论的反事实部署过程:账户使用预先注入的地址支付自己的部署费用,从而产生一个独立存在且不与任何其他账户关联的新账户。

获得测试代币后,使用以下命令部署账户:

sncast account deploy --network sepolia --name <ACCOUNT_NAME>

<ACCOUNT_NAME> 替换为创建账户时使用的名称。对于此示例,我们使用 new_account_1,因为这是我们在 account create 步骤中使用的名称:

sncast account deploy --network sepolia --name new_account_1

运行上述命令后,sncast 会提示你将该账户设为本地默认账户或全局默认账户。本地默认仅适用于当前项目,全局默认适用于所有 Scarb 项目。对于本教程,选择本地。确认后,账户即部署完成:

终端输出:sncast 在 Sepolia 上部署账户的命令,显示成功部署确认和交易哈希

如果你在 Voyager 上检查交易,会注意到交易类型为 DEPLOY_ACCOUNT。这是专用于首次创建账户的协议级交易类型,无需现有账户来赞助部署。

区块浏览器交易标签页,高亮显示 DEPLOY_ACCOUNT 交易类型

在此交易期间,定序器验证部署,使用你的公钥运行账户构造函数,并从预先注入的地址中扣除部署费用。你的账户合约现在已在 Starknet 上激活。

声明 ERC-20 合约类

与由提供者预先声明的账户合约不同,普通合约(如我们的 ERC-20)在部署之前必须由我们自行声明。

声明普通合约的语法是:

sncast --account <ACCOUNT_NAME> \
declare \
--url <URL> \
--contract-name <CONTRACT_NAME>

其中:

  • <ACCOUNT_NAME>:你之前创建并部署的账户名称
  • <URL>:你所部署网络对应的 RPC 端点 URL
  • <CONTRACT_NAME>:合约模块的名称,例如 mod ERC20 {} 中的 ERC20

要在 Sepolia 上声明我们的 ERC-20 合约,运行以下命令,将 {apiKey} 替换为你的 Alchemy API 密钥,并将 new_account_1 替换为你的账户名称:

sncast --account new_account_1 \
   declare \
   --url https://starknet-sepolia.g.alchemy.com/starknet/version/rpc/v0_10/{apiKey} \
   --contract-name ERC20

回想一下,声明会将你的合约代码注册到链上,并返回一个类哈希。以下是此过程中发生的事情:

  • 编译sncast 读取你的 Sierra 文件,在本地编译为 CASM(Cairo 汇编),并从该 CASM 计算编译后的类哈希
  • 交易提交sncast 将 Sierra 类和本地计算的编译后类哈希一起发送给定序器,并由你的账户签名
  • 定序器验证:定序器将 Sierra 编译为 CASM,并验证其编译后的类哈希是否与你提供的一致
  • 网络存储:如果哈希匹配,则 Sierra 类和编译后的类哈希都会存储在 Starknet 上
  • 返回类哈希:定序器返回类哈希(根据 Sierra 计算),用于标识此合约类。

输出同时显示交易哈希和类哈希。复制类哈希,因为稍后需要用它来部署合约实例:

终端显示 sncast 声明命令,包含 contract-name ERC20 和 RPC URL,显示编译后类哈希和交易哈希

sncast 在输出中提供了一条可直接使用的部署命令。你可以直接复制生成的命令,或者使用下面更易读的格式。两种方法效果相同。

通过 UDC 部署合约实例

声明合约类后,我们可以从该类哈希部署任意数量的实例。sncast deploy 命令封装了与 UDC 的交互,并在底层处理部署。

部署合约的语法是:

sncast \
 --account <ACCOUNT_NAME> \
 deploy \
 --class-hash <CLASS_HASH> \
 --arguments "<CONSTRUCTOR_ARGS>" \
 --url <URL>

默认情况下,sncast 会生成一个随机 salt,以确保合约地址唯一。如果你需要预测或重现生成的合约地址,可以通过 --salt 传递一个特定值,例如 --salt 0x146。你还可以传递 --unique,这会使用你的部署者地址修改 salt,以确保地址对你账户而言是唯一的。在本教程中,我们省略这两个标志,让 sncast 自动生成 salt。

要部署 ERC-20 合约的实例,运行以下命令。将 <OWNER_ADDRESS> 替换为你的实际账户地址,{apiKey} 替换为你的 Alchemy API 密钥。你可以使用自己声明步骤得到的类哈希,也可以使用下面提供的类哈希:

sncast \
 --account new_account_1 \
 deploy \
 --class-hash 0x23a5a4819dcac3a6b5fe724596647d3fc1f176ca565a0fb908c4457f1cc875b \
 --arguments "<OWNER_ADDRESS>" \
 --url https://starknet-sepolia.g.alchemy.com/starknet/version/rpc/v0_10/{apiKey}

所有者地址可以用单引号或双引号包裹。该地址将被设为合约所有者,这意味着只有此账户有权铸造代币,因此请确保它是你打算用于与合约交互的地址。

你将在终端中收到交易收据,其中包含合约地址:

终端输出:sncast 部署命令,显示类哈希、构造函数参数和部署成功,并高亮显示合约地址

你可以在 Voyager 上验证部署。注意交易类型是 INVOKE,操作是 deployContract。这确认了 sncast 通过 UDC 部署了合约:

区块浏览器交易视图,显示 INVOKE 类型,包含 deployContract 操作、DECLARE 交易和 DEPLOY_ACCOUNT 交易

点击 交易 哈希打开交易详情。这里显示了传递给 UDC 的 deployContract 函数的精确参数:

区块浏览器显示 deployContract 函数调用详情,包含输入参数 classHash、salt、notFromZero 和 calldata,并高亮显示合约地址输出

当你运行合约部署命令时,sncast 自动处理了 UDC 交互:

  • 由于没有提供 --saltsncast 生成一个随机 salt(如输入数据表第二行所示的 0xd815d280876b878e),以确保合约地址唯一
  • 你的账户向 UDC 发送一个 INVOKE 交易,包含:
    • classHash0x23a5a4819dcac3a6b5fe724596647d3fc1f176ca565a0fb908c4457f1cc875b(输入数据表第一行)
    • salt0xd815d280876b878e(输入数据表第二行)
    • notFromZero0x0(false);由于没有传递 --unique,合约地址是根据部署者地址而非零地址推导的
    • calldata[0x14154fb6dd088b5ceb46df635ecce6e1a9b0455357931ac7df4263a7dbf39a9](输入数据表第四行 - 你的构造函数参数,即所有者地址)
  • UDC 调用 deployContract 创建你的合约实例,返回合约地址 0x02ceed65a4bd731034c01113685c831b01c15d7d432f71afb1cf1634b53a2125(在输出数据字段中用黄色高亮显示)

使用 Starknet.js 部署

作为使用 Starknet Foundry 的 sncast 命令的替代方案,我们也可以使用 Starknet.js 部署合约。这种方法允许以编程方式部署,从而更容易实现部署自动化或将其集成到开发工作流中。

环境设置

由于 ERC-20 合约类已经使用 sncast 方法在链上声明,如果从同一个项目使用相同的编译器版本再次声明相同的代码,将导致“合约已声明”错误,因为相同的类哈希已存在于链上。为避免此错误,我们将创建一个新的 Scarb 项目:

scarb new erc20_starknetjs
cd erc20_starknetjs

将合约设置部分的相同 ERC-20 合约代码复制到 src/lib.cairo 中,然后使用 scarb build 编译项目。这样就为使用 Starknet.js 声明和部署创建了一个干净的状态。

现在,为部署代码创建一个 scripts 文件夹:

mkdir scripts

安装部署脚本所需的依赖包:

npm install starknet dotenv
npm install -D typescript @types/node tsx

各包用途:

  • starknet 是与 Starknet 合约交互的官方库
  • dotenv.env 文件加载环境变量
  • typescript & @types/node 为我们的脚本提供 TypeScript 支持
  • tsx 是现代的 TypeScript 运行器,支持 ES 模块

通过向 package.json 添加 "type": "module" 字段,将项目配置为使用 ES 模块语法(import/export)而非 CommonJS(require/module.exports):

{
  "dependencies": {
    "dotenv": "^17.2.3",
    "starknet": "^9.2.1"
  },
  "type": "module",
  "devDependencies": {
    "@types/node": "^25.0.3",
    "tsx": "^4.21.0",
    "typescript": "^5.9.3"
  }
}

这允许我们使用 import 语句,而不是较旧的 require() 语法。

在项目根目录创建一个 .env 文件:

PRIVATE_KEY=your_private_key_here
ALCHEMY_API_KEY=your_alchemy_api_key_here
OWNER_ADDRESS=your_owner_address_here

由于我们将通过 Voyager 调用已部署合约的写函数,你需要连接一个钱包,如 Ready 或 Braavos。你有两个选项:

选项 1:导入我们创建的账户(推荐)

使用 ~/.starknet_accounts/starknet_open_zeppelin_accounts.json 中的私钥,将我们之前创建的账户导入 Ready 钱包。按照此指南导入你的账户:

导入后,在 .env 文件中设置以下内容:

  • OWNER_ADDRESS:导入账户的地址(与 JSON 文件中的相同)
  • PRIVATE_KEY:JSON 文件中的私钥

选项 2:使用你现有的账户

如果你已有 Ready 或 Braavos 的 Sepolia 账户,可以使用这些凭证:

  • OWNER_ADDRESS:你现有钱包的地址
  • PRIVATE_KEY:你现有钱包的私钥

.env 添加到 .gitignore 文件中,以避免将私钥提交到版本控制。

生成 CASM 文件

在运行部署脚本之前,你需要从 Sierra 文件生成 CASM 文件。较新版本的 Scarb 在运行 scarb build 时不会自动生成 CASM 文件。

这需要 Sierra 编译器,你可以通过运行以下命令安装:

cargo install starknet-sierra-compile

安装后,通过运行以下命令将 Sierra 文件编译为 CASM:

starknet-sierra-compile \
target/dev/erc20_starknetjs_ERC20.contract_class.json \
target/dev/erc20_starknetjs_ERC20.compiled_contract_class.json

starknet-sierra-compile 命令,高亮显示 erc20_starknetjs_ERC20 合约的输入和输出文件路径

此命令接受两个参数:

  • 输入: target/dev/erc20_starknetjs_ERC20.contract_class.json —— scarb build 生成的 Sierra 文件
  • 输出: target/dev/erc20_starknetjs_ERC20.compiled_contract_class.json —— 将要创建的 CASM 字节码文件

文件名遵循 {package_name}_{contract_module_name} 的模式。如果你的合约名称与 erc20_starknetjs_ERC20 不同,请相应调整两个文件路径,然后运行命令。

你会在 target/dev 目录中看到新生成的 .compiled_contract_class.json 文件,其中包含部署所需的 CASM 字节码。

代码编辑器显示 target/dev 目录,高亮显示 erc20_starknetjs_ERC20.compiled_contract_class.json 文件,显示字节码数组

编写部署脚本

我们创建的脚本将自动化部署过程的每一步。它会连接到 Starknet Sepolia,使用你的私钥初始化你的账户,并读取 Sierra 和 CASM 文件。

创建 scripts/deploy.ts,包含以下代码:

import { Account, RpcProvider, json, CallData } from "starknet";
import fs from "fs";
import * as dotenv from "dotenv";
dotenv.config();

async function main() {
  // 使用 Alchemy RPC URL 设置提供者
  const provider = new RpcProvider({
    nodeUrl: `https://starknet-sepolia.g.alchemy.com/starknet/version/rpc/v0_10/${process.env.ALCHEMY_API_KEY}`,
  });

  // 设置账户
  const account = new Account({
    provider: provider,
    address: process.env.OWNER_ADDRESS!,
    signer: process.env.PRIVATE_KEY!,
  });

  // 读取已编译的合约文件
  const compiledSierra = json.parse(
    fs
      .readFileSync("./target/dev/erc20_starknetjs_ERC20.contract_class.json")
      .toString("ascii")
  );
  const compiledCasm = json.parse(
    fs
      .readFileSync(
        "./target/dev/erc20_starknetjs_ERC20.compiled_contract_class.json"
      )
      .toString("ascii")
  );

  // 声明合约
  console.log("\n声明合约...");
  const declareResponse = await account.declare({
    contract: compiledSierra,
    casm: compiledCasm,
  });
  console.log(
    "声明交易哈希:",
    declareResponse.transaction_hash
  );

  await provider.waitForTransaction(declareResponse.transaction_hash);

  // 准备构造函数参数
  const myCallData = new CallData(compiledSierra.abi);
  const constructorCalldata = myCallData.compile("constructor", {
    owner: account.address,
  });

  // 部署合约
  console.log("\n部署合约...");
  const deployResponse = await account.deployContract({
    classHash: declareResponse.class_hash,
    constructorCalldata: constructorCalldata,
  });
  console.log("部署交易哈希:", deployResponse.transaction_hash);

  await provider.waitForTransaction(deployResponse.transaction_hash);

  console.log("\n部署摘要:");
  console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
  console.log("类哈希:", declareResponse.class_hash);
  console.log("合约地址:", deployResponse.contract_address);
  console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
}

main()
  .then(() => process.exit(0))
  .catch((error) => {
    console.error("\n部署失败:", error);
    process.exit(1);
  });

如果你的合约名称与 erc20_starknetjs_ERC20 不同,请将部署脚本中的合约名称更新为正确的名称。

要找到你项目的正确名称,请运行:

ls target/dev/*.contract_class.json

例如,如果你看到 my_token_MyERC20.contract_class.json,请按如下方式更新文件路径:

// 将:
fs.readFileSync("./target/dev/erc20_starknetjs_ERC20.contract_class.json");

// 改为:
fs.readFileSync("./target/dev/my_token_MyERC20.contract_class.json");

.contract_class.json.compiled_contract_class.json 两个文件都执行相同操作。

sncast 的主要区别:

Starknet.js 采用了与 Starknet Foundry 命令行方法不同的方式。它没有分别运行声明和部署命令,而是通过 declareAndDeploy() 函数一步完成两个步骤,这意味着你不必在命令之间复制类哈希。虽然两种方法都依赖 UDC 进行部署,但 Starknet.js 允许我们以编程方式控制诸如 salt 生成和部署类型等方面,从而更容易构建带有适当错误处理的自动化部署脚本。

运行以下命令来部署你的合约:

npx tsx scripts/deploy.ts

如果成功,你将看到如下输出:

终端输出显示 TypeScript 部署脚本声明合约并显示声明交易哈希

在 Voyager 上与已部署合约交互

现在合约已部署,我们可以通过 Voyager 的 Web 界面与之交互。转到 Voyager Sepolia 上的已部署合约。点击“写入合约”标签页。此界面显示了合约的所有公开函数。

连接你的钱包(Ready 或 Braavos),并确保你使用的钱包地址是你设置为所有者的那个。这一点很重要,因为只有所有者才能调用受限制的函数,例如 mint

区块浏览器 Write Contract 标签页显示 transfer 函数界面,带有 Connect Wallet 按钮

铸造代币:在写入部分,找到 mint 函数并填写参数:

区块浏览器显示 mint 函数,包含接收者地址和金额字段,交易确认对话框

点击“写入”并在你的钱包中确认交易。确认后,你将收到交易哈希。

切换到“读取”部分,找到 total_supply。点击“查询”查看结果:

区块浏览器 Read Contract 标签页显示 total_supply 函数返回值

检查余额:在同一个“读取”部分,使用 balance_of,传入你刚刚铸造代币的地址。点击“查询”确认你收到了 1200000000000000000(等于 1000 个铸造的 Rare 代币)。

区块浏览器显示 balance_of 函数,包含账户地址输入,返回余额

你可以进一步尝试:将代币转账到其他地址、铸造更多代币或检查授权,以全面测试合约的功能。

使用 Starknet.js 与 ERC-20 交互

我们已经了解了如何通过 Voyager Web 界面与合约交互。现在让我们改用 Starknet.js 来做同样的事情。下面的脚本向一个账户铸造代币,并显示包含事件详情的交易确认。创建 scripts/interact.ts,包含以下代码:

import { Account, RpcProvider, Contract } from "starknet";
import * as dotenv from "dotenv";
dotenv.config();

async function interactWithERC20() {
  // 初始化 Sepolia 测试网的提供者
  const provider = new RpcProvider({
    nodeUrl: `https://starknet-sepolia.g.alchemy.com/starknet/version/rpc/v0_10/${process.env.ALCHEMY_API_KEY}`,
  });

  // 初始化账户
  const account = new Account({
    provider: provider,
    address: process.env.OWNER_ADDRESS!,
    signer: process.env.PRIVATE_KEY!,
  });

  // 替换为你的已部署合约地址
  const contractAddress =
    "0x1416c331f53ddfc9cd022984a4ef6b9871a3c82e59904de2cd8214cfa59f00c";

  // 从已部署合约获取 ABI
  const { abi } = await provider.getClassAt(contractAddress);
  if (abi === undefined) {
    throw new Error("ABI not found");
  }

  // 创建合约实例
  const contract = new Contract({
    abi,
    address: contractAddress,
    providerOrAccount: provider,
  });

  // 准备 mint 调用的数据
  const call = contract.populate("mint", [
    account.address,
    3000000000000000000000n, // 铸造 3000 个代币(18 位小数)
  ]);

  console.log("铸造代币中...");

  // 通过账户执行交易
  const { transaction_hash: mintTxHash } = await account.execute(call);

  console.log("交易哈希:", mintTxHash);

  // 等待交易确认
  const receipt = await provider.waitForTransaction(mintTxHash);

  if (receipt.isSuccess()) {
    console.log("\n铸造成功!");

    // 解析并显示发出的事件
    const events = contract.parseEvents(receipt);
    console.log("\n发出的事件:");
    console.log(events);
  } else {
    console.error("交易失败");
  }
}

// 运行函数并处理任何错误
interactWithERC20().catch(console.error);

contractAddress 替换为你的已部署合约地址,并使用以下命令运行脚本:

npx tsx scripts/interact.ts

你应该会看到输出显示铸造成功,并发出从零地址到你的账户的 Transfer 事件,如下所示:

终端输出显示成功的代币铸造交易,包含 Transfer 事件详情

与现有代币交互

你也可以通过更改合约地址,与现有的 ERC-20 代币(如 STRK)交互。创建一个新的 interact_existing.ts 文件,并粘贴以下脚本来检查你的 STRK 余额:

import { RpcProvider, Contract } from "starknet";
import * as dotenv from "dotenv";

dotenv.config();

async function checkSTRKBalance() {
  const alchemyApiKey = process.env.ALCHEMY_API_KEY;

  const provider = new RpcProvider({
    nodeUrl: `https://starknet-sepolia.g.alchemy.com/starknet/version/rpc/v0_10/${alchemyApiKey}`,
  });

  // 替换为你的账户地址
  const accountAddress =
    "0x014154fb6Dd088b5ceB46df635eCCe6e1a9B0455357931aC7Df4263A7dBf39a9";

  const strkContractAddress =
    "0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d";

  const { abi } = await provider.getClassAt(strkContractAddress);

  const strkContract = new Contract({
    abi,
    address: strkContractAddress,
    providerOrAccount: provider,
  });

  const balance = await strkContract.balance_of(accountAddress);
  console.log(`STRK 余额(原始): ${balance.toString()}`);

  const balanceInSTRK = Number(balance) / 10 ** 18;
  console.log(`STRK 余额: ${balanceInSTRK} STRK`);
}

checkSTRKBalance().catch(console.error);

使用以下命令运行:npx tsx scripts/interact_existing.ts

终端显示 STRK 代币余额

注意: 当与现有代币(如 STRK)交互时,你可以检查余额,如果持有代币,还可以进行转移或授权操作,但铸造需要是合约所有者。

总结

在本文中,我们介绍了使用 sncastStarknet.js 部署 ERC-20 代币并与之交互的实际示例。在下一篇文章中,我们将探索使用直接 deploy_syscall 的工厂模式,展示合约如何在无需 UDC 的情况下部署其他合约。

  • 原文链接: rareskills.io/post/cairo...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
RareSkills
RareSkills
https://www.rareskills.io/