Solidity语言 - Yul是什么? - Quicknode

  • QuickNode
  • 发布于 2024-12-02 20:26
  • 阅读 15

这篇文章详细介绍了Yul语言,作为以太坊上智能合约的低级语言,具有更高的代码执行效率。文章包括Yul的语法、数据类型、控制流、函数以及Yul和Solidity的对比,并提供了一些示例代码和编译工具的推荐,以帮助开发者理解和应用Yul语言。文章结构清晰,内容丰富,有助于熟悉低级编程概念的开发者进行智能合约优化。

概述

Solidity 一直是以太坊上编写智能合约的主导语言,但随着生态系统的持续发展和成熟,开发者们正在探索优化和定制代码的新方法。Yul 是一种低级语言,提供了更细粒度的控制智能合约执行的能力,是需要从其代码中挤出每一分效率的开发者的理想选择。

你需要的准备

  • 对以太坊和 Solidity 有基本了解(可以从 这里 开始)
  • 学习的渴望!

你将做的事情

  • 学习 Yul
  • 深入探讨 Yul 语法
  • 对比 Yul 和 Solidity

什么是 Yul?

Yul 是一种中间语言,编译到以太坊虚拟机(EVM)字节码。它被设计为一种低级语言,使开发者能够对其智能合约的执行进行高度控制。它在很多方面类似于汇编语言,但具有更高级的特性,使其更易于使用。

Yul 语法

数据类型

Yul 包括几个内置数据类型,用于存储和操作值。以下是 Yul 中一些最常见的数据类型:

整数: 整数用于表示整数。在 Yul 中,整数可以是有符号或无符号,并且可以具有不同的位大小。例如,uint256 是一个 256 位的无符号整数。

字节数组: 字节数组用于存储字节序列。它们通常用于表示数据,如哈希或公钥。在 Yul 中,字节数组使用 bytes 关键字声明,后面跟上要分配的字节数。

结构: 结构用于将相关数据组合在一起。它们类似于其他编程语言中的对象。在 Yul 中,结构使用 struct 关键字声明,后面跟上结构中每个字段的名称和类型。

以下是如何在 Yul 中声明和使用结构的示例:

struct Person {
  string name;
  uint age;
}

变量和常量

变量和常量用于在 Yul 中存储和操作数据。以下是关于如何在 Yul 中声明和使用变量和常量的快速概述:

变量: 变量使用 let 关键字声明,后面跟上变量名称和值。以下是如何在 Yul 中声明变量的示例:

let x = 100
let x //初始值为 0 赋值

运算符

Yul 中的 运算符 用于对值执行算术和逻辑运算。以下是 Yul 中一些最常见的运算符:

加法 (+): 将两个值相加。例如:

add(x,y) // x + y

减法 (-): 从一个值中减去另一个值。例如:

sub(x,y) // x - y

*乘法 ()**: 将两个值相乘。例如:

mul(x,y) // x * y

除法 (/): 将一个值除以另一个值。例如:

div(x,y) // x / y

取模 (%): 计算除法操作的余数。例如:

mod(x,y) // x % y

等价: 检查该值是否相等。例如:

x := y // x = y

控制流语句

Yul 中的控制流语句用于控制合约中的执行流。以下是 Yul 中一些最常见的控制流语句。注意,不能定义 “else” 块。如果需要多个选项,请考虑使用 “switch”(见下文)。

如果语句

if lt(calldatasize(), 4) { revert(0, 0) }

开关语句

{
    let x := 0
    switch calldataload(4)
    case 0 {
        x := calldataload(0x24)
    }
    default {
        x := calldataload(0x44)
    }
    sstore(0, div(x, 2))
}

循环

// 简单的 for 循环
for {let i := 0} lt(i, 10) {i := add(i, 10)} {
    let p := funcCall(i, 10)
    if eq(p, 5) {
        continue
    }

    if eq(p, 90) {
        break
    }
}

函数

Yul 中的函数用于封装可以在以后执行的代码块。以下是如何在 Yul 中声明和使用函数的示例:

function sum(a, b) -> ret : u64
{
    ret := add(b, a)
}

注意,Yul 函数仅限于操作作为参数传递给它们的变量,因为它们无法访问任何超出其范围的变量。可以指定参数和返回值的类型,如果未指定类型,编译器将默认使用 u256 类型。

汇编

Yul 还包括一种内联汇编语言,允许你在 Yul 合约中直接编写低级代码。这对于优化某些操作或直接与以太坊虚拟机(EVM)交互是非常有用的。以下是如何在 Yul 中使用内联汇编的示例:

assembly {
  let x := 0
  let y := 1
  add(x, y)
}

其他内置函数包括:

  • calldata: Yul 中的数据位置,指代传递给函数或合约调用的输入数据。它是只读的,意味着你不能直接修改 calldata。相反,你可以使用 calldataload 操作码将数据从 calldata 复制到其他数据位置,如内存或存储。
  • revert: Yul 中的低级函数,用于中止交易执行并恢复所做的任何状态更改。它通常用于处理智能合约中的错误条件。当调用 revert 时,未使用的任何 gas 将退还给调用者。该函数接受一个可选参数,这是可以返回给调用者的消息,以提供有关错误的更多信息。

剩余的列表可以在 这里 找到。此外,可以像在 Solidity 中一样使用注释,在 Yul 中可以使用 //// 来表示注释,但一个例外是 Yul 中的标识符可以包含点(例如,.)。

有关 Yul 语法的更多信息,请查看 文档

Yul 示例

Yul 代码是在文本编辑器中编写,并使用 solc Solidity 编译器编译的。Yul 代码由一系列指令组成,每个指令在 EVM 上执行特定操作。这些操作包括算术和逻辑操作、内存管理和控制流指令。

以下是一个 ERC-20 代币智能合约的示例(来源,用 Yul 和 Solidity 编写):

  • Yul 中的 ERC-20
  • Solidity 中的 ERC-20
object "Token" {
    code {
        // 将创建者存储在槽零中。
        sstore(0, caller())

        // 部署合约
        datacopy(0, dataoffset("runtime"), datasize("runtime"))
        return(0, datasize("runtime"))
    }
    object "runtime" {
        code {
            // 防止发送以太币
            require(iszero(callvalue()))

            // 调度器
            switch selector()
            case 0x70a08231 /* "balanceOf(address)" */ {
                returnUint(balanceOf(decodeAsAddress(0)))
            }
            case 0x18160ddd /* "totalSupply()" */ {
                returnUint(totalSupply())
            }
            case 0xa9059cbb /* "transfer(address,uint256)" */ {
                transfer(decodeAsAddress(0), decodeAsUint(1))
                returnTrue()
            }
            case 0x23b872dd /* "transferFrom(address,address,uint256)" */ {
                transferFrom(decodeAsAddress(0), decodeAsAddress(1), decodeAsUint(2))
                returnTrue()
            }
            case 0x095ea7b3 /* "approve(address,uint256)" */ {
                approve(decodeAsAddress(0), decodeAsUint(1))
                returnTrue()
            }
            case 0xdd62ed3e /* "allowance(address,address)" */ {
                returnUint(allowance(decodeAsAddress(0), decodeAsAddress(1)))
            }
            case 0x40c10f19 /* "mint(address,uint256)" */ {
                mint(decodeAsAddress(0), decodeAsUint(1))
                returnTrue()
            }
            default {
                revert(0, 0)
            }

            function mint(account, amount) {
                require(calledByOwner())

                mintTokens(amount)
                addToBalance(account, amount)
                emitTransfer(0, account, amount)
            }
            function transfer(to, amount) {
                executeTransfer(caller(), to, amount)
            }
            function approve(spender, amount) {
                revertIfZeroAddress(spender)
                setAllowance(caller(), spender, amount)
                emitApproval(caller(), spender, amount)
            }
            function transferFrom(from, to, amount) {
                decreaseAllowanceBy(from, caller(), amount)
                executeTransfer(from, to, amount)
            }

            function executeTransfer(from, to, amount) {
                revertIfZeroAddress(to)
                deductFromBalance(from, amount)
                addToBalance(to, amount)
                emitTransfer(from, to, amount)
            }

            /* ---------- calldata 解码函数 ----------- */
            function selector() -> s {
                s := div(calldataload(0), 0x100000000000000000000000000000000000000000000000000000000)
            }

            function decodeAsAddress(offset) -> v {
                v := decodeAsUint(offset)
                if iszero(iszero(and(v, not(0xffffffffffffffffffffffffffffffffffffffff)))) {
                    revert(0, 0)
                }
            }
            function decodeAsUint(offset) -> v {
                let pos := add(4, mul(offset, 0x20))
                if lt(calldatasize(), add(pos, 0x20)) {
                    revert(0, 0)
                }
                v := calldataload(pos)
            }
            /* ---------- calldata 编码函数 ---------- */
            function returnUint(v) {
                mstore(0, v)
                return(0, 0x20)
            }
            function returnTrue() {
                returnUint(1)
            }

            /* -------- 事件 ---------- */
            function emitTransfer(from, to, amount) {
                let signatureHash := 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef
                emitEvent(signatureHash, from, to, amount)
            }
            function emitApproval(from, spender, amount) {
                let signatureHash := 0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925
                emitEvent(signatureHash, from, spender, amount)
            }
            function emitEvent(signatureHash, indexed1, indexed2, nonIndexed) {
                mstore(0, nonIndexed)
                log3(0, 0x20, signatureHash, indexed1, indexed2)
            }

            /* -------- 存储布局 ---------- */
            function ownerPos() -> p { p := 0 }
            function totalSupplyPos() -> p { p := 1 }
            function accountToStorageOffset(account) -> offset {
                offset := add(0x1000, account)
            }
            function allowanceStorageOffset(account, spender) -> offset {
                offset := accountToStorageOffset(account)
                mstore(0, offset)
                mstore(0x20, spender)
                offset := keccak256(0, 0x40)
            }

            /* -------- 存储访问 ---------- */
            function owner() -> o {
                o := sload(ownerPos())
            }
            function totalSupply() -> supply {
                supply := sload(totalSupplyPos())
            }
            function mintTokens(amount) {
                sstore(totalSupplyPos(), safeAdd(totalSupply(), amount))
            }
            function balanceOf(account) -> bal {
                bal := sload(accountToStorageOffset(account))
            }
            function addToBalance(account, amount) {
                let offset := accountToStorageOffset(account)
                sstore(offset, safeAdd(sload(offset), amount))
            }
            function deductFromBalance(account, amount) {
                let offset := accountToStorageOffset(account)
                let bal := sload(offset)
                require(lte(amount, bal))
                sstore(offset, sub(bal, amount))
            }
            function allowance(account, spender) -> amount {
                amount := sload(allowanceStorageOffset(account, spender))
            }
            function setAllowance(account, spender, amount) {
                sstore(allowanceStorageOffset(account, spender), amount)
            }
            function decreaseAllowanceBy(account, spender, amount) {
                let offset := allowanceStorageOffset(account, spender)
                let currentAllowance := sload(offset)
                require(lte(amount, currentAllowance))
                sstore(offset, sub(currentAllowance, amount))
            }

            /* ---------- 实用程序函数 ---------- */
            function lte(a, b) -> r {
                r := iszero(gt(a, b))
            }
            function gte(a, b) -> r {
                r := iszero(lt(a, b))
            }
            function safeAdd(a, b) -> r {
                r := add(a, b)
                if or(lt(r, a), lt(r, b)) { revert(0, 0) }
            }
            function calledByOwner() -> cbo {
                cbo := eq(owner(), caller())
            }
            function revertIfZeroAddress(addr) {
                require(addr)
            }
            function require(condition) {
                if iszero(condition) { revert(0, 0) }
            }
        }
    }
}
// SPDX-License-Identifier-Identifier: MIT
// OpenZeppelin Contracts (最后更新于 v4.8.0) (token/ERC20/ERC20.sol)

pragma solidity ^0.8.0;

import "./IERC20.sol";
import "./extensions/IERC20Metadata.sol";
import "../../utils/Context.sol";

/**
 * @dev 实现 {IERC20} 接口。
 *
 * 该实现与创建代币的方式无关。这意味着在派生合约中必须添加供给机制,使用 {_mint}。
 * 对于通用机制,请参见 {ERC20PresetMinterPauser}。
 *
 * 提示:有关详细写法,请参见我们的指南 
 * https://forum.openzeppelin.com/t/how-to-implement-erc20-supply-mechanisms/226[如何实现供给机制]。
 *
 * {decimals} 的默认值为 18。要更改此值,你应该覆盖此函数以返回一个不同的值。
 *
 * 我们遵循了通用 OpenZeppelin Contracts 指南:函数在失败时会 revert,而不是返回 `false`。这种行为是传统的,与 ERC20 应用程序的期望不冲突。
 *
 * 此外,在调用 {transferFrom} 时会发出 {Approval} 事件。这允许应用程序通过监听这些事件重新构建所有账户的额度。EIP 的其他实现可能不会发出这些事件,因为规范并不要求。
 *
 * 最后,已添加非标准的 {decreaseAllowance} 和 {increaseAllowance} 函数,以应对众所周知的关于设置允许额的问题。请参见 {IERC20-approve}。
 */
contract ERC20 is Context, IERC20, IERC20Metadata {
    mapping(address => uint256) private _balances;

    mapping(address => mapping(address => uint256)) private _allowances;

    uint256 private _totalSupply;

    string private _name;
    string private _symbol;

    /**
     * @dev 设置 {name} 和 {symbol} 的值。
     *
     * 这两个值都是不可变的:在构造时只能设置一次。
     */
    constructor(string memory name_, string memory symbol_) {
        _name = name_;
        _symbol = symbol_;
    }

    /**
     * @dev 返回代币的名称。
     */
    function name() public view virtual override returns (string memory) {
        return _name;
    }

    /**
     * @dev 返回代币的符号,通常是名称的简短版本。
     */
    function symbol() public view virtual override returns (string memory) {
        return _symbol;
    }

    /**
     * @dev 返回用于获取用户表示的十进制数的位数。
     * 例如,如果 `decimals` 等于 `2`,则 `505` 代币的余额应显示为 `5.05` (`505 / 10 ** 2`)。
     *
     * 代币通常选择 18 作为值,以模仿以太币和 Wei 之间的关系。除非被覆盖,否则这是该函数返回的默认值。
     *
     * 注意:该信息仅用于 _显示_ 目的:它并不会影响合约的任何算术,包括 {IERC20-balanceOf} 和 {IERC20-transfer}。
     */
    function decimals() public view virtual override returns (uint8) {
        return 18;
    }

    /**
     * @dev 查看 {IERC20-totalSupply}。
     */
    function totalSupply() public view virtual override returns (uint256) {
        return _totalSupply;
    }

    /**
     * @dev 查看 {IERC20-balanceOf}。
     */
    function balanceOf(address account) public view virtual override returns (uint256) {
        return _balances[account];
    }

    /**
     * @dev 查看 {IERC20-transfer}。
     *
     * 需求:
     *
     * - `to` 不能是零地址。
     * - 调用者的余额必须至少为 `amount`。
     */
    function transfer(address to, uint256 amount) public virtual override returns (bool) {
        address owner = _msgSender();
        _transfer(owner, to, amount);
        return true;
    }

    /**
     * @dev 查看 {IERC20-allowance}。
     */
    function allowance(address owner, address spender) public view virtual override returns (uint256) {
        return _allowances[owner][spender];
    }

    /**
     * @dev 查看 {IERC20-approve}。
     *
     * 注意:如果 `amount` 是最大 `uint256`,在 `transferFrom` 时不会更新允许额。这在语义上等同于无限批准。
     *
     * 需求:
     *
     * - `spender` 不能是零地址。
     */
    function approve(address spender, uint256 amount) public virtual override returns (bool) {
        address owner = _msgSender();
        _approve(owner, spender, amount);
        return true;
    }

    /**
     * @dev 查看 {IERC20-transferFrom}。
     *
     * 发出 {Approval} 事件,指示更新的允许额。这不是 EIP 要求。请参见 {ERC20} 开头的说明。
     *
     * 注意:如果当前允许额是最大 `uint256`,则不会更新允许额。
     *
     * 需求:
     *
     * - `from` 和 `to` 不能是零地址。
     * - `from` 必须至少有 `amount` 的余额。
     * - 调用者必须有 `from` 的代币的至少 `amount` 的允许额。
     */
    function transferFrom(address from, address to, uint256 amount) public virtual override returns (bool) {
        address spender = _msgSender();
        _spendAllowance(from, spender, amount);
        _transfer(from, to, amount);
        return true;
    }

    /**
     * @dev 原子性增加授予 `spender` 的允许额。
     *
     * 这是 {approve} 的替代方案,可用于减轻 {IERC20-approve} 中描述的问题。
     *
     * 发出 {Approval} 事件,指示更新的允许额。
     *
     * 需求:
     *
     * - `spender` 不能是零地址。
     */
    function increaseAllowance(address spender, uint256 addedValue) public virtual returns (bool) {
        address owner = _msgSender();
        _approve(owner, spender, allowance(owner, spender) + addedValue);
        return true;
    }

    /**
     * @dev 原子性减少授予 `spender` 的允许额。
     *
     * 这是 {approve} 的替代方案,可用于减轻 {IERC20-approve} 中描述的问题。
     *
     * 发出 {Approval} 事件,指示更新的允许额。
     *
     * 需求:
     *
     * - `spender` 不能是零地址。
     * - `spender` 必须对调用者有至少 `subtractedValue` 的允许额。
     */
    function decreaseAllowance(address spender, uint256 subtractedValue) public virtual returns (bool) {
        address owner = _msgSender();
        uint256 currentAllowance = allowance(owner, spender);
        require(currentAllowance >= subtractedValue, "ERC20: decreased allowance below zero");
        unchecked {
            _approve(owner, spender, currentAllowance - subtractedValue);
        }

        return true;
    }

    /**
     * @dev 将 `amount` 的代币从 `from` 移动到 `to`。
     *
     * 此内部功能相当于 {transfer},可以用来实现自动代币费用、削减机制等。
     *
     * 发出 {Transfer} 事件。
     *
     * 需求:
     *
     * - `from` 不能是零地址。
     * - `to` 不能是零地址。
     * - `from` 必须至少有 `amount` 的余额。
     */
    function _transfer(address from, address to, uint256 amount) internal virtual {
        require(from != address(0), "ERC20: transfer from the zero address");
        require(to != address(0), "ERC20: transfer to the zero address");

        _beforeTokenTransfer(from, to, amount);

        uint256 fromBalance = _balances[from];
        require(fromBalance >= amount, "ERC20: transfer amount exceeds balance");
        unchecked {
            _balances[from] = fromBalance - amount;
            // 溢出不可能发生:所有余额的总和由总供给限制,并且通过
            // 先递减然后递增的方法保持总和。
            _balances[to] += amount;
        }

        emit Transfer(from, to, amount);

        _afterTokenTransfer(from, to, amount);
    }

    /** @dev 创建 `amount` 代币并将其分配给 `account`,增加
     * 总供应量。
     *
     * 发出 {Transfer} 事件,发送自零地址。
     *
     * 需求:
     *
     * - `account` 不能是零地址。
     */
    function _mint(address account, uint256 amount) internal virtual {
        require(account != address(0), "ERC20: mint to the zero address");

        _beforeTokenTransfer(address(0), account, amount);

        _totalSupply += amount;
        unchecked {
            // 溢出不可能:余额 + 数量最多为总供应 + 数量,已在上面检查。
            _balances[account] += amount;
        }
        emit Transfer(address(0), account, amount);

        _afterTokenTransfer(address(0), account, amount);
    }

    /**
     * @dev 从 `account` 销毁 `amount` 代币,减少
     * 总供应量。
     *
     * 发出 {Transfer} 事件,发往零地址。
     *
     * 需求:
     *
     * - `account` 不能是零地址。
     * - `account` 必须至少有 `amount` 代币。
     */
    function _burn(address account, uint256 amount) internal virtual {
        require(account != address(0), "ERC20: burn from the zero address");

        _beforeTokenTransfer(account, address(0), amount);

        uint256 accountBalance = _balances[account];
        require(accountBalance >= amount, "ERC20: burn amount exceeds balance");
        unchecked {
            _balances[account] = accountBalance - amount;
            // 溢出不可能:数量 <= 账户余额 <= 总供应量。
            _totalSupply -= amount;
        }

        emit Transfer(account, address(0), amount);

        _afterTokenTransfer(account, address(0), amount);
    }

    /**
     * @dev 设置 `spender` 对 `owner` 代币的允许 `amount`。
     *
     * 此内部功能相当于 `approve`,可用于
     * 设置某些子系统的自动允许额等。
     *
     * 发出 {Approval} 事件。
     *
     * 需求:
     *
     * - `owner` 不能是零地址。
     * - `spender` 不能是零地址。
     */
    function _approve(address owner, address spender, uint256 amount) internal virtual {
        require(owner != address(0), "ERC20: approve from the zero address");
        require(spender != address(0), "ERC20: approve to the zero address");

        _allowances[owner][spender] = amount;
        emit Approval(owner, spender, amount);
    }

    /**
     * @dev 基于消费的 `amount` 更新 `owner` 的 `spender` 的允许额。
     *
     * 在无限允许的情况下不更新允许额。如果没有足够的允许额,会 revert。
     *
     * 可能会发出 {Approval} 事件。
     */
    function _spendAllowance(address owner, address spender, uint256 amount) internal virtual {
        uint256 currentAllowance = allowance(owner, spender);
        if (currentAllowance != type(uint256).max) {
            require(currentAllowance >= amount, "ERC20: insufficient allowance");
            unchecked {
                _approve(owner, spender, currentAllowance - amount);
            }
        }
    }

    /**
     * @dev 在任何代币转移之前调用的钩子。这包括
     * 创建和销毁。
     *
     * 调用条件:
     *
     * - 当 `from` 和 `to` 都是非零时,将 ``from`` 的 `amount` 代币转移到 `to`。
     * - 当 `from` 为零时,将 `amount` 代币发送给 `to`。
     * - 当 `to` 为零时,将 ``from`` 的 `amount` 代币销毁。
     * - `from` 和 `to` 都永远不会是零。
     *
     * 要了解有关钩子的更多信息,请参阅 xref:ROOT:extending-contracts.adoc#using-hooks[使用钩子]。
     */
    function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual {}

    /**
     * @dev 在任何代币转移之后调用的钩子。这包括
     * 创建和销毁。
     *
     * 调用条件:
     *
     * - 当 `from` 和 `to` 都是非零时,``from`` 的 `amount` 代币已转移到 `to`。
     * - 当 `from` 为零时,将 `amount` 代币已发行给 `to`。
     * - 当 `to` 为零时,``from`` 的 `amount` 代币已被销毁。
     * - `from` 和 `to` 都永远不会是零。
     *
     * 要了解有关钩子的更多信息,请参阅 xref:ROOT:extending-contracts.adoc#using-hooks[使用钩子]。
     */
    function _afterTokenTransfer(address from, address to, uint256 amount) internal virtual {}
}

花几分钟时间查看上面的代码。我们在 Yul 语法 部分中涵盖的大部分语法在上面的代码中已有实现。你还可以切换到 Solidity 中的 ERC-20 选项卡,查看该合约如何用 Solidity 编写。

Yul vs. Solidity

Yul 并不是 Solidity 的替代品,而是一种可以与之协同使用的补充语言。Yul 的层次要比 Solidity 低得多,并旨在提供更直接的合约执行控制。这使得 Yul 代码比等效的 Solidity 代码更高效,但同时也更难以编写和理解。

相反,Solidity 是一种高级语言,提供了更多的抽象和便于开发者使用的特性。它旨在让不熟悉低级编程概念的开发者更易上手。

Yul 工具和资源

如果你有兴趣了解更多关于 Yul 的信息以及如何在智能合约开发中使用 Yul,有许多在线资源可供参考。Solidity 文档包括 Yul 的一个部分以及 Yul 教程。此外,还有许多博客文章、文献和视频更详细地介绍了 Yul。

有多种可用于 Yul 的工具,例如:

  • Remix.IDE:用于编译和部署用 Yul 编写的智能合约
  • Hardhat: 可用于使用 Yul 和/或 Yul+ 开发智能合约的插件
  • Solc 编译器:编译 Yul 代码为字节码

最后思考

Yul 是一个强大的工具,为需要优化其代码的效率和性能的智能合约开发者而设计。虽然它可能并不适合所有开发者,但那些对低级编程概念感到舒适,并希望微调其代码的人会发现 Yul 是他们工具包中一个宝贵的补充。我们希望本指南能够帮助你了解 Yul 及其功能,并鼓励你进一步探索,看看 Yul 能为你带来什么!

如果你遇到问题,或者有任何疑问,或只是想讨论自己正在构建的内容,请在 DiscordTwitter 上给我们留言!

我们 ❤️ 反馈!

如果你对本指南有任何反馈,请 告诉我们。我们期待你的反馈!

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

0 条评论

请先 登录 后评论
QuickNode
QuickNode
江湖只有他的大名,没有他的介绍。