├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── README.md ├── hw_01_helloworld ├── Cargo.lock ├── Cargo.toml ├── src │ └── lib.rs └── tests │ └── lib.rs ├── hw_02_basic_example ├── .gitignore ├── README.md ├── config-overrides.js ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt ├── solana_program │ ├── Cargo.toml │ ├── Xargo.toml │ ├── build │ │ └── solana_program-keypair.json │ └── src │ │ ├── entrypoint.rs │ │ ├── error.rs │ │ ├── instruction.rs │ │ ├── lib.rs │ │ ├── processor.rs │ │ ├── state.rs │ │ └── tools │ │ ├── account.rs │ │ └── mod.rs ├── src │ ├── App.tsx │ ├── components │ │ ├── Button │ │ │ └── index.tsx │ │ ├── Logs │ │ │ ├── Log.tsx │ │ │ └── index.tsx │ │ ├── NoProvider │ │ │ └── index.tsx │ │ ├── Sidebar │ │ │ └── index.tsx │ │ └── index.tsx │ ├── constants.ts │ ├── index.tsx │ ├── react-app-env.d.ts │ ├── reportWebVitals.ts │ ├── types.ts │ └── utils │ │ ├── cCreateDataAccount.ts │ │ ├── cDeleteDataAccount.ts │ │ ├── cMultiParamTest.ts │ │ ├── cQueryDataAccount.ts │ │ ├── cUpdateDataAccount.ts │ │ ├── hexToRGB.ts │ │ ├── index.ts │ │ ├── phantomCreateDataAccount.ts │ │ ├── phantomCreateTransferTransaction.ts │ │ ├── phantomGetProvider.ts │ │ ├── phantomSignMessage.ts │ │ ├── phantomSignTransaction.ts │ │ └── pollSignatureStatus.ts └── tsconfig.json ├── hw_03_simple_token ├── Cargo.toml ├── README.md ├── Xargo.toml └── src │ ├── entrypoint.rs │ ├── error.rs │ ├── instruction.rs │ ├── lib.rs │ ├── native_mint.rs │ ├── processor.rs │ └── state.rs ├── hw_04_token_swap ├── Cargo.toml ├── Xargo.toml ├── cbindgen.toml ├── program-id.md └── src │ ├── constraints.rs │ ├── curve │ ├── base.rs │ ├── calculator.rs │ ├── constant_price.rs │ ├── constant_product.rs │ ├── fees.rs │ ├── mod.rs │ └── offset.rs │ ├── entrypoint.rs │ ├── error.rs │ ├── instruction.rs │ ├── lib.rs │ ├── processor.rs │ └── state.rs └── hw_05_anchor_simple ├── Anchor.toml ├── Cargo.lock ├── Cargo.toml ├── README.md ├── client ├── client.ts └── hw_06_anchor_simple.json ├── migrations └── deploy.ts ├── package.json ├── programs └── hw_05_anchor_simple │ ├── Cargo.toml │ ├── Xargo.toml │ └── src │ ├── error.rs │ ├── lib.rs │ └── state.rs ├── tests └── hw_06_anchor_simple-dapp.ts ├── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | .vscode 3 | bin 4 | config.json 5 | node_modules 6 | ./package-lock.json 7 | hfuzz_target 8 | hfuzz_workspace 9 | **/*.so 10 | **/.DS_Store 11 | test-ledger 12 | docker-target 13 | .idea 14 | .coderrect 15 | 16 | hw_05_anchor_simple/.anchor/ 17 | hw_05_anchor_simple/node_modules/ 18 | hw_05_anchor_simple/target/ 19 | hw_05_anchor_simple/test-ledger/ 20 | 21 | 22 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "hw_01_helloworld", 4 | "hw_02_basic_example/solana_program", 5 | "hw_03_simple_token", 6 | "hw_04_token_swap" 7 | ] 8 | resolver = "2" 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #### 一、Solana智能合约简单说明 2 | ##### 1,Solana智能合约不存储任何状态信息(因为每一个智能合约都会在用户地址上产生一个合约账户),所有的数据都是存储在用户地址上的合约账户里面。这是Etherscan智能合约和Solana智能合约的最大区别 3 | ##### 2,Solana的智能合约存储是需要付费的(也就是如果你操作了智能合约并存储了数据就需要Gas费) 4 | ##### 3,要存储数据的长度可以在数据账户创建时指定 5 | ##### 4,合约地址不能存储和与转移代币,但是发起地址和合约地址一起可以生成一个没有私钥的地址,这个地址的签名只在合约里面完成,可以使用这个地址来持有其他所有代币,然后在合约里面用这个地址来转币 6 | ##### 5,智能合约可使用的堆栈是可以配置的(就是SDK的BpfComputeBudget对象) 7 | ##### 6,Solana上有些Rust SDK是不能用的比如 HashMap 8 | ##### 7,合约币转账如果接收方没有合约账户,发送方需要先帮接收方创建好合约账户再进行转账 9 | ##### 8,一个地址只能拥有一种代币,因为一个地址只能存储一条数据。但是多条数据的所有者可以指定为同一个地址,这样也就实现了一个地址拥有多个币种 10 | ##### 9,一个智能合约调用另一个智能合约这个操作叫CPI 11 | ##### 10,PDA地址是使用预定义种子(比如一串字符串),凹凸种子(从255到0),ProgramId(所属合约地址)生成的一个没有私钥的地址,在合约里面使用PDA地址调用另一个合约需要传PDA地址的种子签名([代码示例](./hw_02_basic_example/solana_program/src/tools/account.rs)) 12 | ##### 11,如果想使用PDA地址创建数据账户,只能在合约里面创建([示例代码](./hw_02_basic_example/solana_program/src/tools/account.rs)),不能在前端直接创建因为它没有私钥不能签名。 13 | 14 | #### 二、安装Solana客户端,[官方文档](https://docs.solana.com/getstarted/local) 15 | ```bash 16 | $ export http_proxy=http://127.0.0.1:58591/ 17 | $ export https_proxy=http://127.0.0.1:58591/ 18 | # 注意:执行这个脚本可能需要代理(默认安装路径:~/.local/share/solana/install/) 19 | $ sh -c "$(curl -sSfL https://release.solana.com/stable/install)" 20 | 21 | # 验证Solana客户端是否安装成功(注意:建议使用1.18.18及以上版本,如果非这个版本,建议使用命令 solana-install init 1.18.18 安装) 22 | $ solana --version 23 | 24 | # 更新Solana客户端(注意:这个命令可能需要代理) 25 | $ solana-install update 26 | 27 | # 安装或使用指定版本Solana客户端(注意:这个命令可能需要代理,建议使用1.18.18及以上的版本) 28 | $ solana-install init 1.18.18 29 | 30 | # 查看已安装的Solana客户端 31 | $ solana-install list 32 | ``` 33 | 34 | #### 三、使用Solana客户端创建代币 35 | ```bash 36 | # 创建代币(注意:spl-token命令是安装好Solana客户端后就会有) 37 | # --decimals 指定精度为12, 38 | # --program-id 指定Solana代币标准程序ID创建代币(注意:这里使用的是Token-2022 Program ID也是当前最新的) 39 | # --enable-metadata 开启元数据(后面可以指定代币的一些信息数据,比如名称等等) 40 | $ spl-token create-token --decimals 12 --program-id TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb --enable-metadata 41 | # 代币地址 # 代币程序地址(就是代币Program Id) 42 | Creating token 8kDYBqzYrKayd2fQ63BPd8ed1sk8hsUWQ2y6JcBUhHdT under program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb 43 | # 代币地址(注意:代币地址存储所有代币数据,该数据所有权就是代币创建者) 44 | Address: 8kDYBqzYrKayd2fQ63BPd8ed1sk8hsUWQ2y6JcBUhHdT 45 | Decimals: 12 46 | # 交易签名也是交易Hash 47 | Signature: 3yc6xVVWZBoVZuqucbmSWKj87QwXtk1YhwkKRpqv2xhu53Vyodwy25t7EjFJxZJmcVRbEBHDBH3kejCsSSBryBSm 48 | 49 | # 为上面刚刚创建的代币初始化元数据(就是设置代币名称,简称,代币网址URL) 50 | # 注意:8kDYBqzYrKayd2fQ63BPd8ed1sk8hsUWQ2y6JcBUhHdT 是代币地址 51 | $ spl-token initialize-metadata 8kDYBqzYrKayd2fQ63BPd8ed1sk8hsUWQ2y6JcBUhHdT "TokenName" "TokenSymbol" "https://spl.solana.com/token-2022" 52 | 53 | # 查询代币分配总量 54 | $ spl-token supply 8kDYBqzYrKayd2fQ63BPd8ed1sk8hsUWQ2y6JcBUhHdT 55 | 56 | # 创建代币持有账户(因为Solana地址本身不能持有ERC20代币,只能创建代币持有账户持有代币,然后我拥有代币持有账户的所有权) 57 | # --fee-payer 指定交易签名地址密钥对文件 58 | # --owner 指定代币持有账户的所有者地址 59 | # 第一个参数是代币地址 60 | $ spl-token create-account 8kDYBqzYrKayd2fQ63BPd8ed1sk8hsUWQ2y6JcBUhHdT --fee-payer ~/.config/solana/id.json --owner CokWw92izG3TrnkZJK3RujGwnUKq1i29pzL4shpUpVaE 61 | # 代币持有账户地址 62 | Creating account 4GLLKhsTCkm7roqFiTEm5fg4LDrjBZmgyT94cJ6kAaTr 63 | # 交易签名也是交易Hash 64 | Signature: SUqvh7h3qgZV3sywXEmYzegiemffr51fQeoAuWbETTzEqfs8vuxr8KAVF6Nx7httt13wripV2mHtMhqhoEFuwHv 65 | 66 | # 为代币持有账户Mint(分配)代币。第一个参数是代币地址,第二个参数是分配数量,第三个参数是代币持有账户地址 67 | $ spl-token mint 8kDYBqzYrKayd2fQ63BPd8ed1sk8hsUWQ2y6JcBUhHdT 10000 -- 4GLLKhsTCkm7roqFiTEm5fg4LDrjBZmgyT94cJ6kAaTr 68 | 69 | # 查看某个代币余额(参数是代币地址) 70 | $ spl-token balance 8kDYBqzYrKayd2fQ63BPd8ed1sk8hsUWQ2y6JcBUhHdT 71 | 72 | # 转帐代币(除前两个选项以外,第一个参数是代币地址,第二个参数是代币数量,第三个参数是接收地址) 73 | # --fund-recipient 表示如果接收地址没有该代币持有账户会帮其创建,如果有则不创建 74 | # --allow-unfunded-recipient 表示如果接收地址没有Solana余额也执行转帐 75 | $ spl-token transfer --fund-recipient --allow-unfunded-recipient 8kDYBqzYrKayd2fQ63BPd8ed1sk8hsUWQ2y6JcBUhHdT 11 3f2PCtqDp1vNm5yJk53t5peTL15ZjXcTm8bsUXpUWE2q 76 | ``` 77 | 78 | #### 四、打包智能合约代码 79 | ```bash 80 | # 打包下载依赖可能需要代理 81 | $ export http_proxy=http://127.0.0.1:58591/ 82 | $ export https_proxy=http://127.0.0.1:58591/ 83 | 84 | # 测试智能合约代码 85 | # cargo test-sbf --manifest-path=./Cargo.toml (效果等同下面) 86 | $ cargo-test-sbf --manifest-path=./Cargo.toml 87 | 88 | # 查看编译打包工具版本以及相关依赖版本 89 | # 注意:下面显示的rustc版本是Solana客户端自带的Rust工具,可使用rustup show命令查看已安装的rust工具,里面的solana就是下面这个 90 | # rust工具默认安装在 ~/.rustup/toolchains 目录 91 | $ cargo-build-sbf --version 92 | solana-cargo-build-sbf 1.18.18 93 | platform-tools v1.41 94 | rustc 1.75.0 95 | 96 | # 打包Solana智能合约程序 97 | # --bpf-out-dir 指定打包后文件输出目录 98 | # cargo build-sbf --manifest-path=./Cargo.toml --sbf-out-dir=build(效果等同下面) 99 | $ cargo-build-sbf --manifest-path=./Cargo.toml --sbf-out-dir=build 100 | 101 | # 清空Solana打包程序 102 | $ cargo clean --manifest-path=./Cargo.toml && rm -rf ./build 103 | ``` 104 | 105 | #### 五、部署智能合约到本地验证节点 106 | ```bash 107 | # 分模块指定Solana本地验证节点日志级别,如果想全局指定可使用命令 export RUST_LOG=ERROR(注意:不指定日志级别它默认打印info日志,打得太多太快) 108 | # 说明:solana_runtime::system_instruction_processor=info 可打印我们代码里面的日志,方便调试代码 109 | $ export RUST_LOG=solana_bpf_loader=error,solana_rbpf=error,solana_runtime::system_instruction_processor=info,solana_runtime::message_processor=error 110 | 111 | # 启动本地Solana验证节点 112 | $ solana-test-validator 113 | 114 | # 创建密钱包(就是部署智能合约的账户),钱包密钥对默认创建在 ~/.config/solana/id.json 115 | # 指定密钥对存储路径示例: solana-keygen new -o ~/data-data/dev-tools/Solana/test-key/id.json 116 | # 可指定参数 --no-outfile 表示不存储密钥对数据 117 | $ solana-keygen new 118 | 119 | # 将Solana服务端设置成本地(或者使用:solana config set --url http://127.0.0.1:8899) 120 | $ solana config set --url localhost 121 | # 将Solana服务端设置成开发网络 122 | $ solana config set --url devnet 123 | # 将Solana服务端设置成测试网络 124 | $ solana config set --url testnet 125 | # 将Solana服务端设置成主网 126 | $ solana config set --url mainnet-beta 127 | # 获取Solana服务端配置 128 | $ solana config get 129 | 130 | # 指定Solana部署所使用的钱包账户(如果不指定会默认使用 ~/.config/solana/id.json) 131 | $ solana config set -k ~/.config/solana/id.json 132 | 133 | # 领取空投(注意:不领取的话账户里面没有钱) 134 | $ solana airdrop 100 135 | 136 | # 查看钱包余额 137 | $ solana balance 138 | 139 | # 部署智能合约(注意:/home/helloworld.so 是已经打包好的合约程序) 140 | # --with-compute-unit-price 指定自定义Gas价格加快部署交易,可防止Blockhash expired(Blockhash过期部署错误)) 141 | # solana program deploy --with-compute-unit-price 1000 /home/helloworld.so 142 | $ solana program deploy /home/helloworld.so 143 | # 部署成功后的合约地址 144 | Program Id: EjS5rkqgXAUWqhvUip9nWN9mmdRzKLxmsfoXnUggn7pM 145 | ``` 146 | 147 | #### 六、Anchor框架开发搭建(现在开发Solana智能合约建议使用该框架) 148 | ```bash 149 | # 当前命令行窗口使用代理 150 | $ export http_proxy=http://127.0.0.1:58591/ 151 | $ export https_proxy=http://127.0.0.1:58591/ 152 | 153 | # 安装 AVM 154 | $ cargo install --git https://github.com/coral-xyz/anchor avm --locked --force 155 | 156 | # 安装最新Anchor套件 157 | $ avm install latest 158 | # 使用最新Anchor套件 159 | $ avm use latest 160 | 161 | # 验证Anchor套件是否安装成功 162 | $ anchor --version 163 | 164 | # 如果前端要使用 Anchor的话需要安装一下插件 165 | $ npm install -g mocha 166 | $ npm install -g @project-serum/anchor 167 | ``` 168 | 169 | #### 七、Anchor框架简单使用 170 | ```bash 171 | # 使用Anchor创建项目 172 | $ anchor init "项目名称" 173 | 174 | # 使用Anchor编译项目,编译完成以后在target/idl目录下会生成IDL json文件(类似于ABI)(注意:该命令需要在项目目录下执行) 175 | $ anchor build 176 | 177 | # 使用客户端JS代码测试链上程序(注意:这个测试代码是写在tests目录下的(具体可参考hw_06_anchor_simple项目)) 178 | # --skip-local-validator 表示不自动启动本地Solana验证节点 179 | $ anchor test --skip-local-validator 180 | ``` 181 | 182 | 183 | 184 | 185 | -------------------------------------------------------------------------------- /hw_01_helloworld/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "hw_01_helloworled" 7 | version = "0.1.0" 8 | -------------------------------------------------------------------------------- /hw_01_helloworld/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hw_01_helloworld" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [features] 7 | no-entrypoint = [] 8 | 9 | [dependencies] 10 | borsh = "0.9.3" 11 | borsh-derive = "0.9.1" 12 | solana-program = "1.10.33" 13 | 14 | [dev-dependencies] 15 | solana-program-test = "1.10.33" 16 | solana-sdk = "1.10.33" 17 | 18 | [lib] 19 | name = "hw_01_helloworld" 20 | crate-type = ["cdylib", "lib"] 21 | -------------------------------------------------------------------------------- /hw_01_helloworld/src/lib.rs: -------------------------------------------------------------------------------- 1 | use borsh::{BorshDeserialize, BorshSerialize}; 2 | use solana_program::{ 3 | account_info::{next_account_info, AccountInfo}, 4 | entrypoint, 5 | entrypoint::ProgramResult, 6 | msg, 7 | program_error::ProgramError, 8 | pubkey::Pubkey, 9 | }; 10 | /** 11 | 这是一个简单的计数器合约(记录账户访问合约的次数) 12 | */ 13 | 14 | /// 存储在账户中的计数器结构体 15 | #[derive(BorshSerialize, BorshDeserialize, Debug)] 16 | pub struct GreetingAccount { 17 | /// 账户访问合约次数 18 | pub counter: u32, 19 | } 20 | 21 | // 配置合约的入口函数(相当于main函数) 22 | entrypoint!(process_instruction); 23 | 24 | // 合约入口函数实现 25 | /** 26 | * @param program_id 合约程序ID(合约地址) 27 | * @param accounts 发起者发起交易时所发送的所有账户信息(这个是在前端控制的) 28 | * @param _instruction_data 调用智能合约的参数 29 | */ 30 | pub fn process_instruction(program_id: &Pubkey,accounts: &[AccountInfo],_instruction_data: &[u8],) -> ProgramResult { 31 | 32 | // 获取合约账户的迭代器 33 | let accounts_iter = &mut accounts.iter(); 34 | 35 | // 获取合约账户 36 | let account = next_account_info(accounts_iter)?; 37 | msg!("开始进入合约入口函数,program_id={},owner={}",program_id,account.owner); 38 | // 判断这个合约账户是不是用来访问当前合约的(就是这个账户是不是用来访问这个合约的,因为每一个智能合约都会在用户地址上产生一个合约账户) 39 | if account.owner != program_id { 40 | msg!("该账户不是用来访问当前合约的,拒绝访问!"); 41 | return Err(ProgramError::IncorrectProgramId); 42 | } 43 | 44 | // 合约程序存储在账户中的信息(就是GreetingAccount 存储在账户中的计数器结构体) 45 | let mut greeting_account = GreetingAccount::try_from_slice(&account.data.borrow())?; 46 | // 计数器加1 47 | greeting_account.counter += 1; 48 | // 再将修改后的数据存储到账户当中 49 | greeting_account.serialize(&mut &mut account.data.borrow_mut()[..])?; 50 | 51 | msg!("当前账户第 {} 次访问合约!", greeting_account.counter); 52 | 53 | Ok(()) 54 | } 55 | 56 | // Sanity tests 57 | #[cfg(test)] 58 | mod test { 59 | use super::*; 60 | use solana_program::clock::Epoch; 61 | use std::mem; 62 | 63 | #[test] 64 | fn test_sanity() { 65 | // 注意:Pubkey::default()就是等于0 66 | let program_id = Pubkey::default(); 67 | let key = Pubkey::default(); 68 | let mut lamports = 0; 69 | let mut data = vec![0; mem::size_of::()]; 70 | // 合约地址 71 | let owner = Pubkey::default(); 72 | // 模拟账户 73 | let account = AccountInfo::new( 74 | &key, 75 | false, 76 | true, 77 | &mut lamports, 78 | &mut data, 79 | &owner, 80 | false, 81 | Epoch::default(), 82 | ); 83 | let instruction_data: Vec = Vec::new(); 84 | // 将账户信息加入集合 85 | let accounts = vec![account]; 86 | // 将合约存储在账户中的信息转换成GreetingAccount对象,并拿到计数 87 | let counter = GreetingAccount::try_from_slice(&accounts[0].data.borrow()).unwrap().counter; 88 | // 判断计数是不是等于0 89 | assert_eq!(counter,0); 90 | // 调用合约入口函数 91 | process_instruction(&program_id, &accounts, &instruction_data).unwrap(); 92 | // 因为上面调用了一次合约,所以这里计数器变成了 1 93 | assert_eq!(GreetingAccount::try_from_slice(&accounts[0].data.borrow()).unwrap().counter,1); 94 | // 再次调用合约入口函数 95 | process_instruction(&program_id, &accounts, &instruction_data).unwrap(); 96 | // 因为上面调用了两次合约,所以这里计数器变成了 2 97 | assert_eq!(GreetingAccount::try_from_slice(&accounts[0].data.borrow()).unwrap().counter,2); 98 | } 99 | } -------------------------------------------------------------------------------- /hw_01_helloworld/tests/lib.rs: -------------------------------------------------------------------------------- 1 | use borsh::BorshDeserialize; 2 | use helloworld::{process_instruction, GreetingAccount}; 3 | use solana_program_test::*; 4 | use solana_sdk::{ 5 | account::Account, 6 | instruction::{AccountMeta, Instruction}, 7 | pubkey::Pubkey, 8 | signature::Signer, 9 | transaction::Transaction, 10 | }; 11 | use std::mem; 12 | 13 | #[tokio::test] 14 | async fn test_helloworld() { 15 | let program_id = Pubkey::new_unique(); 16 | let greeted_pubkey = Pubkey::new_unique(); 17 | println!("program_id={}",program_id); 18 | println!("greeted_pubkey={}",greeted_pubkey); 19 | 20 | /** 21 | * 模拟部署合约 22 | * @param program_name 合约名称 23 | * @param program_id 合约程序ID 24 | * @param process_instruction 合约入口函数 25 | */ 26 | let mut program_test = ProgramTest::new("helloworld",program_id,processor!(process_instruction),); 27 | 28 | /** 29 | * 给用户地址添加合约账户 30 | * @param address 账户地址 31 | * @param account 账户信息 32 | */ 33 | program_test.add_account(greeted_pubkey,Account { 34 | lamports: 5, 35 | data: vec![0_u8; mem::size_of::()], 36 | owner: program_id, 37 | ..Account::default() 38 | }, 39 | ); 40 | // 启动合约 41 | let (mut banks_client, payer, recent_blockhash) = program_test.start().await; 42 | 43 | // 获取用户地址的合约账户 44 | let greeted_account = banks_client.get_account(greeted_pubkey).await.expect("get_account").expect("greeted_account not found"); 45 | // 验证该账户是否没有访问过计数器合约 46 | assert_eq!(GreetingAccount::try_from_slice(&greeted_account.data).unwrap().counter,0); 47 | 48 | /*--------------------------------------第一次模拟交易-------------------------------------------------*/ 49 | // 模拟创建交易 50 | let mut transaction = Transaction::new_with_payer(&[Instruction::new_with_bincode( 51 | program_id, //要访问的智能合约ID(合约地址) 52 | &[0], // ignored but makes the instruction unique in the slot 53 | vec![AccountMeta::new(greeted_pubkey, false)],// 访问者地址(is_signer 表示访问者是否持有私钥,is_writable 表示程序是否可以修改账户信息) 54 | )], 55 | Some(&payer.pubkey()), 56 | ); 57 | // 交易签名 58 | transaction.sign(&[&payer], recent_blockhash); 59 | // 使用合约处理交易(就是访问智能合约) 60 | let res = banks_client.process_transaction(transaction).await.unwrap(); 61 | println!("TransactionRes={:#?}",res); 62 | 63 | // 获取用户地址的合约账户 64 | let greeted_account = banks_client.get_account(greeted_pubkey).await.expect("get_account").expect("greeted_account not found"); 65 | // 验证该账户是否已经访问过1次计数器合约(因为上面模拟交易访问过一次) 66 | assert_eq!(GreetingAccount::try_from_slice(&greeted_account.data).unwrap().counter,1); 67 | 68 | /*--------------------------------------第二次模拟交易-------------------------------------------------*/ 69 | // 再次模拟创建交易 70 | let mut transaction = Transaction::new_with_payer(&[Instruction::new_with_bincode( 71 | program_id, //要访问的智能合约ID(合约地址) 72 | &[1], // ignored but makes the instruction unique in the slot 73 | vec![AccountMeta::new(greeted_pubkey, false)], // 访问者地址(就是先通过这个地址从合约中拿到账户,再用账户去调用合约)。注意:账户信息在上面已经添加到合约当中 74 | )], 75 | Some(&payer.pubkey()), 76 | ); 77 | // 交易签名 78 | transaction.sign(&[&payer], recent_blockhash); 79 | // 使用合约处理交易(就是访问智能合约) 80 | banks_client.process_transaction(transaction).await.unwrap(); 81 | 82 | // 获取用户地址的合约账户 83 | let greeted_account = banks_client.get_account(greeted_pubkey).await.expect("get_account").expect("greeted_account not found"); 84 | // 验证该账户是否已经访问过2次计数器合约(因为上面模拟交易总共访问过2次) 85 | assert_eq!(GreetingAccount::try_from_slice(&greeted_account.data).unwrap().counter,2); 86 | } -------------------------------------------------------------------------------- /hw_02_basic_example/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /hw_02_basic_example/README.md: -------------------------------------------------------------------------------- 1 | #### Solana智能合约开发基础示例 2 | ##### 编译错误说明:项目使用TS而@solana/web3.js兼容TS类型的文件在编译期间没有自动加载,报@solana/buffer-layout/src/Layout.ts no such file or directory错误,所以使用react-app-rewired插件重写webpack配置来规避错误 3 | 4 | -------------------------------------------------------------------------------- /hw_02_basic_example/config-overrides.js: -------------------------------------------------------------------------------- 1 | const { ProvidePlugin } = require('webpack'); 2 | 3 | module.exports = function (config, env) { 4 | return { 5 | ...config, 6 | module: { 7 | ...config.module, 8 | rules: [ 9 | ...config.module.rules, 10 | { 11 | test: /\.m?[jt]sx?$/, 12 | enforce: 'pre', 13 | use: ['source-map-loader'], 14 | }, 15 | { 16 | test: /\.m?[jt]sx?$/, 17 | resolve: { 18 | fullySpecified: false, 19 | }, 20 | }, 21 | ], 22 | }, 23 | plugins: [ 24 | ...config.plugins, 25 | new ProvidePlugin({ 26 | process: 'process/browser', 27 | }), 28 | ], 29 | resolve: { 30 | ...config.resolve, 31 | fallback: { 32 | assert: require.resolve('assert'), 33 | buffer: require.resolve('buffer'), 34 | //stream: require.resolve('stream-browserify'), 35 | //crypto: require.resolve('crypto-browserify'), 36 | }, 37 | }, 38 | ignoreWarnings: [/Failed to parse source map/], 39 | }; 40 | }; -------------------------------------------------------------------------------- /hw_02_basic_example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hw_02_basic_example", 3 | "version": "0.1.0", 4 | "private": true, 5 | "main": "src/index.tsx", 6 | "engines": { 7 | "node": ">=16" 8 | }, 9 | "publishConfig": { 10 | "access": "public" 11 | }, 12 | "files": [ 13 | "public", 14 | "src", 15 | "config-overrides.js", 16 | "package.json", 17 | "README.md", 18 | "tsconfig.json" 19 | ], 20 | "dependencies": { 21 | "@solana/buffer-layout-utils": "^0.2.0", 22 | "@solana/web3.js": "^1.65.0", 23 | "react": "^18.3.1", 24 | "react-dom": "^18.3.1" 25 | }, 26 | "devDependencies": { 27 | "@emotion/react": "^11.13.0", 28 | "@emotion/styled": "^11.13.0", 29 | "@types/node": "^16.18.105", 30 | "@types/react": "^18.3.3", 31 | "@types/react-dom": "^18.3.0", 32 | "process": "^0.11.10", 33 | "react-app-rewired": "^2.2.1", 34 | "react-scripts": "5.0.1", 35 | "source-map-loader": "^5.0.0", 36 | "typescript": "^4.9.5", 37 | "web-vitals": "^2.1.4" 38 | }, 39 | "scripts": { 40 | "start": "react-app-rewired start", 41 | "build": "react-app-rewired build", 42 | "test": "react-app-rewired test --passWithNoTests --env=jsdom", 43 | "eject": "react-scripts eject" 44 | }, 45 | "browserslist": { 46 | "production": [ 47 | ">0.2%", 48 | "not dead", 49 | "not op_mini all" 50 | ], 51 | "development": [ 52 | "last 1 chrome version", 53 | "last 1 firefox version", 54 | "last 1 safari version" 55 | ] 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /hw_02_basic_example/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firechiang/solana-study/673ce603bdd65ee76cb2a7b3ec5c7f209f428e1b/hw_02_basic_example/public/favicon.ico -------------------------------------------------------------------------------- /hw_02_basic_example/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | Phantom Wallet – CodeSandbox 13 | 14 | 24 | 25 | 28 |
29 | 30 | -------------------------------------------------------------------------------- /hw_02_basic_example/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firechiang/solana-study/673ce603bdd65ee76cb2a7b3ec5c7f209f428e1b/hw_02_basic_example/public/logo192.png -------------------------------------------------------------------------------- /hw_02_basic_example/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firechiang/solana-study/673ce603bdd65ee76cb2a7b3ec5c7f209f428e1b/hw_02_basic_example/public/logo512.png -------------------------------------------------------------------------------- /hw_02_basic_example/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /hw_02_basic_example/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /hw_02_basic_example/solana_program/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "solana_program" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [features] 7 | no-entrypoint = [] 8 | test-sbf = [] 9 | 10 | [dependencies] 11 | arrayref = "0.3.6" 12 | num-derive = "0.3" 13 | num-traits = "0.2" 14 | num_enum = "0.5.1" 15 | solana-program = "1.10.33" 16 | thiserror = "1.0" 17 | 18 | [dev-dependencies] 19 | solana-program-test = "1.10.33" 20 | solana-sdk = "1.10.33" 21 | 22 | [lib] 23 | name = "solana_program" 24 | crate-type = ["cdylib", "lib"] 25 | # 禁用文档测试 26 | doctest = false 27 | -------------------------------------------------------------------------------- /hw_02_basic_example/solana_program/Xargo.toml: -------------------------------------------------------------------------------- 1 | [target.bpfel-unknown-unknown.dependencies.std] 2 | features = [] -------------------------------------------------------------------------------- /hw_02_basic_example/solana_program/build/solana_program-keypair.json: -------------------------------------------------------------------------------- 1 | [255,43,128,168,143,225,90,56,37,55,53,102,169,51,109,18,158,67,192,57,233,18,137,54,238,209,141,42,116,222,55,83,126,170,22,228,147,140,160,192,18,76,119,198,237,103,241,118,57,123,89,162,169,50,191,228,22,184,76,109,237,72,105,189] -------------------------------------------------------------------------------- /hw_02_basic_example/solana_program/src/entrypoint.rs: -------------------------------------------------------------------------------- 1 | use solana_program::{ 2 | account_info::AccountInfo, entrypoint, entrypoint::ProgramResult, 3 | program_error::PrintProgramError, pubkey::Pubkey, 4 | }; 5 | use crate::processor::BasicExampleProcessor; 6 | use crate::error::BasicExampleError; 7 | 8 | 9 | // 定义合约入口 10 | entrypoint!(processor_instruction); 11 | 12 | fn processor_instruction(program_id: &Pubkey,accounts: &[AccountInfo],input: &[u8]) -> ProgramResult { 13 | if let Err(error) = BasicExampleProcessor::processor(program_id,accounts,input) { 14 | error.print::(); 15 | return Err(error); 16 | } 17 | Ok(()) 18 | } -------------------------------------------------------------------------------- /hw_02_basic_example/solana_program/src/error.rs: -------------------------------------------------------------------------------- 1 | 2 | use thiserror::Error; 3 | use num_derive::FromPrimitive; 4 | use num_traits::FromPrimitive; 5 | use solana_program::decode_error::DecodeError; 6 | use solana_program::msg; 7 | use solana_program::program_error::{PrintProgramError, ProgramError}; 8 | 9 | /** 10 | * 定义合约错误类型 11 | */ 12 | 13 | #[derive(Clone,Debug,Eq,Error,FromPrimitive,PartialEq)] 14 | pub enum BasicExampleError { 15 | // 抛出这个错误类型返回error包裹的message消息 16 | #[error("Invalid instruction")] 17 | InvalidInstruction, 18 | 19 | #[error("Data account initialized")] 20 | DataAccountInitialized, 21 | } 22 | 23 | impl From for ProgramError { 24 | fn from(e: BasicExampleError) -> Self { 25 | ProgramError::Custom(e as u32) 26 | } 27 | } 28 | 29 | impl DecodeError for BasicExampleError { 30 | fn type_of() -> &'static str { 31 | "BasicExampleError" 32 | } 33 | } 34 | 35 | impl PrintProgramError for BasicExampleError { 36 | fn print(&self) where E: 'static + std::error::Error + DecodeError + PrintProgramError + FromPrimitive, { 37 | match self { 38 | BasicExampleError::InvalidInstruction => msg!("Invalid instruction!"), 39 | BasicExampleError::DataAccountInitialized => msg!("Data account initialized!"), 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /hw_02_basic_example/solana_program/src/instruction.rs: -------------------------------------------------------------------------------- 1 | use std::mem::size_of; 2 | use std::str::from_utf8; 3 | use arrayref::{array_ref, array_refs}; 4 | use num_traits::ToBytes; 5 | use solana_program::program_error::ProgramError; 6 | use crate::error::BasicExampleError; 7 | 8 | /** 9 | * 定义合约所有函数(注意:约定俗成的命名方式是在最后面加上Instruction) 10 | */ 11 | #[repr(C)] 12 | #[derive(Clone,Debug,PartialEq)] 13 | pub enum BasicExampleInstruction { 14 | // 初始化数据账户 15 | Initialize, 16 | // 修改数据 17 | Update { 18 | data: String 19 | }, 20 | // 删除数据 21 | Delete, 22 | // 多参数测试 23 | MultiParamTest { 24 | // 占4个字节 25 | aa: u32, 26 | // 占1个字节 27 | bb: u8, 28 | // 占8个字节 29 | cc: u64, 30 | // 未知长度 31 | dd: String 32 | }, 33 | } 34 | 35 | impl BasicExampleInstruction { 36 | /** 37 | * 解码调用合约的参数数据(参数是u8数组,返回值是我们定义的枚举类型) 38 | */ 39 | pub fn unpack(input: &[u8]) -> Result { 40 | // 将参数的第一个字节和后面的所有字节分开(如果拆分是出现错误则抛出BasicExampleError异常) 41 | let (&tag,rest) = input.split_first().ok_or(BasicExampleError::InvalidInstruction)?; 42 | Ok(match tag { 43 | // 如果参数的第一个字节是0表示调用Initialize函数 44 | 0 => Self::Initialize, 45 | // 如果参数的第一个字节是1表示调用Update函数 46 | 1 => { 47 | let data = String::from(from_utf8(rest).unwrap()); 48 | Self::Update { 49 | data 50 | } 51 | }, 52 | // 如果参数的第一个字节是2表示调用的是Delete函数 53 | 2 => Self::Delete, 54 | // 如果参数的第一个字节是3表示调用的是MultiParamTest函数 55 | 3 => { 56 | // 前面3个参数总共占13字节,故截取数组0到13的位置(注意:第13个位置不会取,实际是取0到12个位置共13字节) 57 | let first_params_array = &rest[..13]; 58 | // 获取前3个参数字节数组的可切片引用 59 | let first_params_array_ref = array_ref![first_params_array,0,13]; 60 | // 拆分字节数组,前4个字节是参数aa,第5个字节是参数bb,后面8个字节是参数cc 61 | let (aa_ref,bb_ref,cc_ref) = array_refs![first_params_array_ref,4,1,8]; 62 | 63 | let aa = u32::from_le_bytes(*aa_ref); 64 | let bb = bb_ref[0]; 65 | 66 | // 初始化一个长度为8的u8字节数组,每个元素用0填充 67 | //let mut cc_array = [0u8;8]; 68 | // 将cc_ref字节数组填充到cc_array字节数组上(注意:填上去的数据长度要和被填数组的长度一致否者报错) 69 | //cc_array.copy_from_slice(cc_ref); 70 | //let cc = u64::from_le_bytes(cc_array); 71 | let cc = u64::from_le_bytes(*cc_ref); 72 | 73 | // 截取数组13到最后的位置,这一步分数据就是参数dd 74 | let dd_array = &rest[13..]; 75 | let dd = String::from(from_utf8(&dd_array).unwrap()); 76 | 77 | Self::MultiParamTest { 78 | aa, 79 | bb, 80 | cc, 81 | dd 82 | } 83 | }, 84 | // 如果参数的第一个字节是其它数字则直接抛出异常 85 | _ => return Err(BasicExampleError::InvalidInstruction.into()), 86 | }) 87 | } 88 | 89 | 90 | /** 91 | * 编码调用合约的参数数据(参数是我们定义的枚举类型,返回值是u8数组) 92 | */ 93 | pub fn pack(&self) -> Vec { 94 | // 获取当前枚举对象的数据长度 95 | let self_len = size_of::(); 96 | // 初始化一个指定大小的数组(注意:为什么长度要加1是因为我们要多写入一个标识位置) 97 | let mut res = Vec::with_capacity(self_len + 1); 98 | match self { 99 | Self::Initialize => { 100 | // 往数组的第一个位置写入数字0 101 | res.push(0); 102 | }, 103 | Self::Update { ref data} => { 104 | // 往数组的一个位置写入数字1 105 | res.push(1); 106 | // 往数组后面的位置写入data数据(注意:这个填充不是从空位置开始而是从数组的长度位置开始,它会扩张数组的长度) 107 | res.extend_from_slice(data.as_bytes()); 108 | }, 109 | Self::Delete => { 110 | // 往数组的第一个位置写入数字2 111 | res.push(2); 112 | }, 113 | Self::MultiParamTest {aa,bb,cc,dd} => { 114 | // 往数组的第一个位置写入数字3 115 | res.push(3); 116 | // 往数组后面的位置写入data数据(注意:这个填充不是从空位置开始而是从数组的长度位置开始,它会扩张数组的长度) 117 | res.extend_from_slice(&aa.to_le_bytes()); 118 | res.extend_from_slice(&bb.to_le_bytes()); 119 | res.extend_from_slice(&cc.to_le_bytes()); 120 | res.extend_from_slice(dd.as_bytes()); 121 | } 122 | } 123 | return res; 124 | } 125 | } 126 | 127 | #[cfg(test)] 128 | mod test { 129 | use solana_program::msg; 130 | use crate::instruction::BasicExampleInstruction; 131 | 132 | #[test] 133 | fn test_unpack() { 134 | let mut input: Vec = Vec::new(); 135 | input.push(0); 136 | input.extend_from_slice("0".as_bytes()); 137 | let instruction = BasicExampleInstruction::unpack(&input).unwrap(); 138 | msg!("Instruction枚举类型解码成功:{:#?}",instruction); 139 | } 140 | 141 | #[test] 142 | fn test_pack() { 143 | let instruction = BasicExampleInstruction::Initialize; 144 | let instruction_bytes = instruction.pack(); 145 | msg!("Instruction枚举类型编码成功:{:#?}",instruction_bytes); 146 | } 147 | 148 | #[test] 149 | fn test_pack_multi_param() { 150 | let instruction = BasicExampleInstruction::MultiParamTest { 151 | aa: 10, 152 | bb: 255, 153 | cc: 54515151, 154 | dd: String::from("ddccbbaa") 155 | }; 156 | let instruction_bytes = instruction.pack(); 157 | msg!("Instruction枚举类型编码成功:{:#?}",instruction_bytes); 158 | } 159 | 160 | #[test] 161 | fn test_unpack_multi_param() { 162 | let mut input: Vec = Vec::new(); 163 | input.push(3); 164 | 165 | let aa: u32 = 33; 166 | let bb: u8 = 3; 167 | let cc: u64 = 414455; 168 | let dd = String::from("aabbccdd"); 169 | 170 | input.extend_from_slice(&aa.to_le_bytes()); 171 | input.extend_from_slice(&bb.to_le_bytes()); 172 | input.extend_from_slice(&cc.to_le_bytes()); 173 | input.extend_from_slice(&dd.as_bytes()); 174 | 175 | let instruction = BasicExampleInstruction::unpack(&input).unwrap(); 176 | msg!("Instruction枚举类型解码成功:{:#?}",instruction); 177 | 178 | } 179 | 180 | } 181 | 182 | -------------------------------------------------------------------------------- /hw_02_basic_example/solana_program/src/lib.rs: -------------------------------------------------------------------------------- 1 | /** 2 | * 声明所有模块依赖(注意:该文件首先创建) 3 | */ 4 | pub mod error; 5 | 6 | pub mod state; 7 | 8 | pub mod processor; 9 | pub mod instruction; 10 | 11 | pub mod tools; 12 | 13 | /** 14 | * 该文件是项目导出依赖 15 | */ 16 | #[cfg(not(feature = "no-entrypoint"))] 17 | pub mod entrypoint; -------------------------------------------------------------------------------- /hw_02_basic_example/solana_program/src/processor.rs: -------------------------------------------------------------------------------- 1 | use solana_program::account_info::{AccountInfo, next_account_info}; 2 | use solana_program::entrypoint::ProgramResult; 3 | use solana_program::{msg, system_program}; 4 | use solana_program::program_error::ProgramError; 5 | use solana_program::program_pack::Pack; 6 | use solana_program::pubkey::Pubkey; 7 | use solana_program::rent::Rent; 8 | use solana_program::sysvar::Sysvar; 9 | use crate::state::{BasicExampleState, BasicExampleStruct}; 10 | use crate::instruction::BasicExampleInstruction; 11 | use crate::tools::account::create_pda_account; 12 | 13 | /** 14 | * 定义合约函数具体实现 15 | */ 16 | pub struct BasicExampleProcessor {} 17 | 18 | impl BasicExampleProcessor { 19 | 20 | /** 21 | * 初始化数据存储账户 22 | * @param program_id 当前程序ID 23 | * @param accounts 账户列表 24 | */ 25 | pub fn initialize(_program_id: &Pubkey,accounts: &[AccountInfo]) -> ProgramResult { 26 | // 获取账户列表可变迭代器 27 | let accounts_iter = &mut accounts.iter(); 28 | // 交易发起者账户 29 | let client_account_info = next_account_info(accounts_iter)?; 30 | // 要创建数据账户的PDA地址 31 | let pda_account_info = next_account_info(accounts_iter)?; 32 | // 系统程序账户 33 | let sys_program_info = next_account_info(accounts_iter)?; 34 | // 验证交易发起者签名 35 | if !client_account_info.is_signer { 36 | return Err(ProgramError::MissingRequiredSignature); 37 | } 38 | // 如果PDA地址已有余额就判断其数据账户是否已经创建 39 | if pda_account_info.lamports() > 0 { 40 | // 解码数据存储对象 41 | let basic_example_struct = BasicExampleStruct::unpack_unchecked(&pda_account_info.data.borrow())?; 42 | // 判断数据账户是否已经初始化 43 | if basic_example_struct.state == BasicExampleState::Initialize { 44 | msg!("数据账户: {:#?} 已初始化。",pda_account_info.key); 45 | return Err(ProgramError::AccountAlreadyInitialized); 46 | } 47 | } 48 | // 数据租金信息 49 | let rent = Rent::get()?; 50 | // 判断余额是否可以抵扣存储数据的租金(注意:lamports 表示账户余额,就是有多少个SOL代币) 51 | if rent.minimum_balance(BasicExampleStruct::LEN) > client_account_info.lamports() { 52 | return Err(ProgramError::AccountNotRentExempt); 53 | }; 54 | // 获取PDA账户地址(注意:这个种子要和前端获取PDA地址的种子一致否则生成的地址会不一样) 55 | let (pda_account_key,bump_seeds) = Pubkey::find_program_address(&[&client_account_info.key.to_bytes()],_program_id); 56 | // 如果前端生成的PDA地址和我们生成的PDA地址不一致,说明所使用的种子不一样,直接报错 57 | if *pda_account_info.key != pda_account_key { 58 | return Err(ProgramError::InvalidSeeds); 59 | } 60 | // PDA地址的所有者如果不是系统程序抛出异常 61 | if *pda_account_info.owner != system_program::id() { 62 | return Err(ProgramError::IllegalOwner); 63 | } 64 | // PDA账户种子签名 65 | let pda_signer_seeds:&[&[_]] = &[&client_account_info.key.to_bytes(),&[bump_seeds]]; 66 | // 创建PDA地址数据账户 67 | create_pda_account( 68 | client_account_info, 69 | &rent, 70 | BasicExampleStruct::LEN, 71 | _program_id, 72 | sys_program_info, 73 | pda_account_info, 74 | pda_signer_seeds)?; 75 | // 解码数据存储对象 76 | let mut basic_example_struct = BasicExampleStruct::unpack_unchecked(&pda_account_info.data.borrow())?; 77 | // 初始数据 78 | basic_example_struct.account_key = *client_account_info.key; 79 | basic_example_struct.state = BasicExampleState::Initialize; 80 | // 编码并存储存储数据 81 | BasicExampleStruct::pack(basic_example_struct,&mut pda_account_info.data.borrow_mut())?; 82 | Ok(()) 83 | } 84 | 85 | /** 86 | * 修改数据 87 | * @param program_id 当前程序ID 88 | * @param accounts 账户列表 89 | * @param data 数据 90 | */ 91 | pub fn update(_program_id: &Pubkey,accounts: &[AccountInfo],data: String) -> ProgramResult { 92 | // 获取账户列表可变迭代器 93 | let accounts_iter = &mut accounts.iter(); 94 | // 交易发起者账户 95 | let client_account = next_account_info(accounts_iter)?; 96 | // 数据账户 97 | let data_account = next_account_info(accounts_iter)?; 98 | // 验证交易发起者签名 99 | if !client_account.is_signer { 100 | return Err(ProgramError::MissingRequiredSignature); 101 | } 102 | // 将旧数据解包生成结构体对象 103 | let mut basic_example_struct = BasicExampleStruct::unpack_unchecked(&data_account.data.borrow())?; 104 | 105 | if basic_example_struct.state != BasicExampleState::Initialize { 106 | return Err(ProgramError::UninitializedAccount); 107 | } 108 | // 修改结构体对象 109 | basic_example_struct.account_key = *client_account.key; 110 | basic_example_struct.data = data; 111 | // 打包并存储数据 112 | BasicExampleStruct::pack(basic_example_struct,&mut data_account.data.borrow_mut())?; 113 | Ok(()) 114 | } 115 | 116 | /** 117 | * 删除数据 118 | * 说明:因为Solana上存储数据是要付费的,删除数据,我们只需要将存储数据账户上的余额全部转走,就等于删除了数据 119 | * @param program_id 当前程序ID 120 | * @param accounts 账户列表 121 | */ 122 | pub fn delete(_program_id: &Pubkey,accounts: &[AccountInfo]) -> ProgramResult { 123 | // 获取账户列表可变迭代器 124 | let accounts_iter = &mut accounts.iter(); 125 | // 交易发起者账户 126 | let client_account = next_account_info(accounts_iter)?; 127 | // 数据账户 128 | let data_account = next_account_info(accounts_iter)?; 129 | // 验证交易发起者签名 130 | if !client_account.is_signer { 131 | return Err(ProgramError::MissingRequiredSignature); 132 | } 133 | // 获取交易发起者账户余额 134 | let client_account_lamports = client_account.lamports(); 135 | // 将数据账户余额全部转到交易发起者账户(逻辑就是:交易发起者账户余额 = 交易发起者账户余额 + 数据账户余额) 136 | **client_account.lamports.borrow_mut() = client_account_lamports + data_account.lamports(); 137 | // 将数据账户余额置为0(这样数据账户上的数据就会自动被删除) 138 | **data_account.lamports.borrow_mut() = 0; 139 | msg!("数据删除完成!"); 140 | Ok(()) 141 | } 142 | 143 | /** 144 | * 查询数据 145 | * @param program_id 当前程序ID 146 | * @param accounts 账户列表 147 | */ 148 | pub fn query(_program_id: &Pubkey,accounts: &[AccountInfo]) -> ProgramResult { 149 | // 获取账户列表可变迭代器 150 | let accounts_iter = &mut accounts.iter(); 151 | // 交易发起者账户 152 | let client_account = next_account_info(accounts_iter)?; 153 | // 数据账户 154 | let data_account = next_account_info(accounts_iter)?; 155 | // 验证交易发起者签名 156 | if !client_account.is_signer { 157 | return Err(ProgramError::MissingRequiredSignature); 158 | } 159 | let basic_example_struct = BasicExampleStruct::unpack_unchecked(&data_account.data.borrow())?; 160 | msg!("查询到链上数据:{:#?}",&basic_example_struct); 161 | Ok(()) 162 | } 163 | 164 | /** 165 | * 合约函数调用分发 166 | * @param program_id 合约程序地址 167 | * @param accounts 账户列表 168 | * @param input 调用合约参数 169 | */ 170 | pub fn processor(_program_id: &Pubkey,accounts: &[AccountInfo],input: &[u8]) -> ProgramResult { 171 | // 解析合约调用参数将其转换为函数定义枚举 172 | let instruction = BasicExampleInstruction::unpack(input)?; 173 | match instruction { 174 | BasicExampleInstruction::Initialize => { 175 | Self::initialize(_program_id,accounts) 176 | }, 177 | BasicExampleInstruction::Update {data} => { 178 | Self::update(_program_id, accounts,data) 179 | }, 180 | BasicExampleInstruction::Delete => { 181 | Self::delete(_program_id ,accounts) 182 | }, 183 | BasicExampleInstruction::MultiParamTest {aa,bb,cc,dd} => { 184 | msg!("调用函数 MultiParamTest 参数 aa={:#?},bb={:#?},cc={:#?},dd={:#?}",aa,bb,cc,dd); 185 | Ok(()) 186 | } 187 | } 188 | } 189 | } 190 | 191 | #[cfg(test)] 192 | mod test { 193 | use std::mem; 194 | use arrayref::array_mut_ref; 195 | use solana_program::account_info::AccountInfo; 196 | use solana_program::clock::Epoch; 197 | use solana_program::program_pack::Pack; 198 | use solana_program::pubkey::Pubkey; 199 | use crate::instruction::BasicExampleInstruction; 200 | use crate::processor::BasicExampleProcessor; 201 | use crate::state::{BasicExampleState, BasicExampleStruct}; 202 | 203 | #[test] 204 | fn test_processor() { 205 | let program_id = Pubkey::default(); 206 | 207 | let client_key = Pubkey::default(); 208 | let mut client_key_lamports: u64 = 100000000; 209 | let mut client_key_data = vec![0;mem::size_of::()]; 210 | 211 | let data_key = Pubkey::default(); 212 | let mut data_key_lamports: u64 = 0; 213 | let mut data_key_data = vec![0;BasicExampleStruct::LEN]; 214 | let data_key_data = array_mut_ref![data_key_data,0,BasicExampleStruct::LEN]; 215 | BasicExampleStruct { 216 | account_key: data_key, 217 | state: BasicExampleState::Initialize, 218 | data: String::from("datadatadata") 219 | }.pack_into_slice(data_key_data); 220 | 221 | let client_account = AccountInfo::new( 222 | &client_key, 223 | true, 224 | true, 225 | &mut client_key_lamports, 226 | &mut client_key_data, 227 | &client_key, 228 | false, 229 | Epoch::default() 230 | ); 231 | 232 | let data_account = AccountInfo::new( 233 | &data_key, 234 | false, 235 | true, 236 | &mut data_key_lamports, 237 | data_key_data, 238 | &data_key, 239 | false, 240 | Epoch::default() 241 | ); 242 | 243 | let basic_example_instruction = BasicExampleInstruction::Delete { 244 | 245 | }; 246 | let input = basic_example_instruction.pack(); 247 | let accounts = vec![client_account,data_account]; 248 | let _=BasicExampleProcessor::processor(&program_id,&accounts,&input); 249 | } 250 | } -------------------------------------------------------------------------------- /hw_02_basic_example/solana_program/src/state.rs: -------------------------------------------------------------------------------- 1 | 2 | use std::str::from_utf8; 3 | use arrayref::{array_mut_ref, array_ref, array_refs, mut_array_refs}; 4 | use num_enum::TryFromPrimitive; 5 | use solana_program::program_error::ProgramError; 6 | use solana_program::program_pack::{IsInitialized, Pack, Sealed}; 7 | use solana_program::pubkey::Pubkey; 8 | 9 | 10 | /** 11 | * 定义数据存储账户状态 12 | * 注意:repr(u8) 表示结构数据转用字节数组表示的时候,每个字段用一个u8字节表示,而且是按顺序来的。0表示Uninitialize,1表示Initialize,2表示Delete 13 | */ 14 | #[repr(u8)] 15 | #[derive(Clone,Copy,Debug,PartialEq,TryFromPrimitive)] 16 | pub enum BasicExampleState { 17 | // 未初始化 18 | Uninitialize, 19 | // 已初始化 20 | Initialize, 21 | // 已删除 22 | Delete, 23 | } 24 | 25 | impl Default for BasicExampleState { 26 | fn default() -> Self { 27 | BasicExampleState::Uninitialize 28 | } 29 | } 30 | 31 | /** 32 | * 定义数据存储结构 33 | * 注意:repr(C) 表示结构体数据用字节数组表示的时候,每个字段用其本身数据类型表示 34 | */ 35 | #[repr(C)] 36 | #[derive(Clone,Debug,Default,PartialEq)] 37 | pub struct BasicExampleStruct { 38 | pub account_key: Pubkey, 39 | pub state: BasicExampleState, 40 | pub data: String 41 | } 42 | 43 | impl Sealed for BasicExampleStruct {} 44 | 45 | impl IsInitialized for BasicExampleStruct { 46 | fn is_initialized(&self) -> bool { 47 | self.state != BasicExampleState::Uninitialize 48 | } 49 | } 50 | 51 | 52 | 53 | 54 | 55 | /** 56 | * 实现结构体数据解包打包(注意:这个实现我们其实可以不用写,直接在结构体上面添加注解 BorshSerialize和BorshDeserialize 即可) 57 | */ 58 | impl Pack for BasicExampleStruct { 59 | // 可存储数据总长度290个字节 60 | const LEN: usize = 290; 61 | 62 | /** 63 | * 数据解包(参数是u8数组,返回值是结构体) 64 | */ 65 | fn unpack_from_slice(src: &[u8]) -> Result { 66 | // 获取参数src的可切片引用(注意:src的总长度是 BasicExampleStruct::LEN 是我们在代码里面早就定好的) 67 | let src = array_ref![src,0,BasicExampleStruct::LEN]; 68 | // 拆分字节数组,前32个字节是公钥,第33个字节是data的长度,后面256个字节是data数据 69 | let (account_key_buf,state_buf,data_len_buf,data_buf) = array_refs![src,32,1,1,BasicExampleStruct::LEN-32-1-1]; 70 | // 转换公钥 71 | let account_key = Pubkey::new_from_array(*account_key_buf); 72 | // 存储数据账户状态 73 | let state = BasicExampleState::try_from_primitive(state_buf[0]).or(Err(ProgramError::InvalidAccountData))?; 74 | // 转换data长度 75 | let data_len = data_len_buf[0] as u8; 76 | // 截取字节数组得到实际data数据 77 | let (data_data,_) = data_buf.split_at(data_len.into()); 78 | // 转化data数据 79 | let data = String::from(from_utf8(data_data).unwrap()); 80 | 81 | Ok(BasicExampleStruct { 82 | account_key, 83 | state, 84 | data 85 | }) 86 | } 87 | 88 | /** 89 | * 打包并存储数据(参数是结构体对象和一个可变数组这个可变数组用于存储打包后的结构体数据,然后由Solana会拿着这个数据进行存储) 90 | */ 91 | fn pack_into_slice(&self, dst: &mut [u8]) { 92 | // 获取dst数组的可变引用(已方便往里面插入数据) 93 | let dst = array_mut_ref![dst,0,BasicExampleStruct::LEN]; 94 | // 获取数组分段引用,前32个字节是公钥,第33个字节是data的长度,后面的256个字节是data数据 95 | let (account_key_buf,state_buf,data_len_buf,data_buf) = mut_array_refs![dst,32,1,1,BasicExampleStruct::LEN-32-1-1]; 96 | // 将公钥转换成字节数组再填充上去(注意:填上去的数据长度要和被填数组的长度一致否者报错) 97 | account_key_buf.copy_from_slice(self.account_key.as_ref()); 98 | // 数据存储账户状态 99 | state_buf[0] = self.state as u8; 100 | // 填充data数据的长度 101 | data_len_buf[0] = self.data.len() as u8; 102 | // 包装data数据使其长度为256个字节 103 | let mut data_bytes = Vec::new(); 104 | // 往数组后面的位置写入data数据(注意:这个填充不是从空位置开始而是从数组的长度位置开始,它会扩张数组的长度) 105 | data_bytes.extend_from_slice(self.data.as_bytes()); 106 | // 重新指定data_bytes数组长度为256个字节,空位置用0填充 107 | data_bytes.resize(BasicExampleStruct::LEN-32-1-1, 0); 108 | // 将新的data数据填充上去(注意:填上去的数据长度要和被填数组的长度一致否者报错) 109 | data_buf.copy_from_slice(&data_bytes); 110 | } 111 | 112 | } 113 | 114 | #[cfg(test)] 115 | mod test { 116 | use arrayref::array_mut_ref; 117 | use solana_program::msg; 118 | use solana_program::program_pack::Pack; 119 | use solana_program::pubkey::Pubkey; 120 | use crate::state::BasicExampleStruct; 121 | use crate::state::BasicExampleState; 122 | 123 | #[test] 124 | fn test_unpack_from_slice() { 125 | let account_key = Pubkey::default(); 126 | let data = String::from("data"); 127 | 128 | let mut array = Vec::new(); 129 | // 往数组里面填充数据(注意:这个填充不是从空位置开始而是从数组的长度位置开始,它会扩张数组的长度) 130 | array.extend_from_slice(account_key.as_ref()); 131 | // 往数组里面填充数据存储账户状态 132 | array.push(2 as u8); 133 | array.push(data.len() as u8); 134 | // 往数组里面填充数据(注意:这个填充不是从空位置开始而是从数组的长度位置开始,它会扩张数组的长度) 135 | array.extend_from_slice(data.as_bytes()); 136 | // 重新指定数组长度为290,空位置用0填充(原因:我们解包时会默认该数组长度为290) 137 | array.resize(BasicExampleStruct::LEN, 0); 138 | let state = BasicExampleStruct::unpack_from_slice(&array).unwrap(); 139 | msg!("BasicExampleState={:#?}",state); 140 | } 141 | 142 | #[test] 143 | fn test_pack_init_slice() { 144 | 145 | let old_state = BasicExampleStruct { 146 | account_key: Pubkey::default(), 147 | state: BasicExampleState::Initialize, 148 | data: String::from("datadatadata") 149 | }; 150 | let mut array = vec![0 as u8;BasicExampleStruct::LEN]; 151 | let dst = array_mut_ref![array,0,BasicExampleStruct::LEN]; 152 | old_state.pack_into_slice(dst); 153 | let new_state = BasicExampleStruct::unpack_from_slice(dst).unwrap(); 154 | msg!("New BasicExampleState={:#?}",new_state); 155 | } 156 | 157 | } 158 | 159 | -------------------------------------------------------------------------------- /hw_02_basic_example/solana_program/src/tools/account.rs: -------------------------------------------------------------------------------- 1 | use solana_program::account_info::AccountInfo; 2 | use solana_program::entrypoint::ProgramResult; 3 | use solana_program::program::{invoke, invoke_signed}; 4 | use solana_program::pubkey::Pubkey; 5 | use solana_program::rent::Rent; 6 | use solana_program::{msg, system_instruction}; 7 | 8 | /** 9 | * 创建数据账户 10 | * @param payer 数据账户所有者 11 | * @param rent 数据租金信息 12 | * @param space 数据空间大小(数据长度) 13 | * @param program_id 数据账户所属程序ID 14 | * @param system_program 系统程序ID 15 | * @param new_pda_account PDA账户地址 16 | * @param new_pda_signer_seeds PDA地址种子签名 17 | */ 18 | pub fn create_pda_account<'a> ( 19 | payer: &AccountInfo<'a>, 20 | rent: &Rent, 21 | space: usize, 22 | program_id: &Pubkey, 23 | system_program: &AccountInfo<'a>, 24 | new_pda_account: &AccountInfo<'a>, 25 | new_pda_signer_seeds: &[&[u8]] 26 | ) -> ProgramResult { 27 | // 要创建的pda数据账户余额大于0 28 | if new_pda_account.lamports() > 0 { 29 | // 计算存储数据还需多少金额(注意:这个计算是建立在数据账户已有余额的前提下) 30 | let required_lamports = rent.minimum_balance(space).max(1).saturating_sub(new_pda_account.lamports()); 31 | // 存储数据还需金额大于0,直接使用所有者账户向其转帐 32 | // 注意:这个invoke函数是跨程序调用俗称CPI,第一个参数是构建instruction操作,第二个参数是调用所需账户(如果被调用函数里面要验证pda地址签名,我们就要使用invoke_signed函数跨程序调用) 33 | if required_lamports > 0 { 34 | invoke( 35 | &system_instruction::transfer(payer.key, new_pda_account.key, required_lamports), 36 | &[payer.clone(),new_pda_account.clone(),system_program.clone()] 37 | )?; 38 | } 39 | // 为pda地址数据账户分配空间(因为pda地址有余额,所以是有空间的,这里调用等于是重新分配空间) 40 | // 注意:这个invoke_signed函数是跨程序调用俗称CPI,第一个参数是构建instruction操作,第二个参数是调用所需账户,第三个参数是pda账户种子 41 | invoke_signed( 42 | &system_instruction::allocate(new_pda_account.key, space as u64), 43 | &[new_pda_account.clone(), system_program.clone()], 44 | &[new_pda_signer_seeds], 45 | )?; 46 | // 指定pda地址数据账户所属程序(因为pda地址有余额,所以很有可能所属于另外某个程序) 47 | invoke_signed( 48 | &system_instruction::assign(new_pda_account.key, program_id), 49 | &[new_pda_account.clone(), system_program.clone()], 50 | &[new_pda_signer_seeds], 51 | ) 52 | } else { 53 | msg!("创建数据账户前一步 space: {:#?},lamports: {:#?}",space as u64,rent.minimum_balance(space).max(1)); 54 | // 创建pda地址的数据账户 55 | // 注意:这个invoke_signed函数是跨程序调用俗称CPI,第一个参数是构建instruction操作,第二个参数是调用所需账户,第三个参数是pda账户种子 56 | invoke_signed( 57 | &system_instruction::create_account( 58 | payer.key, 59 | new_pda_account.key, 60 | rent.minimum_balance(space).max(1), 61 | space as u64, 62 | program_id, 63 | ), 64 | &[ 65 | payer.clone(), 66 | new_pda_account.clone(), 67 | //system_program.clone(), 68 | ], 69 | &[new_pda_signer_seeds], 70 | ) 71 | } 72 | //************************************************************ 73 | // CPI 跨程序调用基础示例 74 | //************************************************************ 75 | // 构建instruction的keys 76 | //let account_metas = vec![ 77 | // AccountMeta::new(*from_pubkey, true), 78 | // AccountMeta::new(*to_pubkey, true), 79 | //]; 80 | 81 | 82 | // 构建instruction 83 | //let instruction = { 84 | // // 被调用程序ID 85 | // program_id: Pubkey, 86 | // // 被调用程序所要的keys(也就是账户信息) 87 | // accounts: Vec, 88 | // // 被调用程序的input data 89 | // data: Vec, 90 | //} 91 | 92 | 93 | // 注意:这个invoke函数是跨程序调用俗称CPI,第一个参数是构建instruction操作,第二个参数是调用所需账户(如果被调用函数里面要验证pda地址签名,我们就要使用invoke_signed函数跨程序调用) 94 | //invoke(&instruction,&[from_pubkey.clone(),to_pubkey.clone()]); 95 | 96 | 97 | // 注意:这个invoke_signed函数是跨程序调用俗称CPI,第一个参数是构建instruction操作,第二个参数是调用所需账户,第三个参数是pda账户种子 98 | //invoke_signed(&instruction,&[from_pubkey.clone(),to_pubkey.clone()],&[构建pda账户的种子]); 99 | 100 | //*************************************************************** 101 | // 关于PDA地址的种子说明 102 | //*************************************************************** 103 | // 前端代码获得pda_address(PDA地址),bump_seed(凹凸种子),在后端是使用 [Buffer.from("test")] + [bump_seed] 作为PDA地址的种子签名,后端的示例代码在最下面 104 | // let [pda_address, bump_seed] = PublicKey.findProgramAddressSync([Buffer.from("test")],programId,); 105 | 106 | 107 | // 在后段端后获得pda_address(PDA地址),bump_seed(凹凸种子)这个和前端的效果一样的,如果参数一致那么所得到的地址也是一样的 108 | // let (pda_address, bump_seed) = Pubkey::find_program_address(&[&address.key.to_bytes()],program_id); 109 | // 获得PDA地址的签名种子,可以拿这个种子去调用其它程序 110 | // let pda_signer_seeds: &[&[_]] = &[&address.key.to_bytes(),&[bump_seed]]; 111 | } -------------------------------------------------------------------------------- /hw_02_basic_example/solana_program/src/tools/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod account; -------------------------------------------------------------------------------- /hw_02_basic_example/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useCallback, useMemo } from 'react'; 2 | import styled from '@emotion/styled'; 3 | import {clusterApiUrl, Connection, Keypair, PublicKey} from '@solana/web3.js'; 4 | 5 | import { TLog } from './types'; 6 | import { Logs, Sidebar, NoProvider } from './components'; 7 | 8 | import { 9 | phantomGetProvider, 10 | phantomSignMessage, 11 | phantomCreateDataAccount, 12 | pollSignatureStatus, 13 | createDataAccount, 14 | updateDataAccount, 15 | deleteDataAccount, 16 | queryDataAccount, 17 | multiParamTest 18 | } from './utils'; 19 | 20 | 21 | const fromB = Keypair.generate(); 22 | const fromS = btoa(String.fromCharCode.apply(null, fromB.secretKey)); 23 | console.info(fromS); 24 | // 修改 fromS 25 | const fromA = Uint8Array.from(atob(fromS), (m) => m.codePointAt(0)); 26 | const fromK = Keypair.fromSecretKey(fromA); 27 | console.info(`From Key: ${fromK.publicKey}`); 28 | 29 | 30 | const remoteNetwork = clusterApiUrl('devnet'); 31 | const localhostNetwork = 'http://localhost:8899'; 32 | const provider = phantomGetProvider(); 33 | const connection = new Connection(localhostNetwork); 34 | const programId = new PublicKey('9XSoH2btchBESWhABvjYRRTp7vovAkkiNMQvV9c7BT2C'); 35 | 36 | // 自定义类型 37 | export type ConnectedMethods = | { name: string; onClick: () => Promise; } | { name: string; onClick: () => Promise; }; 38 | 39 | interface Props { 40 | publicKey: PublicKey | null; 41 | connectedMethods: ConnectedMethods[]; 42 | handleConnect: () => Promise; 43 | logs: TLog[]; 44 | clearLogs: () => void; 45 | }; 46 | 47 | /** 48 | * 自定义Hooks(包含程序所有处理逻辑) 49 | */ 50 | const useProps = (): Props => { 51 | // 日志数据状态管理 52 | const [logs, setLogs] = useState([]); 53 | 54 | // 控制日志数据生成 55 | const createLog = useCallback((log: TLog) => { 56 | // 注意:set函数在执行时如果发现传递给它的是函数,它会将现有值传递给该函数并执行该函数获取新值 57 | setLogs((logs) => [...logs, log]); 58 | },[setLogs]); 59 | 60 | // 清空日志数据 61 | const clearLogs = useCallback(() => { 62 | setLogs([]); 63 | }, [setLogs]); 64 | 65 | // 绑定相关事件(每次日志发生变化重新绑定) 66 | useEffect(() => { 67 | if (!provider) return; 68 | 69 | // 监听Phantom钱包connect连接事件 70 | provider.on('connect', (publicKey: PublicKey) => { 71 | createLog({ 72 | status: 'success', 73 | method: 'connect', 74 | message: `Connected to account ${publicKey.toBase58()}`, 75 | }); 76 | }); 77 | 78 | // 监听Phantom钱包disconnect断开连接事件 79 | provider.on('disconnect', () => { 80 | createLog({ 81 | status: 'warning', 82 | method: 'disconnect', 83 | message: '👋', 84 | }); 85 | }); 86 | 87 | // 监听Phantom钱包accountChanged切换账户事件 88 | provider.on('accountChanged', (publicKey: PublicKey | null) => { 89 | // 已连接Phantom钱包 90 | if (publicKey) { 91 | createLog({ 92 | status: 'info', 93 | method: 'accountChanged', 94 | message: `Switched to account ${publicKey.toBase58()}`, 95 | }); 96 | 97 | // 未连接Phantom钱包 98 | } else { 99 | // 打印日志 100 | createLog({ 101 | status: 'info', 102 | method: 'accountChanged', 103 | message: 'Attempting to switch accounts.', 104 | }); 105 | // 重新连接Phantom钱包 106 | provider.connect().catch((error) => { 107 | createLog({ 108 | status: 'error', 109 | method: 'accountChanged', 110 | message: `Failed to re-connect: ${error.message}`, 111 | }); 112 | }); 113 | } 114 | }); 115 | 116 | return () => { 117 | provider.disconnect(); 118 | }; 119 | 120 | }, [createLog]); 121 | 122 | // 通过Phantom签名提交交易创建数据账户 123 | const handlePhantomCreateDataAccount = useCallback(async () => { 124 | // 没有获取到Phantom插件实例 125 | if (!provider) return; 126 | try { 127 | createLog({ 128 | status: 'info', 129 | method: 'phantomCreateDataAccount', 130 | message: 'Phantom创建数据账户交易提交中...', 131 | }); 132 | const signature = await phantomCreateDataAccount(connection,provider,programId); 133 | createLog({ 134 | status: 'info', 135 | method: 'phantomCreateDataAccount', 136 | message: `交易Hash: ${signature}.`, 137 | }); 138 | // 交易状态查询 139 | pollSignatureStatus(signature, connection, createLog,'createDataAccountByPhantom'); 140 | } catch(error) { 141 | createLog({ 142 | status: 'error', 143 | method: 'phantomCreateDataAccount', 144 | message: error.message, 145 | }); 146 | } 147 | },[createLog]); 148 | 149 | // 创建数据账户 150 | const handleCreateDataAccount = useCallback(async () => { 151 | try { 152 | createLog({ 153 | status: 'info', 154 | method: 'createDataAccount', 155 | message: '创建数据账户交易提交中...', 156 | }); 157 | const signature = await createDataAccount(connection,fromK,programId); 158 | createLog({ 159 | status: 'info', 160 | method: 'createDataAccount', 161 | message: `交易Hash: ${signature}.`, 162 | }); 163 | // 交易状态查询 164 | pollSignatureStatus(signature, connection, createLog,'createDataAccount'); 165 | } catch(error) { 166 | createLog({ 167 | status: 'error', 168 | method: 'createDataAccount', 169 | message: error.message, 170 | }); 171 | } 172 | },[createLog]); 173 | 174 | 175 | // 修改账户数据 176 | const handleUpdateDataAccount = useCallback(async () => { 177 | try { 178 | createLog({ 179 | status: 'info', 180 | method: 'updateDataAccount', 181 | message: '修改账户数据交易提交中...', 182 | }); 183 | const signature = await updateDataAccount(connection,fromK,programId); 184 | createLog({ 185 | status: 'info', 186 | method: 'updateDataAccount', 187 | message: `交易Hash: ${signature}.`, 188 | }); 189 | // 交易状态查询 190 | pollSignatureStatus(signature, connection, createLog,'updateDataAccount'); 191 | } catch(error) { 192 | createLog({ 193 | status: 'error', 194 | method: 'updateDataAccount', 195 | message: error.message, 196 | }); 197 | } 198 | },[createLog]); 199 | 200 | // 删除数据账户 201 | const handleDeleteDataAccount = useCallback(async () => { 202 | try { 203 | createLog({ 204 | status: 'info', 205 | method: 'deleteDataAccount', 206 | message: '删除数据账户交易提交中...', 207 | }); 208 | const signature = await deleteDataAccount(connection,fromK,programId); 209 | createLog({ 210 | status: 'info', 211 | method: 'deleteDataAccount', 212 | message: `交易Hash: ${signature}.`, 213 | }); 214 | // 交易状态查询 215 | pollSignatureStatus(signature, connection, createLog,'deleteDataAccount'); 216 | } catch(error) { 217 | createLog({ 218 | status: 'error', 219 | method: 'deleteDataAccount', 220 | message: error.message, 221 | }); 222 | } 223 | },[createLog]); 224 | 225 | // 查询数据账户 226 | const handleQueryDataAccount = useCallback(async () => { 227 | try { 228 | const res = await queryDataAccount(connection,fromK,programId); 229 | createLog({ 230 | status: 'info', 231 | method: 'queryDataAccount', 232 | message: `查询到数据账户: ${res}.`, 233 | }); 234 | } catch(error) { 235 | createLog({ 236 | status: 'error', 237 | method: 'queryDataAccount', 238 | message: error.message, 239 | }); 240 | } 241 | },[createLog]); 242 | 243 | // 多个参数传递测试 244 | const handleMultiParamTest = useCallback(async () => { 245 | try { 246 | createLog({ 247 | status: 'info', 248 | method: 'multiParamTest', 249 | message: '多个参数传递交易提交中...', 250 | }); 251 | const signature = await multiParamTest(connection,fromK,programId); 252 | createLog({ 253 | status: 'info', 254 | method: 'multiParamTest', 255 | message: `交易Hash: ${signature}.`, 256 | }); 257 | // 交易状态查询 258 | pollSignatureStatus(signature, connection, createLog,'multiParamTest'); 259 | } catch(error) { 260 | createLog({ 261 | status: 'error', 262 | method: 'multiParamTest', 263 | message: error.message, 264 | }); 265 | } 266 | },[createLog]); 267 | 268 | 269 | // Phantom处理消息签名 270 | const handlePhantomSignMessage = useCallback(async () => { 271 | if (!provider) return; 272 | 273 | try { 274 | const message = new Date().getTime().toString(); 275 | // 签名消息 276 | const signedMessage = await phantomSignMessage(provider, message); 277 | createLog({ 278 | status: 'success', 279 | method: 'phantomSignMessage', 280 | message: `Message signed: ${JSON.stringify(signedMessage)}`, 281 | }); 282 | // 返回签名 283 | return signedMessage; 284 | } catch (error) { 285 | createLog({ 286 | status: 'error', 287 | method: 'phantomSignMessage', 288 | message: error.message, 289 | }); 290 | } 291 | }, [createLog]); 292 | 293 | // 处理连接Phantom钱包 294 | const handleConnect = useCallback(async () => { 295 | if (!provider) return; 296 | 297 | try { 298 | // 连接Phantom钱包 299 | await provider.connect(); 300 | } catch (error) { 301 | createLog({ 302 | status: 'error', 303 | method: 'connect', 304 | message: error.message, 305 | }); 306 | } 307 | }, [createLog]); 308 | 309 | // 处理断开连接Phantom钱包 310 | const handlePhantomDisconnect = useCallback(async () => { 311 | if (!provider) return; 312 | try { 313 | await provider.disconnect(); 314 | } catch (error) { 315 | createLog({ 316 | status: 'error', 317 | method: 'disconnect', 318 | message: error.message, 319 | }); 320 | } 321 | }, [createLog]); 322 | 323 | // 定义相关操作 324 | const connectedMethods = useMemo(() => { 325 | return [{ 326 | name: '创建数据账户', 327 | onClick: handleCreateDataAccount, 328 | },{ 329 | name: '修改账户数据', 330 | onClick: handleUpdateDataAccount, 331 | },{ 332 | name: '查询账户数据', 333 | onClick: handleQueryDataAccount, 334 | },{ 335 | name: '删除数据账户', 336 | onClick: handleDeleteDataAccount, 337 | },{ 338 | name: '多个参数传递测试', 339 | onClick: handleMultiParamTest, 340 | },{ 341 | name: 'Phantom创建数据账户', 342 | onClick: handlePhantomCreateDataAccount, 343 | },{ 344 | name: 'Phantom签名数据', 345 | onClick: handlePhantomSignMessage, 346 | },{ 347 | name: 'Phantom断开连接', 348 | onClick: handlePhantomDisconnect, 349 | }]; 350 | },[ 351 | handleCreateDataAccount, 352 | handleUpdateDataAccount, 353 | handleDeleteDataAccount, 354 | handlePhantomCreateDataAccount, 355 | handlePhantomSignMessage, 356 | handlePhantomDisconnect, 357 | ]); 358 | 359 | return { 360 | // @ts-ignore 361 | publicKey: provider?.publicKey || null, 362 | connectedMethods, 363 | handleConnect, 364 | logs, 365 | clearLogs, 366 | }; 367 | }; 368 | 369 | 370 | // 主界面组件 371 | const StatelessApp = React.memo((props: Props) => { 372 | const { publicKey, connectedMethods, handleConnect, logs, clearLogs } = props; 373 | return ( 374 | 375 | 376 | 377 | 378 | ); 379 | }); 380 | 381 | 382 | // 程序入口组件 383 | const App = () => { 384 | const props = useProps(); 385 | 386 | if (!provider) { 387 | return ; 388 | } 389 | 390 | return ; 391 | }; 392 | 393 | export default App; 394 | 395 | // 主界面样式 396 | const StyledApp = styled.div` 397 | display: flex; 398 | flex-direction: row; 399 | height: 100vh; 400 | @media (max-width: 768px) { 401 | flex-direction: column; 402 | } 403 | `; -------------------------------------------------------------------------------- /hw_02_basic_example/src/components/Button/index.tsx: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | import {WHITE,DARK_GRAY,LIGHT_GRAY} from '../../constants'; 3 | import {hexToRGB} from '../../utils'; 4 | 5 | const Button = styled.button` 6 | cursor: pointer; 7 | width: 100%; 8 | color: ${WHITE}; 9 | background-color: ${DARK_GRAY}; 10 | padding: 15px 10px; 11 | font-weight: 600; 12 | outline: 0; 13 | border: 0; 14 | border-radius: 6px; 15 | user-select: none; 16 | &:hover { 17 | background-color: ${hexToRGB(LIGHT_GRAY, 0.9)}; 18 | } 19 | &:focus-visible&:not(:hover) { 20 | background-color: ${hexToRGB(LIGHT_GRAY, 0.8)}; 21 | } 22 | &:active { 23 | background-color: ${LIGHT_GRAY}; 24 | } 25 | `; 26 | 27 | export default Button; -------------------------------------------------------------------------------- /hw_02_basic_example/src/components/Logs/Log.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from '@emotion/styled'; 3 | import {Status,TLog} from '../../types'; 4 | import {RED,YELLOW,GREEN,BLUE,PURPLE} from '../../constants'; 5 | 6 | /** 7 | * 使用React.memo生成的子组件只要props的值不改变子组件不会被重新渲染 8 | */ 9 | const Log = React.memo((props: TLog) => ( 10 | 11 | 12 | 13 | {'>'} {props.status} 14 | 15 | {props.method && [{props.method}]} 16 | 17 | {props.message} 18 | {props.messageTwo && {props.messageTwo}} 19 | 20 | )); 21 | 22 | export default Log; 23 | 24 | const Column = styled.div` 25 | display: flex; 26 | flex-direction: column; 27 | justify-content: center; 28 | line-height: 1.5; 29 | `; 30 | 31 | const Row = styled.div` 32 | display: flex; 33 | flex-direction: row; 34 | align-items: center; 35 | `; 36 | 37 | const StyledSpan = styled.span<{ status: Status }>` 38 | color: ${(props) => { 39 | switch (props.status) { 40 | case 'success': 41 | return GREEN; 42 | case 'warning': 43 | return YELLOW; 44 | case 'error': 45 | return RED; 46 | case 'info': 47 | return BLUE; 48 | } 49 | }}; 50 | margin-right: 5px; 51 | `; 52 | 53 | const Method = styled.p` 54 | color: ${PURPLE}; 55 | margin-right: 10px; 56 | `; 57 | 58 | const Message = styled.p` 59 | overflow-wrap: break-word; 60 | `; -------------------------------------------------------------------------------- /hw_02_basic_example/src/components/Logs/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from '@emotion/styled'; 3 | import {PublicKey} from '@solana/web3.js'; 4 | 5 | import {TLog} from '../../types'; 6 | import {BLACK,GRAY} from '../../constants'; 7 | 8 | import Button from '../Button'; 9 | import Log from './Log'; 10 | 11 | interface Props { 12 | publicKey: PublicKey | undefined, 13 | logs: TLog[], 14 | clearLogs: () => void 15 | } 16 | 17 | /** 18 | * 使用React.memo生成的子组件只要props的值不改变子组件不会被重新渲染 19 | */ 20 | const Logs = React.memo((props: Props) => { 21 | const {publicKey,logs,clearLogs} = props; 22 | return ( 23 | 24 | { 25 | logs.length > 0 ? ( 26 | // 有日志渲染显示日志 27 | <> 28 | { 29 | logs.map((log,i) => ()) 30 | } 31 | 清空日志 32 | 33 | ) : ( 34 | // 没有日志 35 | 36 | {'>'} 37 | 38 | { 39 | publicKey ? ( 40 | // 有publicKey说明已连接钱包 41 | <> 42 | { publicKey.toBase58() } 43 | 44 | 45 | ) : ( 46 | // 没有publicKey说明没有连接钱包 47 | <> 48 | 欢迎来到 Phantom 沙盒。连接到您的 Phantom 钱包并试用...{' '} 49 | 👻 50 | 51 | ) 52 | } 53 | 54 | 55 | ) 56 | } 57 | 58 | ); 59 | }); 60 | 61 | export default Logs; 62 | 63 | const StyledSection = styled.section` 64 | position: relative; 65 | flex: 2; 66 | padding: 20px; 67 | background-color: ${BLACK}; 68 | overflow: auto; 69 | font-family: monospace; 70 | `; 71 | 72 | const ClearLogsButton = styled(Button)` 73 | position: absolute; 74 | top: 20px; 75 | right: 20px; 76 | width: 100px; 77 | `; 78 | 79 | const PlaceholderMessage = styled.p` 80 | color: ${GRAY}; 81 | `; 82 | 83 | const Row = styled.div` 84 | display: flex; 85 | flex-direction: row; 86 | align-items: center; 87 | span { 88 | margin-right: 10px; 89 | } 90 | `; -------------------------------------------------------------------------------- /hw_02_basic_example/src/components/NoProvider/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from '@emotion/styled'; 3 | 4 | import {REACT_GRAY} from '../../constants'; 5 | 6 | const NoProvider = () => { 7 | return ( 8 | 9 |

找不到Phantom钱包

10 |
11 | ); 12 | } 13 | 14 | export default NoProvider; 15 | 16 | 17 | const StyledMain = styled.main` 18 | padding: 20px; 19 | height: 100vh; 20 | background-color: ${REACT_GRAY}; 21 | `; -------------------------------------------------------------------------------- /hw_02_basic_example/src/components/Sidebar/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {PublicKey} from '@solana/web3.js'; 3 | import styled from '@emotion/styled'; 4 | 5 | import {GRAY, REACT_GRAY, PURPLE, WHITE, DARK_GRAY} from '../../constants'; 6 | import {hexToRGB} from '../../utils'; 7 | import Button from '../Button'; 8 | import {ConnectedMethods} from '../../App'; 9 | 10 | /** 11 | * 主界面左边的内容 12 | */ 13 | 14 | interface Props { 15 | publicKey?: PublicKey, 16 | connectedMethods: ConnectedMethods[], 17 | connect: () => Promise 18 | } 19 | 20 | const Sidebar = React.memo((props: Props) => { 21 | const {publicKey,connectedMethods,connect} = props; 22 | const phantomLogo = "https://cdn.sanity.io/images/3nm6d03a/production/2c52333718ae2ae5b69e1acf77400c9a0a62cfc5-1632x700.svg"; 23 | return ( 24 |
25 | 26 | 27 | Phantom 28 | 29 | { 30 | publicKey ? ( 31 | // 有publicKey说明已连接到Phantom钱包 32 | <> 33 |
34 |
Connected as
35 | {publicKey.toBase58()} 36 | 37 |
38 | { 39 | connectedMethods.map((method, i) => ( 40 | 43 | )) 44 | } 45 | 46 | ) : ( 47 | // 没有publicKey说明没有连接Phantom钱包 48 | 49 | ) 50 | } 51 | 52 | Solana和Phantom钱包开发基础示例 53 |
54 | ); 55 | }); 56 | 57 | export default Sidebar; 58 | 59 | 60 | const Main = styled.main` 61 | position: relative; 62 | flex: 1; 63 | display: flex; 64 | flex-direction: column; 65 | justify-content: space-between; 66 | padding: 20px; 67 | align-items: center; 68 | background-color: ${REACT_GRAY}; 69 | > * { 70 | margin-bottom: 10px; 71 | } 72 | @media (max-width: 768px) { 73 | width: 100%; 74 | height: auto; 75 | } 76 | `; 77 | 78 | const Body = styled.div` 79 | display: flex; 80 | flex-direction: column; 81 | align-items: center; 82 | button { 83 | margin-bottom: 15px; 84 | } 85 | `; 86 | 87 | const Link = styled('a',{ 88 | // @ts-ignore 89 | href: 'https://phantom.app/', 90 | target: '_blank', 91 | rel: 'noopener noreferrer', 92 | })` 93 | display: flex; 94 | flex-direction: column; 95 | align-items: flex-end; 96 | text-decoration: none; 97 | margin-bottom: 30px; 98 | padding: 5px; 99 | &:focus-visible { 100 | outline: 2px solid ${hexToRGB(GRAY, 0.5)}; 101 | border-radius: 6px; 102 | } 103 | `; 104 | 105 | const Pre = styled.pre` 106 | margin-bottom: 5px; 107 | `; 108 | 109 | const Badge = styled.div` 110 | margin: 0; 111 | padding: 10px; 112 | width: 100%; 113 | color: ${PURPLE}; 114 | background-color: ${hexToRGB(PURPLE,0.2)}; 115 | font-size: 14px; 116 | border-radius: 6px; 117 | @media (max-width: 400px) { 118 | width: 280px; 119 | white-space: nowrap; 120 | overflow: hidden; 121 | text-overflow: ellipsis; 122 | } 123 | @media (max-width: 320px) { 124 | width: 220px; 125 | white-space: nowrap; 126 | overflow: hidden; 127 | text-overflow: ellipsis; 128 | } 129 | ::selection { 130 | color: ${WHITE}; 131 | background-color: ${hexToRGB(PURPLE,0.5)}; 132 | } 133 | ::-moz-selection { 134 | color: ${WHITE}; 135 | background-color: ${hexToRGB(PURPLE,0.5)}; 136 | } 137 | `; 138 | 139 | const Divider = styled.div` 140 | border: 1px solid ${DARK_GRAY}; 141 | height: 1px; 142 | margin: 20px 0; 143 | `; 144 | 145 | const Tag = styled.p` 146 | text-align: center; 147 | color: ${GRAY}; 148 | a { 149 | color: ${PURPLE}; 150 | text-decoration: none; 151 | ::selection { 152 | color: ${WHITE}; 153 | background-color: ${hexToRGB(PURPLE, 0.5)}; 154 | } 155 | ::-moz-selection { 156 | color: ${WHITE}; 157 | background-color: ${hexToRGB(PURPLE, 0.5)}; 158 | } 159 | } 160 | @media (max-width: 320px) { 161 | font-size: 14px; 162 | } 163 | ::selection { 164 | color: ${WHITE}; 165 | background-color: ${hexToRGB(PURPLE, 0.5)}; 166 | } 167 | ::-moz-selection { 168 | color: ${WHITE}; 169 | background-color: ${hexToRGB(PURPLE, 0.5)}; 170 | } 171 | `; 172 | 173 | -------------------------------------------------------------------------------- /hw_02_basic_example/src/components/index.tsx: -------------------------------------------------------------------------------- 1 | export {default as Button} from './Button'; 2 | export {default as Logs} from './Logs'; 3 | export {default as NoProvider} from './NoProvider'; 4 | export {default as Sidebar} from './Sidebar'; -------------------------------------------------------------------------------- /hw_02_basic_example/src/constants.ts: -------------------------------------------------------------------------------- 1 | 2 | export const RED = '#EB3742'; 3 | export const YELLOW = '#FFDC62'; 4 | export const GREEN = '#21E56F'; 5 | export const BLUE = '#59cff7'; 6 | export const PURPLE = '#8A81F8'; 7 | export const WHITE = '#FFFFFF'; 8 | export const GRAY = '#777777'; 9 | export const REACT_GRAY = '#222222'; 10 | export const DARK_GRAY = '#333333'; 11 | export const LIGHT_GRAY = '#444444'; 12 | export const BLACK = '#000000'; -------------------------------------------------------------------------------- /hw_02_basic_example/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import App from './App'; 4 | import * as buffer from "buffer"; 5 | import reportWebVitals from './reportWebVitals'; 6 | 7 | window.Buffer = buffer.Buffer; 8 | 9 | const root = ReactDOM.createRoot( 10 | document.getElementById('root') as HTMLElement 11 | ); 12 | 13 | root.render( 14 | // 这是严格模式组件在开发环境下有些hook组件会默认执行2次来验证代码,如果不想让它执行2次,可以将 React.StrictMode 注释掉 15 | // 16 | // 17 | // 18 | 19 | ); 20 | 21 | // If you want to start measuring performance in your app, pass a function 22 | // to log results (for example: reportWebVitals(console.log)) 23 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 24 | reportWebVitals(); 25 | -------------------------------------------------------------------------------- /hw_02_basic_example/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /hw_02_basic_example/src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals'; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /hw_02_basic_example/src/types.ts: -------------------------------------------------------------------------------- 1 | import {PublicKey,Transaction,SendOptions} from '@solana/web3.js'; 2 | 3 | // 编码类型 4 | type DisplayEncodeing = 'utf8' | 'hex'; 5 | 6 | // Phantom相关事件类型 7 | type PhantomEvent = 'connect' | 'disconnect' | 'accountChanged'; 8 | 9 | // Phantom请求函数类型 10 | type PhantomRequestMethod = 11 | | 'connect' 12 | | 'disconnect' 13 | | 'phantomCreateDataAccount' 14 | | 'phantomSignMessage' 15 | | 'createDataAccount' 16 | | 'updateDataAccount' 17 | | 'deleteDataAccount' 18 | | 'queryDataAccount' 19 | | 'multiParamTest'; 20 | 21 | interface ConnectOpts { 22 | onlyIfTrusted: boolean; 23 | } 24 | 25 | // 与Phantom相关交互定义 26 | export interface PhantomProvider { 27 | publicKey: PublicKey | null; 28 | isConnected: boolean | null; 29 | signAndSendTransaction: (transaction: Transaction, opts?: SendOptions) => Promise<{ signature: string; publicKey: PublicKey }>; 30 | signTransaction: (transaction: Transaction) => Promise; 31 | signAllTransactions: (transactions: Transaction[]) => Promise; 32 | signMessage: (message: Uint8Array | string, display?: DisplayEncodeing) => Promise; 33 | connect: (opts?: Partial) => Promise<{ publicKey: PublicKey }>; 34 | disconnect: () => Promise; 35 | on: (event: PhantomEvent, handler: (args: any) => void) => void; 36 | request: (method: PhantomRequestMethod, params: any) => Promise; 37 | } 38 | 39 | // 日志级别类型 40 | export type Status = 'success' | 'warning' | 'error' | 'info'; 41 | 42 | // 日志相关定义 43 | export interface TLog { 44 | status: Status; 45 | method?: PhantomRequestMethod | Extract; 46 | message: string; 47 | messageTwo?: string; 48 | } -------------------------------------------------------------------------------- /hw_02_basic_example/src/utils/cCreateDataAccount.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Connection, 3 | Keypair, 4 | PublicKey, 5 | sendAndConfirmTransaction, 6 | SystemProgram, 7 | Transaction, 8 | TransactionInstruction 9 | } from "@solana/web3.js"; 10 | 11 | /** 12 | * 创建数据账户 13 | * @param connection 14 | * @param keypair 15 | * @param programId 16 | */ 17 | const CCreateDataAccount = async (connection: Connection, keypair: Keypair, programId: PublicKey): Promise => { 18 | try { 19 | /** 20 | * 创建数据账户地址(注意:这个就是创建PDA地址,但是数据账户是我们在合约里面创建的而不是直接在前端创建的,因为PDA地址不能直接在前端创建数据账户,原因是它没有私钥不能签名) 21 | * 说明:因为我们的程序只需要用户创建一个数据账户,所以直接使用用户地址作为种子创建PDA地址。 22 | * 如果程序是需要用户创建多个数据账户,那么每个PDA地址的种子是需要不一样的,可根据每个数据账户的作用来定。 23 | * 比如第一个数据账户的种子:[Buffer.from('1')] 24 | */ 25 | const [pdaAccountKey, _bump] = PublicKey.findProgramAddressSync([keypair.publicKey.toBuffer()],programId); 26 | console.info(`Create DataAccountKey: ${pdaAccountKey}`); 27 | // 构建调用智能合约Instruction操作 28 | const instruction = new TransactionInstruction({ 29 | // 调用只能合约所需要的账户(注意:账户的顺序要和合约里面的一致) 30 | keys: [{ 31 | pubkey: keypair.publicKey, 32 | isSigner: true, 33 | isWritable: true 34 | },{ 35 | pubkey: pdaAccountKey, 36 | isSigner: false, 37 | isWritable: true 38 | },{ 39 | pubkey: SystemProgram.programId, 40 | isSigner: false, 41 | isWritable: false 42 | }], 43 | // 调用智能合约的参数(就是input) 44 | data: Buffer.from(new Uint8Array([0])), 45 | // 合约地址 46 | programId: programId 47 | }); 48 | // 构建交易 49 | let transaction = new Transaction().add(instruction); 50 | const signature = await sendAndConfirmTransaction(connection, transaction, [keypair]); 51 | return signature; 52 | } catch(error) { 53 | console.error(error); 54 | // @ts-ignore 55 | throw new Error(error.message); 56 | } 57 | } 58 | 59 | export default CCreateDataAccount; -------------------------------------------------------------------------------- /hw_02_basic_example/src/utils/cDeleteDataAccount.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Connection, Keypair, 3 | PublicKey, sendAndConfirmTransaction, 4 | Transaction, 5 | TransactionInstruction 6 | } from "@solana/web3.js"; 7 | 8 | /** 9 | * 删除数据账户 10 | * @param connection 11 | * @param keypair 12 | * @param programId 13 | */ 14 | const CDeleteDataAccount = async (connection: Connection, keypair: Keypair, programId: PublicKey): Promise => { 15 | try { 16 | /** 17 | * 我们生成PDA地址的时候就是用用户地址作为种子生成的,所以这里我们直接用这种方式拿到就可以了(注意:种子不变所生成的地址就不会变) 18 | */ 19 | const [dataAccountKey, _bump] = PublicKey.findProgramAddressSync([keypair.publicKey.toBuffer()],programId); 20 | console.info(`Remove DataAccountKey: ${dataAccountKey}`); 21 | // 构建调用智能合约Instruction操作 22 | const instruction = new TransactionInstruction({ 23 | // 调用只能合约所需要的账户(注意:账户的顺序要和合约里面的一致) 24 | keys: [{ 25 | pubkey: keypair.publicKey, 26 | isSigner: true, 27 | isWritable: true 28 | },{ 29 | pubkey: dataAccountKey, 30 | isSigner: false, 31 | isWritable: true 32 | }], 33 | // 调用智能合约的参数(就是input) 34 | data: Buffer.from(new Uint8Array([2])), 35 | // 合约地址 36 | programId: programId 37 | }); 38 | // 构建交易 39 | let transaction = new Transaction().add(instruction); 40 | const signature = await sendAndConfirmTransaction(connection, transaction, [keypair]); 41 | return signature; 42 | } catch(error) { 43 | console.error(error); 44 | // @ts-ignore 45 | throw new Error(error.message); 46 | } 47 | } 48 | 49 | export default CDeleteDataAccount; -------------------------------------------------------------------------------- /hw_02_basic_example/src/utils/cMultiParamTest.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Connection, Keypair, 3 | PublicKey, sendAndConfirmTransaction, 4 | Transaction, 5 | TransactionInstruction 6 | } from "@solana/web3.js"; 7 | 8 | import {struct,u8,u32,nu64,blob} from '@solana/buffer-layout'; 9 | 10 | // 定义参数结构体 11 | interface ParamStruct { 12 | i: number, 13 | aa: number, 14 | bb: number, 15 | cc: number, 16 | dd: Uint8Array 17 | } 18 | 19 | /** 20 | * 多参数调用合约测试(使用Solana buffer-layout装配合约调用参数) 21 | * @param connection 22 | * @param keypair 23 | * @param programId 24 | */ 25 | const CMultiParamTest = async (connection: Connection, keypair: Keypair, programId: PublicKey): Promise => { 26 | try { 27 | // 参数 28 | let param:ParamStruct = { 29 | i: 3, 30 | aa: 10211, 31 | bb: 233, 32 | cc: 1456161561546, 33 | dd: new TextEncoder().encode(new Date().getTime().toString()) 34 | } 35 | console.info(`aaValue=${param.aa},bbValue=${param.bb},ccValue=${param.cc},ddValue=${new TextDecoder().decode(param.dd)}`); 36 | // 构建参数编码器(注意:顺序要和智能合约里面解析参数的顺序一致) 37 | let paramEncode = struct([ 38 | u8('i'), 39 | u32('aa'), 40 | u8('bb'), 41 | nu64('cc'), 42 | blob(param.dd.length,"dd"), 43 | ]); 44 | // 初始化参数字节数组缓冲区 45 | const inputArray = Buffer.alloc(paramEncode.span); 46 | // 编码参数(将所有参数的值转换成一个字节数组) 47 | paramEncode.encode(param,inputArray); 48 | // 构建调用智能合约Instruction操作 49 | const instruction = new TransactionInstruction({ 50 | // 调用只能合约所需要的账户(注意:账户的顺序要和合约里面的一致) 51 | keys: [{ 52 | pubkey: keypair.publicKey, 53 | isSigner: true, 54 | isWritable: true 55 | }], 56 | // 调用智能合约的参数(就是input) 57 | data: inputArray, 58 | // 合约地址 59 | programId: programId 60 | }); 61 | // 构建交易 62 | let transaction = new Transaction().add(instruction); 63 | const signature = await sendAndConfirmTransaction(connection, transaction, [keypair]); 64 | return signature; 65 | } catch(error) { 66 | console.error(error); 67 | // @ts-ignore 68 | throw new Error(error.message); 69 | } 70 | } 71 | 72 | export default CMultiParamTest; -------------------------------------------------------------------------------- /hw_02_basic_example/src/utils/cQueryDataAccount.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Connection, 3 | Keypair, 4 | PublicKey, 5 | } from "@solana/web3.js"; 6 | 7 | import { cstr, u8, struct } from "@solana/buffer-layout"; 8 | import { publicKey } from "@solana/buffer-layout-utils"; 9 | 10 | // 定义账户数据结构体 11 | interface DataStruct { 12 | accountKey: PublicKey, 13 | state: number, 14 | dataLen: number, 15 | data: String 16 | } 17 | // 构建账户数据结构体解码器(注意:顺序要和智能合约里面编码结构体的顺序一致) 18 | const DataStructDecode = struct([ 19 | publicKey('accountKey'), 20 | u8('state'), 21 | u8('dataLen'), 22 | cstr('data') 23 | ]); 24 | 25 | /** 26 | * 查询账户数据 27 | * @param connection 28 | * @param keypair 29 | * @param programId 30 | */ 31 | const CQueryDataAccount = async (connection: Connection, keypair: Keypair, programId: PublicKey): Promise => { 32 | try { 33 | // 数据账户地址(我们创建数据账户的时候就是用这种方式生成的地址) 34 | const [pdaAccountKey, _bump] = PublicKey.findProgramAddressSync([keypair.publicKey.toBuffer()],programId); 35 | console.info(`Query DataAccountKey: ${pdaAccountKey}`); 36 | // 查询数据对象 37 | let accountInfo = await connection.getAccountInfo(pdaAccountKey); 38 | // 解码数据 39 | let dataInfo = DataStructDecode.decode(accountInfo.data); 40 | // @ts-ignore 41 | return `account_key: ${dataInfo.accountKey.toBase58()},state: ${dataInfo.state},data_len; ${dataInfo.dataLen},data: ${dataInfo.data}`; 42 | } catch(error) { 43 | console.error(error); 44 | // @ts-ignore 45 | throw new Error(error.message); 46 | } 47 | } 48 | 49 | export default CQueryDataAccount; -------------------------------------------------------------------------------- /hw_02_basic_example/src/utils/cUpdateDataAccount.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Connection, Keypair, 3 | PublicKey, sendAndConfirmTransaction, 4 | Transaction, 5 | TransactionInstruction 6 | } from "@solana/web3.js"; 7 | 8 | /** 9 | * 修改数据账户的数据(注意:构建调用合约的参数不要使用下面这种方式,太基础了,建议使用buffer-layout,示例代码可查看 cMultiParamTest.ts 文件) 10 | * @param connection 11 | * @param keypair 12 | * @param programId 13 | */ 14 | const CUpdateDataAccount = async (connection: Connection, keypair: Keypair, programId: PublicKey): Promise => { 15 | try { 16 | /** 17 | * 我们生成PDA地址的时候就是用用户地址作为种子生成的,所以这里我们直接用这种方式拿到就可以了(注意:种子不变所生成的地址就不会变) 18 | */ 19 | const [dataAccountKey, _bump] = PublicKey.findProgramAddressSync([keypair.publicKey.toBuffer()],programId); 20 | console.info(`Update DataAccountKey: ${dataAccountKey}`); 21 | let dataStr = new Date().getTime().toString(); 22 | console.info(`Update dataStr = ${dataStr}`); 23 | // 第一个字节是1表示是修改数据账户的数据(合约里面定义的规则) 24 | let firstBitArray = new Uint8Array([1]); 25 | // 数据账户的数据 26 | let dataArray = new TextEncoder().encode(dataStr); 27 | // 合并两个字节数组(注意:构建调用合约的参数不要使用下面这种方式,太基础了,建议使用buffer-layout,示例代码可查看 cMultiParamTest.ts 文件) 28 | let inputArray = new Uint8Array(firstBitArray.length + dataArray.length); 29 | // 从开始位置往数组里面填充 firstBitArray 数据(注意:set函数如果不传数据填充的开始位置,默认从第0个位置开始填充) 30 | inputArray.set(firstBitArray); 31 | // 从 firstBitArray.length 位置开始往数组里面填充 dataArray 数据 32 | inputArray.set(dataArray,firstBitArray.length); 33 | // 构建调用智能合约Instruction操作 34 | const instruction = new TransactionInstruction({ 35 | // 调用只能合约所需要的账户(注意:账户的顺序要和合约里面的一致) 36 | keys: [{ 37 | pubkey: keypair.publicKey, 38 | isSigner: true, 39 | isWritable: true 40 | },{ 41 | pubkey: dataAccountKey, 42 | isSigner: false, 43 | isWritable: true 44 | }], 45 | // 调用智能合约的参数(就是input) 46 | data: Buffer.from(inputArray), 47 | // 合约地址 48 | programId: programId 49 | }); 50 | // 构建交易 51 | let transaction = new Transaction().add(instruction); 52 | const signature = await sendAndConfirmTransaction(connection, transaction, [keypair]); 53 | return signature; 54 | } catch(error) { 55 | console.error(error); 56 | // @ts-ignore 57 | throw new Error(error.message); 58 | } 59 | } 60 | 61 | export default CUpdateDataAccount; -------------------------------------------------------------------------------- /hw_02_basic_example/src/utils/hexToRGB.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 十六进制字符串转数字颜色 3 | * @param hex 4 | * @param alpha 5 | */ 6 | const hexToRGB = (hex: string, alpha: number) => { 7 | const r = parseInt(hex.slice(1, 3), 16); 8 | const g = parseInt(hex.slice(3, 5), 16); 9 | const b = parseInt(hex.slice(5, 7), 16); 10 | 11 | return `rgba(${r},${g},${b},${alpha})`; 12 | }; 13 | 14 | export default hexToRGB; -------------------------------------------------------------------------------- /hw_02_basic_example/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export {default as phantomGetProvider} from './phantomGetProvider'; 2 | export {default as hexToRGB} from './hexToRGB'; 3 | export {default as pollSignatureStatus} from './pollSignatureStatus'; 4 | 5 | export {default as phantomSignMessage} from './phantomSignMessage'; 6 | export {default as phantomCreateDataAccount} from './phantomCreateDataAccount'; 7 | 8 | export {default as createDataAccount} from './cCreateDataAccount'; 9 | export {default as updateDataAccount} from './cUpdateDataAccount'; 10 | export {default as deleteDataAccount} from './cDeleteDataAccount'; 11 | export {default as queryDataAccount} from './cQueryDataAccount'; 12 | export {default as multiParamTest} from './cMultiParamTest'; 13 | -------------------------------------------------------------------------------- /hw_02_basic_example/src/utils/phantomCreateDataAccount.ts: -------------------------------------------------------------------------------- 1 | import {PhantomProvider} from "../types"; 2 | import { 3 | Connection, 4 | PublicKey, 5 | SystemProgram, 6 | Transaction, 7 | TransactionInstruction 8 | } from "@solana/web3.js"; 9 | 10 | /** 11 | * 通过Phantom签名提交交易创建数据账户 12 | * @param connection 13 | * @param provider 14 | * @param programId 15 | */ 16 | const phantomCreateDataAccount = async (connection: Connection, provider: PhantomProvider, programId: PublicKey): Promise => { 17 | try { 18 | /** 19 | * 创建数据账户地址(注意:这个就是创建PDA地址,但是数据账户是我们在合约里面创建的而不是直接在前端创建的,因为PDA地址不能直接在前端创建数据账户,原因是它没有私钥不能签名) 20 | * 说明:因为我们的程序只需要用户创建一个数据账户,所以直接使用用户地址作为种子创建PDA地址。 21 | * 如果程序是需要用户创建多个数据账户,那么每个PDA地址的种子是需要不一样的,可根据每个数据账户的作用来定。 22 | * 比如第一个数据账户的种子:[Buffer.from('1')] 23 | */ 24 | const [pdaAccountKey, _bump] = PublicKey.findProgramAddressSync([provider.publicKey.toBuffer()],programId); 25 | // 构建调用智能合约Instruction操作 26 | const instruction = new TransactionInstruction({ 27 | // 调用只能合约所需要的账户(注意:账户的顺序要和合约里面的一致) 28 | keys: [{ 29 | pubkey: provider.publicKey, 30 | isSigner: true, 31 | isWritable: true 32 | },{ 33 | pubkey: pdaAccountKey, 34 | isSigner: false, 35 | isWritable: true 36 | },{ 37 | pubkey: SystemProgram.programId, 38 | isSigner: false, 39 | isWritable: false 40 | }], 41 | // 调用智能合约的参数(就是input) 42 | data: Buffer.from(new Uint8Array([0])), 43 | // 合约地址 44 | programId: programId 45 | }); 46 | // 构建交易 47 | let transaction = new Transaction().add(instruction); 48 | const anyTransaction: any = transaction; 49 | // gas费地址 50 | anyTransaction.feePayer = provider.publicKey; 51 | // 获取最新的BlockHash 52 | anyTransaction.recentBlockhash = (await connection.getLatestBlockhash()).blockhash; 53 | // 签名并发送交易 54 | const {signature} = await provider.signAndSendTransaction(transaction); 55 | return signature; 56 | } catch(error) { 57 | console.info(error); 58 | // @ts-ignore 59 | throw new Error(error.message); 60 | } 61 | } 62 | 63 | export default phantomCreateDataAccount; -------------------------------------------------------------------------------- /hw_02_basic_example/src/utils/phantomCreateTransferTransaction.ts: -------------------------------------------------------------------------------- 1 | import {Transaction,SystemProgram,Connection,PublicKey} from '@solana/web3.js'; 2 | 3 | /** 4 | * 创建Phantom使用的转帐交易 5 | * @param publicKey 6 | * @param connection 7 | */ 8 | const phantomCreateTransferTransaction = async (publicKey: PublicKey, connection: Connection):Promise => { 9 | const transaction = new Transaction().add( 10 | SystemProgram.transfer({ 11 | fromPubkey: publicKey, 12 | toPubkey: SystemProgram.programId, 13 | lamports: 100 14 | }) 15 | ); 16 | // gas费交易地址 17 | transaction.feePayer = publicKey; 18 | const anyTransaction: any = transaction; 19 | // 获取交易Hash 20 | anyTransaction.recentBlockhash = (await connection.getLatestBlockhash()).blockhash; 21 | 22 | return transaction; 23 | } 24 | 25 | export default phantomCreateTransferTransaction; -------------------------------------------------------------------------------- /hw_02_basic_example/src/utils/phantomGetProvider.ts: -------------------------------------------------------------------------------- 1 | import {PhantomProvider} from '../types' 2 | 3 | /** 4 | * 获取Phantom插件实例 5 | */ 6 | const phantomGetProvider = () : PhantomProvider | undefined => { 7 | if('phantom' in window) { 8 | const anyWindow: any = window; 9 | const provider = anyWindow.phantom?.solana; 10 | if(provider?.isPhantom) { 11 | return provider; 12 | } 13 | } 14 | window.open('https://phantom.app/','_blank') 15 | } 16 | 17 | export default phantomGetProvider; -------------------------------------------------------------------------------- /hw_02_basic_example/src/utils/phantomSignMessage.ts: -------------------------------------------------------------------------------- 1 | import {PhantomProvider} from '../types' 2 | 3 | /** 4 | * Phantom消息签名 5 | * @param provider 6 | * @param message 7 | */ 8 | const phantomSignMessage = async (provider: PhantomProvider, message: string): Promise => { 9 | try { 10 | // 编码消息 11 | const encodedMessage = new TextEncoder().encode(message); 12 | // 消息签名 13 | const signMessage = await provider.signMessage(encodedMessage); 14 | return signMessage; 15 | } catch(error) { 16 | console.warn(error); 17 | // @ts-ignore 18 | throw new Error(error.message); 19 | } 20 | } 21 | 22 | export default phantomSignMessage; -------------------------------------------------------------------------------- /hw_02_basic_example/src/utils/phantomSignTransaction.ts: -------------------------------------------------------------------------------- 1 | import {Transaction} from '@solana/web3.js'; 2 | import {PhantomProvider} from '../types'; 3 | 4 | /** 5 | * Phantom签名交易 6 | * @param provider 7 | * @param transaction 8 | */ 9 | const phantomSignTransaction = async (provider: PhantomProvider,transaction: Transaction):Promise => { 10 | try{ 11 | // 签名交易 12 | const signTransaction = await provider.signTransaction(transaction); 13 | // 签名多个交易 14 | //const transactions = await provider.signAllTransactions([transaction1,transaction2]); 15 | // 使用密钥继续签名 16 | //signTransaction.partialSign(keypair); 17 | return signTransaction; 18 | }catch(error) { 19 | console.warn(error); 20 | // @ts-ignore 21 | throw new Error(error.message); 22 | } 23 | } 24 | 25 | export default phantomSignTransaction; -------------------------------------------------------------------------------- /hw_02_basic_example/src/utils/pollSignatureStatus.ts: -------------------------------------------------------------------------------- 1 | import {Connection} from '@solana/web3.js'; 2 | import {TLog} from '../types'; 3 | 4 | // 交易查询时间间隔(单位毫秒) 5 | const POLLING_INTERVAL = 1000; 6 | // 交易确认最大时间(单位秒,交易在规定时间内未确认前端直接显示失败) 7 | const MAX_POLLS = 30; 8 | 9 | /** 10 | * 交易状态查询 11 | * @param signature 12 | * @param connection 13 | * @param createLog 14 | */ 15 | const pollSignatureStatus = async(signature: string,connection: Connection,createLog: (log: TLog) => void,method: any): Promise => { 16 | let count = 0; 17 | const interval = setInterval(async () => { 18 | if(count === MAX_POLLS) { 19 | clearInterval(interval); 20 | createLog({ 21 | status: 'error', 22 | method: method, 23 | message: `Transaction: ${signature}`, 24 | messageTwo: `交易未能在 ${MAX_POLLS} 秒内确认交易. 交易可能成功,也可能失败.`, 25 | }); 26 | return; 27 | } 28 | // 查询交易状态 29 | const {value} = await connection.getSignatureStatus(signature); 30 | // 交易状态 31 | const confirmationStatus = value?.confirmationStatus; 32 | if(confirmationStatus) { 33 | // 交易状态为已提交或已完成,该值则为true 34 | const hasReachedSufficientCommitment = confirmationStatus === 'confirmed' || confirmationStatus === 'finalized'; 35 | createLog({ 36 | status: hasReachedSufficientCommitment ? 'success' : 'info', 37 | method: method, 38 | message: `Transaction ${signature}`, 39 | messageTwo: `交易状态: ${confirmationStatus.charAt(0).toUpperCase() + confirmationStatus.slice(1)}`, 40 | }); 41 | // 交易状态为已提交或已完成 42 | if(hasReachedSufficientCommitment) { 43 | clearInterval(interval); 44 | return; 45 | } 46 | } else { 47 | createLog({ 48 | status: 'info', 49 | method: method, 50 | message: `Transaction ${signature}`, 51 | messageTwo: '交易状态:等待确认...', 52 | }); 53 | } 54 | count++; 55 | },POLLING_INTERVAL); 56 | } 57 | 58 | export default pollSignatureStatus; -------------------------------------------------------------------------------- /hw_02_basic_example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "./src/**/*" 4 | ], 5 | "exclude": [ 6 | "node_modules" 7 | ], 8 | "compilerOptions": { 9 | "strict": false, 10 | "esModuleInterop": true, 11 | "lib": [ 12 | "dom", 13 | "dom.iterable", 14 | "esnext", 15 | "es2015" 16 | ], 17 | "jsx": "react-jsx", 18 | "target": "es6", 19 | "allowJs": true, 20 | "skipLibCheck": true, 21 | "allowSyntheticDefaultImports": true, 22 | "forceConsistentCasingInFileNames": true, 23 | "noFallthroughCasesInSwitch": false, 24 | "module": "esnext", 25 | "moduleResolution": "node", 26 | "resolveJsonModule": true, 27 | "isolatedModules": true, 28 | "noEmit": true 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /hw_03_simple_token/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hw_03_simple_token" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [features] 7 | no-entrypoint = [] 8 | test-sbf = [] 9 | 10 | [dependencies] 11 | arrayref = "0.3.6" 12 | bytemuck = "1.7.2" 13 | num-derive = "0.3.3" 14 | num-traits = "0.2.15" 15 | num_enum = "0.5.7" 16 | solana-program = "1.10.33" 17 | thiserror = "1.0.31" 18 | 19 | [dev-dependencies] 20 | lazy_static = "1.4.0" 21 | proptest = "1.0.0" 22 | serial_test = "0.6.0" 23 | solana-program-test = "1.10.33" 24 | solana-sdk = "1.10.33" 25 | 26 | 27 | [lib] 28 | name = "hw_03_simple_token" 29 | crate-type = ["cdylib", "lib"] 30 | -------------------------------------------------------------------------------- /hw_03_simple_token/README.md: -------------------------------------------------------------------------------- 1 | #### 官方简单Token实现 -------------------------------------------------------------------------------- /hw_03_simple_token/Xargo.toml: -------------------------------------------------------------------------------- 1 | [target.bpfel-unknown-unknown.dependencies.std] 2 | features = [] -------------------------------------------------------------------------------- /hw_03_simple_token/src/entrypoint.rs: -------------------------------------------------------------------------------- 1 | use crate::{error::TokenError,processor::Processor}; 2 | use solana_program:: { 3 | account_info::AccountInfo,entrypoint,entrypoint::ProgramResult, 4 | program_error::PrintProgramError,pubkey::Pubkey 5 | }; 6 | 7 | entrypoint!(process_instruction); 8 | 9 | fn process_instruction(program_id: &Pubkey,accounts: &[AccountInfo],input: &[u8]) -> ProgramResult { 10 | if let Err(error) = Processor::process(program_id,accounts,input) { 11 | error.print::(); 12 | return Err(error); 13 | } 14 | Ok(()) 15 | } 16 | 17 | -------------------------------------------------------------------------------- /hw_03_simple_token/src/error.rs: -------------------------------------------------------------------------------- 1 | //! Error types 2 | 3 | use num_derive::FromPrimitive; 4 | use solana_program::{ 5 | decode_error::DecodeError, 6 | msg, 7 | program_error::{PrintProgramError, ProgramError}, 8 | }; 9 | use thiserror::Error; 10 | 11 | /// Errors that may be returned by the Token program. 12 | #[derive(Clone, Debug, Eq, Error, FromPrimitive, PartialEq)] 13 | pub enum TokenError { 14 | // 0 15 | /// Lamport balance below rent-exempt threshold. 16 | #[error("Lamport balance below rent-exempt threshold")] 17 | NotRentExempt, 18 | /// Insufficient funds for the operation requested. 19 | #[error("Insufficient funds")] 20 | InsufficientFunds, 21 | /// Invalid Mint. 22 | #[error("Invalid Mint")] 23 | InvalidMint, 24 | /// Account not associated with this Mint. 25 | #[error("Account not associated with this Mint")] 26 | MintMismatch, 27 | /// Owner does not match. 28 | #[error("Owner does not match")] 29 | OwnerMismatch, 30 | 31 | // 5 32 | /// This token's supply is fixed and new tokens cannot be minted. 33 | #[error("Fixed supply")] 34 | FixedSupply, 35 | /// The account cannot be initialized because it is already being used. 36 | #[error("Already in use")] 37 | AlreadyInUse, 38 | /// Invalid number of provided signers. 39 | #[error("Invalid number of provided signers")] 40 | InvalidNumberOfProvidedSigners, 41 | /// Invalid number of required signers. 42 | #[error("Invalid number of required signers")] 43 | InvalidNumberOfRequiredSigners, 44 | /// State is uninitialized. 45 | #[error("State is unititialized")] 46 | UninitializedState, 47 | 48 | // 10 49 | /// Instruction does not support native tokens 50 | #[error("Instruction does not support native tokens")] 51 | NativeNotSupported, 52 | /// Non-native account can only be closed if its balance is zero 53 | #[error("Non-native account can only be closed if its balance is zero")] 54 | NonNativeHasBalance, 55 | /// Invalid instruction 56 | #[error("Invalid instruction")] 57 | InvalidInstruction, 58 | /// State is invalid for requested operation. 59 | #[error("State is invalid for requested operation")] 60 | InvalidState, 61 | /// Operation overflowed 62 | #[error("Operation overflowed")] 63 | Overflow, 64 | 65 | // 15 66 | /// Account does not support specified authority type. 67 | #[error("Account does not support specified authority type")] 68 | AuthorityTypeNotSupported, 69 | /// This token mint cannot freeze accounts. 70 | #[error("This token mint cannot freeze accounts")] 71 | MintCannotFreeze, 72 | /// Account is frozen; all account operations will fail 73 | #[error("Account is frozen")] 74 | AccountFrozen, 75 | /// Mint decimals mismatch between the client and mint 76 | #[error("The provided decimals value different from the Mint decimals")] 77 | MintDecimalsMismatch, 78 | /// Instruction does not support non-native tokens 79 | #[error("Instruction does not support non-native tokens")] 80 | NonNativeNotSupported, 81 | } 82 | impl From for ProgramError { 83 | fn from(e: TokenError) -> Self { 84 | ProgramError::Custom(e as u32) 85 | } 86 | } 87 | impl DecodeError for TokenError { 88 | fn type_of() -> &'static str { 89 | "TokenError" 90 | } 91 | } 92 | 93 | // 打印错误信息 94 | impl PrintProgramError for TokenError { 95 | fn print(&self) 96 | where 97 | E: 'static 98 | + std::error::Error 99 | + DecodeError 100 | + PrintProgramError 101 | + num_traits::FromPrimitive, 102 | { 103 | match self { 104 | TokenError::NotRentExempt => msg!("Error: Lamport balance below rent-exempt threshold"), 105 | TokenError::InsufficientFunds => msg!("Error: insufficient funds"), 106 | TokenError::InvalidMint => msg!("Error: Invalid Mint"), 107 | TokenError::MintMismatch => msg!("Error: Account not associated with this Mint"), 108 | TokenError::OwnerMismatch => msg!("Error: owner does not match"), 109 | TokenError::FixedSupply => msg!("Error: the total supply of this token is fixed"), 110 | TokenError::AlreadyInUse => msg!("Error: account or token already in use"), 111 | TokenError::InvalidNumberOfProvidedSigners => { 112 | msg!("Error: Invalid number of provided signers") 113 | } 114 | TokenError::InvalidNumberOfRequiredSigners => { 115 | msg!("Error: Invalid number of required signers") 116 | } 117 | TokenError::UninitializedState => msg!("Error: State is uninitialized"), 118 | TokenError::NativeNotSupported => { 119 | msg!("Error: Instruction does not support native tokens") 120 | } 121 | TokenError::NonNativeHasBalance => { 122 | msg!("Error: Non-native account can only be closed if its balance is zero") 123 | } 124 | TokenError::InvalidInstruction => msg!("Error: Invalid instruction"), 125 | TokenError::InvalidState => msg!("Error: Invalid account state for operation"), 126 | TokenError::Overflow => msg!("Error: Operation overflowed"), 127 | TokenError::AuthorityTypeNotSupported => { 128 | msg!("Error: Account does not support specified authority type") 129 | } 130 | TokenError::MintCannotFreeze => msg!("Error: This token mint cannot freeze accounts"), 131 | TokenError::AccountFrozen => msg!("Error: Account is frozen"), 132 | TokenError::MintDecimalsMismatch => { 133 | msg!("Error: decimals different from the Mint decimals") 134 | } 135 | TokenError::NonNativeNotSupported => { 136 | msg!("Error: Instruction does not support non-native tokens") 137 | } 138 | } 139 | } 140 | } -------------------------------------------------------------------------------- /hw_03_simple_token/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::integer_arithmetic)] 2 | #![deny(missing_docs)] 3 | #![cfg_attr(not(test), forbid(unsafe_code))] 4 | 5 | //! An ERC20-like Token program for the Solana blockchain 6 | 7 | pub mod error; 8 | pub mod instruction; 9 | pub mod native_mint; 10 | pub mod processor; 11 | pub mod state; 12 | 13 | #[cfg(not(feature = "no-entrypoint"))] 14 | mod entrypoint; 15 | 16 | // Export current sdk types for downstream users building with a different sdk version 17 | pub use solana_program; 18 | use solana_program::{entrypoint::ProgramResult, program_error::ProgramError, pubkey::Pubkey}; 19 | 20 | /// Convert the UI representation of a token amount (using the decimals field defined in its mint) 21 | /// to the raw amount 22 | pub fn ui_amount_to_amount(ui_amount: f64, decimals: u8) -> u64 { 23 | (ui_amount * 10_usize.pow(decimals as u32) as f64) as u64 24 | } 25 | 26 | /// Convert a raw amount to its UI representation (using the decimals field defined in its mint) 27 | pub fn amount_to_ui_amount(amount: u64, decimals: u8) -> f64 { 28 | amount as f64 / 10_usize.pow(decimals as u32) as f64 29 | } 30 | 31 | /// Convert a raw amount to its UI representation (using the decimals field defined in its mint) 32 | pub fn amount_to_ui_amount_string(amount: u64, decimals: u8) -> String { 33 | let decimals = decimals as usize; 34 | if decimals > 0 { 35 | // Left-pad zeros to decimals + 1, so we at least have an integer zero 36 | let mut s = format!("{:01$}", amount, decimals + 1); 37 | // Add the decimal point (Sorry, "," locales!) 38 | s.insert(s.len() - decimals, '.'); 39 | s 40 | } else { 41 | amount.to_string() 42 | } 43 | } 44 | 45 | /// Convert a raw amount to its UI representation using the given decimals field 46 | /// Excess zeroes or unneeded decimal point are trimmed. 47 | pub fn amount_to_ui_amount_string_trimmed(amount: u64, decimals: u8) -> String { 48 | let mut s = amount_to_ui_amount_string(amount, decimals); 49 | if decimals > 0 { 50 | let zeros_trimmed = s.trim_end_matches('0'); 51 | s = zeros_trimmed.trim_end_matches('.').to_string(); 52 | } 53 | s 54 | } 55 | 56 | /// Try to convert a UI represenation of a token amount to its raw amount using the given decimals 57 | /// field 58 | pub fn try_ui_amount_into_amount(ui_amount: String, decimals: u8) -> Result { 59 | let decimals = decimals as usize; 60 | let mut parts = ui_amount.split('.'); 61 | let mut amount_str = parts.next().unwrap().to_string(); // splitting a string, even an empty one, will always yield an iterator of at least len == 1 62 | let after_decimal = parts.next().unwrap_or(""); 63 | let after_decimal = after_decimal.trim_end_matches('0'); 64 | if (amount_str.is_empty() && after_decimal.is_empty()) 65 | || parts.next().is_some() 66 | || after_decimal.len() > decimals 67 | { 68 | return Err(ProgramError::InvalidArgument); 69 | } 70 | 71 | amount_str.push_str(after_decimal); 72 | for _ in 0..decimals.saturating_sub(after_decimal.len()) { 73 | amount_str.push('0'); 74 | } 75 | amount_str 76 | .parse::() 77 | .map_err(|_| ProgramError::InvalidArgument) 78 | } 79 | 80 | solana_program::declare_id!("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"); 81 | 82 | /// Checks that the supplied program ID is the correct one for SPL-token 83 | pub fn check_program_account(spl_token_program_id: &Pubkey) -> ProgramResult { 84 | if spl_token_program_id != &id() { 85 | return Err(ProgramError::IncorrectProgramId); 86 | } 87 | Ok(()) 88 | } 89 | 90 | #[cfg(test)] 91 | mod Test { 92 | 93 | use crate::amount_to_ui_amount_string_trimmed; 94 | 95 | #[test] 96 | fn test_amount_to_ui_amount_string_trimmed() { 97 | let amount:u64 = 18446744073709551610; 98 | let decimals:u8 = 8; 99 | let val:String = amount_to_ui_amount_string_trimmed(amount,decimals); 100 | println!("val={}",val); 101 | } 102 | 103 | } -------------------------------------------------------------------------------- /hw_03_simple_token/src/native_mint.rs: -------------------------------------------------------------------------------- 1 | //! The Mint that represents the native token 2 | 3 | /// There are 10^9 lamports in one SOL 4 | pub const DECIMALS: u8 = 9; 5 | 6 | // The Mint for native SOL Token accounts 7 | solana_program::declare_id!("So11111111111111111111111111111111111111112"); 8 | 9 | #[cfg(test)] 10 | mod tests { 11 | use super::*; 12 | use solana_program::native_token::*; 13 | 14 | #[test] 15 | fn test_decimals() { 16 | assert!( 17 | (lamports_to_sol(42) - crate::amount_to_ui_amount(42, DECIMALS)).abs() < f64::EPSILON 18 | ); 19 | assert_eq!( 20 | sol_to_lamports(42.), 21 | crate::ui_amount_to_amount(42., DECIMALS) 22 | ); 23 | } 24 | } -------------------------------------------------------------------------------- /hw_03_simple_token/src/state.rs: -------------------------------------------------------------------------------- 1 | //! State transition types 2 | 3 | use crate::instruction::MAX_SIGNERS; 4 | use arrayref::{array_mut_ref, array_ref, array_refs, mut_array_refs}; 5 | use num_enum::TryFromPrimitive; 6 | use solana_program::{ 7 | program_error::ProgramError, 8 | program_option::COption, 9 | program_pack::{IsInitialized, Pack, Sealed}, 10 | pubkey::{Pubkey, PUBKEY_BYTES}, 11 | }; 12 | 13 | /// Mint data. 14 | /// 类似于代币合约信息,因为每发行一个代币就会存储一个Mint信息(简单理解就是ERC20合约信息) 15 | /// 说明:Solana上发行代币是通过同一个合约完成的(不像Solidity每发行一个代币都要部署一个智能合约) 16 | #[repr(C)] 17 | #[derive(Clone, Copy, Debug, Default, PartialEq)] 18 | pub struct Mint { 19 | /// Optional authority used to mint new tokens. The mint authority may only be provided during 20 | /// mint creation. If no mint authority is present then the mint has a fixed supply and no 21 | /// further tokens may be minted. 22 | /// 谁可以铸造代币 23 | pub mint_authority: COption, 24 | /// Total supply of tokens. 25 | /// 代币总量(注意:这个数字是每铸造了多少代币就会增加) 26 | pub supply: u64, 27 | /// Number of base 10 digits to the right of the decimal place. 28 | /// 代币精度 29 | pub decimals: u8, 30 | /// Is `true` if this structure has been initialized 31 | /// 代币是否已经存在 32 | pub is_initialized: bool, 33 | /// Optional authority to freeze token accounts. 34 | /// 谁可以回收代币 35 | pub freeze_authority: COption, 36 | } 37 | impl Sealed for Mint {} 38 | impl IsInitialized for Mint { 39 | fn is_initialized(&self) -> bool { 40 | self.is_initialized 41 | } 42 | } 43 | impl Pack for Mint { 44 | const LEN: usize = 82; 45 | fn unpack_from_slice(src: &[u8]) -> Result { 46 | let src = array_ref![src, 0, 82]; 47 | let (mint_authority, supply, decimals, is_initialized, freeze_authority) = 48 | array_refs![src, 36, 8, 1, 1, 36]; 49 | let mint_authority = unpack_coption_key(mint_authority)?; 50 | let supply = u64::from_le_bytes(*supply); 51 | let decimals = decimals[0]; 52 | let is_initialized = match is_initialized { 53 | [0] => false, 54 | [1] => true, 55 | _ => return Err(ProgramError::InvalidAccountData), 56 | }; 57 | let freeze_authority = unpack_coption_key(freeze_authority)?; 58 | Ok(Mint { 59 | mint_authority, 60 | supply, 61 | decimals, 62 | is_initialized, 63 | freeze_authority, 64 | }) 65 | } 66 | fn pack_into_slice(&self, dst: &mut [u8]) { 67 | let dst = array_mut_ref![dst, 0, 82]; 68 | let ( 69 | mint_authority_dst, 70 | supply_dst, 71 | decimals_dst, 72 | is_initialized_dst, 73 | freeze_authority_dst, 74 | ) = mut_array_refs![dst, 36, 8, 1, 1, 36]; 75 | let &Mint { 76 | ref mint_authority, 77 | supply, 78 | decimals, 79 | is_initialized, 80 | ref freeze_authority, 81 | } = self; 82 | pack_coption_key(mint_authority, mint_authority_dst); 83 | *supply_dst = supply.to_le_bytes(); 84 | decimals_dst[0] = decimals; 85 | is_initialized_dst[0] = is_initialized as u8; 86 | pack_coption_key(freeze_authority, freeze_authority_dst); 87 | } 88 | } 89 | 90 | /// Account data. 91 | /// 一个用户地址所拥有的某个代币账户信息 92 | #[repr(C)] 93 | #[derive(Clone, Copy, Debug, Default, PartialEq)] 94 | pub struct Account { 95 | /// The mint associated with this account 96 | /// 代币地址(简单理解就是合约地址) 97 | pub mint: Pubkey, 98 | /// The owner of this account. 99 | /// 账户拥有者 100 | pub owner: Pubkey, 101 | /// The amount of tokens this account holds. 102 | /// 代币数量 103 | pub amount: u64, 104 | /// If `delegate` is `Some` then `delegated_amount` represents 105 | /// the amount authorized by the delegate 106 | /// 授权地址(就是授权某个地址可以操控该账户) 107 | pub delegate: COption, 108 | /// The account's state 109 | /// 账户状态信息 110 | pub state: AccountState, 111 | /// If is_native.is_some, this is a native token, and the value logs the rent-exempt reserve. An 112 | /// Account is required to be rent-exempt, so the value is used by the Processor to ensure that 113 | /// wrapped SOL accounts do not drop below this threshold. 114 | /// 是不是系统代币(类似于Etherscan上的 WETH) 115 | pub is_native: COption, 116 | /// The amount delegated 117 | /// 授权金额 118 | pub delegated_amount: u64, 119 | /// Optional authority to close the account. 120 | /// 可关闭当前账户的地址 121 | pub close_authority: COption, 122 | } 123 | impl Account { 124 | /// Checks if account is frozen 125 | pub fn is_frozen(&self) -> bool { 126 | self.state == AccountState::Frozen 127 | } 128 | /// Checks if account is native 129 | pub fn is_native(&self) -> bool { 130 | self.is_native.is_some() 131 | } 132 | /// Checks if a token Account's owner is the system_program or the incinerator 133 | pub fn is_owned_by_system_program_or_incinerator(&self) -> bool { 134 | solana_program::system_program::check_id(&self.owner) || solana_program::incinerator::check_id(&self.owner) 135 | } 136 | } 137 | impl Sealed for Account {} 138 | impl IsInitialized for Account { 139 | fn is_initialized(&self) -> bool { 140 | self.state != AccountState::Uninitialized 141 | } 142 | } 143 | impl Pack for Account { 144 | const LEN: usize = 165; 145 | fn unpack_from_slice(src: &[u8]) -> Result { 146 | let src = array_ref![src, 0, 165]; 147 | let (mint, owner, amount, delegate, state, is_native, delegated_amount, close_authority) = 148 | array_refs![src, 32, 32, 8, 36, 1, 12, 8, 36]; 149 | Ok(Account { 150 | mint: Pubkey::new_from_array(*mint), 151 | owner: Pubkey::new_from_array(*owner), 152 | amount: u64::from_le_bytes(*amount), 153 | delegate: unpack_coption_key(delegate)?, 154 | state: AccountState::try_from_primitive(state[0]) 155 | .or(Err(ProgramError::InvalidAccountData))?, 156 | is_native: unpack_coption_u64(is_native)?, 157 | delegated_amount: u64::from_le_bytes(*delegated_amount), 158 | close_authority: unpack_coption_key(close_authority)?, 159 | }) 160 | } 161 | fn pack_into_slice(&self, dst: &mut [u8]) { 162 | let dst = array_mut_ref![dst, 0, 165]; 163 | let ( 164 | mint_dst, 165 | owner_dst, 166 | amount_dst, 167 | delegate_dst, 168 | state_dst, 169 | is_native_dst, 170 | delegated_amount_dst, 171 | close_authority_dst, 172 | ) = mut_array_refs![dst, 32, 32, 8, 36, 1, 12, 8, 36]; 173 | let &Account { 174 | ref mint, 175 | ref owner, 176 | amount, 177 | ref delegate, 178 | state, 179 | ref is_native, 180 | delegated_amount, 181 | ref close_authority, 182 | } = self; 183 | mint_dst.copy_from_slice(mint.as_ref()); 184 | owner_dst.copy_from_slice(owner.as_ref()); 185 | *amount_dst = amount.to_le_bytes(); 186 | pack_coption_key(delegate, delegate_dst); 187 | state_dst[0] = state as u8; 188 | pack_coption_u64(is_native, is_native_dst); 189 | *delegated_amount_dst = delegated_amount.to_le_bytes(); 190 | pack_coption_key(close_authority, close_authority_dst); 191 | } 192 | } 193 | 194 | /// Account state. 195 | #[repr(u8)] 196 | #[derive(Clone, Copy, Debug, PartialEq, TryFromPrimitive)] 197 | pub enum AccountState { 198 | /// Account is not yet initialized 199 | Uninitialized, 200 | /// Account is initialized; the account owner and/or delegate may perform permitted operations 201 | /// on this account 202 | Initialized, 203 | /// Account has been frozen by the mint freeze authority. Neither the account owner nor 204 | /// the delegate are able to perform operations on this account. 205 | Frozen, 206 | } 207 | 208 | impl Default for AccountState { 209 | fn default() -> Self { 210 | AccountState::Uninitialized 211 | } 212 | } 213 | 214 | /// Multisignature data. 215 | /// 多钱签钱包数据结构体 216 | #[repr(C)] 217 | #[derive(Clone, Copy, Debug, Default, PartialEq)] 218 | pub struct Multisig { 219 | /// Number of signers required 220 | /// 需要通过的签名数量 221 | pub m: u8, 222 | /// Number of valid signers 223 | /// 需要验证的签名数量 224 | pub n: u8, 225 | /// Is `true` if this structure has been initialized 226 | /// 是否已初始化 227 | pub is_initialized: bool, 228 | /// Signer public keys 229 | /// 参与签名者地址 230 | pub signers: [Pubkey; MAX_SIGNERS], 231 | } 232 | impl Sealed for Multisig {} 233 | impl IsInitialized for Multisig { 234 | fn is_initialized(&self) -> bool { 235 | self.is_initialized 236 | } 237 | } 238 | impl Pack for Multisig { 239 | const LEN: usize = 355; 240 | fn unpack_from_slice(src: &[u8]) -> Result { 241 | let src = array_ref![src, 0, 355]; 242 | #[allow(clippy::ptr_offset_with_cast)] 243 | let (m, n, is_initialized, signers_flat) = array_refs![src, 1, 1, 1, 32 * MAX_SIGNERS]; 244 | let mut result = Multisig { 245 | m: m[0], 246 | n: n[0], 247 | is_initialized: match is_initialized { 248 | [0] => false, 249 | [1] => true, 250 | _ => return Err(ProgramError::InvalidAccountData), 251 | }, 252 | signers: [Pubkey::new_from_array([0u8; 32]); MAX_SIGNERS], 253 | }; 254 | for (src, dst) in signers_flat.chunks(32).zip(result.signers.iter_mut()) { 255 | *dst = Pubkey::new(src); 256 | } 257 | Ok(result) 258 | } 259 | fn pack_into_slice(&self, dst: &mut [u8]) { 260 | let dst = array_mut_ref![dst, 0, 355]; 261 | #[allow(clippy::ptr_offset_with_cast)] 262 | let (m, n, is_initialized, signers_flat) = mut_array_refs![dst, 1, 1, 1, 32 * MAX_SIGNERS]; 263 | *m = [self.m]; 264 | *n = [self.n]; 265 | *is_initialized = [self.is_initialized as u8]; 266 | for (i, src) in self.signers.iter().enumerate() { 267 | let dst_array = array_mut_ref![signers_flat, 32 * i, 32]; 268 | dst_array.copy_from_slice(src.as_ref()); 269 | } 270 | } 271 | } 272 | 273 | // Helpers 274 | fn pack_coption_key(src: &COption, dst: &mut [u8; 36]) { 275 | let (tag, body) = mut_array_refs![dst, 4, 32]; 276 | match src { 277 | COption::Some(key) => { 278 | *tag = [1, 0, 0, 0]; 279 | body.copy_from_slice(key.as_ref()); 280 | } 281 | COption::None => { 282 | *tag = [0; 4]; 283 | } 284 | } 285 | } 286 | fn unpack_coption_key(src: &[u8; 36]) -> Result, ProgramError> { 287 | let (tag, body) = array_refs![src, 4, 32]; 288 | match *tag { 289 | [0, 0, 0, 0] => Ok(COption::None), 290 | [1, 0, 0, 0] => Ok(COption::Some(Pubkey::new_from_array(*body))), 291 | _ => Err(ProgramError::InvalidAccountData), 292 | } 293 | } 294 | fn pack_coption_u64(src: &COption, dst: &mut [u8; 12]) { 295 | let (tag, body) = mut_array_refs![dst, 4, 8]; 296 | match src { 297 | COption::Some(amount) => { 298 | *tag = [1, 0, 0, 0]; 299 | *body = amount.to_le_bytes(); 300 | } 301 | COption::None => { 302 | *tag = [0; 4]; 303 | } 304 | } 305 | } 306 | fn unpack_coption_u64(src: &[u8; 12]) -> Result, ProgramError> { 307 | let (tag, body) = array_refs![src, 4, 8]; 308 | match *tag { 309 | [0, 0, 0, 0] => Ok(COption::None), 310 | [1, 0, 0, 0] => Ok(COption::Some(u64::from_le_bytes(*body))), 311 | _ => Err(ProgramError::InvalidAccountData), 312 | } 313 | } 314 | 315 | const SPL_TOKEN_ACCOUNT_MINT_OFFSET: usize = 0; 316 | const SPL_TOKEN_ACCOUNT_OWNER_OFFSET: usize = 32; 317 | 318 | /// A trait for token Account structs to enable efficiently unpacking various fields 319 | /// without unpacking the complete state. 320 | pub trait GenericTokenAccount { 321 | /// Check if the account data is a valid token account 322 | fn valid_account_data(account_data: &[u8]) -> bool; 323 | 324 | /// Call after account length has already been verified to unpack the account owner 325 | fn unpack_account_owner_unchecked(account_data: &[u8]) -> &Pubkey { 326 | Self::unpack_pubkey_unchecked(account_data, SPL_TOKEN_ACCOUNT_OWNER_OFFSET) 327 | } 328 | 329 | /// Call after account length has already been verified to unpack the account mint 330 | fn unpack_account_mint_unchecked(account_data: &[u8]) -> &Pubkey { 331 | Self::unpack_pubkey_unchecked(account_data, SPL_TOKEN_ACCOUNT_MINT_OFFSET) 332 | } 333 | 334 | /// Call after account length has already been verified to unpack a Pubkey at 335 | /// the specified offset. Panics if `account_data.len()` is less than `PUBKEY_BYTES` 336 | fn unpack_pubkey_unchecked(account_data: &[u8], offset: usize) -> &Pubkey { 337 | bytemuck::from_bytes(&account_data[offset..offset + PUBKEY_BYTES]) 338 | } 339 | 340 | /// Unpacks an account's owner from opaque account data. 341 | fn unpack_account_owner(account_data: &[u8]) -> Option<&Pubkey> { 342 | if Self::valid_account_data(account_data) { 343 | Some(Self::unpack_account_owner_unchecked(account_data)) 344 | } else { 345 | None 346 | } 347 | } 348 | 349 | /// Unpacks an account's mint from opaque account data. 350 | fn unpack_account_mint(account_data: &[u8]) -> Option<&Pubkey> { 351 | if Self::valid_account_data(account_data) { 352 | Some(Self::unpack_account_mint_unchecked(account_data)) 353 | } else { 354 | None 355 | } 356 | } 357 | } 358 | 359 | /// The offset of state field in Account's C representation 360 | pub const ACCOUNT_INITIALIZED_INDEX: usize = 108; 361 | 362 | /// Check if the account data buffer represents an initialized account. 363 | /// This is checking the `state` (AccountState) field of an Account object. 364 | pub fn is_initialized_account(account_data: &[u8]) -> bool { 365 | *account_data 366 | .get(ACCOUNT_INITIALIZED_INDEX) 367 | .unwrap_or(&(AccountState::Uninitialized as u8)) 368 | != AccountState::Uninitialized as u8 369 | } 370 | 371 | impl GenericTokenAccount for Account { 372 | fn valid_account_data(account_data: &[u8]) -> bool { 373 | account_data.len() == Account::LEN && is_initialized_account(account_data) 374 | } 375 | } 376 | 377 | #[cfg(test)] 378 | mod tests { 379 | use super::*; 380 | 381 | #[test] 382 | fn test_mint_unpack_from_slice() { 383 | let src: [u8; 82] = [0; 82]; 384 | let mint = Mint::unpack_from_slice(&src).unwrap(); 385 | assert!(!mint.is_initialized); 386 | 387 | let mut src: [u8; 82] = [0; 82]; 388 | src[45] = 2; 389 | let mint = Mint::unpack_from_slice(&src).unwrap_err(); 390 | assert_eq!(mint, ProgramError::InvalidAccountData); 391 | } 392 | 393 | #[test] 394 | fn test_account_state() { 395 | let account_state = AccountState::default(); 396 | assert_eq!(account_state, AccountState::Uninitialized); 397 | } 398 | 399 | #[test] 400 | fn test_multisig_unpack_from_slice() { 401 | let src: [u8; 355] = [0; 355]; 402 | let multisig = Multisig::unpack_from_slice(&src).unwrap(); 403 | assert_eq!(multisig.m, 0); 404 | assert_eq!(multisig.n, 0); 405 | assert!(!multisig.is_initialized); 406 | 407 | let mut src: [u8; 355] = [0; 355]; 408 | src[0] = 1; 409 | src[1] = 1; 410 | src[2] = 1; 411 | let multisig = Multisig::unpack_from_slice(&src).unwrap(); 412 | assert_eq!(multisig.m, 1); 413 | assert_eq!(multisig.n, 1); 414 | assert!(multisig.is_initialized); 415 | 416 | let mut src: [u8; 355] = [0; 355]; 417 | src[2] = 2; 418 | let multisig = Multisig::unpack_from_slice(&src).unwrap_err(); 419 | assert_eq!(multisig, ProgramError::InvalidAccountData); 420 | } 421 | 422 | #[test] 423 | fn test_unpack_coption_key() { 424 | let src: [u8; 36] = [0; 36]; 425 | let result = unpack_coption_key(&src).unwrap(); 426 | assert_eq!(result, COption::None); 427 | 428 | let mut src: [u8; 36] = [0; 36]; 429 | src[1] = 1; 430 | let result = unpack_coption_key(&src).unwrap_err(); 431 | assert_eq!(result, ProgramError::InvalidAccountData); 432 | } 433 | 434 | #[test] 435 | fn test_unpack_coption_u64() { 436 | let src: [u8; 12] = [0; 12]; 437 | let result = unpack_coption_u64(&src).unwrap(); 438 | assert_eq!(result, COption::None); 439 | 440 | let mut src: [u8; 12] = [0; 12]; 441 | src[0] = 1; 442 | let result = unpack_coption_u64(&src).unwrap(); 443 | assert_eq!(result, COption::Some(0)); 444 | 445 | let mut src: [u8; 12] = [0; 12]; 446 | src[1] = 1; 447 | let result = unpack_coption_u64(&src).unwrap_err(); 448 | assert_eq!(result, ProgramError::InvalidAccountData); 449 | } 450 | 451 | #[test] 452 | fn test_unpack_token_owner() { 453 | // Account data length < Account::LEN, unpack will not return a key 454 | let src: [u8; 12] = [0; 12]; 455 | let result = Account::unpack_account_owner(&src); 456 | assert_eq!(result, Option::None); 457 | 458 | // The right account data size and intialized, unpack will return some key 459 | let mut src: [u8; Account::LEN] = [0; Account::LEN]; 460 | src[ACCOUNT_INITIALIZED_INDEX] = AccountState::Initialized as u8; 461 | let result = Account::unpack_account_owner(&src); 462 | assert!(result.is_some()); 463 | 464 | // The right account data size and frozen, unpack will return some key 465 | src[ACCOUNT_INITIALIZED_INDEX] = AccountState::Frozen as u8; 466 | let result = Account::unpack_account_owner(&src); 467 | assert!(result.is_some()); 468 | 469 | // The right account data size and uninitialized, unpack will return None 470 | src[ACCOUNT_INITIALIZED_INDEX] = AccountState::Uninitialized as u8; 471 | let result = Account::unpack_account_mint(&src); 472 | assert_eq!(result, Option::None); 473 | 474 | // Account data length > account data size, unpack will not return a key 475 | let src: [u8; Account::LEN + 5] = [0; Account::LEN + 5]; 476 | let result = Account::unpack_account_owner(&src); 477 | assert_eq!(result, Option::None); 478 | } 479 | 480 | #[test] 481 | fn test_unpack_token_mint() { 482 | // Account data length < Account::LEN, unpack will not return a key 483 | let src: [u8; 12] = [0; 12]; 484 | let result = Account::unpack_account_mint(&src); 485 | assert_eq!(result, Option::None); 486 | 487 | // The right account data size and initialized, unpack will return some key 488 | let mut src: [u8; Account::LEN] = [0; Account::LEN]; 489 | src[ACCOUNT_INITIALIZED_INDEX] = AccountState::Initialized as u8; 490 | let result = Account::unpack_account_mint(&src); 491 | assert!(result.is_some()); 492 | 493 | // The right account data size and frozen, unpack will return some key 494 | src[ACCOUNT_INITIALIZED_INDEX] = AccountState::Frozen as u8; 495 | let result = Account::unpack_account_mint(&src); 496 | assert!(result.is_some()); 497 | 498 | // The right account data size and uninitialized, unpack will return None 499 | src[ACCOUNT_INITIALIZED_INDEX] = AccountState::Uninitialized as u8; 500 | let result = Account::unpack_account_mint(&src); 501 | assert_eq!(result, Option::None); 502 | 503 | // Account data length > account data size, unpack will not return a key 504 | let src: [u8; Account::LEN + 5] = [0; Account::LEN + 5]; 505 | let result = Account::unpack_account_mint(&src); 506 | assert_eq!(result, Option::None); 507 | } 508 | } -------------------------------------------------------------------------------- /hw_04_token_swap/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hw_04_token_swap" 3 | version = "3.0.0" 4 | description = "Solana Program Library Token Swap" 5 | repository = "https://github.com/solana-labs/solana-program-library" 6 | license = "Apache-2.0" 7 | edition = "2021" 8 | 9 | [features] 10 | no-entrypoint = [] 11 | production = [] 12 | fuzz = ["arbitrary", "roots"] 13 | 14 | [dependencies] 15 | arrayref = "0.3.6" 16 | enum_dispatch = "0.3.7" 17 | num-derive = "0.3.3" 18 | num-traits = "0.2.15" 19 | solana-program = "1.10.33" 20 | spl-math = {version="0.1.0",features = [ "no-entrypoint" ] } 21 | spl-token = {version="3.5.0",features = [ "no-entrypoint" ]} 22 | spl-token-2022 = {version="0.4.2", features = [ "no-entrypoint" ] } 23 | thiserror = "1.0.31" 24 | arbitrary = { version = "1.0", features = ["derive"], optional = true } 25 | roots = { version = "0.0.7", optional = true } 26 | 27 | [dev-dependencies] 28 | proptest = "1.0.0" 29 | roots = "0.0.7" 30 | solana-sdk = "1.10.33" 31 | test-case = "2.2" 32 | 33 | [lib] 34 | name = "hw_04_token_swap" 35 | crate-type = ["cdylib", "lib"] 36 | 37 | [package.metadata.docs.rs] 38 | targets = ["x86_64-unknown-linux-gnu"] 39 | -------------------------------------------------------------------------------- /hw_04_token_swap/Xargo.toml: -------------------------------------------------------------------------------- 1 | [target.bpfel-unknown-unknown.dependencies.std] 2 | features = [] -------------------------------------------------------------------------------- /hw_04_token_swap/cbindgen.toml: -------------------------------------------------------------------------------- 1 | language = "C" 2 | header = "/* Autogenerated SPL Token-Swap program C Bindings */" 3 | pragma_once = true 4 | cpp_compat = true 5 | line_length = 80 6 | tab_width = 4 7 | style = "both" 8 | 9 | [export] 10 | prefix = "TokenSwap_" 11 | include = ["SwapInstruction", "State"] 12 | 13 | [parse] 14 | parse_deps = true 15 | include = ["solana-sdk"] 16 | -------------------------------------------------------------------------------- /hw_04_token_swap/program-id.md: -------------------------------------------------------------------------------- 1 | SwapsVeCiPHMUAtzQWZw7RjsKjgCjhwU55QGu4U1Szw 2 | -------------------------------------------------------------------------------- /hw_04_token_swap/src/constraints.rs: -------------------------------------------------------------------------------- 1 | //! Various constraints as required for production environments 2 | 3 | use crate::{ 4 | curve::{ 5 | base::{CurveType, SwapCurve}, 6 | fees::Fees, 7 | }, 8 | error::SwapError, 9 | }; 10 | use solana_program::program_error::ProgramError; 11 | 12 | #[cfg(feature = "production")] 13 | use std::env; 14 | 15 | /// Encodes fee constraints, used in multihost environments where the program 16 | /// may be used by multiple frontends, to ensure that proper fees are being 17 | /// assessed. 18 | /// Since this struct needs to be created at compile-time, we only have access 19 | /// to const functions and constructors. Since SwapCurve contains a Arc, it 20 | /// cannot be used, so we have to split the curves based on their types. 21 | pub struct SwapConstraints<'a> { 22 | /// Owner of the program 23 | pub owner_key: &'a str, 24 | /// Valid curve types 25 | pub valid_curve_types: &'a [CurveType], 26 | /// Valid fees 27 | pub fees: &'a Fees, 28 | } 29 | 30 | impl<'a> SwapConstraints<'a> { 31 | /// Checks that the provided curve is valid for the given constraints 32 | pub fn validate_curve(&self, swap_curve: &SwapCurve) -> Result<(), ProgramError> { 33 | if self 34 | .valid_curve_types 35 | .iter() 36 | .any(|x| *x == swap_curve.curve_type) 37 | { 38 | Ok(()) 39 | } else { 40 | Err(SwapError::UnsupportedCurveType.into()) 41 | } 42 | } 43 | 44 | /// Checks that the provided curve is valid for the given constraints 45 | /// 验证费用配置 46 | pub fn validate_fees(&self, fees: &Fees) -> Result<(), ProgramError> { 47 | if fees.trade_fee_numerator >= self.fees.trade_fee_numerator 48 | && fees.trade_fee_denominator == self.fees.trade_fee_denominator 49 | && fees.owner_trade_fee_numerator >= self.fees.owner_trade_fee_numerator 50 | && fees.owner_trade_fee_denominator == self.fees.owner_trade_fee_denominator 51 | && fees.owner_withdraw_fee_numerator >= self.fees.owner_withdraw_fee_numerator 52 | && fees.owner_withdraw_fee_denominator == self.fees.owner_withdraw_fee_denominator 53 | && fees.host_fee_numerator == self.fees.host_fee_numerator 54 | && fees.host_fee_denominator == self.fees.host_fee_denominator 55 | { 56 | Ok(()) 57 | } else { 58 | Err(SwapError::InvalidFee.into()) 59 | } 60 | } 61 | } 62 | 63 | #[cfg(feature = "production")] 64 | const OWNER_KEY: &str = env!("SWAP_PROGRAM_OWNER_FEE_ADDRESS"); 65 | #[cfg(feature = "production")] 66 | const FEES: &Fees = &Fees { 67 | trade_fee_numerator: 0, 68 | trade_fee_denominator: 10000, 69 | owner_trade_fee_numerator: 5, 70 | owner_trade_fee_denominator: 10000, 71 | owner_withdraw_fee_numerator: 0, 72 | owner_withdraw_fee_denominator: 0, 73 | host_fee_numerator: 20, 74 | host_fee_denominator: 100, 75 | }; 76 | #[cfg(feature = "production")] 77 | const VALID_CURVE_TYPES: &[CurveType] = &[CurveType::ConstantPrice, CurveType::ConstantProduct]; 78 | 79 | /// Fee structure defined by program creator in order to enforce certain 80 | /// fees when others use the program. Adds checks on pool creation and 81 | /// swapping to ensure the correct fees and account owners are passed. 82 | /// Fees provided during production build currently are considered min 83 | /// fees that creator of the pool can specify. Host fee is a fixed 84 | /// percentage that host receives as a portion of owner fees 85 | pub const SWAP_CONSTRAINTS: Option = { 86 | #[cfg(feature = "production")] 87 | { 88 | Some(SwapConstraints { 89 | owner_key: OWNER_KEY, 90 | valid_curve_types: VALID_CURVE_TYPES, 91 | fees: FEES, 92 | }) 93 | } 94 | #[cfg(not(feature = "production"))] 95 | { 96 | None 97 | } 98 | }; 99 | 100 | #[cfg(test)] 101 | mod tests { 102 | use super::*; 103 | use crate::curve::{base::CurveType, constant_product::ConstantProductCurve}; 104 | use std::sync::Arc; 105 | 106 | #[test] 107 | fn validate_fees() { 108 | let trade_fee_numerator = 1; 109 | let trade_fee_denominator = 4; 110 | let owner_trade_fee_numerator = 2; 111 | let owner_trade_fee_denominator = 5; 112 | let owner_withdraw_fee_numerator = 4; 113 | let owner_withdraw_fee_denominator = 10; 114 | let host_fee_numerator = 10; 115 | let host_fee_denominator = 100; 116 | let owner_key = ""; 117 | let curve_type = CurveType::ConstantProduct; 118 | let valid_fees = Fees { 119 | trade_fee_numerator, 120 | trade_fee_denominator, 121 | owner_trade_fee_numerator, 122 | owner_trade_fee_denominator, 123 | owner_withdraw_fee_numerator, 124 | owner_withdraw_fee_denominator, 125 | host_fee_numerator, 126 | host_fee_denominator, 127 | }; 128 | let calculator = ConstantProductCurve {}; 129 | let swap_curve = SwapCurve { 130 | curve_type, 131 | calculator: Arc::new(calculator.clone()), 132 | }; 133 | let constraints = SwapConstraints { 134 | owner_key, 135 | valid_curve_types: &[curve_type], 136 | fees: &valid_fees, 137 | }; 138 | 139 | constraints.validate_curve(&swap_curve).unwrap(); 140 | constraints.validate_fees(&valid_fees).unwrap(); 141 | 142 | let mut fees = valid_fees.clone(); 143 | fees.trade_fee_numerator = trade_fee_numerator - 1; 144 | assert_eq!( 145 | Err(SwapError::InvalidFee.into()), 146 | constraints.validate_fees(&fees), 147 | ); 148 | fees.trade_fee_numerator = trade_fee_numerator; 149 | 150 | // passing higher fee is ok 151 | fees.trade_fee_numerator = trade_fee_numerator - 1; 152 | assert_eq!(constraints.validate_fees(&valid_fees), Ok(())); 153 | fees.trade_fee_numerator = trade_fee_numerator; 154 | 155 | fees.trade_fee_denominator = trade_fee_denominator - 1; 156 | assert_eq!( 157 | Err(SwapError::InvalidFee.into()), 158 | constraints.validate_fees(&fees), 159 | ); 160 | fees.trade_fee_denominator = trade_fee_denominator; 161 | 162 | fees.trade_fee_denominator = trade_fee_denominator + 1; 163 | assert_eq!( 164 | Err(SwapError::InvalidFee.into()), 165 | constraints.validate_fees(&fees), 166 | ); 167 | fees.trade_fee_denominator = trade_fee_denominator; 168 | 169 | fees.owner_trade_fee_numerator = owner_trade_fee_numerator - 1; 170 | assert_eq!( 171 | Err(SwapError::InvalidFee.into()), 172 | constraints.validate_fees(&fees), 173 | ); 174 | fees.owner_trade_fee_numerator = owner_trade_fee_numerator; 175 | 176 | // passing higher fee is ok 177 | fees.owner_trade_fee_numerator = owner_trade_fee_numerator - 1; 178 | assert_eq!(constraints.validate_fees(&valid_fees), Ok(())); 179 | fees.owner_trade_fee_numerator = owner_trade_fee_numerator; 180 | 181 | fees.owner_trade_fee_denominator = owner_trade_fee_denominator - 1; 182 | assert_eq!( 183 | Err(SwapError::InvalidFee.into()), 184 | constraints.validate_fees(&fees), 185 | ); 186 | fees.owner_trade_fee_denominator = owner_trade_fee_denominator; 187 | 188 | let swap_curve = SwapCurve { 189 | curve_type: CurveType::ConstantPrice, 190 | calculator: Arc::new(calculator), 191 | }; 192 | assert_eq!( 193 | Err(SwapError::UnsupportedCurveType.into()), 194 | constraints.validate_curve(&swap_curve), 195 | ); 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /hw_04_token_swap/src/curve/base.rs: -------------------------------------------------------------------------------- 1 | //! Base curve implementation 2 | 3 | use solana_program::{ 4 | program_error::ProgramError, 5 | program_pack::{Pack, Sealed}, 6 | }; 7 | 8 | use crate::curve::{ 9 | calculator::{CurveCalculator, SwapWithoutFeesResult, TradeDirection}, 10 | constant_price::ConstantPriceCurve, 11 | constant_product::ConstantProductCurve, 12 | fees::Fees, 13 | offset::OffsetCurve, 14 | }; 15 | use arrayref::{array_mut_ref, array_ref, array_refs, mut_array_refs}; 16 | use std::convert::{TryFrom, TryInto}; 17 | use std::fmt::Debug; 18 | use std::sync::Arc; 19 | 20 | #[cfg(feature = "fuzz")] 21 | use arbitrary::Arbitrary; 22 | 23 | /// Curve types supported by the token-swap program. 24 | /// 兑换价格类型 25 | #[cfg_attr(feature = "fuzz", derive(Arbitrary))] 26 | #[repr(C)] 27 | #[derive(Clone, Copy, Debug, PartialEq)] 28 | pub enum CurveType { 29 | /// Uniswap-style constant product curve, invariant = token_a_amount * token_b_amount 30 | ConstantProduct, 31 | /// Flat line, always providing 1:1 from one token to another 32 | /// 一比一兑换 33 | ConstantPrice, 34 | /// Offset curve, like Uniswap, but the token B side has a faked offset 35 | /// 自动做市和 Uniswap 类似 36 | Offset, 37 | } 38 | 39 | /// Encodes all results of swapping from a source token to a destination token 40 | /// 兑换结果信息结构体 41 | #[derive(Debug, PartialEq)] 42 | pub struct SwapResult { 43 | /// New amount of source token 44 | pub new_swap_source_amount: u128, 45 | /// New amount of destination token 46 | pub new_swap_destination_amount: u128, 47 | /// Amount of source token swapped (includes fees) 48 | /// 实际兑换源币种转出数量(可能池中没有钱实际可兑换的转出数量小于传入的转出数量) 49 | pub source_amount_swapped: u128, 50 | /// Amount of destination token swapped 51 | /// 实际兑换目标币种转入数量(就是可兑换的目标数量) 52 | pub destination_amount_swapped: u128, 53 | /// Amount of source tokens going to pool holders 54 | pub trade_fee: u128, 55 | /// Amount of source tokens going to owner 56 | pub owner_fee: u128, 57 | } 58 | 59 | /// Concrete struct to wrap around the trait object which performs calculation. 60 | #[repr(C)] 61 | #[derive(Debug)] 62 | pub struct SwapCurve { 63 | /// The type of curve contained in the calculator, helpful for outside 64 | /// queries 65 | /// 兑换价格计算类型 66 | pub curve_type: CurveType, 67 | /// The actual calculator, represented as a trait object to allow for many 68 | /// different types of curves 69 | /// 兑换价格计算实现 70 | pub calculator: Arc, 71 | } 72 | 73 | impl SwapCurve { 74 | /// Subtract fees and calculate how much destination token will be provided 75 | /// given an amount of source token. 76 | pub fn swap( 77 | &self, 78 | source_amount: u128, 79 | swap_source_amount: u128, 80 | swap_destination_amount: u128, 81 | trade_direction: TradeDirection, 82 | fees: &Fees, 83 | ) -> Option { 84 | // debit the fee to calculate the amount swapped 85 | let trade_fee = fees.trading_fee(source_amount)?; 86 | let owner_fee = fees.owner_trading_fee(source_amount)?; 87 | 88 | let total_fees = trade_fee.checked_add(owner_fee)?; 89 | let source_amount_less_fees = source_amount.checked_sub(total_fees)?; 90 | 91 | let SwapWithoutFeesResult { 92 | source_amount_swapped, 93 | destination_amount_swapped, 94 | } = self.calculator.swap_without_fees( 95 | source_amount_less_fees, 96 | swap_source_amount, 97 | swap_destination_amount, 98 | trade_direction, 99 | )?; 100 | 101 | let source_amount_swapped = source_amount_swapped.checked_add(total_fees)?; 102 | Some(SwapResult { 103 | new_swap_source_amount: swap_source_amount.checked_add(source_amount_swapped)?, 104 | new_swap_destination_amount: swap_destination_amount 105 | .checked_sub(destination_amount_swapped)?, 106 | source_amount_swapped, 107 | destination_amount_swapped, 108 | trade_fee, 109 | owner_fee, 110 | }) 111 | } 112 | 113 | /// Get the amount of pool tokens for the deposited amount of token A or B 114 | pub fn deposit_single_token_type( 115 | &self, 116 | source_amount: u128, 117 | swap_token_a_amount: u128, 118 | swap_token_b_amount: u128, 119 | pool_supply: u128, 120 | trade_direction: TradeDirection, 121 | fees: &Fees, 122 | ) -> Option { 123 | if source_amount == 0 { 124 | return Some(0); 125 | } 126 | // Get the trading fee incurred if *half* the source amount is swapped 127 | // for the other side. Reference at: 128 | // https://github.com/balancer-labs/balancer-core/blob/f4ed5d65362a8d6cec21662fb6eae233b0babc1f/contracts/BMath.sol#L117 129 | let half_source_amount = std::cmp::max(1, source_amount.checked_div(2)?); 130 | let trade_fee = fees.trading_fee(half_source_amount)?; 131 | let source_amount = source_amount.checked_sub(trade_fee)?; 132 | self.calculator.deposit_single_token_type( 133 | source_amount, 134 | swap_token_a_amount, 135 | swap_token_b_amount, 136 | pool_supply, 137 | trade_direction, 138 | ) 139 | } 140 | 141 | /// Get the amount of pool tokens for the withdrawn amount of token A or B 142 | pub fn withdraw_single_token_type_exact_out( 143 | &self, 144 | source_amount: u128, 145 | swap_token_a_amount: u128, 146 | swap_token_b_amount: u128, 147 | pool_supply: u128, 148 | trade_direction: TradeDirection, 149 | fees: &Fees, 150 | ) -> Option { 151 | if source_amount == 0 { 152 | return Some(0); 153 | } 154 | // Get the trading fee incurred if *half* the source amount is swapped 155 | // for the other side. Reference at: 156 | // https://github.com/balancer-labs/balancer-core/blob/f4ed5d65362a8d6cec21662fb6eae233b0babc1f/contracts/BMath.sol#L117 157 | let half_source_amount = std::cmp::max(1, source_amount.checked_div(2)?); 158 | let trade_fee = fees.trading_fee(half_source_amount)?; 159 | let source_amount = source_amount.checked_sub(trade_fee)?; 160 | self.calculator.withdraw_single_token_type_exact_out( 161 | source_amount, 162 | swap_token_a_amount, 163 | swap_token_b_amount, 164 | pool_supply, 165 | trade_direction, 166 | ) 167 | } 168 | } 169 | 170 | /// Default implementation for SwapCurve cannot be derived because of 171 | /// the contained Arc. 172 | impl Default for SwapCurve { 173 | fn default() -> Self { 174 | let curve_type: CurveType = Default::default(); 175 | let calculator: ConstantProductCurve = Default::default(); 176 | Self { 177 | curve_type, 178 | calculator: Arc::new(calculator), 179 | } 180 | } 181 | } 182 | 183 | /// Clone takes advantage of pack / unpack to get around the difficulty of 184 | /// cloning dynamic objects. 185 | /// Note that this is only to be used for testing. 186 | #[cfg(any(test, feature = "fuzz"))] 187 | impl Clone for SwapCurve { 188 | fn clone(&self) -> Self { 189 | let mut packed_self = [0u8; Self::LEN]; 190 | Self::pack_into_slice(self, &mut packed_self); 191 | Self::unpack_from_slice(&packed_self).unwrap() 192 | } 193 | } 194 | 195 | /// Simple implementation for PartialEq which assumes that the output of 196 | /// `Pack` is enough to guarantee equality 197 | impl PartialEq for SwapCurve { 198 | fn eq(&self, other: &Self) -> bool { 199 | let mut packed_self = [0u8; Self::LEN]; 200 | Self::pack_into_slice(self, &mut packed_self); 201 | let mut packed_other = [0u8; Self::LEN]; 202 | Self::pack_into_slice(other, &mut packed_other); 203 | packed_self[..] == packed_other[..] 204 | } 205 | } 206 | 207 | impl Sealed for SwapCurve {} 208 | impl Pack for SwapCurve { 209 | /// Size of encoding of all curve parameters, which include fees and any other 210 | /// constants used to calculate swaps, deposits, and withdrawals. 211 | /// This includes 1 byte for the type, and 72 for the calculator to use as 212 | /// it needs. Some calculators may be smaller than 72 bytes. 213 | const LEN: usize = 33; 214 | 215 | /// Unpacks a byte buffer into a SwapCurve 216 | fn unpack_from_slice(input: &[u8]) -> Result { 217 | let input = array_ref![input, 0, 33]; 218 | #[allow(clippy::ptr_offset_with_cast)] 219 | let (curve_type, calculator) = array_refs![input, 1, 32]; 220 | let curve_type = curve_type[0].try_into()?; 221 | Ok(Self { 222 | curve_type, 223 | calculator: match curve_type { 224 | CurveType::ConstantProduct => { 225 | Arc::new(ConstantProductCurve::unpack_from_slice(calculator)?) 226 | } 227 | CurveType::ConstantPrice => { 228 | Arc::new(ConstantPriceCurve::unpack_from_slice(calculator)?) 229 | } 230 | CurveType::Offset => Arc::new(OffsetCurve::unpack_from_slice(calculator)?), 231 | }, 232 | }) 233 | } 234 | 235 | /// Pack SwapCurve into a byte buffer 236 | fn pack_into_slice(&self, output: &mut [u8]) { 237 | let output = array_mut_ref![output, 0, 33]; 238 | let (curve_type, calculator) = mut_array_refs![output, 1, 32]; 239 | curve_type[0] = self.curve_type as u8; 240 | self.calculator.pack_into_slice(&mut calculator[..]); 241 | } 242 | } 243 | 244 | /// Sensible default of CurveType to ConstantProduct, the most popular and 245 | /// well-known curve type. 246 | impl Default for CurveType { 247 | fn default() -> Self { 248 | CurveType::ConstantProduct 249 | } 250 | } 251 | 252 | impl TryFrom for CurveType { 253 | type Error = ProgramError; 254 | 255 | fn try_from(curve_type: u8) -> Result { 256 | match curve_type { 257 | 0 => Ok(CurveType::ConstantProduct), 258 | 1 => Ok(CurveType::ConstantPrice), 259 | 2 => Ok(CurveType::Offset), 260 | _ => Err(ProgramError::InvalidAccountData), 261 | } 262 | } 263 | } 264 | 265 | #[cfg(test)] 266 | mod tests { 267 | use super::*; 268 | 269 | #[test] 270 | fn pack_swap_curve() { 271 | let curve = ConstantProductCurve {}; 272 | let curve_type = CurveType::ConstantProduct; 273 | let swap_curve = SwapCurve { 274 | curve_type, 275 | calculator: Arc::new(curve), 276 | }; 277 | 278 | let mut packed = [0u8; SwapCurve::LEN]; 279 | Pack::pack_into_slice(&swap_curve, &mut packed[..]); 280 | let unpacked = SwapCurve::unpack_from_slice(&packed).unwrap(); 281 | assert_eq!(swap_curve, unpacked); 282 | 283 | let mut packed = vec![curve_type as u8]; 284 | packed.extend_from_slice(&[0u8; 32]); // 32 bytes reserved for curve 285 | let unpacked = SwapCurve::unpack_from_slice(&packed).unwrap(); 286 | assert_eq!(swap_curve, unpacked); 287 | } 288 | 289 | #[test] 290 | fn constant_product_trade_fee() { 291 | // calculation on https://github.com/solana-labs/solana-program-library/issues/341 292 | let swap_source_amount = 1000; 293 | let swap_destination_amount = 50000; 294 | let trade_fee_numerator = 1; 295 | let trade_fee_denominator = 100; 296 | let owner_trade_fee_numerator = 0; 297 | let owner_trade_fee_denominator = 0; 298 | let owner_withdraw_fee_numerator = 0; 299 | let owner_withdraw_fee_denominator = 0; 300 | let host_fee_numerator = 0; 301 | let host_fee_denominator = 0; 302 | 303 | let fees = Fees { 304 | trade_fee_numerator, 305 | trade_fee_denominator, 306 | owner_trade_fee_numerator, 307 | owner_trade_fee_denominator, 308 | owner_withdraw_fee_numerator, 309 | owner_withdraw_fee_denominator, 310 | host_fee_numerator, 311 | host_fee_denominator, 312 | }; 313 | let source_amount = 100; 314 | let curve = ConstantProductCurve {}; 315 | let swap_curve = SwapCurve { 316 | curve_type: CurveType::ConstantProduct, 317 | calculator: Arc::new(curve), 318 | }; 319 | let result = swap_curve 320 | .swap( 321 | source_amount, 322 | swap_source_amount, 323 | swap_destination_amount, 324 | TradeDirection::AtoB, 325 | &fees, 326 | ) 327 | .unwrap(); 328 | assert_eq!(result.new_swap_source_amount, 1100); 329 | assert_eq!(result.destination_amount_swapped, 4504); 330 | assert_eq!(result.new_swap_destination_amount, 45496); 331 | assert_eq!(result.trade_fee, 1); 332 | assert_eq!(result.owner_fee, 0); 333 | } 334 | 335 | #[test] 336 | fn constant_product_owner_fee() { 337 | // calculation on https://github.com/solana-labs/solana-program-library/issues/341 338 | let swap_source_amount = 1000; 339 | let swap_destination_amount = 50000; 340 | let trade_fee_numerator = 0; 341 | let trade_fee_denominator = 0; 342 | let owner_trade_fee_numerator = 1; 343 | let owner_trade_fee_denominator = 100; 344 | let owner_withdraw_fee_numerator = 0; 345 | let owner_withdraw_fee_denominator = 0; 346 | let host_fee_numerator = 0; 347 | let host_fee_denominator = 0; 348 | let fees = Fees { 349 | trade_fee_numerator, 350 | trade_fee_denominator, 351 | owner_trade_fee_numerator, 352 | owner_trade_fee_denominator, 353 | owner_withdraw_fee_numerator, 354 | owner_withdraw_fee_denominator, 355 | host_fee_numerator, 356 | host_fee_denominator, 357 | }; 358 | let source_amount: u128 = 100; 359 | let curve = ConstantProductCurve {}; 360 | let swap_curve = SwapCurve { 361 | curve_type: CurveType::ConstantProduct, 362 | calculator: Arc::new(curve), 363 | }; 364 | let result = swap_curve 365 | .swap( 366 | source_amount, 367 | swap_source_amount, 368 | swap_destination_amount, 369 | TradeDirection::AtoB, 370 | &fees, 371 | ) 372 | .unwrap(); 373 | assert_eq!(result.new_swap_source_amount, 1100); 374 | assert_eq!(result.destination_amount_swapped, 4504); 375 | assert_eq!(result.new_swap_destination_amount, 45496); 376 | assert_eq!(result.trade_fee, 0); 377 | assert_eq!(result.owner_fee, 1); 378 | } 379 | 380 | #[test] 381 | fn constant_product_no_fee() { 382 | let swap_source_amount: u128 = 1_000; 383 | let swap_destination_amount: u128 = 50_000; 384 | let source_amount: u128 = 100; 385 | let curve = ConstantProductCurve::default(); 386 | let fees = Fees::default(); 387 | let swap_curve = SwapCurve { 388 | curve_type: CurveType::ConstantProduct, 389 | calculator: Arc::new(curve), 390 | }; 391 | let result = swap_curve 392 | .swap( 393 | source_amount, 394 | swap_source_amount, 395 | swap_destination_amount, 396 | TradeDirection::AtoB, 397 | &fees, 398 | ) 399 | .unwrap(); 400 | assert_eq!(result.new_swap_source_amount, 1100); 401 | assert_eq!(result.destination_amount_swapped, 4545); 402 | assert_eq!(result.new_swap_destination_amount, 45455); 403 | } 404 | } 405 | -------------------------------------------------------------------------------- /hw_04_token_swap/src/curve/fees.rs: -------------------------------------------------------------------------------- 1 | //! All fee information, to be used for validation currently 2 | 3 | use crate::error::SwapError; 4 | use arrayref::{array_mut_ref, array_ref, array_refs, mut_array_refs}; 5 | use solana_program::{ 6 | program_error::ProgramError, 7 | program_pack::{IsInitialized, Pack, Sealed}, 8 | }; 9 | use std::convert::TryFrom; 10 | 11 | /// Encapsulates all fee information and calculations for swap operations 12 | /// 费用信息 13 | #[derive(Clone, Debug, Default, PartialEq)] 14 | pub struct Fees { 15 | /// Trade fees are extra token amounts that are held inside the token 16 | /// accounts during a trade, making the value of liquidity tokens rise. 17 | /// Trade fee numerator 18 | pub trade_fee_numerator: u64, 19 | /// Trade fee denominator 20 | pub trade_fee_denominator: u64, 21 | 22 | /// Owner trading fees are extra token amounts that are held inside the token 23 | /// accounts during a trade, with the equivalent in pool tokens minted to 24 | /// the owner of the program. 25 | /// Owner trade fee numerator 26 | pub owner_trade_fee_numerator: u64, 27 | /// Owner trade fee denominator 28 | pub owner_trade_fee_denominator: u64, 29 | 30 | /// Owner withdraw fees are extra liquidity pool token amounts that are 31 | /// sent to the owner on every withdrawal. 32 | /// Owner withdraw fee numerator 33 | pub owner_withdraw_fee_numerator: u64, 34 | /// Owner withdraw fee denominator 35 | pub owner_withdraw_fee_denominator: u64, 36 | 37 | /// Host fees are a proportion of the owner trading fees, sent to an 38 | /// extra account provided during the trade. 39 | /// Host trading fee numerator 40 | pub host_fee_numerator: u64, 41 | /// Host trading fee denominator 42 | pub host_fee_denominator: u64, 43 | } 44 | 45 | /// Helper function for calculating swap fee 46 | pub fn calculate_fee( 47 | token_amount: u128, 48 | fee_numerator: u128, 49 | fee_denominator: u128, 50 | ) -> Option { 51 | if fee_numerator == 0 || token_amount == 0 { 52 | Some(0) 53 | } else { 54 | let fee = token_amount 55 | .checked_mul(fee_numerator)? 56 | .checked_div(fee_denominator)?; 57 | if fee == 0 { 58 | Some(1) // minimum fee of one token 59 | } else { 60 | Some(fee) 61 | } 62 | } 63 | } 64 | 65 | fn validate_fraction(numerator: u64, denominator: u64) -> Result<(), SwapError> { 66 | if denominator == 0 && numerator == 0 { 67 | Ok(()) 68 | } else if numerator >= denominator { 69 | Err(SwapError::InvalidFee) 70 | } else { 71 | Ok(()) 72 | } 73 | } 74 | 75 | impl Fees { 76 | /// Calculate the withdraw fee in pool tokens 77 | pub fn owner_withdraw_fee(&self, pool_tokens: u128) -> Option { 78 | calculate_fee( 79 | pool_tokens, 80 | u128::try_from(self.owner_withdraw_fee_numerator).ok()?, 81 | u128::try_from(self.owner_withdraw_fee_denominator).ok()?, 82 | ) 83 | } 84 | 85 | /// Calculate the trading fee in trading tokens 86 | pub fn trading_fee(&self, trading_tokens: u128) -> Option { 87 | calculate_fee( 88 | trading_tokens, 89 | u128::try_from(self.trade_fee_numerator).ok()?, 90 | u128::try_from(self.trade_fee_denominator).ok()?, 91 | ) 92 | } 93 | 94 | /// Calculate the owner trading fee in trading tokens 95 | pub fn owner_trading_fee(&self, trading_tokens: u128) -> Option { 96 | calculate_fee( 97 | trading_tokens, 98 | u128::try_from(self.owner_trade_fee_numerator).ok()?, 99 | u128::try_from(self.owner_trade_fee_denominator).ok()?, 100 | ) 101 | } 102 | 103 | /// Calculate the host fee based on the owner fee, only used in production 104 | /// situations where a program is hosted by multiple frontends 105 | pub fn host_fee(&self, owner_fee: u128) -> Option { 106 | calculate_fee( 107 | owner_fee, 108 | u128::try_from(self.host_fee_numerator).ok()?, 109 | u128::try_from(self.host_fee_denominator).ok()?, 110 | ) 111 | } 112 | 113 | /// Validate that the fees are reasonable 114 | pub fn validate(&self) -> Result<(), SwapError> { 115 | validate_fraction(self.trade_fee_numerator, self.trade_fee_denominator)?; 116 | validate_fraction( 117 | self.owner_trade_fee_numerator, 118 | self.owner_trade_fee_denominator, 119 | )?; 120 | validate_fraction( 121 | self.owner_withdraw_fee_numerator, 122 | self.owner_withdraw_fee_denominator, 123 | )?; 124 | validate_fraction(self.host_fee_numerator, self.host_fee_denominator)?; 125 | Ok(()) 126 | } 127 | } 128 | 129 | /// IsInitialized is required to use `Pack::pack` and `Pack::unpack` 130 | impl IsInitialized for Fees { 131 | fn is_initialized(&self) -> bool { 132 | true 133 | } 134 | } 135 | 136 | impl Sealed for Fees {} 137 | impl Pack for Fees { 138 | const LEN: usize = 64; 139 | fn pack_into_slice(&self, output: &mut [u8]) { 140 | let output = array_mut_ref![output, 0, 64]; 141 | let ( 142 | trade_fee_numerator, 143 | trade_fee_denominator, 144 | owner_trade_fee_numerator, 145 | owner_trade_fee_denominator, 146 | owner_withdraw_fee_numerator, 147 | owner_withdraw_fee_denominator, 148 | host_fee_numerator, 149 | host_fee_denominator, 150 | ) = mut_array_refs![output, 8, 8, 8, 8, 8, 8, 8, 8]; 151 | *trade_fee_numerator = self.trade_fee_numerator.to_le_bytes(); 152 | *trade_fee_denominator = self.trade_fee_denominator.to_le_bytes(); 153 | *owner_trade_fee_numerator = self.owner_trade_fee_numerator.to_le_bytes(); 154 | *owner_trade_fee_denominator = self.owner_trade_fee_denominator.to_le_bytes(); 155 | *owner_withdraw_fee_numerator = self.owner_withdraw_fee_numerator.to_le_bytes(); 156 | *owner_withdraw_fee_denominator = self.owner_withdraw_fee_denominator.to_le_bytes(); 157 | *host_fee_numerator = self.host_fee_numerator.to_le_bytes(); 158 | *host_fee_denominator = self.host_fee_denominator.to_le_bytes(); 159 | } 160 | 161 | fn unpack_from_slice(input: &[u8]) -> Result { 162 | let input = array_ref![input, 0, 64]; 163 | #[allow(clippy::ptr_offset_with_cast)] 164 | let ( 165 | trade_fee_numerator, 166 | trade_fee_denominator, 167 | owner_trade_fee_numerator, 168 | owner_trade_fee_denominator, 169 | owner_withdraw_fee_numerator, 170 | owner_withdraw_fee_denominator, 171 | host_fee_numerator, 172 | host_fee_denominator, 173 | ) = array_refs![input, 8, 8, 8, 8, 8, 8, 8, 8]; 174 | Ok(Self { 175 | trade_fee_numerator: u64::from_le_bytes(*trade_fee_numerator), 176 | trade_fee_denominator: u64::from_le_bytes(*trade_fee_denominator), 177 | owner_trade_fee_numerator: u64::from_le_bytes(*owner_trade_fee_numerator), 178 | owner_trade_fee_denominator: u64::from_le_bytes(*owner_trade_fee_denominator), 179 | owner_withdraw_fee_numerator: u64::from_le_bytes(*owner_withdraw_fee_numerator), 180 | owner_withdraw_fee_denominator: u64::from_le_bytes(*owner_withdraw_fee_denominator), 181 | host_fee_numerator: u64::from_le_bytes(*host_fee_numerator), 182 | host_fee_denominator: u64::from_le_bytes(*host_fee_denominator), 183 | }) 184 | } 185 | } 186 | 187 | #[cfg(test)] 188 | mod tests { 189 | use super::*; 190 | 191 | #[test] 192 | fn pack_fees() { 193 | let trade_fee_numerator = 1; 194 | let trade_fee_denominator = 4; 195 | let owner_trade_fee_numerator = 2; 196 | let owner_trade_fee_denominator = 5; 197 | let owner_withdraw_fee_numerator = 4; 198 | let owner_withdraw_fee_denominator = 10; 199 | let host_fee_numerator = 7; 200 | let host_fee_denominator = 100; 201 | let fees = Fees { 202 | trade_fee_numerator, 203 | trade_fee_denominator, 204 | owner_trade_fee_numerator, 205 | owner_trade_fee_denominator, 206 | owner_withdraw_fee_numerator, 207 | owner_withdraw_fee_denominator, 208 | host_fee_numerator, 209 | host_fee_denominator, 210 | }; 211 | 212 | let mut packed = [0u8; Fees::LEN]; 213 | Pack::pack_into_slice(&fees, &mut packed[..]); 214 | let unpacked = Fees::unpack_from_slice(&packed).unwrap(); 215 | assert_eq!(fees, unpacked); 216 | 217 | let mut packed = vec![]; 218 | packed.extend_from_slice(&trade_fee_numerator.to_le_bytes()); 219 | packed.extend_from_slice(&trade_fee_denominator.to_le_bytes()); 220 | packed.extend_from_slice(&owner_trade_fee_numerator.to_le_bytes()); 221 | packed.extend_from_slice(&owner_trade_fee_denominator.to_le_bytes()); 222 | packed.extend_from_slice(&owner_withdraw_fee_numerator.to_le_bytes()); 223 | packed.extend_from_slice(&owner_withdraw_fee_denominator.to_le_bytes()); 224 | packed.extend_from_slice(&host_fee_numerator.to_le_bytes()); 225 | packed.extend_from_slice(&host_fee_denominator.to_le_bytes()); 226 | let unpacked = Fees::unpack_from_slice(&packed).unwrap(); 227 | assert_eq!(fees, unpacked); 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /hw_04_token_swap/src/curve/mod.rs: -------------------------------------------------------------------------------- 1 | //! Curve invariant implementations 2 | 3 | pub mod base; 4 | pub mod calculator; 5 | pub mod constant_price; 6 | pub mod constant_product; 7 | pub mod fees; 8 | pub mod offset; 9 | -------------------------------------------------------------------------------- /hw_04_token_swap/src/entrypoint.rs: -------------------------------------------------------------------------------- 1 | //! Program entrypoint definitions 2 | 3 | use crate::{error::SwapError, processor::Processor}; 4 | use solana_program::{ 5 | account_info::AccountInfo, entrypoint, entrypoint::ProgramResult, 6 | program_error::PrintProgramError, pubkey::Pubkey, 7 | }; 8 | 9 | entrypoint!(process_instruction); 10 | fn process_instruction( 11 | program_id: &Pubkey, 12 | accounts: &[AccountInfo], 13 | instruction_data: &[u8], 14 | ) -> ProgramResult { 15 | if let Err(error) = Processor::process(program_id, accounts, instruction_data) { 16 | // catch the error so we can print it 17 | error.print::(); 18 | return Err(error); 19 | } 20 | Ok(()) 21 | } 22 | -------------------------------------------------------------------------------- /hw_04_token_swap/src/error.rs: -------------------------------------------------------------------------------- 1 | //! Error types 2 | 3 | use num_derive::FromPrimitive; 4 | use solana_program::{ 5 | decode_error::DecodeError, 6 | msg, 7 | program_error::{PrintProgramError, ProgramError}, 8 | }; 9 | use thiserror::Error; 10 | 11 | /// Errors that may be returned by the TokenSwap program. 12 | #[derive(Clone, Debug, Eq, Error, FromPrimitive, PartialEq)] 13 | pub enum SwapError { 14 | // 0. 15 | /// The account cannot be initialized because it is already being used. 16 | #[error("Swap account already in use")] 17 | AlreadyInUse, 18 | /// The program address provided doesn't match the value generated by the program. 19 | #[error("Invalid program address generated from bump seed and key")] 20 | InvalidProgramAddress, 21 | /// The owner of the input isn't set to the program address generated by the program. 22 | #[error("Input account owner is not the program address")] 23 | InvalidOwner, 24 | /// The owner of the pool token output is set to the program address generated by the program. 25 | #[error("Output pool account owner cannot be the program address")] 26 | InvalidOutputOwner, 27 | /// The deserialization of the account returned something besides State::Mint. 28 | #[error("Deserialized account is not an SPL Token mint")] 29 | ExpectedMint, 30 | 31 | // 5. 32 | /// The deserialization of the account returned something besides State::Account. 33 | #[error("Deserialized account is not an SPL Token account")] 34 | ExpectedAccount, 35 | /// The input token account is empty. 36 | #[error("Input token account empty")] 37 | EmptySupply, 38 | /// The pool token mint has a non-zero supply. 39 | #[error("Pool token mint has a non-zero supply")] 40 | InvalidSupply, 41 | /// The provided token account has a delegate. 42 | #[error("Token account has a delegate")] 43 | InvalidDelegate, 44 | /// The input token is invalid for swap. 45 | #[error("InvalidInput")] 46 | InvalidInput, 47 | 48 | // 10. 49 | /// Address of the provided swap token account is incorrect. 50 | #[error("Address of the provided swap token account is incorrect")] 51 | IncorrectSwapAccount, 52 | /// Address of the provided pool token mint is incorrect 53 | #[error("Address of the provided pool token mint is incorrect")] 54 | IncorrectPoolMint, 55 | /// The output token is invalid for swap. 56 | #[error("InvalidOutput")] 57 | InvalidOutput, 58 | /// General calculation failure due to overflow or underflow 59 | #[error("General calculation failure due to overflow or underflow")] 60 | CalculationFailure, 61 | /// Invalid instruction number passed in. 62 | #[error("Invalid instruction")] 63 | InvalidInstruction, 64 | 65 | // 15. 66 | /// Swap input token accounts have the same mint 67 | #[error("Swap input token accounts have the same mint")] 68 | RepeatedMint, 69 | /// Swap instruction exceeds desired slippage limit 70 | #[error("Swap instruction exceeds desired slippage limit")] 71 | ExceededSlippage, 72 | /// The provided token account has a close authority. 73 | #[error("Token account has a close authority")] 74 | InvalidCloseAuthority, 75 | /// The pool token mint has a freeze authority. 76 | #[error("Pool token mint has a freeze authority")] 77 | InvalidFreezeAuthority, 78 | /// The pool fee token account is incorrect 79 | #[error("Pool fee token account incorrect")] 80 | IncorrectFeeAccount, 81 | 82 | // 20. 83 | /// Given pool token amount results in zero trading tokens 84 | #[error("Given pool token amount results in zero trading tokens")] 85 | ZeroTradingTokens, 86 | /// The fee calculation failed due to overflow, underflow, or unexpected 0 87 | #[error("Fee calculation failed due to overflow, underflow, or unexpected 0")] 88 | FeeCalculationFailure, 89 | /// ConversionFailure 90 | #[error("Conversion to u64 failed with an overflow or underflow")] 91 | ConversionFailure, 92 | /// The provided fee does not match the program owner's constraints 93 | #[error("The provided fee does not match the program owner's constraints")] 94 | InvalidFee, 95 | /// The provided token program does not match the token program expected by the swap 96 | #[error("The provided token program does not match the token program expected by the swap")] 97 | IncorrectTokenProgramId, 98 | 99 | // 25. 100 | /// The provided curve type is not supported by the program owner 101 | #[error("The provided curve type is not supported by the program owner")] 102 | UnsupportedCurveType, 103 | /// The provided curve parameters are invalid 104 | #[error("The provided curve parameters are invalid")] 105 | InvalidCurve, 106 | /// The operation cannot be performed on the given curve 107 | #[error("The operation cannot be performed on the given curve")] 108 | UnsupportedCurveOperation, 109 | /// The pool fee account is invalid. 110 | #[error("The pool fee account is invalid")] 111 | InvalidFeeAccount, 112 | } 113 | impl From for ProgramError { 114 | fn from(e: SwapError) -> Self { 115 | ProgramError::Custom(e as u32) 116 | } 117 | } 118 | impl DecodeError for SwapError { 119 | fn type_of() -> &'static str { 120 | "Swap Error" 121 | } 122 | } 123 | 124 | impl PrintProgramError for SwapError { 125 | fn print(&self) 126 | where 127 | E: 'static 128 | + std::error::Error 129 | + DecodeError 130 | + PrintProgramError 131 | + num_traits::FromPrimitive, 132 | { 133 | match self { 134 | SwapError::AlreadyInUse => msg!("Error: Swap account already in use"), 135 | SwapError::InvalidProgramAddress => { 136 | msg!("Error: Invalid program address generated from bump seed and key") 137 | } 138 | SwapError::InvalidOwner => { 139 | msg!("Error: The input account owner is not the program address") 140 | } 141 | SwapError::InvalidOutputOwner => { 142 | msg!("Error: Output pool account owner cannot be the program address") 143 | } 144 | SwapError::ExpectedMint => msg!("Error: Deserialized account is not an SPL Token mint"), 145 | SwapError::ExpectedAccount => { 146 | msg!("Error: Deserialized account is not an SPL Token account") 147 | } 148 | SwapError::EmptySupply => msg!("Error: Input token account empty"), 149 | SwapError::InvalidSupply => msg!("Error: Pool token mint has a non-zero supply"), 150 | SwapError::RepeatedMint => msg!("Error: Swap input token accounts have the same mint"), 151 | SwapError::InvalidDelegate => msg!("Error: Token account has a delegate"), 152 | SwapError::InvalidInput => msg!("Error: InvalidInput"), 153 | SwapError::IncorrectSwapAccount => { 154 | msg!("Error: Address of the provided swap token account is incorrect") 155 | } 156 | SwapError::IncorrectPoolMint => { 157 | msg!("Error: Address of the provided pool token mint is incorrect") 158 | } 159 | SwapError::InvalidOutput => msg!("Error: InvalidOutput"), 160 | SwapError::CalculationFailure => msg!("Error: CalculationFailure"), 161 | SwapError::InvalidInstruction => msg!("Error: InvalidInstruction"), 162 | SwapError::ExceededSlippage => { 163 | msg!("Error: Swap instruction exceeds desired slippage limit") 164 | } 165 | SwapError::InvalidCloseAuthority => msg!("Error: Token account has a close authority"), 166 | SwapError::InvalidFreezeAuthority => { 167 | msg!("Error: Pool token mint has a freeze authority") 168 | } 169 | SwapError::IncorrectFeeAccount => msg!("Error: Pool fee token account incorrect"), 170 | SwapError::ZeroTradingTokens => { 171 | msg!("Error: Given pool token amount results in zero trading tokens") 172 | } 173 | SwapError::FeeCalculationFailure => msg!( 174 | "Error: The fee calculation failed due to overflow, underflow, or unexpected 0" 175 | ), 176 | SwapError::ConversionFailure => msg!("Error: Conversion to or from u64 failed."), 177 | SwapError::InvalidFee => { 178 | msg!("Error: The provided fee does not match the program owner's constraints") 179 | } 180 | SwapError::IncorrectTokenProgramId => { 181 | msg!("Error: The provided token program does not match the token program expected by the swap") 182 | } 183 | SwapError::UnsupportedCurveType => { 184 | msg!("Error: The provided curve type is not supported by the program owner") 185 | } 186 | SwapError::InvalidCurve => { 187 | msg!("Error: The provided curve parameters are invalid") 188 | } 189 | SwapError::UnsupportedCurveOperation => { 190 | msg!("Error: The operation cannot be performed on the given curve") 191 | } 192 | SwapError::InvalidFeeAccount => { 193 | msg!("Error: The pool fee account is invalid") 194 | } 195 | } 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /hw_04_token_swap/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::integer_arithmetic)] 2 | #![deny(missing_docs)] 3 | 4 | //! An Uniswap-like program for the Solana blockchain. 5 | 6 | pub mod constraints; 7 | pub mod curve; 8 | pub mod error; 9 | pub mod instruction; 10 | pub mod processor; 11 | pub mod state; 12 | 13 | #[cfg(not(feature = "no-entrypoint"))] 14 | mod entrypoint; 15 | 16 | // Export current sdk types for downstream users building with a different sdk version 17 | pub use solana_program; 18 | 19 | solana_program::declare_id!("SwapsVeCiPHMUAtzQWZw7RjsKjgCjhwU55QGu4U1Szw"); 20 | -------------------------------------------------------------------------------- /hw_04_token_swap/src/state.rs: -------------------------------------------------------------------------------- 1 | //! State transition types 2 | 3 | use crate::{ 4 | curve::{base::SwapCurve, fees::Fees}, 5 | error::SwapError, 6 | }; 7 | use arrayref::{array_mut_ref, array_ref, array_refs, mut_array_refs}; 8 | use enum_dispatch::enum_dispatch; 9 | use solana_program::{ 10 | account_info::AccountInfo, 11 | msg, 12 | program_error::ProgramError, 13 | program_pack::{IsInitialized, Pack, Sealed}, 14 | pubkey::Pubkey, 15 | }; 16 | use spl_token_2022::{ 17 | extension::StateWithExtensions, 18 | state::{Account, AccountState}, 19 | }; 20 | use std::sync::Arc; 21 | 22 | /// Trait representing access to program state across all versions 23 | #[enum_dispatch] 24 | pub trait SwapState { 25 | /// Is the swap initialized, with data written to it 26 | fn is_initialized(&self) -> bool; 27 | /// Bump seed used to generate the program address / authority 28 | fn bump_seed(&self) -> u8; 29 | /// Token program ID associated with the swap 30 | fn token_program_id(&self) -> &Pubkey; 31 | /// Address of token A liquidity account 32 | fn token_a_account(&self) -> &Pubkey; 33 | /// Address of token B liquidity account 34 | fn token_b_account(&self) -> &Pubkey; 35 | /// Address of pool token mint 36 | fn pool_mint(&self) -> &Pubkey; 37 | 38 | /// Address of token A mint 39 | fn token_a_mint(&self) -> &Pubkey; 40 | /// Address of token B mint 41 | fn token_b_mint(&self) -> &Pubkey; 42 | 43 | /// Address of pool fee account 44 | fn pool_fee_account(&self) -> &Pubkey; 45 | /// Check if the pool fee info is a valid token program account 46 | /// capable of receiving tokens from the mint. 47 | fn check_pool_fee_info(&self, pool_fee_info: &AccountInfo) -> Result<(), ProgramError>; 48 | 49 | /// Fees associated with swap 50 | fn fees(&self) -> &Fees; 51 | /// Curve associated with swap 52 | fn swap_curve(&self) -> &SwapCurve; 53 | } 54 | 55 | /// All versions of SwapState 56 | #[enum_dispatch(SwapState)] 57 | pub enum SwapVersion { 58 | /// Latest version, used for all new swaps 59 | SwapV1, 60 | } 61 | 62 | /// SwapVersion does not implement program_pack::Pack because there are size 63 | /// checks on pack and unpack that would break backwards compatibility, so 64 | /// special implementations are provided here 65 | impl SwapVersion { 66 | /// Size of the latest version of the SwapState 67 | pub const LATEST_LEN: usize = 1 + SwapV1::LEN; // add one for the version enum 68 | 69 | /// Pack a swap into a byte array, based on its version 70 | pub fn pack(src: Self, dst: &mut [u8]) -> Result<(), ProgramError> { 71 | match src { 72 | Self::SwapV1(swap_info) => { 73 | dst[0] = 1; 74 | SwapV1::pack(swap_info, &mut dst[1..]) 75 | } 76 | } 77 | } 78 | 79 | /// Unpack the swap account based on its version, returning the result as a 80 | /// SwapState trait object 81 | pub fn unpack(input: &[u8]) -> Result, ProgramError> { 82 | let (&version, rest) = input 83 | .split_first() 84 | .ok_or(ProgramError::InvalidAccountData)?; 85 | match version { 86 | 1 => Ok(Arc::new(SwapV1::unpack(rest)?)), 87 | _ => Err(ProgramError::UninitializedAccount), 88 | } 89 | } 90 | 91 | /// Special check to be done before any instruction processing, works for 92 | /// all versions 93 | pub fn is_initialized(input: &[u8]) -> bool { 94 | match Self::unpack(input) { 95 | Ok(swap) => swap.is_initialized(), 96 | Err(_) => false, 97 | } 98 | } 99 | } 100 | 101 | /// Program states. 102 | /// 交易对相关信息结构体数据 103 | #[repr(C)] 104 | #[derive(Debug, Default, PartialEq)] 105 | pub struct SwapV1 { 106 | /// Initialized state. 107 | /// 交易对是否已经初始化 108 | pub is_initialized: bool, 109 | /// Bump seed used in program address. 110 | /// The program address is created deterministically with the bump seed, 111 | /// swap program id, and swap account pubkey. This program address has 112 | /// authority over the swap's token A account, token B account, and pool 113 | /// token mint. 114 | /// 交易对信息账户的种子(如果在合约里面要调用另一个合约可以使用该种子签名) 115 | pub bump_seed: u8, 116 | 117 | /// Program ID of the tokens being exchanged. 118 | /// Token合约地址 119 | pub token_program_id: Pubkey, 120 | 121 | /// Token A 账户地址 122 | pub token_a: Pubkey, 123 | /// Token B账户地址 124 | pub token_b: Pubkey, 125 | 126 | /// Pool tokens are issued when A or B tokens are deposited. 127 | /// Pool tokens can be withdrawn back to the original A or B token. 128 | /// 池代币币种信息地址 129 | pub pool_mint: Pubkey, 130 | 131 | /// Mint information for token A 132 | /// Token A币种信息地址 133 | pub token_a_mint: Pubkey, 134 | /// Mint information for token B 135 | /// Token B币种信息地址 136 | pub token_b_mint: Pubkey, 137 | 138 | /// Pool token account to receive trading and / or withdrawal fees 139 | /// 交易和取款手续费存放账户地址(注意:该币种需和池代币币种相同) 140 | pub pool_fee_account: Pubkey, 141 | 142 | /// All fee information 143 | /// 费用相关配置 144 | pub fees: Fees, 145 | 146 | /// Swap curve parameters, to be unpacked and used by the SwapCurve, which 147 | /// calculates swaps, deposits, and withdrawals 148 | /// 代币兑换价格计算实现 149 | pub swap_curve: SwapCurve, 150 | } 151 | 152 | impl SwapState for SwapV1 { 153 | fn is_initialized(&self) -> bool { 154 | self.is_initialized 155 | } 156 | 157 | fn bump_seed(&self) -> u8 { 158 | self.bump_seed 159 | } 160 | 161 | fn token_program_id(&self) -> &Pubkey { 162 | &self.token_program_id 163 | } 164 | 165 | fn token_a_account(&self) -> &Pubkey { 166 | &self.token_a 167 | } 168 | 169 | fn token_b_account(&self) -> &Pubkey { 170 | &self.token_b 171 | } 172 | 173 | fn pool_mint(&self) -> &Pubkey { 174 | &self.pool_mint 175 | } 176 | 177 | fn token_a_mint(&self) -> &Pubkey { 178 | &self.token_a_mint 179 | } 180 | 181 | fn token_b_mint(&self) -> &Pubkey { 182 | &self.token_b_mint 183 | } 184 | 185 | fn pool_fee_account(&self) -> &Pubkey { 186 | &self.pool_fee_account 187 | } 188 | 189 | fn check_pool_fee_info(&self, pool_fee_info: &AccountInfo) -> Result<(), ProgramError> { 190 | let data = &pool_fee_info.data.borrow(); 191 | let token_account = 192 | StateWithExtensions::::unpack(data).map_err(|err| match err { 193 | ProgramError::InvalidAccountData | ProgramError::UninitializedAccount => { 194 | SwapError::InvalidFeeAccount.into() 195 | } 196 | _ => err, 197 | })?; 198 | if pool_fee_info.owner != &self.token_program_id 199 | || token_account.base.state != AccountState::Initialized 200 | || token_account.base.mint != self.pool_mint 201 | { 202 | msg!("Pool fee account is not owned by token program, is not initialized, or does not match stake pool's mint"); 203 | return Err(SwapError::InvalidFeeAccount.into()); 204 | } 205 | Ok(()) 206 | } 207 | 208 | fn fees(&self) -> &Fees { 209 | &self.fees 210 | } 211 | 212 | fn swap_curve(&self) -> &SwapCurve { 213 | &self.swap_curve 214 | } 215 | } 216 | 217 | impl Sealed for SwapV1 {} 218 | impl IsInitialized for SwapV1 { 219 | fn is_initialized(&self) -> bool { 220 | self.is_initialized 221 | } 222 | } 223 | 224 | impl Pack for SwapV1 { 225 | const LEN: usize = 323; 226 | 227 | fn pack_into_slice(&self, output: &mut [u8]) { 228 | let output = array_mut_ref![output, 0, 323]; 229 | let ( 230 | is_initialized, 231 | bump_seed, 232 | token_program_id, 233 | token_a, 234 | token_b, 235 | pool_mint, 236 | token_a_mint, 237 | token_b_mint, 238 | pool_fee_account, 239 | fees, 240 | swap_curve, 241 | ) = mut_array_refs![output, 1, 1, 32, 32, 32, 32, 32, 32, 32, 64, 33]; 242 | is_initialized[0] = self.is_initialized as u8; 243 | bump_seed[0] = self.bump_seed; 244 | token_program_id.copy_from_slice(self.token_program_id.as_ref()); 245 | token_a.copy_from_slice(self.token_a.as_ref()); 246 | token_b.copy_from_slice(self.token_b.as_ref()); 247 | pool_mint.copy_from_slice(self.pool_mint.as_ref()); 248 | token_a_mint.copy_from_slice(self.token_a_mint.as_ref()); 249 | token_b_mint.copy_from_slice(self.token_b_mint.as_ref()); 250 | pool_fee_account.copy_from_slice(self.pool_fee_account.as_ref()); 251 | self.fees.pack_into_slice(&mut fees[..]); 252 | self.swap_curve.pack_into_slice(&mut swap_curve[..]); 253 | } 254 | 255 | /// Unpacks a byte buffer into a [SwapV1](struct.SwapV1.html). 256 | fn unpack_from_slice(input: &[u8]) -> Result { 257 | let input = array_ref![input, 0, 323]; 258 | #[allow(clippy::ptr_offset_with_cast)] 259 | let ( 260 | is_initialized, 261 | bump_seed, 262 | token_program_id, 263 | token_a, 264 | token_b, 265 | pool_mint, 266 | token_a_mint, 267 | token_b_mint, 268 | pool_fee_account, 269 | fees, 270 | swap_curve, 271 | ) = array_refs![input, 1, 1, 32, 32, 32, 32, 32, 32, 32, 64, 33]; 272 | Ok(Self { 273 | is_initialized: match is_initialized { 274 | [0] => false, 275 | [1] => true, 276 | _ => return Err(ProgramError::InvalidAccountData), 277 | }, 278 | bump_seed: bump_seed[0], 279 | token_program_id: Pubkey::new_from_array(*token_program_id), 280 | token_a: Pubkey::new_from_array(*token_a), 281 | token_b: Pubkey::new_from_array(*token_b), 282 | pool_mint: Pubkey::new_from_array(*pool_mint), 283 | token_a_mint: Pubkey::new_from_array(*token_a_mint), 284 | token_b_mint: Pubkey::new_from_array(*token_b_mint), 285 | pool_fee_account: Pubkey::new_from_array(*pool_fee_account), 286 | fees: Fees::unpack_from_slice(fees)?, 287 | swap_curve: SwapCurve::unpack_from_slice(swap_curve)?, 288 | }) 289 | } 290 | } 291 | 292 | #[cfg(test)] 293 | mod tests { 294 | use super::*; 295 | use crate::curve::offset::OffsetCurve; 296 | 297 | use std::convert::TryInto; 298 | 299 | const TEST_FEES: Fees = Fees { 300 | trade_fee_numerator: 1, 301 | trade_fee_denominator: 4, 302 | owner_trade_fee_numerator: 3, 303 | owner_trade_fee_denominator: 10, 304 | owner_withdraw_fee_numerator: 2, 305 | owner_withdraw_fee_denominator: 7, 306 | host_fee_numerator: 5, 307 | host_fee_denominator: 20, 308 | }; 309 | 310 | const TEST_BUMP_SEED: u8 = 255; 311 | const TEST_TOKEN_PROGRAM_ID: Pubkey = Pubkey::new_from_array([1u8; 32]); 312 | const TEST_TOKEN_A: Pubkey = Pubkey::new_from_array([2u8; 32]); 313 | const TEST_TOKEN_B: Pubkey = Pubkey::new_from_array([3u8; 32]); 314 | const TEST_POOL_MINT: Pubkey = Pubkey::new_from_array([4u8; 32]); 315 | const TEST_TOKEN_A_MINT: Pubkey = Pubkey::new_from_array([5u8; 32]); 316 | const TEST_TOKEN_B_MINT: Pubkey = Pubkey::new_from_array([6u8; 32]); 317 | const TEST_POOL_FEE_ACCOUNT: Pubkey = Pubkey::new_from_array([7u8; 32]); 318 | 319 | const TEST_CURVE_TYPE: u8 = 2; 320 | const TEST_TOKEN_B_OFFSET: u64 = 1_000_000_000; 321 | const TEST_CURVE: OffsetCurve = OffsetCurve { 322 | token_b_offset: TEST_TOKEN_B_OFFSET, 323 | }; 324 | 325 | #[test] 326 | fn swap_version_pack() { 327 | let curve_type = TEST_CURVE_TYPE.try_into().unwrap(); 328 | let calculator = Arc::new(TEST_CURVE); 329 | let swap_curve = SwapCurve { 330 | curve_type, 331 | calculator, 332 | }; 333 | let swap_info = SwapVersion::SwapV1(SwapV1 { 334 | is_initialized: true, 335 | bump_seed: TEST_BUMP_SEED, 336 | token_program_id: TEST_TOKEN_PROGRAM_ID, 337 | token_a: TEST_TOKEN_A, 338 | token_b: TEST_TOKEN_B, 339 | pool_mint: TEST_POOL_MINT, 340 | token_a_mint: TEST_TOKEN_A_MINT, 341 | token_b_mint: TEST_TOKEN_B_MINT, 342 | pool_fee_account: TEST_POOL_FEE_ACCOUNT, 343 | fees: TEST_FEES, 344 | swap_curve: swap_curve.clone(), 345 | }); 346 | 347 | let mut packed = [0u8; SwapVersion::LATEST_LEN]; 348 | SwapVersion::pack(swap_info, &mut packed).unwrap(); 349 | let unpacked = SwapVersion::unpack(&packed).unwrap(); 350 | 351 | assert!(unpacked.is_initialized()); 352 | assert_eq!(unpacked.bump_seed(), TEST_BUMP_SEED); 353 | assert_eq!(*unpacked.token_program_id(), TEST_TOKEN_PROGRAM_ID); 354 | assert_eq!(*unpacked.token_a_account(), TEST_TOKEN_A); 355 | assert_eq!(*unpacked.token_b_account(), TEST_TOKEN_B); 356 | assert_eq!(*unpacked.pool_mint(), TEST_POOL_MINT); 357 | assert_eq!(*unpacked.token_a_mint(), TEST_TOKEN_A_MINT); 358 | assert_eq!(*unpacked.token_b_mint(), TEST_TOKEN_B_MINT); 359 | assert_eq!(*unpacked.pool_fee_account(), TEST_POOL_FEE_ACCOUNT); 360 | assert_eq!(*unpacked.fees(), TEST_FEES); 361 | assert_eq!(*unpacked.swap_curve(), swap_curve); 362 | } 363 | 364 | #[test] 365 | fn swap_v1_pack() { 366 | let curve_type = TEST_CURVE_TYPE.try_into().unwrap(); 367 | let calculator = Arc::new(TEST_CURVE); 368 | let swap_curve = SwapCurve { 369 | curve_type, 370 | calculator, 371 | }; 372 | let swap_info = SwapV1 { 373 | is_initialized: true, 374 | bump_seed: TEST_BUMP_SEED, 375 | token_program_id: TEST_TOKEN_PROGRAM_ID, 376 | token_a: TEST_TOKEN_A, 377 | token_b: TEST_TOKEN_B, 378 | pool_mint: TEST_POOL_MINT, 379 | token_a_mint: TEST_TOKEN_A_MINT, 380 | token_b_mint: TEST_TOKEN_B_MINT, 381 | pool_fee_account: TEST_POOL_FEE_ACCOUNT, 382 | fees: TEST_FEES, 383 | swap_curve, 384 | }; 385 | 386 | let mut packed = [0u8; SwapV1::LEN]; 387 | SwapV1::pack_into_slice(&swap_info, &mut packed); 388 | let unpacked = SwapV1::unpack(&packed).unwrap(); 389 | assert_eq!(swap_info, unpacked); 390 | 391 | let mut packed = vec![1u8, TEST_BUMP_SEED]; 392 | packed.extend_from_slice(&TEST_TOKEN_PROGRAM_ID.to_bytes()); 393 | packed.extend_from_slice(&TEST_TOKEN_A.to_bytes()); 394 | packed.extend_from_slice(&TEST_TOKEN_B.to_bytes()); 395 | packed.extend_from_slice(&TEST_POOL_MINT.to_bytes()); 396 | packed.extend_from_slice(&TEST_TOKEN_A_MINT.to_bytes()); 397 | packed.extend_from_slice(&TEST_TOKEN_B_MINT.to_bytes()); 398 | packed.extend_from_slice(&TEST_POOL_FEE_ACCOUNT.to_bytes()); 399 | packed.extend_from_slice(&TEST_FEES.trade_fee_numerator.to_le_bytes()); 400 | packed.extend_from_slice(&TEST_FEES.trade_fee_denominator.to_le_bytes()); 401 | packed.extend_from_slice(&TEST_FEES.owner_trade_fee_numerator.to_le_bytes()); 402 | packed.extend_from_slice(&TEST_FEES.owner_trade_fee_denominator.to_le_bytes()); 403 | packed.extend_from_slice(&TEST_FEES.owner_withdraw_fee_numerator.to_le_bytes()); 404 | packed.extend_from_slice(&TEST_FEES.owner_withdraw_fee_denominator.to_le_bytes()); 405 | packed.extend_from_slice(&TEST_FEES.host_fee_numerator.to_le_bytes()); 406 | packed.extend_from_slice(&TEST_FEES.host_fee_denominator.to_le_bytes()); 407 | packed.push(TEST_CURVE_TYPE); 408 | packed.extend_from_slice(&TEST_TOKEN_B_OFFSET.to_le_bytes()); 409 | packed.extend_from_slice(&[0u8; 24]); 410 | let unpacked = SwapV1::unpack(&packed).unwrap(); 411 | assert_eq!(swap_info, unpacked); 412 | 413 | let packed = [0u8; SwapV1::LEN]; 414 | let swap_info: SwapV1 = Default::default(); 415 | let unpack_unchecked = SwapV1::unpack_unchecked(&packed).unwrap(); 416 | assert_eq!(unpack_unchecked, swap_info); 417 | let err = SwapV1::unpack(&packed).unwrap_err(); 418 | assert_eq!(err, ProgramError::UninitializedAccount); 419 | } 420 | } 421 | -------------------------------------------------------------------------------- /hw_05_anchor_simple/Anchor.toml: -------------------------------------------------------------------------------- 1 | # Anchor 框架相关配置 2 | [features] 3 | seeds = false 4 | skip-lint = false 5 | [programs.localnet] 6 | # 指定合约部署地址(就是如果合约部署就使用该地址) 7 | hw_05_anchor_simple = "Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS" 8 | 9 | [registry] 10 | url = "https://api.apr.dev" 11 | 12 | [provider] 13 | cluster = "localnet" 14 | # 指定合约部署时所使用的钱包 15 | wallet = "~/.config/solana/id.json" 16 | 17 | [scripts] 18 | test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts" 19 | 20 | [test] 21 | startup_wait = 100000 22 | -------------------------------------------------------------------------------- /hw_05_anchor_simple/Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "programs/*" 4 | ] 5 | 6 | [profile.release] 7 | overflow-checks = true 8 | lto = "fat" 9 | codegen-units = 1 10 | [profile.release.build-override] 11 | opt-level = 3 12 | incremental = false 13 | codegen-units = 1 14 | -------------------------------------------------------------------------------- /hw_05_anchor_simple/README.md: -------------------------------------------------------------------------------- 1 | ##### 使用说明(注意:如果要测试部署请先修改程序地址;程序地址在 /programs/hw_05_anchor_simple/src/lib.rs 文件 和 /Anchor.toml 文件 以及 /client/client.ts文件里面) 2 | ```bash 3 | # 编译源码(注意:该命令实际执行的是 anchor build 就是利用Anchor框架来编译Rust源码) 4 | $ npm run build 5 | 6 | # 将程序部署到开发网络 7 | $ npm run deploy:devnet 8 | ``` -------------------------------------------------------------------------------- /hw_05_anchor_simple/client/client.ts: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const path = require("path"); 3 | const { Connection, Account } = require("@solana/web3.js"); 4 | const { Provider, Wallet, Program, web3 } = require("@project-serum/anchor"); 5 | const { createTokenAccountInstrs, getMintInfo } = require('@project-serum/common'); 6 | const { TokenInstructions } = require('@project-serum/serum'); 7 | 8 | const useWallet = (endpoint, secretKeyPath) => { 9 | const connection = new Connection(endpoint, "recent"); 10 | const secretKey = fs.readFileSync(secretKeyPath); 11 | const payer = new Account(JSON.parse(secretKey)); 12 | const wallet = new Wallet(payer) 13 | const provider = new Provider(connection, wallet, { 14 | commitment: 'recent' 15 | }); 16 | 17 | return { 18 | provider, 19 | wallet, 20 | } 21 | } 22 | 23 | const useFaucetProgram = (provider, programId, programIdl) => { 24 | return new Program( 25 | programIdl, 26 | programId, 27 | provider 28 | ) 29 | } 30 | 31 | const getTokenAccount = async (provider, tokenMint, owner) => { 32 | const { value } = await provider.connection.getTokenAccountsByOwner( 33 | owner, 34 | { mint: tokenMint }, 35 | 'recent' 36 | ); 37 | return value.length ? value[0].pubkey : undefined; 38 | } 39 | 40 | const getAirdrop = async (provider, faucetProgram, owner, tokenMint, faucetConfig) => { 41 | const signers = []; 42 | const instructions = []; 43 | 44 | 45 | let receiverTokenAccountPk = await getTokenAccount(provider, tokenMint, owner); 46 | 47 | if (!receiverTokenAccountPk) { 48 | const receiverTokenAccount = new web3.Account(); 49 | receiverTokenAccountPk = receiverTokenAccount.publicKey; 50 | instructions.push( 51 | ...(await createTokenAccountInstrs( 52 | provider, 53 | receiverTokenAccount.publicKey, 54 | tokenMint, 55 | owner 56 | )) 57 | ); 58 | signers.push(receiverTokenAccount); 59 | } 60 | 61 | const tokenMintInfo = await getMintInfo(provider, tokenMint); 62 | 63 | await faucetProgram.rpc.drip({ 64 | accounts: { 65 | faucetConfig, 66 | receiver: receiverTokenAccountPk, 67 | tokenProgram: TokenInstructions.TOKEN_PROGRAM_ID, 68 | tokenMint, 69 | tokenAuthority: tokenMintInfo.mintAuthority 70 | }, 71 | instructions: instructions.length ? instructions : undefined, 72 | signers: signers.length ? signers : undefined 73 | }); 74 | 75 | return { 76 | tokenAccount: receiverTokenAccountPk 77 | }; 78 | } 79 | 80 | const main = async () => { 81 | const secretKeyPath = path.resolve(process.env.HOME, ".config/solana/id.json"); 82 | const endpoint = "https://api.devnet.solana.com"; 83 | 84 | const { provider, wallet } = useWallet(endpoint, secretKeyPath); 85 | 86 | console.log("use wallet: ", wallet.publicKey.toBase58()); 87 | 88 | const SOLBalance = await provider.connection.getBalance(wallet.publicKey); 89 | 90 | console.log("SOL Balance: ", SOLBalance.toString()) 91 | 92 | const programId = "Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"; 93 | const programIdl = require("./hw_06_anchor_simple.json"); 94 | 95 | const faucetProgram = useFaucetProgram(provider, programId, programIdl); 96 | 97 | // const BTCMint = new web3.PublicKey("ANHiz1KEuXKw3JVCdqYTFVUi2V3SXuA8TycF88XEf1QJ"); 98 | // const BTCFaucetConfig = new web3.PublicKey("FPfjDG7beUUiEqYbWbYar3XYH2DW5sNXnAq6986ZXJuY"); 99 | // const ETHMint = new web3.PublicKey("E8okefVR6d6RJTrdvCuPFW6Gcg9LZuPgryagUDrztpXS"); 100 | // const ETHFaucetConfig = new web3.PublicKey("4LWsno5UJxbHannaFjsoCmHnjJvd7jREM7XQtLtMy5ZR"); 101 | 102 | //console.log('Get BTC airdrop') 103 | 104 | //const { tokenAccount } = await getAirdrop(provider, faucetProgram, wallet.publicKey, BTCMint, BTCFaucetConfig); 105 | 106 | //const { value: newTokenBalance } = await provider.connection.getTokenAccountBalance(tokenAccount); 107 | 108 | //console.log('BTC balance: ', newTokenBalance.amount); 109 | 110 | } 111 | 112 | main(); -------------------------------------------------------------------------------- /hw_05_anchor_simple/client/hw_06_anchor_simple.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.1.0", 3 | "name": "hw_06_anchor_simple", 4 | "instructions": [ 5 | { 6 | "name": "initialize", 7 | "docs": [ 8 | "* 初始化一个水龙头(就是创建一个水龙头)\n * @ctx 上下文(里面包含调用该函数所需要的所有AccountInfo账户)\n * @none\n * @drip_volume" 9 | ], 10 | "accounts": [ 11 | { 12 | "name": "faucetConfig", 13 | "isMut": true, 14 | "isSigner": true 15 | }, 16 | { 17 | "name": "tokenProgram", 18 | "isMut": false, 19 | "isSigner": false 20 | }, 21 | { 22 | "name": "tokenMint", 23 | "isMut": true, 24 | "isSigner": false 25 | }, 26 | { 27 | "name": "tokenAuthority", 28 | "isMut": false, 29 | "isSigner": false 30 | }, 31 | { 32 | "name": "rent", 33 | "isMut": false, 34 | "isSigner": false 35 | }, 36 | { 37 | "name": "user", 38 | "isMut": true, 39 | "isSigner": true 40 | }, 41 | { 42 | "name": "systemProgram", 43 | "isMut": false, 44 | "isSigner": false 45 | } 46 | ], 47 | "args": [ 48 | { 49 | "name": "nonce", 50 | "type": "u8" 51 | }, 52 | { 53 | "name": "dripVolume", 54 | "type": "u64" 55 | } 56 | ] 57 | }, 58 | { 59 | "name": "drip", 60 | "docs": [ 61 | "* 空投\n * @ctx 上下文(里面包含调用该函数所需要的所有AccountInfo账户)" 62 | ], 63 | "accounts": [ 64 | { 65 | "name": "faucetConfig", 66 | "isMut": false, 67 | "isSigner": false 68 | }, 69 | { 70 | "name": "tokenProgram", 71 | "isMut": false, 72 | "isSigner": false 73 | }, 74 | { 75 | "name": "tokenMint", 76 | "isMut": true, 77 | "isSigner": false 78 | }, 79 | { 80 | "name": "tokenAuthority", 81 | "isMut": false, 82 | "isSigner": false 83 | }, 84 | { 85 | "name": "receiver", 86 | "isMut": true, 87 | "isSigner": false 88 | } 89 | ], 90 | "args": [] 91 | }, 92 | { 93 | "name": "setDripVolume", 94 | "docs": [ 95 | "* 修改水龙头一次给多少币" 96 | ], 97 | "accounts": [ 98 | { 99 | "name": "faucetConfig", 100 | "isMut": true, 101 | "isSigner": false 102 | }, 103 | { 104 | "name": "authority", 105 | "isMut": false, 106 | "isSigner": true 107 | } 108 | ], 109 | "args": [ 110 | { 111 | "name": "dripVolume", 112 | "type": "u64" 113 | } 114 | ] 115 | } 116 | ], 117 | "accounts": [ 118 | { 119 | "name": "FaucetConfig", 120 | "type": { 121 | "kind": "struct", 122 | "fields": [ 123 | { 124 | "name": "tokenProgram", 125 | "type": "publicKey" 126 | }, 127 | { 128 | "name": "tokenMint", 129 | "type": "publicKey" 130 | }, 131 | { 132 | "name": "tokenAuthority", 133 | "type": "publicKey" 134 | }, 135 | { 136 | "name": "nonce", 137 | "type": "u8" 138 | }, 139 | { 140 | "name": "dripVolume", 141 | "type": "u64" 142 | }, 143 | { 144 | "name": "authority", 145 | "type": "publicKey" 146 | } 147 | ] 148 | } 149 | } 150 | ], 151 | "errors": [ 152 | { 153 | "code": 6000, 154 | "name": "Forbidden", 155 | "msg": "Authority error" 156 | } 157 | ] 158 | } -------------------------------------------------------------------------------- /hw_05_anchor_simple/migrations/deploy.ts: -------------------------------------------------------------------------------- 1 | // Migrations are an early feature. Currently, they're nothing more than this 2 | // single deploy script that's invoked from the CLI, injecting a provider 3 | // configured from the workspace's Anchor.toml. 4 | // 初始化链上数据相关逻辑脚本 5 | 6 | const { web3, workspace, BN, setProvider } = require("@project-serum/anchor"); 7 | const { TokenInstructions } = require('@project-serum/serum'); 8 | const { createMint } = require("@project-serum/common") 9 | 10 | const createToken = async (provider, program, tokenConfig) => { 11 | const tokenOwnerAccount = web3.Keypair.generate(); 12 | 13 | const [tokenAuthority, tokenNonce] = await web3.PublicKey.findProgramAddress( 14 | [tokenOwnerAccount.publicKey.toBuffer()], 15 | program.programId 16 | ); 17 | 18 | const splToken = await createMint( 19 | provider, 20 | tokenAuthority, 21 | tokenConfig.decimals 22 | ); 23 | 24 | console.log(`Created ${tokenConfig.symbol} Token`, splToken.toBase58()); 25 | 26 | return { 27 | tokenOwnerAccount, 28 | splToken, 29 | tokenNonce, 30 | tokenAuthority, 31 | }; 32 | } 33 | 34 | module.exports = async function (provider) { 35 | setProvider(provider); 36 | 37 | const faucetProgram = workspace.Faucet; 38 | const wallet = provider.wallet; 39 | 40 | const tokenConfigs = [ 41 | { 42 | symbol: 'btc', 43 | name: 'Wrapped Bitcoin', 44 | decimals: 8, 45 | dripVolume: new BN(10 ** 8) 46 | }, 47 | { 48 | symbol: 'eth', 49 | name: 'Wrapped Ether', 50 | decimals: 8, 51 | dripVolume: new BN(10 ** 8) 52 | } 53 | ]; 54 | 55 | for (const tokenConfig of tokenConfigs) { 56 | const { tokenOwnerAccount: faucetConfigAccount, splToken, tokenNonce, tokenAuthority } = await createToken(provider, faucetProgram, tokenConfig); 57 | 58 | console.log(tokenConfig.symbol, "faucet_config address: ", faucetConfigAccount.publicKey.toBase58()); 59 | 60 | await faucetProgram.rpc.initialize(tokenNonce, tokenConfig.dripVolume, { 61 | accounts: { 62 | faucetConfig: faucetConfigAccount.publicKey, 63 | tokenMint: splToken, 64 | tokenProgram: TokenInstructions.TOKEN_PROGRAM_ID, 65 | tokenAuthority, 66 | rent: web3.SYSVAR_RENT_PUBKEY 67 | }, 68 | signers: [faucetConfigAccount], 69 | instructions: [ 70 | await faucetProgram.account.faucetConfig.createInstruction(faucetConfigAccount) 71 | ], 72 | }); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /hw_05_anchor_simple/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hw_05_anchor_simple", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "test": "anchor test", 7 | "build": "anchor build", 8 | "predeploy:devnet": "npm run build", 9 | "comment": "下面这个命令是部署到测试网络", 10 | "deploy:devnet": "anchor deploy --provider.cluster https://api.devnet.solana.com", 11 | "comment": "下面这个命令是程序部署好了以后可能需要一些链上数据初始化,执行该命令具体逻辑写在 migrate 目录下", 12 | "migrate:devnet": "anchor migrate --provider.cluster https://api.devnet.solana.com" 13 | }, 14 | "dependencies": { 15 | "@project-serum/anchor": "^0.25.0", 16 | "@project-serum/common": "^0.0.1-beta.3", 17 | "@project-serum/serum": "^0.13.65", 18 | "@solana/spl-token": "^0.3.5" 19 | }, 20 | "devDependencies": { 21 | "@types/bn.js": "^5.1.0", 22 | "@types/chai": "^4.3.0", 23 | "@types/mocha": "^9.0.0", 24 | "assert": "^2.0.0", 25 | "chai": "^4.3.4", 26 | "mocha": "^9.0.3", 27 | "prettier": "^2.6.2", 28 | "ts-mocha": "^10.0.0", 29 | "typescript": "^4.3.5" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /hw_05_anchor_simple/programs/hw_05_anchor_simple/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hw_05_anchor_simple" 3 | version = "0.1.0" 4 | description = "Created with Anchor" 5 | edition = "2021" 6 | 7 | [lib] 8 | crate-type = ["cdylib", "lib"] 9 | name = "hw_06_anchor_simple" 10 | 11 | [features] 12 | no-entrypoint = [] 13 | no-idl = [] 14 | no-log-ix-name = [] 15 | cpi = ["no-entrypoint"] 16 | default = [] 17 | 18 | [dependencies] 19 | anchor-lang = "0.25.0" 20 | anchor-spl = "0.25.0" 21 | -------------------------------------------------------------------------------- /hw_05_anchor_simple/programs/hw_05_anchor_simple/Xargo.toml: -------------------------------------------------------------------------------- 1 | [target.bpfel-unknown-unknown.dependencies.std] 2 | features = [] 3 | -------------------------------------------------------------------------------- /hw_05_anchor_simple/programs/hw_05_anchor_simple/src/error.rs: -------------------------------------------------------------------------------- 1 | // 注意:所有和Anchor框架有关系的都需要添加这个依赖(否则编译无法通过) 2 | use anchor_lang::prelude::*; 3 | 4 | /** 5 | * 自定义 Anchor 框架异常 6 | */ 7 | #[error_code] 8 | pub enum FaucetError { 9 | 10 | #[msg("Authority error")] 11 | Forbidden, 12 | 13 | } -------------------------------------------------------------------------------- /hw_05_anchor_simple/programs/hw_05_anchor_simple/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod state; 2 | mod error; 3 | 4 | use state::*; 5 | // 注意:所有和Anchor框架有关系的都需要添加这个依赖(否则编译无法通过) 6 | use anchor_lang::prelude::*; 7 | use anchor_spl::token::{MintTo}; 8 | 9 | // 指定合约部署地址(就是如果合约部署就使用该地址) 10 | declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"); 11 | 12 | // 指定该模块是一个 Solana Program(注意:该模块下的每一个pub函数都是Program函数,在前端可以调用) 13 | #[program] 14 | pub mod hw_06_anchor_simple { 15 | use super::*; 16 | 17 | /** 18 | * 初始化一个水龙头(就是创建一个水龙头) 19 | * @ctx 上下文(里面包含调用该函数所需要的所有AccountInfo账户) 20 | * @none 21 | * @drip_volume 22 | */ 23 | pub fn initialize(ctx: Context,nonce: u8,drip_volume: u64) -> Result<()> { 24 | // 获取faucet_config账户里面的数据并解码成 FaucetConfig 结构体 25 | let faucet_config = &mut ctx.accounts.faucet_config; 26 | faucet_config.token_program = *ctx.accounts.token_program.key; 27 | faucet_config.token_mint = *ctx.accounts.token_mint.key; 28 | faucet_config.token_authority = *ctx.accounts.token_authority.key; 29 | faucet_config.authority = *ctx.accounts.user.key; 30 | faucet_config.nonce = nonce; 31 | faucet_config.drip_volume = drip_volume; 32 | // 注意:这个到最后数据会自动存储 33 | Ok(()) 34 | } 35 | 36 | /** 37 | * 空投 38 | * @ctx 上下文(里面包含调用该函数所需要的所有AccountInfo账户) 39 | */ 40 | pub fn drip(ctx: Context) -> Result<()> { 41 | let faucet_config = ctx.accounts.faucet_config.clone(); 42 | // 获取种子(可参考token-swap程序) 43 | let seeds = &[ 44 | faucet_config.to_account_info().key.as_ref(), 45 | &[faucet_config.nonce], 46 | ]; 47 | // 签名种子 48 | let signer_seeds = &[&seeds[..]]; 49 | // 调用目标合约mint_to函数所需要的账户(这个目标合约是Token合约) 50 | let cpi_accounts = MintTo { 51 | // 代币信息账户 52 | mint: ctx.accounts.token_mint.to_account_info(), 53 | // 代币接收账户 54 | to: ctx.accounts.receiver.to_account_info(), 55 | // 代币信息账户所有者 56 | authority: ctx.accounts.token_authority.to_account_info(), 57 | }; 58 | // 目标合约地址 59 | let cpi_program = ctx.accounts.token_program.clone(); 60 | // 签名 61 | let cpi_ctx = CpiContext::new_with_signer(cpi_program,cpi_accounts,signer_seeds); 62 | // 调用目标合约,也就是Token合约的mint_to函数 63 | anchor_spl::token::mint_to(cpi_ctx, faucet_config.drip_volume)?; 64 | Ok(()) 65 | } 66 | 67 | /** 68 | * 修改水龙头一次给多少币 69 | */ 70 | pub fn set_drip_volume(ctx: Context, drip_volume: u64) -> Result<()> { 71 | // 获取faucet_config账户里面的数据并解码成 FaucetConfig 结构体 72 | let faucet_config = &mut ctx.accounts.faucet_config; 73 | faucet_config.drip_volume = drip_volume; 74 | // 注意:这个到最后会数据自动修改并存储 75 | Ok(()) 76 | } 77 | } -------------------------------------------------------------------------------- /hw_05_anchor_simple/programs/hw_05_anchor_simple/src/state.rs: -------------------------------------------------------------------------------- 1 | use crate::Pubkey; 2 | // 注意:所有和Anchor框架有关系的都需要添加这个依赖(否则编译无法通过) 3 | use anchor_lang::prelude::*; 4 | // self 表示当前模块可使用 token::xxx 来调用函数 5 | use anchor_spl::{token::{self, Token}}; 6 | use crate::error::FaucetError; 7 | 8 | 9 | // 配置调用initialize函数所需要的AccountInfo账户 10 | #[derive(Accounts)] 11 | pub struct InitializeFaucet<'info> { 12 | 13 | // 要存储水龙头配置信息的账户,配置信息的结构体是 FaucetConfig 14 | // init 表示初始数据,就是数据没有就创建,但是不能修改数据(注意:这里指的数据是 FaucetConfig 结构体数据) 15 | // payer 指定存储该数据由谁付钱(这里指定的是user账户,那么创建水龙头就是右user账户签名并付款) 16 | // space 指定数据空间(就是该数据最大存储空间) 17 | // 具体各个account的属性说明请参考:https://docs.rs/anchor-lang/0.25.0/anchor_lang/derive.Accounts.html 18 | #[account(init, payer = user, space = 8 + 8 + 8*1024)] 19 | pub faucet_config: Account<'info,FaucetConfig>, 20 | 21 | // Token程序地址 22 | // 注意:双引号引起来的是判断配置,如果 token_program.key != token::ID 会抛出异常 23 | // 还有在当前属性上判断并获取当前属性不需要加 & 符号取引用,但是获取其它属性需要加 & 符号,取引用 24 | // 说明:token::ID表示调用token的ID函数,这里的token表示公共的Token程序(就是官方的Token程序),也就是如果token_program的地址不等于官方的Token程序地址就抛出异常 25 | //#[account("token_program.key == &token::ID")] 26 | //pub token_program: AccountInfo<'info>, 下面这个是新的写法 27 | // (注意:这个/// CHECK: 表示该字段不需要验证,如果不加这个Anchor框架编译会报错,它会提示你说这个字段没有做权限验证) 28 | /// CHECK: 29 | pub token_program: Program<'info, Token>, 30 | 31 | // 代币信息账户(注意:mut声明,表示程序要可以修改代币信息账户里面的数据) 32 | // (注意:这个/// CHECK: 表示该字段不需要验证,如果不加这个Anchor框架编译会报错,它会提示你说这个字段没有做权限验证) 33 | /// CHECK: 34 | #[account(mut)] 35 | pub token_mint: AccountInfo<'info>, 36 | 37 | // 代币信息账户所有者 38 | // (注意:这个/// CHECK: 表示该字段不需要验证,如果不加这个Anchor框架编译会报错,它会提示你说这个字段没有做权限验证) 39 | /// CHECK: 40 | #[account()] 41 | pub token_authority: AccountInfo<'info>, 42 | 43 | // 数据存储费用(因为该函数是要初始化存储数据的,所以需要这个,如果是修改数据就不需要传这个了) 44 | pub rent: Sysvar<'info,Rent>, 45 | 46 | // 签名以及付款用户 47 | #[account(mut)] 48 | pub user: Signer<'info>, 49 | 50 | pub system_program: Program<'info, System>, 51 | } 52 | 53 | // 配置调用drip函数所需要的AccountInfo账户 54 | #[derive(Accounts)] 55 | pub struct Drip<'info> { 56 | // 水龙头配置信息账户,并自动将数据解码成FaucetConfig结构体 57 | #[account()] 58 | pub faucet_config: Account<'info,FaucetConfig>, 59 | 60 | // Token程序地址 61 | // 注意:双引号引起来的是判断配置,如果 token_program.key != token::ID 会抛出异常 62 | // 还有在当前属性上判断并获取当前属性不需要加 & 符号取引用,但是获取其它属性需要加 & 符号,取引用 63 | // 说明:token::ID表示调用token的ID函数,这里的token表示公共的Token程序(就是官方的Token程序),也就是如果token_program的地址不等于官方的Token程序地址就抛出异常 64 | //#[account("token_program.key == &token::ID")] 下面这个是新的写法 65 | // 注意:使用 &token::ID 当前文件顶部必须声明 使用 token 模块 66 | // (注意:这个/// CHECK: 表示该字段不需要验证,如果不加这个Anchor框架编译会报错,它会提示你说这个字段没有做权限验证) 67 | /// CHECK: 68 | #[account(constraint = token_program.key == &token::ID)] 69 | pub token_program: AccountInfo<'info>, 70 | 71 | // 代币信息账户(注意:mut声明,表示程序要可以修改代币信息账户里面的数据) 72 | //#[account(mut,"&faucet_config.token_mint == token_mint.key")] 下面这个是新的写法 73 | // (注意:这个/// CHECK: 表示该字段不需要验证,如果不加这个Anchor框架编译会报错,它会提示你说这个字段没有做权限验证) 74 | /// CHECK: 75 | #[account(mut, constraint = &faucet_config.token_mint == token_mint.key)] 76 | pub token_mint: AccountInfo<'info>, 77 | 78 | // 代币信息账户所有者 79 | //#[account("&faucet_config.token_authority == token_authority.key")] 下面这个是新的写法 80 | // (注意:这个/// CHECK: 表示该字段不需要验证,如果不加这个Anchor框架编译会报错,它会提示你说这个字段没有做权限验证) 81 | /// CHECK: 82 | #[account(constraint = &faucet_config.token_authority == token_authority.key)] 83 | pub token_authority: AccountInfo<'info>, 84 | 85 | // 代币接收账户 86 | // (注意:这个/// CHECK: 表示该字段不需要验证,如果不加这个Anchor框架编译会报错,它会提示你说这个字段没有做权限验证) 87 | /// CHECK: 88 | #[account(mut)] 89 | pub receiver: AccountInfo<'info>, 90 | } 91 | 92 | 93 | // 配置调用set_drip_volume函数所需要的AccountInfo账户 94 | #[derive(Accounts)] 95 | pub struct DripVolume<'info> { 96 | 97 | // has_one=authority 表示检查 FaucetConfig 结构体里面的authority属性是否与当前结构里面的属性authority是同一个地址 98 | // @ FaucetError::Forbidden 表示如果 has_one=authority 没有验证通过就抛 FaucetError::Forbidden 异常 99 | // 具体各个account的属性说明请参考:https://docs.rs/anchor-lang/0.25.0/anchor_lang/derive.Accounts.html 100 | #[account(mut, has_one = authority @ FaucetError::Forbidden)] 101 | pub faucet_config: Account<'info, FaucetConfig>, 102 | 103 | pub authority: Signer<'info>, 104 | } 105 | 106 | // 水龙头配置信息(用户账户会存储该信息) 107 | #[account] 108 | pub struct FaucetConfig { 109 | // Token程序地址 110 | pub token_program: Pubkey, 111 | // 代币信息账户 112 | pub token_mint: Pubkey, 113 | // 代币信息账户所有者 114 | pub token_authority: Pubkey, 115 | // 种子(注意:这个值是在前端使用 findProgramAddress([swap_info.publicKey],program_id) 所得到的) 116 | // 注意:该值也可以在Solana程序里面使用Pubkey::find_program_address函数获取,具体可参考token-swap程序 117 | pub nonce: u8, 118 | // 水龙头一次给多少币 119 | pub drip_volume: u64, 120 | // 水龙头的所有者(就是创建该水龙头时的签名付款账户) 121 | pub authority: Pubkey, 122 | } -------------------------------------------------------------------------------- /hw_05_anchor_simple/tests/hw_06_anchor_simple-dapp.ts: -------------------------------------------------------------------------------- 1 | // 测试相关逻辑 2 | 3 | import { BN, getProvider, web3, workspace } from "@project-serum/anchor"; 4 | // 注意:@project-serum/common库以过期无法使用,会抱函数不存在的错误 5 | import { 6 | createMint, 7 | createTokenAccountInstrs, 8 | getMintInfo, 9 | getTokenAccount, 10 | } from "@project-serum/common"; 11 | import { TokenInstructions } from "@project-serum/serum"; 12 | import assert from "assert"; 13 | import * as anchor from '@project-serum/anchor'; 14 | import { Program } from '@project-serum/anchor'; 15 | const { SystemProgram } = anchor.web3; 16 | import { PublicKey } from '@solana/web3.js'; 17 | const { TOKEN_PROGRAM_ID, Token, ASSOCIATED_TOKEN_PROGRAM_ID } = require("@solana/spl-token"); 18 | import { Hw06AnchorSimple } from '../target/types/hw_06_anchor_simple'; 19 | 20 | 21 | describe("Hw06AnchorSimple", () => { 22 | console.log("start..."); 23 | //const provider = anchor.Provider.env(); 24 | // @ts-ignore 25 | const provider = new anchor.getProvider(); 26 | anchor.setProvider(provider); 27 | const faucetProgram = workspace.Hw06AnchorSimple as Program; 28 | 29 | let faucetConfig: web3.Keypair; 30 | let testTokenMint: web3.PublicKey; 31 | let testTokenAuthority: web3.PublicKey; 32 | let nonce: number; 33 | 34 | const testTokenDecimals = 9; 35 | const dripVolume: BN = new BN(10 ** testTokenDecimals); 36 | const dripVolume_next: BN = new BN(10 ** testTokenDecimals + 1); 37 | 38 | // 测试开始先做一些初始化的操作(注意:由于@project-serum/common库以过期无法使用,会抱函数不存在的错误) 39 | before(async () => { 40 | faucetConfig = web3.Keypair.generate(); 41 | [testTokenAuthority, nonce] = await web3.PublicKey.findProgramAddress( 42 | [faucetConfig.publicKey.toBuffer()], 43 | faucetProgram.programId 44 | ); 45 | console.log("createMint..."); 46 | testTokenMint = await createMint(provider, testTokenAuthority, testTokenDecimals); 47 | console.log("faucetConfig:", faucetConfig.publicKey.toString()); 48 | console.log("faucetProgram.programId", faucetProgram.programId.toString()); 49 | console.log("testTokenAuthority:", testTokenAuthority.toString()); 50 | console.log("nonce", nonce); 51 | console.log("testTokenMint", testTokenMint.toString()); 52 | }); 53 | 54 | // 测试合约initialize函数(注意:由于@project-serum/common库以过期无法使用,会抱函数不存在的错误) 55 | describe("# initialize", () => { 56 | it("Should init successful", async () => { 57 | await faucetProgram.rpc.initialize(nonce, dripVolume, { 58 | accounts: { 59 | faucetConfig: faucetConfig.publicKey, 60 | tokenProgram: TokenInstructions.TOKEN_PROGRAM_ID, 61 | tokenMint: testTokenMint, 62 | tokenAuthority: testTokenAuthority, 63 | rent: web3.SYSVAR_RENT_PUBKEY, 64 | 65 | user: provider.wallet.publicKey, 66 | systemProgram: SystemProgram.programId 67 | }, 68 | signers: [faucetConfig], 69 | }); 70 | 71 | const faucetConfigAccount = await faucetProgram.account.faucetConfig.fetch(faucetConfig.publicKey); 72 | 73 | assert.strictEqual( 74 | faucetConfigAccount.tokenProgram.toBase58(), 75 | TokenInstructions.TOKEN_PROGRAM_ID.toBase58() 76 | ); 77 | assert.strictEqual( 78 | faucetConfigAccount.tokenMint.toBase58(), 79 | testTokenMint.toBase58() 80 | ); 81 | assert.strictEqual( 82 | faucetConfigAccount.tokenAuthority.toBase58(), 83 | testTokenAuthority.toBase58() 84 | ); 85 | assert.strictEqual(faucetConfigAccount.nonce, nonce); 86 | assert.strictEqual( 87 | faucetConfigAccount.dripVolume.toNumber(), 88 | dripVolume.toNumber() 89 | ); 90 | }); 91 | it("Updates Drip Volume", async () => { 92 | await faucetProgram.rpc.setDripVolume(dripVolume_next, { 93 | accounts: { 94 | faucetConfig: faucetConfig.publicKey, 95 | authority: provider.wallet.publicKey, 96 | }, 97 | }); 98 | 99 | const configAccount = await faucetProgram.account.faucetConfig.fetch(faucetConfig.publicKey); 100 | 101 | assert.ok(configAccount.authority.equals(provider.wallet.publicKey)); 102 | assert.ok(configAccount.dripVolume.toNumber() == dripVolume_next.toNumber()); 103 | }); 104 | }); 105 | 106 | // 测试合约drip函数(注意:由于@project-serum/common库以过期无法使用,会抱函数不存在的错误) 107 | describe("# drip", () => { 108 | it("Should drip successful", async () => { 109 | const signers: web3.Keypair[] = []; 110 | const instructions: web3.TransactionInstruction[] = []; 111 | const receiver = web3.Keypair.generate(); 112 | const receiverTokenAccount = web3.Keypair.generate(); 113 | instructions.push( 114 | ...(await createTokenAccountInstrs( 115 | provider, 116 | receiverTokenAccount.publicKey, 117 | testTokenMint, 118 | receiver.publicKey 119 | )) 120 | ); 121 | signers.push(receiverTokenAccount); 122 | 123 | const tokenMintInfo = await getMintInfo(provider, testTokenMint); 124 | await faucetProgram.rpc.drip({ 125 | accounts: { 126 | faucetConfig: faucetConfig.publicKey, 127 | tokenProgram: TokenInstructions.TOKEN_PROGRAM_ID, 128 | tokenMint: testTokenMint, 129 | receiver: receiverTokenAccount.publicKey, 130 | tokenAuthority: tokenMintInfo.mintAuthority!! 131 | }, 132 | instructions: instructions, 133 | signers: signers, 134 | }); 135 | 136 | const tokenAccount = await getTokenAccount( 137 | provider, 138 | receiverTokenAccount.publicKey 139 | ); 140 | 141 | assert.strictEqual(tokenAccount.amount.toNumber(), dripVolume_next.toNumber()); 142 | }); 143 | }); 144 | }); 145 | -------------------------------------------------------------------------------- /hw_05_anchor_simple/tsconfig.json: -------------------------------------------------------------------------------- 1 | // TypeScript 相关配置 2 | { 3 | "compilerOptions": { 4 | "types": ["mocha", "chai"], 5 | "typeRoots": ["./node_modules/@types"], 6 | "lib": ["es2015"], 7 | "module": "commonjs", 8 | "target": "es6", 9 | "moduleResolution": "node", 10 | "strictNullChecks": true, 11 | "baseUrl": ".", 12 | "esModuleInterop": true 13 | }, 14 | "exclude": [ 15 | "node_modules" 16 | ], 17 | "include": [ 18 | "./tests/**/*" 19 | ] 20 | } 21 | --------------------------------------------------------------------------------