本文介绍: Solana代币合约源码入口部分代码学习

本文是学习Solana 程序库代币合约系列,需要有一定的Rust基础

我们今天学习spl/token/program/src/lib.rsentrypoint.rs文件,也就是Solana 统一代币合约的入口文件。

我们首先学习lib.rs文件,其代码只有93行,也比较简单,我们来快速学习一下。

一 内部属性

内部属性应用于定义它的元素整体,因为它定义在作用的元素内部,所以在内部属性。相应的,定义在元素之外的叫外部属性。关于属性,这里有一篇文章,看完就基本明白了。

【Rust每周一知】 Attribute 属性

我们的lib.rs的前三行代码正好是定义了三个内部属性:

#![allow(clippy::arithmetic_side_effects)]
#![deny(missing_docs)]
#![cfg_attr(not(test), forbid(unsafe_code))]

第一行是允许做什么(允许工具属性),第二行是拒绝什么,第三行是配置属性。具体含义大家可以参考上面那篇文章,我也并没有仔细研究。

二 定义的module

接下来定义了5个公共的module和一个内部的module。

pub mod error;
pub mod instruction;
pub mod native_mint;
pub mod processor;
pub mod state;

#[cfg(not(feature = "no-entrypoint"))]
mod entrypoint;

我们知道,公共模块是暴露给外部的,内部模块是隐藏的。但是这里有一个问题,其实整个程序的入口是entrypoint模块中的process_instruction函数,并且该函数也为内部的,那么该入口函数是怎样被调用的?需要后面进一步查看entrypoint!宏的用法。

注意:

entrypoint模块(入口模块)定义上方有一个配置属性,是非no-entrypoint特性。为什么这么配置呢?Solana官方文档上讲的很清楚,一个程序是可以作为库被引入到另一个程序中的,如果这两个程序都会有入口,就会起冲突。因此,我们定义了一个叫no-entrypoint的features,只有在非该特性的条件下才会定义entrypoint模块。当我们的程序作为第三方库引入到其它程序中时,其它程序只要在依赖定义里指定features = [ "no-entrypoint" ]就行了。这样两个程序加起来也会只有一个入口,就不会起冲突了。

三 导入其它库

接下来18-19行是导入了solana_program库及其特定的结构体,注意Rust中的一般原则,结构,枚举等定义直接导入全部路径,而函数一般只导入相应的包,使用时采用包名::函数名的语法,这样就为了方便的区分该函数是外部包定义的还是本包定义的。

pub use solana_program;
use solana_program::{entrypoint::ProgramResult, program_error::ProgramError, pubkey::Pubkey};

注意这里pub use是导入的同时并重新导出整个solana_program包,这样做的原因是方便其它包引入我们的程序时,如果想访问solana_program包中元素,直接使用spl-token::solana_program就行,而不用重新在依赖库里定义solana_program.

四 数值转换

接下来五个函数是用来进行数值转换的,这里是模仿ERC20的概念,代币是有精度的,假如美元,1美元等于100美分,把它当作一个代币的话,它的精度就是2(100 = 10.pow(2))。它的基本单位就是美元,最小单位是美分。然而区块链上一般是整数操作,因此操作的单位是美分(这样就不会有小数了),但我们平常使用习惯是美元。因此1.5美元等于多少美分,或者234美分等于多少美元?接下来这五个函数就是作这种转换的,其实就是乘上/除于相应的精度(10.pow(精度))。

这五个函数分别为(这里的ui_amount就是人们的习惯单位,例如美元,amount就是最小的不可分隔的单位,例如美分):

注: 这里的乘上/除于精度其实是指乘上/除于 10.pow(精度)。

  • ui_amount_to_amount 从美元到美分,乘上精度即可。
  • amount_to_ui_amount 从美分到美元,除以精度即可。因为我们可能会得到小数,所以最终结果是f64类型。
  • amount_to_ui_amount_string 从美分到美元的字符串形式,这里有些奇怪,为什么不采用amount_to_ui_amount结果的字符串形式呢?这里经过实际测试,amount_to_ui_amount_string 这种形式会在最后补上多余的0(小数位不够精度时),这样可以看出精度是多少。例如11.000000,后者可以看出精度是多少。
  • amount_to_ui_amount_string_trimmed 这里trimed应该是截断了后面的0,所以应该和amount_to_ui_amount结果的字符串形式相同,简单测试了几下也是如此,但为什么中间实现要采用amount_to_ui_amount_string呢,不得而知。
  • try_ui_amount_into_amount 函数,是将ui_amount的字符串形式转化为amount,因为字符串转数字有可能失败(例如字符串不合法),因此返回结果为一个Result,中间的实现过程有些复杂,我们就不管了。因为它可能失败返回Result,所以函数以try开头表明该含义。

注意,这里几个函数未考虑到溢出的情况,例如超过了u64的最大值(精度为18时很容易),但Token合约创建的代币精度为9,Supply也是u64,所以正常情况下是不会溢出的,毕竟数量不能超过supply.

declare_id!

该宏定义了本程序的账号地址,这个地址在编译合约时就可以得到,然后进行替换就行。这里并未研究程序地址的计算方法,只是知道就是这么做的。

自己写合约时,随便复制一个id写上,编译完成后再换上正确的ID,然后一定要重新编译(切记),否则部署的程序地址和ID对不上。

六 check_program_account 函数

这个函数用于检测程序ID(程序地址)是否为本合约的ID,用于作为包引入到其它程序时进行相关判断。注意,它返回的不是bool,而是一个ProgramResult。

这里的 id() 函数应该是上面的declare_id宏产生的,它估计返回一个静态的Pubkey,注意这里比较的中两个引用,在Rust中,比较引用其实是比较指向的值(当然引用类型得相同),比较引用地址是否相同有专门的函数。

显然,Pubkey 实现了 PartialEq特型,否则无法比较是否不相等。实质上,Pubkey的定义是这样子的:

#[wasm_bindgen]
#[repr(transparent)]
#[derive(
    AbiExample,
    BorshDeserialize,
    BorshSchema,
    BorshSerialize,
    Clone,
    Copy,
    Default,
    Deserialize,
    Eq,
    Hash,
    Ord,
    PartialEq,
    PartialOrd,
    Pod,
    Serialize,
    Zeroable,
)]
pub struct Pubkey(pub(crate) [u8; 32]);

我们可以看到,它的内部(底层)其实是一个32元素的u8数组(所以其大小为256位),除了 PartialEq特型,它还实现了很多常用特型,例如CloneCopy等。因为它的底层是u8数组,所以直接使用derive派生宏来实现部分特型就行。

七 entrypoint模块

entrypoint模块 代码很少,除了必要的引入外,就只有一个宏调用和程序入口函数定义。我们来看这个函数process_instruction

顾名思义,用来处理指令 ,它的参数列表是固定的。

fn process_instruction(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8],
) -> ProgramResult {
    if let Err(error) = Processor::process(program_id, accounts, instruction_data) {
        // catch the error so we can print it
        error.print::<TokenError>();
        return Err(error);
    }
    Ok(())
}

第一个参数,program_id,很好理解,就是调用的程序的id(地址),其实这里并没有什么实际作用,如果不知道program_id,是无法调用程序的,所以这里的program_id必定是本程序的id。系统自动传过来意义不大,也许有其它用处。

第二个参数,accounts 这个参数是指令调用时所有涉及到的账号信息,这个账号是在客户端输入的,因此用户可以输入任意账号信息,所以必须对其合法性和有效性作验证,特别是只读账号,因为如果是写账号,会有写操作权限判定,会好一些。

第三个参数 指令数据,其实就是一串16进制数据,它也是用户输入的,需要对其进行有效性判断和解码,从而进行下一步操作。

函数内部直接调用了Processor结构体的处理函数进行处理,这里以后再学。

entrypoint!

该宏的定义是这样的

#[macro_export]
macro_rules! entrypoint {
    ($process_instruction:ident) => {
        /// # Safety
        #[no_mangle]
        pub unsafe extern "C" fn entrypoint(input: *mut u8) -> u64 {
            let (program_id, accounts, instruction_data) =
                unsafe { $crate::entrypoint::deserialize(input) };
            match $process_instruction(&program_id, &accounts, &instruction_data) {
                Ok(()) => $crate::entrypoint::SUCCESS,
                Err(error) => error.into(),
            }
        }
        $crate::custom_heap_default!();
        $crate::custom_panic_default!();
    };
}

这里我们可以看到,它其实定义了一个 pub 函数 entrypoint,用来解析用户输入并将它作为参数传递给我们的process_instruction函数。但是为什么可以调用entrypoint包(非外部包)的这个entrypoint函数,还需要仔细看相关文档,

这里 extern 关键字是用创建允许其它语言调用Rust的接口

还有一点是#[macro_export]宏导出,

默认情况下,宏没有基于路径的作用域。但是如果该宏带有 #[macro_export] 属性,则相当于它在当前 crate 的根作用域的顶部被声明。标有 #[macro_export] 的宏始终是 pub 的.

属性no_mangle,用来关闭 Rust 的名称修改(name mangling)功能。Mangling 是编译器在解析名称时,修改我们定义的函数名称,增加一些用于其编译过程的额外信息。

所以为了使 Rust 函数能在其它语言中被调用,必须禁用 Rust 编译器的名称修改功能。通过在1.1的示例代码中增加属性 #[no_mangle] ,告诉 Rust 编译器不要修改此函数的名称。

这个宏定义上也写了,这是全局的,因此只能only once,所以引入其它定义了entrypoint!宏的包时,需要启用 no-entrypoint特性,当然这个特性名称其实是可以自取的。

我们知道这里是大致怎么一回事就行了,有时间时再详细研究。

因为只是个人的学习记录,因此肯定有理解错误的地方,欢迎大家指正,共同学习提高!

原文地址:https://blog.csdn.net/weixin_39430411/article/details/136002879

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。

如若转载,请注明出处:http://www.7code.cn/show_66957.html

如若内容造成侵权/违法违规/事实不符,请联系代码007邮箱:suwngjj01@126.com进行投诉反馈,一经查实,立即删除!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注