使用SmartPy在Tezos上进行代币化和协议开发

区块链 2026-03-28

我们将使用SmartPy作为主要开发工具,深入了解Tezos智能合约。我们将创建功能齐全的FA1.2同质化代币合约并使用它,为更高级的开发技巧打下基础。

深入了解Tezos上的代币化

代币的概念

在区块链领域,“代币”一词表示一种数字资产。代币可以代表特定生态系统中的各种资产或实用功能,包括区块链的原生货币和数字或实物资产的所有权。

同质化代币

同质化代币可以相互交换,可以将它们视为区块链网络中的相同项目,每个项目都具有相同的价值。这一特性与传统货币密切相关。在传统货币中,每单位货币都与任何其他单位货币的价值相同。比特币(BTC)、以太币(ETH)和Tezos(XTZ)等加密货币都属于同质化代币。正是由于这种同质化特性,您可以自由地将一个比特币兑换成另一个比特币,而不会损失任何价值或在实用性上有任何差异。

半同质化代币

半同质化代币是一种混合型代币,融合了同质化代币和非同质化代币的优点。在同一类别中,半同质化代币可以互换,如特定音乐会的门票,但无法跨类别互换,如音乐会门票不能与足球比赛门票互换。它们提供了许多现实场景所需的灵活性,如票务和某些类型的游戏。

非同质化代币

非同质化代币(NFT)代表独特的资产。与同质化代币不同,NFT可以相互区分,每个代币都有独特的价值。它们类似于收藏品,每件物品都有独特的特征,因此不可替代。这种独特性促进了NFT在数字艺术、音乐和虚拟房地产等领域的兴起。在这些领域,每件作品、歌曲或财产都是独一无二的,具有独特的意义。

Tezos中的代币标准

270

在Tezos区块链上,代币标准主要分为三类: FA1、FA1.2和FA2。这些标准是Tezos互操作性提案(Tezos Interoperability Proposal,简称 TZIP)

Tezos互操作性提案(TZIP)

TZIP代表Tezos互操作性提案,解释了如何通过新的现代标准和理念(例如智能合约要求)来增强Tezos区块链。

FA1(TZIP 5摘要分类账)

FA1是最初的Tezos代币标准,本质上是一个最小版本的账本。它旨在将身份映射到余额,为合约开发人员、库、客户端工具等提供使用同质化资产的机制。但是,Tezos代币标准之间没有强制性的继承关系,因此,所有后续标准都不需要与FA1保持兼容。此标准已被弃用。

FA1.2(TZIP 7可批准账本)

FA1.2标准结合了FA1标准和以太坊中使用的EIP-20标准。其特征是能够批准其他账户的代币支出,但仅适用于同质化代币。使用FA1.2标准实现代币时,您需要在其界面中包含以下入口点:

transfer(转出账户、转入账户、值)

approve(消费者、值)

getAllowance(所有者、消费者)

getBalance(所有者)

getTotalSupply

在FA1.2标准中,开发人员可以在代币合约中增加额外功能。例如,FA1.2的SmartPy模板包括铸造和销毁代币以及治理管理等活动的补充入口点。

FA2(TZIP 12多资产接口)

FA2标准是最新的Tezos代币标准,提供了更大的灵活性,并支持多种类型的资产,既包括同质化代币,也包括非同质化代币。需要理解的是,FA2并不是FA1.2的直接接替者,二者有以下区别:

与FA1.2不同,FA2支持多种资产类型,包括同质化代币和非同质化代币,反映了以太坊EIP-1155多代币标准的功能。

FA2处理代币转移权限的方式与FA1.2不同。在FA2中,可以使用update_operators入口点授予权限。根据FA2规范,操作员是一个地址,可以代表代币的所有者发起交易。

FA2标准的界面需要包含以下入口点:

transfer (transfer_list)

balance_of (requests, callback)

update_operators (operator_updates)

getBalance (owner, token_id)

total_supply (token_id)

all_tokens

了解以上Tezos代币标准后,我们可以进入课程的实践部分。由于FA1标准已不再使用,我们将重点关注FA1.2和FA2标准。在接下来的课程中,我们将学习如何编写可以与FA1.2和FA2标准交互的智能合约。

继续学习

在本课程中,我们将主要学习FA1.2代币标准。我们将指导您创建一个FA1.2代币合约,让您能够铸造自己的代币、添加管理控制,并扩展合约以实现自定义功能。

随着课程的进行,我们将深入探讨FA1.2合约的具体细节,并通过铸币、销毁、暂停等功能对其进行扩展。通过本课程的学习,您将全面了解在Tezos区块链上创建同质化代币的过程和机制。

请记住,Tezos甚至整个区块链上的代币化过程虽然一开始很具有挑战性,但是随着时间的推移和实践的深入,您会逐渐理解这些复杂性,并能够发掘这些数字资产的潜力。不要害怕,让我们一起深入研究Tezos上有趣的代币化世界吧!

与第一部分课程一样,我们将使用SmartPy语言进行讲解!

SmartPy:Tezos的智能合约语言

在Tezos上创建智能合约时,我们将使用SmartPy语言,它是一个用于开发Tezos区块链智能合约的Python库。SmartPy是一种直观有效的语言,用于表达合约及其相关的测试场景。

SmartPy最显著的特点是它与世界上最受欢迎和增长最快的编程语言之一Python的交互。如果您已经熟悉Python,您会发现学习SmartPy非常容易。

了解SmartPy并开发您的第一份合约

访问SmartPy IDE

SmartPy包含一个功能齐全的集成开发环境(IDE),可从您的Web浏览器访问。前往SmartPy IDE,开始编写您的第一个智能合约吧。

270

使用FA1.2标准创建第一个代币

分步指南

访问SmartPy IDE。

首先,打开SmartPy在线IDE,网址为https://smartpy.io/ide/。我们将在这里编写、测试和部署智能合约。

初始化FA1.2模板。

单击左侧面板的“Templates by Type”,选择“FA1.2”。在新打开的选项卡中,我们会看到FA1.2合约模板。这是一个符合FA1.2标准的可直接使用的合约模板。

270

了解FA1.2模板。

该模板具有同质化代币的基本功能,包括转移代币、批准转移、查询余额以及查看代币的总供应量。该合约还包括铸造和销毁代币以及治理管理的附加功能。

学习此模板并了解它的各项功能。如果您目前不理解所有内容,也没关系,但可以尽力对这个合约可以执行的操作有一个大致的了解。

您可以从SmartPy IDE或下面的模板中复制代码:

Python
# Fungible Assets - FA12
# Inspired by https://gitlab.com/tzip/tzip/blob/master/A/FA1.2.md
import smartpy as sp

# The metadata below is just an example, it serves as a base,
# the contents are used to build the metadata JSON that users
# can copy and upload to IPFS.
TZIP16_Metadata_Base = {
    "name": "SmartPy FA1.2 Token Template",
    "description": "Example Template for an FA1.2 Contract from SmartPy",
    "authors": ["SmartPy Dev Team"],
    "homepage": "https://smartpy.io",
    "interfaces": ["TZIP-007-2021-04-17", "TZIP-016-2021-04-17"],
}

@sp.module
def m():
    class AdminInterface(sp.Contract):
        @sp.private(with_storage="read-only")
        def is_administrator_(self, sender):
            sp.cast(sp.sender, sp.address)
            """Not standard, may be re-defined through inheritance."""
            return True
    class CommonInterface(AdminInterface):
        def __init__(self):
            AdminInterface.__init__(self)
            self.data.balances = sp.cast(
                sp.big_map(),
                sp.big_map[
                    sp.address,
                    sp.record(approvals=sp.map[sp.address, sp.nat], balance=sp.nat),
                ],
            )
            self.data.total_supply = 0
            self.data.token_metadata = sp.cast(
                sp.big_map(),
                sp.big_map[
                    sp.nat,
                    sp.record(token_id=sp.nat, token_info=sp.map[sp.string, sp.bytes]),
                ],
            )
            self.data.metadata = sp.cast(
                sp.big_map(),
                sp.big_map[sp.string, sp.bytes],
            )
            self.data.balances = sp.cast(
                sp.big_map(),
                sp.big_map[
                    sp.address,
                    sp.record(approvals=sp.map[sp.address, sp.nat], balance=sp.nat),
                ],
            )
            self.data.total_supply = 0
            self.data.token_metadata = sp.cast(
                sp.big_map(),
                sp.big_map[
                    sp.nat,
                    sp.record(token_id=sp.nat, token_info=sp.map[sp.string, sp.bytes]),
                ],
            )
            self.data.metadata = sp.cast(
                sp.big_map(),
                sp.big_map[sp.string, sp.bytes],
            )
        @sp.private(with_storage="read-only")
        def is_paused_(self):
            """Not standard, may be re-defined through inheritance."""
            return False
    class Fa1_2(CommonInterface):
        def __init__(self, metadata, ledger, token_metadata):
            """
            token_metadata spec: https://gitlab.com/tzip/tzip/-/blob/master/proposals/tzip-12/tzip-12.md#token-metadata
            Token-specific metadata is stored/presented as a Michelson value of type (map string bytes).
            A few of the keys are reserved and predefined:
            - ""          : Should correspond to a TZIP-016 URI which points to a JSON representation of the token metadata.
            - "name"      : Should be a UTF-8 string giving a "display name" to the token.
            - "symbol"    : Should be a UTF-8 string for the short identifier of the token (e.g. XTZ, EUR, …).
            - "decimals"  : Should be an integer (converted to a UTF-8 string in decimal)
                which defines the position of the decimal point in token balances for display purposes.
            contract_metadata spec: https://gitlab.com/tzip/tzip/-/blob/master/proposals/tzip-16/tzip-16.md
            """
            CommonInterface.__init__(self)
            self.data.metadata = metadata
            self.data.token_metadata = sp.big_map(
                {0: sp.record(token_id=0, token_info=token_metadata)}
            )
            for owner in ledger.items():
                self.data.balances[owner.key] = owner.value
                self.data.total_supply += owner.value.balance
            # TODO: Activate when this feature is implemented.
            # self.init_metadata("metadata", metadata)
        @sp.entrypoint
        def transfer(self, param):
            sp.cast(
                param,
                sp.record(from_=sp.address, to_=sp.address, value=sp.nat).layout(
                    ("from_ as from", ("to_ as to", "value"))
                ),
            )
            balance_from = self.data.balances.get(
                param.from_, default=sp.record(balance=0, approvals={})
            )
            balance_to = self.data.balances.get(
                param.to_, default=sp.record(balance=0, approvals={})
            )
            balance_from.balance = sp.as_nat(
                balance_from.balance - param.value, error="FA1.2_InsufficientBalance"
            )
            balance_to.balance += param.value
            if not self.is_administrator_(sp.sender):
                assert not self.is_paused_(), "FA1.2_Paused"
                if param.from_ != sp.sender:
                    balance_from.approvals[sp.sender] = sp.as_nat(
                        balance_from.approvals[sp.sender] - param.value,
                        error="FA1.2_NotAllowed",
                    )
            self.data.balances[param.from_] = balance_from
            self.data.balances[param.to_] = balance_to
        @sp.entrypoint
        def approve(self, param):
            sp.cast(
                param,
                sp.record(spender=sp.address, value=sp.nat).layout(
                    ("spender", "value")
                ),
            )
            assert not self.is_paused_(), "FA1.2_Paused"
            spender_balance = self.data.balances.get(
                sp.sender, default=sp.record(balance=0, approvals={})
            )
            alreadyApproved = spender_balance.approvals.get(param.spender, default=0)
            assert (
                alreadyApproved == 0 or param.value == 0
            ), "FA1.2_UnsafeAllowanceChange"
            spender_balance.approvals[param.spender] = param.value
            self.data.balances[sp.sender] = spender_balance
        @sp.entrypoint
        def getBalance(self, param):
            (address, callback) = param
            result = self.data.balances.get(
                address, default=sp.record(balance=0, approvals={})
            ).balance
            sp.transfer(result, sp.tez(0), callback)
        @sp.entrypoint
        def getAllowance(self, param):
            (args, callback) = param
            result = self.data.balances.get(
                args.owner, default=sp.record(balance=0, approvals={})
            ).approvals.get(args.spender, default=0)
            sp.transfer(result, sp.tez(0), callback)
        @sp.entrypoint
        def getTotalSupply(self, param):
            sp.cast(param, sp.pair[sp.unit, sp.contract[sp.nat]])
            sp.transfer(self.data.total_supply, sp.tez(0), sp.snd(param))
        @sp.offchain_view()
        def token_metadata(self, token_id):
            """Return the token-metadata URI for the given token. (token_id must be 0)."""
            sp.cast(token_id, sp.nat)
            return self.data.token_metadata[token_id]
    ##########
    # Mixins #
    ##########
    class Admin(sp.Contract):
        def __init__(self, administrator):
            self.data.administrator = administrator
        @sp.private(with_storage="read-only")
        def is_administrator_(self, sender):
            return sender == self.data.administrator
        @sp.entrypoint
        def setAdministrator(self, params):
            sp.cast(params, sp.address)
            assert self.is_administrator_(sp.sender), "Fa1.2_NotAdmin"
            self.data.administrator = params
        @sp.entrypoint()
        def getAdministrator(self, param):
            sp.cast(param, sp.pair[sp.unit, sp.contract[sp.address]])
            sp.transfer(self.data.administrator, sp.tez(0), sp.snd(param))
        @sp.onchain_view()
        def get_administrator(self):
            return self.data.administrator
    class Pause(AdminInterface):
        def __init__(self):
            AdminInterface.__init__(self)
            self.data.paused = False
        @sp.private(with_storage="read-only")
        def is_paused_(self):
            return self.data.paused
        @sp.entrypoint
        def setPause(self, param):
            sp.cast(param, sp.bool)
            assert self.is_administrator_(sp.sender), "Fa1.2_NotAdmin"
            self.data.paused = param
    class Mint(CommonInterface):
        def __init__(self):
            CommonInterface.__init__(self)
        @sp.entrypoint
        def mint(self, param):
            sp.cast(param, sp.record(address=sp.address, value=sp.nat))
            assert self.is_administrator_(sp.sender), "Fa1.2_NotAdmin"
            receiver_balance = self.data.balances.get(
                param.address, default=sp.record(balance=0, approvals={})
            )
            receiver_balance.balance += param.value
            self.data.balances[param.address] = receiver_balance
            self.data.total_supply += param.value
    class Burn(CommonInterface):
        def __init__(self):
            CommonInterface.__init__(self)
        @sp.entrypoint
        def burn(self, param):
            sp.cast(param, sp.record(address=sp.address, value=sp.nat))
            assert self.is_administrator_(sp.sender), "Fa1.2_NotAdmin"
            receiver_balance = self.data.balances.get(
                param.address, default=sp.record(balance=0, approvals={})
            )
            receiver_balance.balance = sp.as_nat(
                receiver_balance.balance - param.value,
                error="FA1.2_InsufficientBalance",
            )
            self.data.balances[param.address] = receiver_balance
            self.data.total_supply = sp.as_nat(self.data.total_supply - param.value)
    class ChangeMetadata(CommonInterface):
        def __init__(self):
            CommonInterface.__init__(self)
        @sp.entrypoint
        def update_metadata(self, key, value):
            """An entrypoint to allow the contract metadata to be updated."""
            assert self.is_administrator_(sp.sender), "Fa1.2_NotAdmin"
            self.data.metadata[key] = value
    ##########
    # Tests #
    ##########
    class Fa1_2TestFull(Admin, Pause, Fa1_2, Mint, Burn, ChangeMetadata):
        def __init__(self, administrator, metadata, ledger, token_metadata):
            ChangeMetadata.__init__(self)
            Burn.__init__(self)
            Mint.__init__(self)
            Fa1_2.__init__(self, metadata, ledger, token_metadata)
            Pause.__init__(self)
            Admin.__init__(self, administrator)
    class Viewer_nat(sp.Contract):
        def __init__(self):
            self.data.last = sp.cast(None, sp.option[sp.nat])
        @sp.entrypoint
        def target(self, params):
            self.data.last = sp.Some(params)
    class Viewer_address(sp.Contract):
        def __init__(self):
            self.data.last = sp.cast(None, sp.option[sp.address])
        @sp.entrypoint
        def target(self, params):
            self.data.last = sp.Some(params)

if "templates" not in __name__:
    @sp.add_test(name="FA12")
    def test():
        sc = sp.test_scenario(m)
        sc.h1("FA1.2 template - Fungible assets")
        # sp.test_account generates ED25519 key-pairs deterministically:
        admin = sp.test_account("Administrator")
        alice = sp.test_account("Alice")
        bob = sp.test_account("Robert")
        # Let's display the accounts:
        sc.h1("Accounts")
        sc.show([admin, alice, bob])
        sc.h1("Contract")
        token_metadata = {
            "decimals": sp.utils.bytes_of_string("18"),  # Mandatory by the spec
            "name": sp.utils.bytes_of_string("My Great Token"),  # Recommended
            "symbol": sp.utils.bytes_of_string("MGT"),  # Recommended
            # Extra fields
            "icon": sp.utils.bytes_of_string(
                "https://smartpy.io/static/img/logo-only.svg"
            ),
        }
        contract_metadata = sp.utils.metadata_of_url(
            "ipfs://QmaiAUj1FFNGYTu8rLBjc3eeN9cSKwaF8EGMBNDmhzPNFd"
        )
        c1 = m.Fa1_2TestFull(
            administrator=admin.address,
            metadata=contract_metadata,
            token_metadata=token_metadata,
            ledger={},
        )
        sc += c1
        sc.h1("Offchain view - token_metadata")
        sc.verify_equal(
            sp.View(c1, "token_metadata")(0),
            sp.record(
                token_id=0,
                token_info=sp.map(
                    {
                        "decimals": sp.utils.bytes_of_string("18"),
                        "name": sp.utils.bytes_of_string("My Great Token"),
                        "symbol": sp.utils.bytes_of_string("MGT"),
                        "icon": sp.utils.bytes_of_string(
                            "https://smartpy.io/static/img/logo-only.svg"
                        ),
                    }
                ),
            ),
        )
        sc.h1("Attempt to update metadata")
        sc.verify(
            c1.data.metadata[""]
            == sp.utils.bytes_of_string(
                "ipfs://QmaiAUj1FFNGYTu8rLBjc3eeN9cSKwaF8EGMBNDmhzPNFd"
            )
        )
        c1.update_metadata(key="", value=sp.bytes("0x00")).run(sender=admin)
        sc.verify(c1.data.metadata[""] == sp.bytes("0x00"))
        sc.h1("Entrypoints")
        sc.h2("Admin mints a few coins")
        c1.mint(address=alice.address, value=12).run(sender=admin)
        c1.mint(address=alice.address, value=3).run(sender=admin)
        c1.mint(address=alice.address, value=3).run(sender=admin)
        sc.h2("Alice transfers to Bob")
        c1.transfer(from_=alice.address, to_=bob.address, value=4).run(sender=alice)
        sc.verify(c1.data.balances[alice.address].balance == 14)
        sc.h2("Bob tries to transfer from Alice but he doesn't have her approval")
        c1.transfer(from_=alice.address, to_=bob.address, value=4).run(
            sender=bob, valid=False
        )
        sc.h2("Alice approves Bob and Bob transfers")
        c1.approve(spender=bob.address, value=5).run(sender=alice)
        c1.transfer(from_=alice.address, to_=bob.address, value=4).run(sender=bob)
        sc.h2("Bob tries to over-transfer from Alice")
        c1.transfer(from_=alice.address, to_=bob.address, value=4).run(
            sender=bob, valid=False
        )
        sc.h2("Admin burns Bob token")
        c1.burn(address=bob.address, value=1).run(sender=admin)
        sc.verify(c1.data.balances[alice.address].balance == 10)
        sc.h2("Alice tries to burn Bob token")
        c1.burn(address=bob.address, value=1).run(sender=alice, valid=False)
        sc.h2("Admin pauses the contract and Alice cannot transfer anymore")
        c1.setPause(True).run(sender=admin)
        c1.transfer(from_=alice.address, to_=bob.address, value=4).run(
            sender=alice, valid=False
        )
        sc.verify(c1.data.balances[alice.address].balance == 10)
        sc.h2("Admin transfers while on pause")
        c1.transfer(from_=alice.address, to_=bob.address, value=1).run(sender=admin)
        sc.h2("Admin unpauses the contract and transfers are allowed")
        c1.setPause(False).run(sender=admin)
        sc.verify(c1.data.balances[alice.address].balance == 9)
        c1.transfer(from_=alice.address, to_=bob.address, value=1).run(sender=alice)
        sc.verify(c1.data.total_supply == 17)
        sc.verify(c1.data.balances[alice.address].balance == 8)
        sc.verify(c1.data.balances[bob.address].balance == 9)
        sc.h1("Views")
        sc.h2("Balance")
        view_balance = m.Viewer_nat()
        sc += view_balance
        target = sp.contract(sp.TNat, view_balance.address, "target").open_some()
        c1.getBalance((alice.address, target))
        sc.verify_equal(view_balance.data.last, sp.some(8))
        sc.h2("Administrator")
        view_administrator = m.Viewer_address()
        sc += view_administrator
        target = sp.contract(
            sp.TAddress, view_administrator.address, "target"
        ).open_some()
        c1.getAdministrator((sp.unit, target))
        sc.verify_equal(view_administrator.data.last, sp.some(admin.address))
        sc.h2("Total Supply")
        view_totalSupply = m.Viewer_nat()
        sc += view_totalSupply
        target = sp.contract(sp.TNat, view_totalSupply.address, "target").open_some()
        c1.getTotalSupply((sp.unit, target))
        sc.verify_equal(view_totalSupply.data.last, sp.some(17))
        sc.h2("Allowance")
        view_allowance = m.Viewer_nat()
        sc += view_allowance
        target = sp.contract(sp.TNat, view_allowance.address, "target").open_some()
        c1.getAllowance((sp.record(owner=alice.address, spender=bob.address), target))
        sc.verify_equal(view_allowance.data.last, sp.some(1))

运行合约。您将看到以下内容:

270

原始FA1.2合约具有转移代币、批准转移、检查余额、查看代币总供应量等基本功能。现在我们将增强这些功能。

管理员(Admin):我们将引入一个合约,允许管理员执行特定操作(如暂停合约)并阻止其他帐户使用这些功能。

暂停(Pause):此功能可以暂停和取消暂停合约。合约被暂停后,只有管理员可以使用它。

铸币(Mint):此合约功能允许管理员创建新的代币。

销毁(Burn):此合约功能允许管理员销毁代币。

更改元数据(ChangeMetadata):此功能允许管理员更新合约的元数据。

这些功能分别在不同的类中定义。

恭喜!您已经使用FA1.2标准在Tezos上创建了第一个同质化代币!

了解FA1.2合约

1. 管理员(Admin)合约

我们的代币合约中的Admin合约类负责定义管理权限。它包括一个单一的入口点:setAdministrator。此入口点允许当前管理员分配新的管理员。

Python
class Admin(sp.Contract):
    def __init__(self, administrator):
        self.init(administrator=administrator)
    @sp.entrypointdef setAdministrator(self, params):
        sp.verify(sp.sender == self.data.administrator)
        self.data.administrator = params

使用setAdministrator函数用于验证只有当前管理员可以执行此函数。如果验证失败,则拒绝该操作。如果验证通过,则继续分配新的管理员。

2. 暂停(Pause)合约

合约Pause类提供了一种暂停和取消暂停合约操作的机制,它包括一个入口点setPause,可以更改合约的暂停状态。

Python
class Pause(sp.Contract):
    def __init__(self):
        self.init(paused=False)
    @sp.entrypointdef setPause(self, params):
        sp.verify(sp.sender == self.data.administrator)
        self.data.paused = params

函数setPause首先检查操作是否由管理员执行。若是,则更新合约的暂停状态。

3. 铸币(Mint)合约

使用Mint合约类可以增加代币供应量。它带有一个名为mint的入口点,可以增加总供应量并更新特定地址的余额。

Python
class Mint(sp.Contract):
    @sp.entrypointdef mint(self, params):
        sp.verify(sp.sender == self.data.administrator)
        self.data.total_supply += params.value
        self.data.balances[params.address].balance += params.value

mint函数首先验证发送者是否为管理员。若是,则增加总供应量和指定地址的余额。

4. 销毁(Burn)合约

Burn合约类用于减少代币供应。它有一个burn入口点,用于减少总供应和特定地址的余额。

Python
class Burn(sp.Contract):
    @sp.entrypointdef burn(self, params):
        sp.verify(sp.sender == self.data.administrator)
        self.data.total_supply -= params.value
        self.data.balances[params.address].balance -= params.value

burn函数的操作类似于mint函数,但用途是减少总供应和指定地址的余额。

以下代码可以让管理员销毁Bob代币。

Python
  sc.h2("Admin burns Bob token")
        c1.burn(address=bob.address, value=1).run(sender=admin)

270
5. 修改元数据(ChangeMetadata)合约
ChangeMetadata合约类用于更新合约的元数据。它包括函数update_metadata,用于更新元数据中的键值对。
Python
class ChangeMetadata(sp.Contract):
    @sp.entrypointdef update_metadata(self, key, value):
        sp.verify(sp.sender == self.data.administrator)
        self.data.metadata[key] = value
与前面的函数类似,update_metadata函数验证发送者是否是管理员。若是,则会更新元数据中指定的键值对。
合约测试
引言
智能合约一旦部署在区块链上,将无法更改。因此,任何错误或安全漏洞都可能产生严重后果,测试便成为开发过程中不可或缺的一环。
在本节中,我们将介绍Fa1_2TestFull合约,其中包括一系列旨在验证代币合约功能的测试。
Fa1_2TestFull合约
Fa1_2TestFull合约整合了不同合约的所有功能,包括管理员、暂停、Fa1_2、铸币、销毁和更改元数据。该合约将所有这些功能结合起来并执行详尽的测试,以确保合约按预期工作。
Python
class Fa1_2TestFull(Admin, Pause, Fa1_2, Mint, Burn, ChangeMetadata):
        def __init__(self, administrator, metadata, ledger, token_metadata):
            ChangeMetadata.__init__(self)
            Burn.__init__(self)
            Mint.__init__(self)
            Fa1_2.__init__(self, metadata, ledger, token_metadata)
            Pause.__init__(self)
            Admin.__init__(self, administrator)
Fa1_2TestFull类构造函数将所有功能初始化。
设置测试场景
我们首先为测试帐户和合约初始化设置测试场景。我们将通过用@sp.add_test修饰的测试函数来完成。
Python
@sp.add_test(name="FA12")def test():
    # Initialize test scenario and accounts
    sc = sp.test_scenario(m)
    admin = sp.test_account("Administrator")
    alice = sp.test_account("Alice")
    bob = sp.test_account("Robert")
    # Initialize contract with some initial values
    token_metadata = {
        "decimals": sp.utils.bytes_of_string("18"),  # Mandatory by the spec"name": sp.utils.bytes_of_string("My Great Token"),  # Recommended"symbol": sp.utils.bytes_of_string("MGT"),  # Recommended# Extra fields"icon": sp.utils.bytes_of_string("https://smartpy.io/static/img/logo-only.svg"),
    }
    contract_metadata = sp.utils.metadata_of_url("ipfs://QmaiAUj1FFNGYTu8rLBjc3eeN9cSKwaF8EGMBNDmhzPNFd")
    c1 = m.Fa1_2TestFull(administrator=admin.address,metadata=contract_metadata,token_metadata=token_metadata,ledger={},)
    sc += c1
我们首先定义三个测试帐户:Admin、Alice和Bob,然后用一些初始值对合约Fa1_2TestFull进行初始化。+=运算符会将合约添加到场景中。
为了让大家更好地理解代码,我们将在左侧展示代码,在右侧展示高亮部分代码的可视化,详见下图:
270
运行测试
接下来便是运行测试,这将触发不同的合约函数并验证结果。
例如,要测试铸币功能,我们将运行以下代码:
Python
sc.h2("Admin mints a few coins")
c1.mint(address=alice.address, value=12).run(sender=admin)
270
此行代码运行了以下测试:管理员为Alice铸造12个代币。如果该函数成功铸造了代币并正确更新了Alice的余额,则此测试通过。
验证结果
SmartPy提供了verify函数来确保条件成立。如果条件不满足,则测试失败。例如:
Python
 c1.update_metadata(key="", value=sp.bytes("0x00")).run(sender=admin)
        sc.verify(c1.data.metadata[""] == sp.bytes("0x00"))
以上代码验证合约的元数据是否正确更新为"0x00"。若未正确更新,则测试失败。
270
高级测试
您的智能合约测试应涵盖所有可能的用例,包括边缘情况和潜在故障,如超过用户余额的转账、合约暂停期间销毁代币等。
例如,失败的转账测试可能如下:
Python
sc.h2("Bob tries to transfer from Alice but he doesn't have her approval")
c1.transfer(from_=alice.address, to_=bob.address, value=4).run(sender=bob, valid=False)
270
进入部署界面后,您可以选择部署合约的网络。在本教程中,请选择“Testnet(测试网)”。在将合约部署到主网上之前,建议大家一定先在测试网上进行测试。
创建好Tezos钱包后,您可以单击此处请求水龙头测试网代币通过浏览器与您的钱包连接。
这里,Bob试图在未经批准的情况下从Alice的账户中转移4个代币。由于此操作会失败,我们在run函数中设置valid=False。如果合约能成功阻止转移,则测试通过。
总结
测试在智能合约开发中至关重要。鉴于区块链的不可篡改性,合约中的任何错误都可能产生永久性、高成本的后果。编写全面的测试可确保所有功能按预期运行,使合约安全、强大。
需要记住的是,一定要同时为正向和负向情况编写测试。正向情况验证函数在按预期使用时是否正常工作。负向情况确保合约在处理不正确或意外输入时能够正确运行。
合约的部署和使用
部署合约
首先,回到合约所在的SmartPy在线IDE。页面顶部有一个“Compile”按钮。单击此按钮将合约编译成Tezos区块链可以理解的底层语言Michelson。
编译完成后,页面底部会出现“Deploy Michelson Contract”按钮。点击此按钮开始部署流程。
270
进入部署界面后,您可以选择部署合约的网络。在本教程中,请选择“Testnet(测试网)”。在将合约部署到主网上之前,建议大家一定先在测试网上进行测试。
270
您需要一个Tezos钱包来支付部署费用。如果您没有测试网钱包,可以访问此处来获取测试网XTZ并支付部署费。
支持多个浏览器扩展钱包。
270
创建好Tezos钱包后,您可以单击此处请求水龙头测试网代币通过浏览器与您的钱包连接。
270
选择好测试网后,输入您的测试网Tezos地址和私钥。确保您的私钥是安全可靠的!单击“Deploy”按钮开始部署合约。
此时,您会看到一个对话框,其中包含有关操作的信息。如果信息正确,请确认操作。随后,合约将部署到Tezos测试网。此过程可能需要几分钟时间。
成功部署合约后,您将收到一个合约地址。牢记此地址,在使用合约时您将需要该地址。
使用合约
合约部署完成后,您可以通过SmartPy IDE中的“Contract Interactions”功能来使用合约。
进入“Contract Interactions”页面,输入已部署合约的地址。
该界面将显示合约的入口点,您可以通过这些入口点来使用合约。
要调用合约的入口点,请单击其名称,填写必要的参数,然后单击“Execute(执行)”。
例如,要铸造新代币,请选择“Mint”入口点,输入接收者地址和要铸造的代币数量,然后单击“Execute”。
如果操作成功,代币将被铸造并添加到接收者的余额中。您可以通过查询接收者地址的余额进行验证。
需要注意的是,在区块链上使用合约需要支付gas费,因此,您需要确保钱包中有足够的余额。
自定义代币
在我们的合约范例中,代币名称是在我们在test()函数中初始化测试场景时定义的。它作为token_metadata映射的一部分被包含在内:
Python
token_metadata = {
    "decimals": sp.utils.bytes_of_string("18"),  # Mandatory by the spec
    "name": sp.utils.bytes_of_string("My Great Token"),  # Recommended
    "symbol": sp.utils.bytes_of_string("MGT"),  # Recommended
    # Extra fields
    "icon": sp.utils.bytes_of_string(
        "https://smartpy.io/static/img/logo-only.svg"
    ),
}
在这段代码中,“My Great Token”是代币的初始名称。要为代币设置不同的名称,您只需将其替换为您想要的名称即可。例如,如果要将代币命名为“GateLearn”,我们需要将代码修改如下:
Python
token_metadata = {
    "decimals": sp.utils.bytes_of_string("18"),  # Mandatory by the spec
    "name": sp.utils.bytes_of_string("GateLearn"),  # Recommended
    "symbol": sp.utils.bytes_of_string("GL"),  # Recommended
    # Extra fields
    "icon": sp.utils.bytes_of_string(
        "https://smartpy.io/static/img/logo-only.svg"
    ),
}
通过这种方式,我们便能创建名为“GateLearn”的代币。该名称在合约中表示为字节字符串,并将显示在与我们的合约交互并支持FA1.2标准的应用中。需要注意的是,代币的名称以及token_metadata映射中包含的其他详细信息是在合约部署时设置的。合约一旦部署,这些信息将无法更改,除非您在合约中实现了允许此类修改的功能。
结语
恭喜!您已经完成了《中级Tezos开发》课程的学习。我们使用SmartPy作为我们的主要开发工具,深入探讨了Tezos智能合约。
首先,我们简单介绍了同质化代币和FA1.2标准,然后在Tezos测试网上编写并部署了一个功能完整的FA1.2同质化代币合约。我们详细解释了合约的核心组成部分和关键功能,并讨论了如何使用合约,让用户能够铸造、转移和销毁代币。
请记住,这只是您Tezos开发之旅中的一个起点。在第一部分课程《Tezos开发简介:使用SmartPy构建一个简单的dApp》中,我们从区块链和Tezos的基础知识开始介绍,创建了一个简单的dApp。如果您尚未学习该课程,强烈建议您去学习一下,以加强您对基础知识的理解。
在未来,还有很多东西需要学习。Tezos提供了一个丰富且快速发展的生态系统,用于开发复杂且安全的去中心化应用。
在即将推出的第三门课程《高级Tezos开发》中,我们将深入探讨更高阶的主题,包括多重签名合约、游戏开发等高级Tezos功能。本课程将进一步为您提供在Tezos区块链上创建复杂且安全的去中心化应用所需的技能和知识。
我们需要谨记,在学习新的语言或工具时,实践是关键。隐私,请不要犹豫,大胆地将本课程中所学的内容付诸实践,尝试编写和部署自己的合约并使用它们。我们希望本课程能够为您的Tezos开发之旅提供信息丰富、引人入胜和有价值的内容。非常期待大家继续进行下一部分课程的学习,并在Tezos区块链上构建让人眼前一亮的项目!
免责声明:本网站、超链接、相关应用程序、论坛、博客等媒体账户以及其他平台和用户发布的所有内容均来源于第三方平台及平台用户。网站及其内容不作任何类型的保证,网站所有区块链相关数据以及其他内容资料仅供用户学习及研究之用,不构成任何投资、法律等其他领域的建议和依据。用户以及其他第三方平台在本网站发布的任何内容均由其个人负责,与本网无关。

相关文章