#token #smart-contracts #ink #psp34

no-std psp34-full

纯 ink! 实现的 PSP34 令牌标准的最小化版本!

1 个不稳定版本

0.2.1 2024 年 2 月 29 日

#2685 in 神奇豆子

Apache-2.0

52KB
762

PSP34 非同质化令牌

请注意,实现和 psp34 标准都在开发中,并可能发生变化。目前尚未准备好投入生产。

PSP34 是为在基于 Substrate 框架的区块链上运行的 WebAssembly 智能合约设计的非同质化令牌标准。它是 Ethereum 的 ERC-721 的等效标准。PSP34 标准的定义可以在 这里 找到。

此仓库包含 PSP34 令牌在 ink! 编程语言中的简单、最小化实现。

如何使用此仓库

要使用此软件包,请将以下行添加到您的 Cargo.toml

psp34 = { git = "https://github.com/Cardinal-Cryptography/PSP34.git", default-features = false }

此仓库的内容可以用以下方式使用

1. 即用型合约

文件 lib.rs 包含了基本 PSP34 令牌合约的即用型实现。要使用它,请检出此仓库,并使用带 "contract" 功能的 cargo-contract 编译其内容

$ cargo contract build --release --features "contract"

2. 使用 traits 进行跨合约调用

PSP34 trait 包含了 PSP34 标准中定义的所有方法。此 trait 可以与 ink!'s contract_ref 宏一起使用,以允许方便地进行跨合约调用。

在您的合约中,如果您想调用实现 PSP34 标准的其他合约,您只需要做以下操作

use ink::contract_ref;
use psp34::PSP34;

let mut token: contract_ref!(PSP34) = other_address.into();

// Now `token` has all the PSP34 methods
let balance = token.balance_of(some_account);
token.transfer(recipient, value, vec![]); // returns Result<(), PSP34Error>

可以使用相同的方法与其他特性(如 PSP34MetadataPSP34BurnablePSP34Mintable)一起使用,这些特性是在此包中定义的。请参阅 traits.rs 的内容。

3. 使用 PSP34Data 自定义 PSP34 逻辑实现

PSP34Data 类可以用来扩展您的合同以包含 PSP34 代币逻辑。换句话说,您可以轻松构建同时实现 PSP34 接口和项目业务逻辑定义的其他功能的合同。

PSP34Data 类的方法直接对应于由 PSP34 代币标准定义的查询和操作。为了让您的合同成为 PSP34 代币,您需要

  • 在合同存储中放置一个 PSP34Data 实例,并用一些代币的初始供应量对其进行初始化。
  • 在合同主体中添加 TransferApprovalAttributeSet 事件的定义。
  • 添加 impl PSP34 for [struct_name] 块,使用 PSP34Data 方法实现 PSP34 特性消息。每个修改代币数据库状态的方法都会返回一个包含该操作生成所有事件的 Result<Vec<PSP34Event>, PSP34Error>。请确保正确处理错误并发出结果事件(请参阅 emit_events 函数)。
  • 可选地实现 PSP34Metadata 特性,以便您的代币与其他生态系统工具兼容。

lib.rs 中的合同包含了一个按照上述所有步骤实现的示例。您可以自由地复制粘贴其中的一部分。

4. 可燃烧和可铸造扩展

PSP34Data 类还包含 burnmint 方法,这些方法可用于实现 PSP34BurnablePSP34Mintable 扩展,使您的代币可燃烧和/或可铸造。示例实现遵循与基本特性相同的模式

impl PSP34Burnable for Token {
    #[ink(message)]
    fn burn(&mut self, value: u128) -> Result<(), PSP34Error> {
        // Check if the caller is allowed to burn!
        let events = self.data.burn(self.env().caller(), value)?;
        self.emit_events(events);
        Ok(())
    }
}

请注意,PSP34Databurnmint 方法不强制执行任何形式的访问控制。任何人都可以随时铸造和燃烧代币可能不是一个好主意。在实现可燃烧和可铸造扩展时,请确保其使用根据您项目的业务逻辑受到限制。例如

#[ink(storage)]
pub struct Token {
    data: PSP34Data,
    owner: AccountId, // creator of the token
}

impl Token {
    #[ink(constructor)]
    pub fn new() -> Self {
        Self {
            data: PSP34Data::new(),
            owner: Self::env().caller(),
        }
    }
// ...
}

impl PSP34Burnable for Token {
    #[ink(message)]
    fn burn(&mut self, id: Id) -> Result<(), PSP34Error> {
        if self.env().caller() != self.owner {
            return PSP34Error::Custom(String::from("Only owner can burn"));
        }
        let events = self.data.burn(self.env().caller(), id)?;
        self.emit_events(events);
        Ok(())
    }
}

5. 可枚举扩展

这是一个可选扩展,允许在链上枚举代币。启用该扩展将引入大量的气体开销。

可以通过在编译存储库内容时启用 enumerable 功能来实现。要访问其消息,只需为您的代币实现 PSP34Enumerable 特性即可

#[ink(storage)]
pub struct Token {
    data: PSP34Data,
}

//...

impl PSP34Enumerable for Token {
    #[ink(message)]
    fn owners_token_by_index(&self, owner: AccountId, index: u128) -> Result<Id, PSP34Error> {
        self.data.owners_token_by_index(owner, index)
    }

    #[ink(message)]
    fn token_by_index(&self, index: u128) -> Result<Id, PSP34Error> {
        self.data.token_by_index(index)
    }
}

6. 元数据扩展

在包的 metadata::Data 中还有一个 set_attribute() 方法。它通常与 mint() 一起使用,后者来自 PSP34Mintable。请注意,set_attribute() 方法将发出 AttributeSet 事件。

7. 单元测试

本库包含了一组针对 PSP34 令牌的单元测试。它可以通过辅助宏 tests! 简单地添加到您的合约单元测试中。为了使宏正常工作,您需要实现 PSP34BurnablePSP34Mintable 特性。宏应该在主合约模块(即带有 #[ink::contract] 注解的模块)内调用。

#[ink::contract]
mod mycontract {
    ...
    #[ink(storage)]
    pub struct MyContract { ... }
    ...
    #[cfg(test)]
    mod tests { 
        crate::tests!(Token, Token::new);
    }
}

如上代码片段所示,tests! 宏接受两个参数。第一个参数应该是实现 PSP34 特性的结构体名称(通常是您的合约存储结构体)。第二个参数应该是合约的令牌构造函数。换句话说,第二个参数应该是返回 PSP34 结构体的函数名称。

实现特定细节

在某些情况下,PSP34 标准并未严格定义某些行为,本节概述了当前实现中未指定的行为。这里讨论的方法可以在 data.rs 中找到。

1. Id 类型

该类型没有实现自定义的等于方法。因此,例如,Id::U8(1) 不等于 Id::U16(1)

2. 批准

approve() 方法遵循单个令牌的“穿透”批准逻辑。如果已批准的用户调用此方法,他们将随后向第三方操作员发出对所有者令牌的批准。

此行为不适用于“全面”批准。对所有令牌的批准仅授予对调用者拥有令牌的批准。另外,为了提高安全性,approve() 不允许在操作员批准所有令牌时撤销单个令牌的批准。

approve(caller, operator, None::<Id>, true)

3. 元数据

推荐在 metadata.rs 中实现 set_attribute() 方法。这是一个好习惯,应该与 mint() 方法一起使用。

4. 余额

balance_of() 方法的返回类型是 u32,而 total_supply 值是 u128,请注意可能的溢出。

依赖项

~8–12MB
~212K SLoC