├── src ├── api_routes │ ├── mod.rs │ ├── data.rs │ └── construction.rs ├── consts.rs ├── operations │ ├── mod.rs │ ├── system.rs │ ├── vote.rs │ ├── stake.rs │ ├── spltoken.rs │ ├── utils.rs │ └── matcher.rs ├── call.rs ├── block.rs ├── utils.rs ├── network.rs ├── main.rs ├── account.rs ├── features.rs ├── error.rs ├── types.rs └── construction.rs ├── docker-compose.yml ├── Dockerfile ├── .dockerignore ├── .gitignore ├── .vscode └── launch.json ├── rosetta-cli-conf ├── devnet.json └── solana.ros ├── Cargo.toml ├── README.md └── LICENSE.txt /src/api_routes/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod construction; 2 | pub mod data; 3 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | rosetta-solana: 4 | image: rosetta-solana 5 | container_name: rosetta-solana 6 | environment: 7 | - HOST=0.0.0.0 8 | - RUST_BACKTRACE=1 9 | ports: 10 | - "8080:8080" 11 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rustlang/rust:nightly as builder 2 | WORKDIR /app 3 | COPY . . 4 | RUN apt-get update && apt-get install -y libudev-dev 5 | RUN cargo build --release --bin rosetta-solana 6 | 7 | FROM rust as runtime 8 | COPY --from=builder /app/target/release/rosetta-solana /app/rosetta-solana 9 | EXPOSE 8080 10 | CMD ["/app/rosetta-solana"] -------------------------------------------------------------------------------- /src/consts.rs: -------------------------------------------------------------------------------- 1 | pub const BLOCKCHAIN: &str = "solana"; 2 | pub const MIDDLEWARE_VERSION: &str = env!("CARGO_PKG_VERSION"); 3 | pub const ROSETTA_VERSION: &str = "1.4.4"; 4 | pub const NODE_VERSION: &str = "1.4.17"; 5 | 6 | pub const NATIVE_SYMBOL: &str = "SOL"; 7 | pub const NATIVE_DECIMALS: u8 = 9; 8 | pub const SEPARATOR: &str = "__"; //TODO: This should be only once in str or breaks 9 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 7 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 8 | Cargo.lock 9 | 10 | # These are backup files generated by rustfmt 11 | **/*.rs.bk 12 | 13 | rosetta-cli 14 | testcnfgs/cli-data/ -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 7 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 8 | Cargo.lock 9 | 10 | # These are backup files generated by rustfmt 11 | **/*.rs.bk 12 | 13 | rosetta-cli 14 | rosetta-cli-conf/cli-data/ -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "lldb", 9 | "request": "launch", 10 | "name": "Debug", 11 | "program": "${workspaceFolder}/", 12 | "args": [], 13 | "cwd": "${workspaceFolder}" 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /rosetta-cli-conf/devnet.json: -------------------------------------------------------------------------------- 1 | { 2 | "network": { 3 | "blockchain": "solana", 4 | "network": "devnet" 5 | }, 6 | "online_url": "http://127.0.0.1:8080", 7 | "data_directory": "cli-data", 8 | "http_timeout": 30000, 9 | "max_retries": 15, 10 | "max_online_connections": 500, 11 | "max_sync_concurrency": 64, 12 | "tip_delay": 120, 13 | "compression_disabled": true, 14 | "memory_limit_disabled": true, 15 | "construction": { 16 | "stale_depth": 1000000, 17 | "broadcast_limit": 5, 18 | "constructor_dsl_file": "solana.ros", 19 | "end_conditions": { 20 | "transfer": 5 21 | } 22 | }, 23 | "data": { 24 | "start_index": 31195318, 25 | "initial_balance_fetch_disabled": true, 26 | "active_reconciliation_concurrency": 32, 27 | "end_conditions": { 28 | "index": 31195418, 29 | "reconciliation_coverage": { 30 | "coverage": 0.95, 31 | "from_tip": true 32 | } 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /src/operations/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod matcher; 2 | pub mod spltoken; 3 | pub mod stake; 4 | pub mod system; 5 | pub mod utils; 6 | pub mod vote; 7 | 8 | use crate::{ 9 | error::ApiError, 10 | types::{Operation, OptionalInternalOperationMetadatas}, 11 | }; 12 | 13 | use serde::{Deserialize, Serialize}; 14 | use solana_sdk::{message::Message, transaction::Transaction}; 15 | 16 | use self::matcher::Matcher; 17 | 18 | impl Operation { 19 | pub fn amount(&self) -> f64 { 20 | self.amount.clone().unwrap().value.parse::().unwrap() 21 | } 22 | pub fn address(&self) -> String { 23 | self.account.clone().unwrap().address 24 | } 25 | } 26 | 27 | pub fn get_tx_from_str(s: &str) -> Result { 28 | let try_bs58 = bs58::decode(&s).into_vec().unwrap_or(vec![]); 29 | if try_bs58.len() == 0 { 30 | return Err(ApiError::InvalidSignedTransaction); 31 | } 32 | let data = try_bs58; 33 | /* 34 | let try_base64 = base64::decode(&s); 35 | let data = if try_base64.is_err() { 36 | } else { 37 | try_base64.unwrap() 38 | }; 39 | */ 40 | Ok(bincode::deserialize(&data).unwrap()) 41 | } 42 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rosetta-solana" 3 | version = "0.1.0" 4 | authors = ["imerkle "] 5 | edition = "2018" 6 | description="Rosetta Server for Solana Blockchain" 7 | license="Apache-2.0" 8 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 9 | 10 | [dependencies] 11 | serde = { version = "1.0", features = ["derive"] } 12 | serde_json = "1.0" 13 | rocket="0.4.6" 14 | rocket_contrib="0.4.6" 15 | thiserror="1.0.23" 16 | bincode="1.3.1" 17 | hex="0.4.2" 18 | bs58="0.4.0" 19 | base64="0.13.0" 20 | strum = { version = "0.18", features = ["derive"] } 21 | strum_macros="0.20.1" 22 | convert_case="0.4.0" 23 | merge="0.1.0" 24 | 25 | spl-token = { version="3.0.1", features = ["no-entrypoint"] } 26 | spl-feature-proposal = { version="1.0.0" } 27 | #spl-token-v3 = { version="3.0.1", features = ["no-entrypoint"] } 28 | spl-associated-token-account = { version="1.0.2", features = ["no-entrypoint"] } 29 | solana-sdk = { version="1.4.17" } 30 | solana-client = { version="1.4.17" } 31 | solana-cli-config = { version="1.4.17" } 32 | solana-metrics = {version="1.4.17" } 33 | solana-transaction-status = { version="1.4.17"} 34 | solana-account-decoder = { version="1.4.17"} 35 | solana-stake-program = { version="1.4.17"} 36 | solana-vote-program = { version="1.4.17"} 37 | 38 | [dev-dependencies] 39 | ed25519-dalek = { version = "=1.0.0-pre.4" } 40 | -------------------------------------------------------------------------------- /src/call.rs: -------------------------------------------------------------------------------- 1 | use rocket_contrib::json::Json; 2 | use solana_client::{http_sender::HttpSender, rpc_request::RpcRequest, rpc_sender::RpcSender}; 3 | 4 | use crate::{ 5 | error::ApiError, 6 | is_bad_network, 7 | types::{CallRequest, CallResponse}, 8 | Options, Options2, 9 | }; 10 | 11 | //temporary workaround sender when rpc client adds send()->Value fn this can be removed 12 | pub struct RpcSender2 { 13 | sender: HttpSender, 14 | } 15 | impl RpcSender2 { 16 | pub fn new(rpc_url: String) -> RpcSender2 { 17 | let sender = HttpSender::new(rpc_url); 18 | RpcSender2 { sender } 19 | } 20 | pub fn send( 21 | &self, 22 | request: RpcRequest, 23 | params: serde_json::Value, 24 | ) -> Result { 25 | let response = self 26 | .sender 27 | .send(request, params) 28 | .map_err(|err| err.into_with_request(request))?; 29 | Ok(response) 30 | } 31 | } 32 | pub fn call_direct( 33 | req: CallRequest, 34 | options: &Options, 35 | options2: &Options2, 36 | ) -> Result, ApiError> { 37 | is_bad_network(&options, &req.network_identifier)?; 38 | 39 | if !(req.parameters.is_array() || req.parameters.is_null()) { 40 | return Err(ApiError::BadRequest); 41 | } 42 | 43 | let result = options2.rpc2.send( 44 | solana_client::rpc_request::RpcRequest::from(req.method), 45 | req.parameters, 46 | )?; 47 | let response = CallResponse { 48 | result, 49 | idempotent: false, 50 | }; 51 | Ok(Json(response)) 52 | } 53 | -------------------------------------------------------------------------------- /src/block.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | 3 | use crate::{ 4 | error::ApiError, 5 | is_bad_network, 6 | types::Block, 7 | types::BlockRequest, 8 | types::BlockTransactionResponse, 9 | types::{BlockIdentifier, BlockResponse, BlockTransactionRequest, Transaction}, 10 | Options, 11 | }; 12 | 13 | use rocket_contrib::json::Json; 14 | 15 | use solana_sdk::signature::Signature; 16 | use solana_transaction_status::UiTransactionEncoding; 17 | 18 | pub fn block( 19 | block_request: BlockRequest, 20 | options: &Options, 21 | ) -> Result, ApiError> { 22 | is_bad_network(&options, &block_request.network_identifier)?; 23 | 24 | let block_index = block_request 25 | .block_identifier 26 | .index 27 | .ok_or_else(|| ApiError::BadRequest)?; 28 | let block_result = options.rpc.get_confirmed_block_with_encoding( 29 | block_index, 30 | solana_transaction_status::UiTransactionEncoding::JsonParsed, 31 | ); 32 | let block; 33 | if block_result.is_err() { 34 | return Ok(Json(BlockResponse { block: None })); 35 | } else { 36 | block = block_result.unwrap(); 37 | } 38 | let transactions = block 39 | .transactions 40 | .iter() 41 | .map(|x| Transaction::from(x)) 42 | .collect::>(); 43 | let response = BlockResponse { 44 | block: Some(Block { 45 | block_identifier: BlockIdentifier { 46 | index: block_index, 47 | hash: block.blockhash, 48 | }, 49 | parent_block_identifier: BlockIdentifier { 50 | index: block.parent_slot, 51 | hash: block.previous_blockhash, 52 | }, 53 | timestamp: (block.block_time.unwrap_or(1611091000) as u64) * 1000, 54 | transactions: transactions, 55 | }), 56 | }; 57 | Ok(Json(response)) 58 | } 59 | 60 | pub fn block_transaction( 61 | block_transaction_request: BlockTransactionRequest, 62 | options: &Options, 63 | ) -> Result, ApiError> { 64 | is_bad_network(&options, &block_transaction_request.network_identifier)?; 65 | let hash = Signature::from_str( 66 | &block_transaction_request 67 | .transaction_identifier 68 | .hash 69 | .as_str(), 70 | )?; 71 | 72 | //FIXME: invalid type: null, expected struct EncodedConfirmedTransaction when tx dont exists should return Option instead of directly to prevent this shold be fixed in sdk itself 73 | let tx = options 74 | .rpc 75 | .get_confirmed_transaction(&hash, UiTransactionEncoding::JsonParsed)?; 76 | let response = BlockTransactionResponse { 77 | transaction: Transaction::from(&tx.transaction), 78 | }; 79 | Ok(Json(response)) 80 | } 81 | -------------------------------------------------------------------------------- /rosetta-cli-conf/solana.ros: -------------------------------------------------------------------------------- 1 | request_funds(1){ 2 | find_account{ 3 | currency = {"symbol":"SOL", "decimals":9}; 4 | random_account = find_balance({ 5 | "minimum_balance":{ 6 | "value": "0", 7 | "currency": {{currency}} 8 | }, 9 | "create_limit":1 10 | }); 11 | }, 12 | 13 | // Create a separate scenario to request funds so that 14 | // the address we are using to request funds does not 15 | // get rolled back if funds do not yet exist. 16 | request{ 17 | loaded_account = find_balance({ 18 | "account_identifier": {{random_account.account_identifier}}, 19 | "minimum_balance":{ 20 | "value": "1", 21 | "currency": {{currency}} 22 | } 23 | }); 24 | } 25 | } 26 | 27 | create_account(1){ 28 | create{ 29 | network = {"network":"devnet", "blockchain":"solana"}; 30 | key = generate_key({"curve_type": "edwards25519"}); 31 | account = derive({ 32 | "network_identifier": {{network}}, 33 | "public_key": {{key.public_key}} 34 | }); 35 | 36 | // If the account is not saved, the key will be lost! 37 | save_account({ 38 | "account_identifier": {{account.account_identifier}}, 39 | "keypair": {{key}} 40 | }); 41 | } 42 | } 43 | 44 | transfer(1){ 45 | transfer{ 46 | transfer.network = {"network":"devnet", "blockchain":"solana"}; 47 | currency = {"symbol":"SOL", "decimals":9}; 48 | sender = find_balance({ 49 | "minimum_balance":{ 50 | "value": "1", 51 | "currency": {{currency}} 52 | } 53 | }); 54 | 55 | // Set the recipient_amount as some value <= sender.balance-max_fee 56 | max_fee = "100000"; 57 | available_amount = {{sender.balance.value}} - {{max_fee}}; 58 | //available_amount = "1000"; 59 | recipient_amount = random_number({"minimum": "1", "maximum": {{available_amount}}}); 60 | print_message({"recipient_amount":{{recipient_amount}}}); 61 | 62 | // Find recipient and construct operations 63 | sender_amount = 0 - {{recipient_amount}}; 64 | recipient = find_balance({ 65 | "not_account_identifier":[{{sender.account_identifier}}], 66 | "minimum_balance":{ 67 | "value": "0", 68 | "currency": {{currency}} 69 | }, 70 | "create_limit": 100, 71 | "create_probability": 50 72 | }); 73 | transfer.confirmation_depth = "1"; 74 | transfer.operations = [ 75 | { 76 | "operation_identifier":{"index":0}, 77 | "type":"System__Transfer", 78 | "account":{{sender.account_identifier}}, 79 | "amount":{ 80 | "value":{{sender_amount}}, 81 | "currency":{{currency}} 82 | } 83 | }, 84 | { 85 | "operation_identifier":{"index":1}, 86 | "type":"System__Transfer", 87 | "account":{{recipient.account_identifier}}, 88 | "amount":{ 89 | "value":{{recipient_amount}}, 90 | "currency":{{currency}} 91 | } 92 | } 93 | ]; 94 | } 95 | } -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | 3 | use crate::{ 4 | consts, 5 | operations::utils::get_operations_from_encoded_tx, 6 | types::{OperationStatusType, OperationType, Transaction, TransactionIdentifier}, 7 | }; 8 | use convert_case::{Case, Casing}; 9 | 10 | use solana_sdk::pubkey::Pubkey; 11 | use solana_transaction_status::EncodedTransactionWithStatusMeta; 12 | 13 | impl From<&EncodedTransactionWithStatusMeta> for Transaction { 14 | fn from(x: &EncodedTransactionWithStatusMeta) -> Self { 15 | let mut status = OperationStatusType::Success; 16 | if let Some(x) = &x.meta { 17 | if let Some(_) = &x.err { 18 | status = OperationStatusType::Faliure; 19 | } 20 | }; 21 | let (operations, tx_hash) = get_operations_from_encoded_tx(&x.transaction, Some(status)); 22 | Transaction { 23 | transaction_identifier: TransactionIdentifier { hash: tx_hash }, 24 | metadata: x.meta.clone(), 25 | operations: operations, 26 | } 27 | } 28 | } 29 | pub fn to_pub(s: &str) -> Pubkey { 30 | Pubkey::from_str(&s).unwrap() 31 | } 32 | pub fn to_pub_optional(s: Option) -> Option { 33 | if let Some(x) = &s { 34 | Some(Pubkey::from_str(x).unwrap()) 35 | } else { 36 | None 37 | } 38 | } 39 | 40 | pub fn get_operation_type_with_program(program: &str, s: &str) -> OperationType { 41 | let to_pascal = program.to_case(Case::Pascal); 42 | 43 | let newstr = format!( 44 | "{}{}{}", 45 | to_pascal, 46 | consts::SEPARATOR, 47 | s.to_case(Case::Pascal) 48 | ); 49 | OperationType::from_str(&newstr).unwrap_or(OperationType::Unknown) 50 | } 51 | pub fn get_operation_type(s: &str) -> OperationType { 52 | let x = s.split(consts::SEPARATOR).collect::>(); 53 | if x.len() < 2 { 54 | return OperationType::Unknown; 55 | } 56 | get_operation_type_with_program(x[0], x[1]) 57 | } 58 | 59 | #[cfg(test)] 60 | mod tests { 61 | use crate::types::*; 62 | use serde_json::json; 63 | 64 | use super::*; 65 | #[test] 66 | fn op_type_test() { 67 | assert_eq!( 68 | get_operation_type("spl-token__transfer"), 69 | OperationType::SplToken__Transfer 70 | ); 71 | assert_eq!( 72 | get_operation_type_with_program("spl-token", "transfer"), 73 | OperationType::SplToken__Transfer 74 | ); 75 | assert_eq!( 76 | get_operation_type_with_program("spl-token", "transferChecked"), 77 | OperationType::SplToken__TransferChecked 78 | ); 79 | assert_eq!( 80 | get_operation_type_with_program("system", "withdrawFromNonce"), 81 | OperationType::System__WithdrawFromNonce 82 | ); 83 | assert_eq!( 84 | get_operation_type("system__transfer"), 85 | OperationType::System__Transfer 86 | ); 87 | assert_eq!(get_operation_type("Invalid"), OperationType::Unknown); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/api_routes/data.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | account, block, 3 | call::call_direct, 4 | error::ApiError, 5 | features::get_features, 6 | network, 7 | types::CallRequest, 8 | types::FeaturesRequest, 9 | types::{ 10 | AccountBalanceRequest, AccountBalanceResponse, BlockRequest, BlockResponse, 11 | BlockTransactionRequest, BlockTransactionResponse, CallResponse, FeaturesResponse, 12 | NetworkListResponse, NetworkOptionsResponse, NetworkRequest, NetworkStatusResponse, 13 | }, 14 | Options, Options2, 15 | }; 16 | use rocket::State; 17 | use rocket_contrib::json::Json; 18 | use solana_client::{http_sender::HttpSender, rpc_request::RpcRequest, rpc_sender::RpcSender}; 19 | 20 | #[post("/network/list")] 21 | pub fn network_list(options: State) -> Result, ApiError> { 22 | network::network_list(options.inner()) 23 | } 24 | #[post("/network/options", data = "")] 25 | pub fn network_options( 26 | network_request: Json, 27 | options: State, 28 | ) -> Result, ApiError> { 29 | network::network_options(network_request.into_inner(), options.inner()) 30 | } 31 | #[post("/network/status", data = "")] 32 | pub fn network_status( 33 | network_request: Json, 34 | options: State, 35 | ) -> Result, ApiError> { 36 | network::network_status(network_request.into_inner(), options.inner()) 37 | } 38 | 39 | #[post("/account/balance", data = "")] 40 | pub fn account_balance( 41 | account_balance_request: Json, 42 | options: State, 43 | ) -> Result, ApiError> { 44 | account::account_balance(account_balance_request.into_inner(), options.inner()) 45 | } 46 | 47 | #[post("/block", data = "")] 48 | pub fn get_block( 49 | block_request: Json, 50 | options: State, 51 | ) -> Result, ApiError> { 52 | block::block(block_request.into_inner(), options.inner()) 53 | } 54 | #[post("/block/transaction", data = "")] 55 | pub fn block_transaction( 56 | block_transaction_request: Json, 57 | options: State, 58 | ) -> Result, ApiError> { 59 | block::block_transaction(block_transaction_request.into_inner(), options.inner()) 60 | } 61 | 62 | #[post("/call", data = "")] 63 | pub fn call( 64 | call_request: Json, 65 | options: State, 66 | options2: State, 67 | ) -> Result, ApiError> { 68 | call_direct(call_request.into_inner(), options.inner(), options2.inner()) 69 | } 70 | 71 | #[post("/features", data = "")] 72 | pub fn features( 73 | features_request: Json, 74 | options: State, 75 | ) -> Result, ApiError> { 76 | get_features(features_request.into_inner(), options.inner()) 77 | } 78 | -------------------------------------------------------------------------------- /src/operations/system.rs: -------------------------------------------------------------------------------- 1 | use crate::{error::ApiError, types::OperationType, utils::to_pub}; 2 | use merge::Merge; 3 | use serde::{Deserialize, Serialize}; 4 | use solana_sdk::{instruction::Instruction, system_instruction}; 5 | 6 | #[derive(Merge, Clone, Debug, Deserialize, Serialize, Default)] 7 | pub struct SystemOperationMetadata { 8 | pub source: Option, 9 | pub destination: Option, 10 | pub space: Option, 11 | pub lamports: Option, 12 | pub authority: Option, 13 | pub new_authority: Option, 14 | } 15 | pub fn to_instruction( 16 | type_: OperationType, 17 | metadata: SystemOperationMetadata, 18 | ) -> Result, ApiError> { 19 | let instructions = match type_ { 20 | OperationType::System__CreateAccount => vec![system_instruction::create_account( 21 | &to_pub(&metadata.source.unwrap()), 22 | &to_pub(&metadata.destination.unwrap()), 23 | metadata.lamports.unwrap(), 24 | metadata.space.unwrap(), 25 | &spl_token::id(), 26 | )], 27 | OperationType::System__Assign => vec![system_instruction::assign( 28 | &to_pub(&metadata.source.unwrap()), 29 | &spl_token::id(), 30 | )], 31 | OperationType::System__Transfer => vec![system_instruction::transfer( 32 | &to_pub(&metadata.source.unwrap()), 33 | &to_pub(&metadata.destination.unwrap()), 34 | metadata.lamports.unwrap(), 35 | )], 36 | OperationType::System__CreateNonceAccount => system_instruction::create_nonce_account( 37 | &to_pub(&metadata.source.unwrap()), 38 | &to_pub(&metadata.destination.unwrap()), 39 | &to_pub(&metadata.authority.unwrap()), 40 | metadata.lamports.unwrap_or(1000000000), 41 | ), 42 | 43 | OperationType::System__AdvanceNonce => vec![system_instruction::advance_nonce_account( 44 | &to_pub(&metadata.destination.unwrap()), 45 | &to_pub(&metadata.authority.unwrap()), 46 | )], 47 | OperationType::System__WithdrawFromNonce => { 48 | vec![system_instruction::withdraw_nonce_account( 49 | &to_pub(&metadata.source.unwrap()), 50 | &to_pub(&metadata.authority.unwrap()), 51 | &to_pub(&metadata.destination.unwrap()), 52 | metadata.lamports.unwrap(), 53 | )] 54 | } 55 | OperationType::System__AuthorizeNonce => vec![system_instruction::authorize_nonce_account( 56 | &to_pub(&metadata.destination.unwrap()), 57 | &to_pub(&metadata.authority.unwrap()), 58 | &to_pub(&metadata.new_authority.unwrap()), 59 | )], 60 | OperationType::System__Allocate => vec![system_instruction::allocate( 61 | &to_pub(&metadata.source.unwrap()), 62 | metadata.space.unwrap(), 63 | )], 64 | _ => { 65 | return Err(ApiError::BadOperations("Invalid Operation".to_string())); 66 | } 67 | }; 68 | Ok(instructions) 69 | } 70 | -------------------------------------------------------------------------------- /src/operations/vote.rs: -------------------------------------------------------------------------------- 1 | use crate::{error::ApiError, types::OperationType, utils::to_pub}; 2 | use merge::Merge; 3 | use serde::{Deserialize, Serialize}; 4 | use solana_sdk::instruction::Instruction; 5 | use solana_vote_program::{ 6 | vote_instruction, 7 | vote_state::{VoteAuthorize, VoteInit}, 8 | }; 9 | 10 | use super::matcher::InternalOperation; 11 | 12 | #[derive(Merge, Default, Clone, Debug, Deserialize, Serialize)] 13 | pub struct VoteOperationMetadata { 14 | pub source: Option, 15 | pub destination: Option, 16 | pub lamports: Option, 17 | pub authority: Option, 18 | pub voter: Option, 19 | pub withdrawer: Option, 20 | pub vote_pubkey: Option, 21 | pub comission: Option, 22 | } 23 | pub fn to_instruction( 24 | type_: OperationType, 25 | metadata: VoteOperationMetadata, 26 | ) -> Result, ApiError> { 27 | let instructions = match type_ { 28 | OperationType::Vote__CreateAccount => { 29 | let authority = to_pub(&metadata.authority.unwrap()); 30 | //TODO: Add in meta later 31 | let vote_init = VoteInit { 32 | node_pubkey: authority.clone(), 33 | authorized_voter: authority.clone(), 34 | authorized_withdrawer: authority.clone(), 35 | commission: 100, 36 | }; 37 | vote_instruction::create_account( 38 | &to_pub(&metadata.source.unwrap()), 39 | &to_pub(&metadata.destination.unwrap()), 40 | &vote_init, 41 | metadata.lamports.unwrap(), 42 | ) 43 | } 44 | OperationType::Vote__Authorize => { 45 | let mut inx = vec![]; 46 | if let Some(x) = &metadata.voter { 47 | inx.push(vote_instruction::authorize( 48 | &to_pub(&metadata.destination.clone().unwrap()), 49 | &to_pub(&metadata.source.clone().unwrap()), 50 | &to_pub(x), 51 | VoteAuthorize::Voter, 52 | )) 53 | } 54 | if let Some(x) = &metadata.withdrawer { 55 | inx.push(vote_instruction::authorize( 56 | &to_pub(&metadata.destination.unwrap()), 57 | &to_pub(&metadata.source.unwrap()), 58 | &to_pub(x), 59 | VoteAuthorize::Withdrawer, 60 | )) 61 | } 62 | inx 63 | } 64 | OperationType::Vote__Withdraw => vec![vote_instruction::withdraw( 65 | &to_pub(&metadata.source.unwrap()), 66 | &to_pub(&metadata.authority.unwrap()), 67 | metadata.lamports.unwrap(), 68 | &to_pub(&metadata.destination.unwrap()), 69 | )], 70 | OperationType::Vote__UpdateValidatorIdentity => { 71 | vec![vote_instruction::update_validator_identity( 72 | &to_pub(&metadata.vote_pubkey.unwrap()), 73 | &to_pub(&metadata.withdrawer.unwrap()), 74 | &to_pub(&metadata.voter.unwrap()), 75 | )] 76 | } 77 | OperationType::Vote__UpdateCommission => vec![vote_instruction::update_commission( 78 | &to_pub(&metadata.vote_pubkey.unwrap()), 79 | &to_pub(&metadata.withdrawer.unwrap()), 80 | metadata.comission.unwrap(), 81 | )], 82 | _ => { 83 | return Err(ApiError::BadOperations("Invalid Operation".to_string())); 84 | } 85 | }; 86 | Ok(instructions) 87 | } 88 | -------------------------------------------------------------------------------- /src/network.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | consts, 3 | error::ApiError, 4 | is_bad_network, 5 | types::Allow, 6 | types::RpcRequestInternal, 7 | types::{ 8 | BlockIdentifier, NetworkIdentifier, NetworkListResponse, NetworkOptionsResponse, 9 | NetworkRequest, NetworkStatusResponse, OperationStatus, OperationStatusType, OperationType, 10 | Peer, Version, 11 | }, 12 | Options, 13 | }; 14 | 15 | use rocket_contrib::json::Json; 16 | use strum::IntoEnumIterator; 17 | 18 | pub fn network_list(options: &Options) -> Result, ApiError> { 19 | let response = NetworkListResponse { 20 | network_identifiers: vec![NetworkIdentifier { 21 | blockchain: consts::BLOCKCHAIN.to_string(), 22 | network: options.network.clone(), //TODO: genesis config cluster type 23 | sub_network_identifier: None, 24 | }], 25 | }; 26 | Ok(Json(response)) 27 | } 28 | pub fn network_options( 29 | network_request: NetworkRequest, 30 | options: &Options, 31 | ) -> Result, ApiError> { 32 | is_bad_network(&options, &network_request.network_identifier)?; 33 | 34 | let version = Version { 35 | rosetta_version: consts::ROSETTA_VERSION.to_string(), 36 | node_version: consts::NODE_VERSION.to_string(), 37 | middleware_version: consts::MIDDLEWARE_VERSION.to_string(), 38 | }; 39 | 40 | let operation_statuses = vec![ 41 | OperationStatus { 42 | status: OperationStatusType::Success, 43 | successful: true, 44 | }, 45 | OperationStatus { 46 | status: OperationStatusType::Faliure, 47 | successful: false, 48 | }, 49 | ]; 50 | 51 | let errors = ApiError::all_errors(); 52 | 53 | let allow = Allow { 54 | operation_statuses, 55 | operation_types: OperationType::iter().collect(), 56 | errors, 57 | historical_balance_lookup: false, 58 | timestamp_start_index: Some(0), // TODO: find this 59 | call_methods: RpcRequestInternal::iter().collect(), 60 | balance_exemptions: vec![], 61 | }; 62 | 63 | let response = NetworkOptionsResponse { version, allow }; 64 | Ok(Json(response)) 65 | } 66 | pub fn network_status( 67 | network_request: NetworkRequest, 68 | options: &Options, 69 | ) -> Result, ApiError> { 70 | is_bad_network(&options, &network_request.network_identifier)?; 71 | 72 | let genesis = options.rpc.get_genesis_hash()?; 73 | let index = options.rpc.get_first_available_block()?; 74 | let genesis_block_identifier = BlockIdentifier { 75 | index: index, 76 | hash: genesis.to_string(), 77 | }; 78 | let (slot, slot_time, current_block_identifier) = get_current_block(&options)?; 79 | let current_block_timestamp = (slot_time * 1000) as u64; 80 | let cluster_nodes = options.rpc.get_cluster_nodes()?; 81 | let peers: Vec = cluster_nodes 82 | .into_iter() 83 | .map(|x| Peer { peer_id: x.pubkey }) 84 | .collect(); 85 | 86 | let response = NetworkStatusResponse { 87 | current_block_identifier, 88 | current_block_timestamp, 89 | genesis_block_identifier, 90 | peers, 91 | }; 92 | 93 | Ok(Json(response)) 94 | } 95 | 96 | pub fn get_current_block(options: &Options) -> Result<(u64, i64, BlockIdentifier), ApiError> { 97 | let slot = options.rpc.get_slot()?; 98 | let slot_time = options.rpc.get_block_time(slot)?; 99 | let current_block_identifier = BlockIdentifier { 100 | index: slot, 101 | hash: slot.to_string(), //TODO: should be hash not slot 102 | }; 103 | Ok((slot, slot_time, current_block_identifier)) 104 | } 105 | -------------------------------------------------------------------------------- /src/api_routes/construction.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | types::{ 3 | ConstructionCombineRequest, ConstructionCombineResponse, ConstructionDeriveRequest, 4 | ConstructionDeriveResponse, ConstructionHashRequest, ConstructionMetadataRequest, 5 | ConstructionMetadataResponse, ConstructionParseRequest, ConstructionParseResponse, 6 | ConstructionPayloadsRequest, ConstructionPayloadsResponse, ConstructionPreprocessRequest, 7 | ConstructionPreprocessResponse, ConstructionSubmitRequest, ConstructionSubmitResponse, 8 | TransactionIdentifierResponse, 9 | }, 10 | *, 11 | }; 12 | use rocket::State; 13 | use rocket_contrib::json::Json; 14 | 15 | #[post("/construction/derive", data = "")] 16 | pub fn construction_derive( 17 | construction_derive_request: Json, 18 | options: State, 19 | ) -> Result, ApiError> { 20 | construction::construction_derive(construction_derive_request.into_inner(), options.inner()) 21 | } 22 | #[post("/construction/hash", data = "")] 23 | pub fn construction_hash( 24 | construction_hash_request: Json, 25 | options: State, 26 | ) -> Result, ApiError> { 27 | construction::construction_hash(construction_hash_request.into_inner(), options.inner()) 28 | } 29 | //Create Metadata Request to send to construction/metadata 30 | #[post("/construction/preprocess", data = "")] 31 | pub fn construction_preprocess( 32 | construction_preprocess_request: Json, 33 | options: State, 34 | ) -> Result, ApiError> { 35 | construction::construction_preprocess( 36 | construction_preprocess_request.into_inner(), 37 | options.inner(), 38 | ) 39 | } 40 | //Get recent blockhash and other metadata 41 | #[post("/construction/metadata", data = "")] 42 | pub fn construction_metadata( 43 | construction_metadata_request: Json, 44 | options: State, 45 | ) -> Result, ApiError> { 46 | construction::construction_metadata(construction_metadata_request.into_inner(), options.inner()) 47 | } 48 | //Construct Payloads to Sign 49 | #[post("/construction/payloads", data = "")] 50 | pub fn construction_payloads( 51 | construction_payloads_request: Json, 52 | options: State, 53 | ) -> Result, ApiError> { 54 | construction::construction_payloads(construction_payloads_request.into_inner(), options.inner()) 55 | } 56 | 57 | //Parse Unsigned Transaction to to Confirm Correctness 58 | #[post("/construction/parse", data = "")] 59 | pub fn construction_parse( 60 | construction_parse_request: Json, 61 | options: State, 62 | ) -> Result, ApiError> { 63 | construction::construction_parse(construction_parse_request.into_inner(), options.inner()) 64 | } 65 | 66 | //combine sign 67 | #[post("/construction/combine", data = "")] 68 | pub fn construction_combine( 69 | construction_combine_request: Json, 70 | options: State, 71 | ) -> Result, ApiError> { 72 | construction::construction_combine(construction_combine_request.into_inner(), options.inner()) 73 | } 74 | 75 | //broadcast signed tx 76 | #[post("/construction/submit", data = "")] 77 | pub fn construction_submit( 78 | construction_submit_request: Json, 79 | options: State, 80 | ) -> Result, ApiError> { 81 | construction::construction_submit(construction_submit_request.into_inner(), options.inner()) 82 | } 83 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![feature(proc_macro_hygiene, decl_macro)] 2 | #[macro_use] 3 | extern crate rocket; 4 | 5 | mod account; 6 | mod api_routes; 7 | mod block; 8 | mod call; 9 | mod construction; 10 | mod consts; 11 | mod error; 12 | mod features; 13 | mod network; 14 | mod operations; 15 | mod types; 16 | mod utils; 17 | 18 | use std::{env, time::Duration}; 19 | 20 | use api_routes::construction::*; 21 | use api_routes::data::*; 22 | use call::RpcSender2; 23 | use error::ApiError; 24 | //use routes::construction::*; 25 | use rocket::{ 26 | config::Environment, fairing::Fairing, fairing::Info, fairing::Kind, http::Header, Config, 27 | Request, Response, 28 | }; 29 | use solana_client::rpc_client::RpcClient; 30 | use types::NetworkIdentifier; 31 | 32 | pub struct Options { 33 | rpc: RpcClient, 34 | network: String, 35 | } 36 | pub struct Options2 { 37 | rpc2: RpcSender2, 38 | } 39 | 40 | #[cfg(debug_assertions)] 41 | fn get_rocket_env() -> Environment { 42 | Environment::Development 43 | } 44 | 45 | #[cfg(not(debug_assertions))] 46 | fn get_rocket_env() -> Environment { 47 | Environment::Production 48 | } 49 | 50 | pub struct CORS(); 51 | 52 | impl Fairing for CORS { 53 | fn info(&self) -> Info { 54 | Info { 55 | name: "Add CORS headers to requests", 56 | kind: Kind::Response, 57 | } 58 | } 59 | 60 | fn on_response(&self, request: &Request, response: &mut Response) { 61 | response.set_header(Header::new("Access-Control-Allow-Origin", "*")); 62 | response.set_header(Header::new( 63 | "Access-Control-Allow-Methods", 64 | "POST, GET, PATCH, OPTIONS", 65 | )); 66 | response.set_header(Header::new("Access-Control-Allow-Headers", "*")); 67 | response.set_header(Header::new("Access-Control-Allow-Credentials", "true")); 68 | } 69 | } 70 | 71 | fn main() { 72 | let rpc_url = env::var("RPC_URL").unwrap_or("https://devnet.solana.com".to_string()); 73 | let network = env::var("NETWORK_NAME").unwrap_or("devnet".to_string()); 74 | let host = env::var("HOST").unwrap_or("127.0.0.1".to_string()); 75 | let port = env::var("PORT").unwrap_or("8080".to_string()); 76 | let mode = env::var("MODE").unwrap_or("online".to_string()); 77 | let rpc = create_rpc_client(rpc_url.clone()); 78 | let options = Options { rpc, network }; 79 | 80 | let config = Config::build(get_rocket_env()) 81 | .keep_alive(0) //rosetta cli giving eof error if this is not disabled 82 | .address(host) 83 | .port(port.parse::().unwrap()) 84 | .finalize() 85 | .unwrap(); 86 | 87 | let r = if mode == "offline" { 88 | routes![ 89 | network_list, 90 | network_options, 91 | construction_derive, 92 | construction_preprocess, 93 | construction_payloads, 94 | construction_combine, 95 | construction_parse, 96 | construction_hash 97 | ] 98 | } else { 99 | routes![ 100 | network_list, 101 | network_options, 102 | network_status, 103 | account_balance, 104 | get_block, // /block 105 | block_transaction, 106 | call, 107 | features, //optional extra solana specific 108 | //TODO: offline paths are not disabled in online mode 109 | construction_combine, 110 | construction_derive, 111 | construction_hash, 112 | construction_metadata, 113 | construction_parse, 114 | construction_payloads, 115 | construction_preprocess, 116 | construction_submit, 117 | ] 118 | }; 119 | 120 | let rpc2 = RpcSender2::new(rpc_url); 121 | let options2 = Options2 { rpc2 }; 122 | 123 | rocket::custom(config) 124 | .mount("/", r) 125 | .manage(options) 126 | .manage(options2) 127 | .attach(CORS()) 128 | //.register(catchers![internal_error]) 129 | .launch(); 130 | } 131 | 132 | const DEFAULT_RPC_TIMEOUT_SECONDS: u64 = 30; 133 | fn create_rpc_client(url: String) -> RpcClient { 134 | //let json_rpc_url = solana_cli_config::Config::default().json_rpc_url; 135 | let rpc_timeout = Duration::from_secs(DEFAULT_RPC_TIMEOUT_SECONDS); 136 | RpcClient::new_with_timeout(url, rpc_timeout) 137 | } 138 | 139 | pub fn is_bad_network( 140 | options: &Options, 141 | network_identifier: &NetworkIdentifier, 142 | ) -> Result<(), ApiError> { 143 | if network_identifier.blockchain != consts::BLOCKCHAIN 144 | || network_identifier.network != options.network 145 | { 146 | return Err(ApiError::BadNetwork); 147 | } 148 | Ok(()) 149 | } 150 | -------------------------------------------------------------------------------- /src/account.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | consts, 3 | error::ApiError, 4 | is_bad_network, 5 | network::get_current_block, 6 | types::AccountBalanceRequest, 7 | types::AccountBalanceResponse, 8 | types::{Amount, Currency}, 9 | Options, 10 | }; 11 | use rocket::State; 12 | use rocket_contrib::json::Json; 13 | use serde::Deserialize; 14 | use solana_client::rpc_request::TokenAccountsFilter; 15 | use solana_sdk::pubkey::Pubkey; 16 | 17 | #[derive(Debug, Deserialize)] 18 | struct TokenAmount { 19 | amount: String, 20 | decimals: u8, 21 | } 22 | #[derive(Debug, Deserialize)] 23 | struct Info { 24 | #[serde(rename = "tokenAmount")] 25 | token_amount: TokenAmount, 26 | mint: String, 27 | owner: String, 28 | } 29 | #[derive(Debug, Deserialize)] 30 | struct Parsed { 31 | info: Info, 32 | } 33 | pub fn account_balance( 34 | account_balance_request: AccountBalanceRequest, 35 | options: &Options, 36 | ) -> Result, ApiError> { 37 | is_bad_network(&options, &account_balance_request.network_identifier)?; 38 | 39 | if account_balance_request.block_identifier.is_some() { 40 | return Err(ApiError::HistoricBalancesUnsupported); 41 | } 42 | 43 | let mut balances = vec![]; 44 | let address = &account_balance_request.account_identifier.address; 45 | let pubkey = address.parse::()?; 46 | let balance = options.rpc.get_balance(&pubkey)?; 47 | let native_balance = Amount { 48 | currency: Currency { 49 | symbol: consts::NATIVE_SYMBOL.to_string(), 50 | decimals: consts::NATIVE_DECIMALS, 51 | metadata: None, 52 | }, 53 | value: balance.to_string(), 54 | }; 55 | let symbols = if let Some(x) = &account_balance_request.currencies { 56 | x.iter().map(|c| c.symbol.as_str()).collect::>() 57 | } else { 58 | vec![] 59 | }; 60 | 61 | let token_acc = options.rpc.get_token_account(&pubkey); 62 | if let Ok(Some(x)) = token_acc { 63 | let symbol = x.mint; 64 | 65 | if symbols.len() == 0 || symbols.contains(&symbol.as_str()) { 66 | balances.push(Amount { 67 | currency: Currency { 68 | symbol, 69 | decimals: x.token_amount.decimals, 70 | metadata: None, 71 | }, 72 | value: x.token_amount.amount, 73 | }); 74 | } 75 | } 76 | 77 | let account_tokens = options 78 | .rpc 79 | .get_token_accounts_by_owner(&pubkey, TokenAccountsFilter::ProgramId(spl_token::id()))?; 80 | account_tokens.into_iter().for_each(|x| { 81 | if let solana_account_decoder::UiAccountData::Json(parsed_acc) = x.account.data { 82 | let parsed = serde_json::from_value::(parsed_acc.parsed).unwrap(); 83 | let amount = parsed.info.token_amount.amount; 84 | let decimals = parsed.info.token_amount.decimals; 85 | let symbol = parsed.info.mint; 86 | if symbols.len() == 0 || symbols.contains(&symbol.as_str()) { 87 | balances.push(Amount { 88 | currency: Currency { 89 | symbol, 90 | decimals, 91 | metadata: Some(serde_json::json!({"pubkey": x.pubkey})), 92 | }, 93 | value: amount, 94 | }); 95 | } 96 | }; 97 | }); 98 | 99 | if symbols.len() == 0 || symbols.contains(&consts::NATIVE_SYMBOL.to_string().as_str()) { 100 | balances.push(native_balance); 101 | } 102 | 103 | let (_, _, current_block_identifier) = get_current_block(&options)?; 104 | 105 | let response = AccountBalanceResponse { 106 | block_identifier: current_block_identifier, 107 | balances: balances, 108 | }; 109 | Ok(Json(response)) 110 | } 111 | 112 | #[cfg(test)] 113 | mod tests { 114 | use crate::{ 115 | consts, create_rpc_client, types::AccountBalanceRequest, types::AccountIdentifier, 116 | types::NetworkIdentifier, Options, 117 | }; 118 | 119 | use super::account_balance; 120 | 121 | #[test] 122 | #[ignore] 123 | fn test_balance() { 124 | let rpc = create_rpc_client("https://devnet.solana.com".to_string()); 125 | 126 | let options = Options { 127 | rpc: rpc, 128 | network: "devnet".to_string(), 129 | }; 130 | let network_identifier = NetworkIdentifier { 131 | blockchain: consts::BLOCKCHAIN.to_string(), 132 | network: "devnet".to_string(), 133 | sub_network_identifier: None, 134 | }; 135 | 136 | let acc = account_balance( 137 | AccountBalanceRequest { 138 | network_identifier: network_identifier, 139 | account_identifier: AccountIdentifier { 140 | address: "Cnqmx3sbJf35852dAWvwf7GhuxMGWm5gGgw3biebSsBM".to_string(), 141 | sub_account: None, 142 | }, 143 | block_identifier: None, 144 | currencies: None, 145 | }, 146 | &options, 147 | ); 148 | assert_eq!(true, acc.is_ok()); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/operations/stake.rs: -------------------------------------------------------------------------------- 1 | use merge::Merge; 2 | use serde::{Deserialize, Serialize}; 3 | use solana_sdk::instruction::Instruction; 4 | use solana_stake_program::{ 5 | stake_instruction, 6 | stake_instruction::LockupArgs, 7 | stake_state::{Lockup, StakeAuthorize}, 8 | }; 9 | 10 | use crate::{error::ApiError, types::OperationType, utils::to_pub}; 11 | #[derive(Merge, Default, Clone, Debug, Deserialize, Serialize)] 12 | pub struct StakeOperationMetadata { 13 | pub source: Option, 14 | #[serde(alias = "stake_pubkey")] 15 | pub destination: Option, 16 | pub lamports: Option, 17 | pub authority: Option, 18 | pub staker: Option, 19 | pub withdrawer: Option, 20 | pub vote_pubkey: Option, 21 | pub lockup: Option, 22 | } 23 | #[derive(Clone, Debug, Deserialize, Serialize)] 24 | pub struct StakeOperation { 25 | #[serde(rename = "type")] 26 | pub type_: OperationType, 27 | pub metadata: StakeOperationMetadata, 28 | } 29 | 30 | #[derive(Clone, Debug, Serialize, Deserialize, Default)] 31 | pub struct LockupMeta { 32 | /// UnixTimestamp at which this stake will allow withdrawal, unless the 33 | /// transaction is signed by the custodian 34 | pub unix_timestamp: Option, 35 | /// epoch height at which this stake will allow withdrawal, unless the 36 | /// transaction is signed by the custodian 37 | pub epoch: Option, 38 | /// custodian signature on a transaction exempts the operation from 39 | /// lockup constraints 40 | pub custodian: Option, 41 | } 42 | impl From for Lockup { 43 | fn from(meta: LockupMeta) -> Self { 44 | Lockup { 45 | unix_timestamp: meta.unix_timestamp.unwrap_or(0), 46 | epoch: meta.epoch.unwrap_or(0), 47 | custodian: to_pub( 48 | &meta 49 | .custodian 50 | .unwrap_or("11111111111111111111111111111111".to_string()), //TODO: Find this 51 | ), 52 | } 53 | } 54 | } 55 | impl From for LockupArgs { 56 | fn from(meta: LockupMeta) -> Self { 57 | let new_custodian = if let Some(x) = meta.custodian { 58 | Some(to_pub(&x)) 59 | } else { 60 | None 61 | }; 62 | LockupArgs { 63 | unix_timestamp: meta.unix_timestamp, 64 | epoch: meta.epoch, 65 | custodian: new_custodian, 66 | } 67 | } 68 | } 69 | pub fn to_instruction( 70 | type_: OperationType, 71 | metadata: StakeOperationMetadata, 72 | ) -> Result, ApiError> { 73 | let instructions = match type_ { 74 | OperationType::Stake__CreateAccount => { 75 | let authorized = solana_stake_program::stake_state::Authorized { 76 | staker: to_pub( 77 | &metadata 78 | .staker 79 | .unwrap_or(metadata.source.clone().unwrap().clone()), 80 | ), 81 | withdrawer: to_pub( 82 | &metadata 83 | .withdrawer 84 | .unwrap_or(metadata.source.clone().unwrap()), 85 | ), 86 | }; 87 | stake_instruction::create_account( 88 | &to_pub(&metadata.source.unwrap()), 89 | &to_pub(&metadata.destination.unwrap()), 90 | &authorized, 91 | &Lockup::from(metadata.lockup.unwrap()), 92 | metadata.lamports.unwrap(), 93 | ) 94 | } 95 | OperationType::Stake__Delegate => vec![stake_instruction::delegate_stake( 96 | &to_pub(&metadata.destination.unwrap()), 97 | &to_pub(&metadata.authority.unwrap_or(metadata.source.unwrap())), 98 | &to_pub(&metadata.vote_pubkey.unwrap()), 99 | )], 100 | OperationType::Stake__Split => stake_instruction::split( 101 | &to_pub(&metadata.source.unwrap()), 102 | &to_pub(&metadata.authority.unwrap()), 103 | metadata.lamports.unwrap(), 104 | &to_pub(&metadata.destination.unwrap()), 105 | ), 106 | OperationType::Stake__Merge => stake_instruction::merge( 107 | &to_pub(&metadata.destination.unwrap()), 108 | &to_pub(&metadata.source.unwrap()), 109 | &to_pub(&metadata.authority.unwrap()), 110 | ), 111 | OperationType::Stake__Authorize => { 112 | let mut inx = vec![]; 113 | if let Some(x) = &metadata.staker { 114 | inx.push(stake_instruction::authorize( 115 | &to_pub(&metadata.destination.clone().unwrap()), 116 | &to_pub(&metadata.source.clone().unwrap()), 117 | &to_pub(x), 118 | StakeAuthorize::Staker, 119 | )) 120 | } 121 | if let Some(x) = &metadata.withdrawer { 122 | inx.push(stake_instruction::authorize( 123 | &to_pub(&metadata.destination.unwrap()), 124 | &to_pub(&metadata.source.unwrap()), 125 | &to_pub(x), 126 | StakeAuthorize::Withdrawer, 127 | )) 128 | } 129 | inx 130 | } 131 | OperationType::Stake__Withdraw => vec![stake_instruction::withdraw( 132 | &to_pub(&metadata.source.unwrap()), 133 | &to_pub(&metadata.withdrawer.unwrap()), 134 | &to_pub(&metadata.destination.unwrap()), 135 | metadata.lamports.unwrap(), 136 | //to_pub_optional(metadata.custodian), 137 | None, 138 | )], 139 | OperationType::Stake__Deactivate => vec![stake_instruction::deactivate_stake( 140 | &to_pub(&metadata.destination.unwrap()), 141 | &to_pub(&metadata.authority.unwrap_or(metadata.source.unwrap())), 142 | )], 143 | OperationType::Stake__SetLockup => vec![stake_instruction::set_lockup( 144 | &to_pub(&metadata.destination.unwrap()), 145 | &LockupArgs::from(metadata.lockup.unwrap()), 146 | &to_pub(&metadata.authority.unwrap_or(metadata.source.unwrap())), 147 | )], 148 | _ => { 149 | return Err(ApiError::BadOperations("Invalid Operation".to_string())); 150 | } 151 | }; 152 | Ok(instructions) 153 | } 154 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | Rosetta 4 | 5 |

6 | 7 |

8 | Rosetta Solana 9 |

10 | 11 |

12 | ROSETTA-SOLANA IS CONSIDERED ALPHA SOFTWARE. 13 | USE AT YOUR OWN RISK! 14 |

15 | 16 | ## Overview 17 | `rosetta-solana` provides a reference implementation of the Rosetta API for 18 | Solana in Rust. If you haven't heard of the Rosetta API, you can find more 19 | information [here](https://rosetta-api.org). 20 | 21 | ## Features 22 | * Rosetta API implementation (both Data API and Construction API) 23 | * Stateless, offline, curve-based transaction construction 24 | * Simpler alternative Operations structure using metadata 25 | * Supports most system and spl instructions 26 | 27 | ## Usage 28 | As specified in the [Rosetta API Principles](https://www.rosetta-api.org/docs/automated_deployment.html), 29 | all Rosetta implementations must be deployable via Docker and support running via either an 30 | [`online` or `offline` mode](https://www.rosetta-api.org/docs/node_deployment.html#multiple-modes). 31 | 32 | ### Docker Install 33 | Running the following commands will create a Docker image called `rosetta-solana:latest`. 34 | 35 | ##### From Source 36 | After cloning this repository, run: 37 | ```text 38 | docker build -t rosetta-solana . 39 | docker-compose up 40 | ``` 41 | 42 | ### Direct Install 43 | After cloning this repository, run: 44 | ```text 45 | cargo run build --release 46 | ``` 47 | 48 | ## Testing with rosetta-cli 49 | To validate `rosetta-solana`, [install `rosetta-cli`](https://github.com/coinbase/rosetta-cli#install) 50 | and run one of the following commands: 51 | * `rosetta-cli check:data --configuration-file rosetta-cli-conf/devnet.json` 52 | * `rosetta-cli check:construction --configuration-file rosetta-cli-conf/devnet.json` 53 | 54 | Note: If cli test gives EOF error it's probably due to golang trying to reuse connection and server closing it. I have disabled keep-alive for that reason. It's recommended to run cli test in dev mode. 55 | 56 | ## Development 57 | * `cargo run` to run server 58 | * `cargo run test` to run tests 59 | * `cargo docs` to create docs 60 | 61 | ## Details 62 | 63 | ### Endpoints Implemented 64 | 65 | ``` 66 | /network/list (network_list) 67 | /network/options (network_options) 68 | /network/status (network_status) 69 | /account/balance (account_balance) 70 | /block (get_block) 71 | /block/transaction (block_transaction) 72 | /call (call) 73 | /construction/combine (construction_combine) 74 | /construction/derive (construction_derive) 75 | /construction/hash (construction_hash) 76 | /construction/metadata (construction_metadata) 77 | /construction/parse (construction_parse) 78 | /construction/payloads (construction_payloads) 79 | /construction/preprocess (construction_preprocess) 80 | /construction/submit (construction_submit) 81 | 82 | ``` 83 | #### Default environment variables 84 | ``` 85 | RPC_URL = "https://devnet.solana.com" 86 | NETWORK_NAME = "devnet" 87 | HOST = "127.0.0.1" 88 | PORT = "8080" 89 | MODE = "online" //online/offline 90 | ``` 91 | 92 | #### Operations supported 93 | See `types::OperationType` to see full list of current operations supported . This list might not be up to date. 94 | 95 | ``` 96 | 97 | System__CreateAccount, 98 | System__Assign, 99 | System__Transfer, 100 | System__CreateNonceAccount, 101 | System__AdvanceNonce, 102 | System__WithdrawFromNonce, 103 | System__AuthorizeNonce, 104 | System__Allocate, 105 | SplToken__InitializeMint, 106 | SplToken__InitializeAccount, 107 | SplToken__CreateToken, 108 | SplToken__CreateAccount, 109 | SplToken__Transfer, 110 | SplToken__Approve, 111 | SplToken__Revoke, 112 | SplToken__MintTo, 113 | SplToken__Burn, 114 | SplToken__CloseAccount, 115 | SplToken__FreezeAccount, 116 | SplToken__ThawAccount, 117 | SplToken__TransferChecked, 118 | SplToken__CreateAssocAccount, 119 | 120 | Stake__CreateAccount, 121 | Stake__Delegate, 122 | Stake__Split, 123 | Stake__Merge, 124 | Stake__Authorize, 125 | Stake__Withdraw, 126 | Stake__Deactivate, 127 | Stake__SetLockup, 128 | 129 | Vote__CreateAccount, 130 | Vote__Authorize, 131 | Vote__Withdraw, 132 | Vote__UpdateValidatorIdentity, 133 | Vote__UpdateCommission, 134 | Unknown, 135 | ``` 136 | 137 | #### Simpler Operations 138 | 139 | This implementation also supports writing operations using metadata only. Instead of writing two operations for a simple transfer transaction one can simply write a single operation and fill it's metadata e.g `source`, `destination`, `authority`, `lamports`. 140 | 141 | e.g 142 | ``` 143 | [ 144 | Operation{ 145 | account: {address: "Sender"}, 146 | amount: { value: "-10",...}, 147 | ... 148 | }, 149 | Operation{ 150 | account: {address: "Receiver"}, 151 | amount: {value: "10",...}, 152 | ... 153 | } 154 | ] 155 | ``` 156 | ``` 157 | [ 158 | Operation{ 159 | metadata: { 160 | source: "Sender", 161 | destination: "Receiver", 162 | lamports: 10 163 | }, 164 | ... 165 | }, 166 | ] 167 | ``` 168 | Both are same operations although the first one (Rosetta spec) always overwrites the second one. 169 | 170 | #### Nonce transfers 171 | 172 | To send a transaction with a nonce you need to add metadata to construction_preprosess with `{"metadata": {"with_nonce": {"account": "address of nonce account"}}}` 173 | 174 | #### Balance changing Operations 175 | 176 | See imp of `OperationType` in `src/types.rs` for list of balance changing operations. They might also require additional metadata depending on operation. Operation where only change is fees are not considered balance changing operation. Operation with only 1 balance change with no equal opposite signed opration are also not balance changing e.g mint or burn. 177 | 178 | 179 | ### Examples 180 | See tests in `src/construction.rs` to see complete working examples. 181 | 182 | ## TODO 183 | 184 | * Add optional commitment option to every operation that accepts 185 | * All preprocess metadata fetching for every operation type 186 | * Suport all operation types 187 | * Better errors 188 | * Separate crates for proper docs 189 | 190 | ## License 191 | This project is available open source under the terms of the [Apache 2.0 License](https://opensource.org/licenses/Apache-2.0). 192 | -------------------------------------------------------------------------------- /src/operations/spltoken.rs: -------------------------------------------------------------------------------- 1 | use crate::{error::ApiError, types::OperationType, utils::to_pub}; 2 | use merge::Merge; 3 | use serde::{Deserialize, Serialize}; 4 | use solana_sdk::{instruction::Instruction, program_pack::Pack, system_instruction}; 5 | use spl_token::state::Mint; 6 | 7 | const MIN_RENT: u64 = 100000; 8 | #[derive(Merge, Default, Clone, Debug, Deserialize, Serialize)] 9 | pub struct SplTokenOperationMetadata { 10 | pub source: Option, 11 | pub destination: Option, 12 | pub mint: Option, 13 | pub authority: Option, 14 | pub freeze_authority: Option, 15 | pub amount: Option, 16 | pub decimals: Option, 17 | } 18 | 19 | pub fn to_instruction( 20 | type_: OperationType, 21 | metadata: SplTokenOperationMetadata, 22 | ) -> Result, ApiError> { 23 | let p; 24 | let freeze_authority = if let Some(f) = &metadata.freeze_authority { 25 | p = to_pub(f); 26 | Some(&p) 27 | } else { 28 | None 29 | }; 30 | let source = if let Some(s) = &metadata.source { 31 | to_pub(s) 32 | } else { 33 | return Err(ApiError::PlaceHolderError("Source missing".to_string())); 34 | }; 35 | let instruction = match type_ { 36 | OperationType::SplToken__InitializeMint => vec![spl_token::instruction::initialize_mint( 37 | &spl_token::id(), 38 | &to_pub(&metadata.mint.unwrap()), 39 | &to_pub(&metadata.source.unwrap()), 40 | freeze_authority, 41 | metadata.decimals.unwrap(), 42 | )?], 43 | OperationType::SplToken__InitializeAccount => { 44 | vec![spl_token::instruction::initialize_account( 45 | &spl_token::id(), 46 | &to_pub(&metadata.destination.unwrap()), 47 | &to_pub(&metadata.mint.unwrap()), 48 | &source, 49 | )?] 50 | } 51 | OperationType::SplToken__CreateToken => vec![ 52 | system_instruction::create_account( 53 | &source, 54 | &to_pub(&metadata.mint.clone().unwrap()), 55 | metadata.amount.unwrap(), 56 | Mint::LEN as u64, 57 | &spl_token::id(), 58 | ), 59 | spl_token::instruction::initialize_mint( 60 | &spl_token::id(), 61 | &to_pub(&metadata.mint.unwrap()), 62 | &to_pub(&metadata.authority.unwrap()), 63 | freeze_authority, 64 | metadata.decimals.unwrap_or(2), 65 | )?, 66 | ], 67 | OperationType::SplToken__CreateAccount => vec![ 68 | system_instruction::create_account( 69 | &source, 70 | &to_pub(&metadata.destination.clone().unwrap()), 71 | metadata.amount.unwrap(), 72 | spl_token::state::Account::LEN as u64, 73 | &spl_token::id(), 74 | ), 75 | spl_token::instruction::initialize_account( 76 | &spl_token::id(), 77 | &to_pub(&metadata.destination.unwrap()), 78 | &to_pub(&metadata.mint.unwrap()), 79 | &to_pub(&metadata.authority.unwrap()), 80 | )?, 81 | ], 82 | OperationType::SplToken__Approve => vec![spl_token::instruction::approve( 83 | &spl_token::id(), 84 | &source, 85 | &to_pub(&metadata.destination.unwrap()), 86 | &to_pub(&metadata.authority.unwrap()), 87 | &vec![], 88 | metadata.amount.unwrap(), 89 | )?], 90 | OperationType::SplToken__Revoke => vec![spl_token::instruction::revoke( 91 | &spl_token::id(), 92 | &source, 93 | &to_pub(&metadata.authority.unwrap()), 94 | &vec![], 95 | )?], 96 | 97 | OperationType::SplToken__MintTo => vec![spl_token::instruction::mint_to( 98 | &spl_token::id(), 99 | &to_pub(&metadata.mint.unwrap()), 100 | &source, 101 | &to_pub(&metadata.authority.unwrap()), 102 | &vec![], 103 | metadata.amount.unwrap(), 104 | )?], 105 | 106 | OperationType::SplToken__Burn => vec![spl_token::instruction::burn( 107 | &spl_token::id(), 108 | &source, 109 | &to_pub(&metadata.mint.unwrap()), 110 | &source, 111 | &vec![], 112 | metadata.amount.unwrap(), 113 | )?], 114 | OperationType::SplToken__CloseAccount => vec![spl_token::instruction::close_account( 115 | &spl_token::id(), 116 | &source, 117 | &to_pub(&metadata.authority.clone().unwrap()), 118 | &to_pub(&metadata.authority.unwrap()), 119 | &vec![], 120 | )?], 121 | OperationType::SplToken__FreezeAccount => vec![spl_token::instruction::freeze_account( 122 | &spl_token::id(), 123 | &source, 124 | &to_pub(&metadata.mint.clone().unwrap()), 125 | &to_pub(&metadata.authority.unwrap()), 126 | &vec![], 127 | )?], 128 | OperationType::SplToken__ThawAccount => vec![spl_token::instruction::thaw_account( 129 | &spl_token::id(), 130 | &source, 131 | &to_pub(&metadata.mint.clone().unwrap()), 132 | &to_pub(&metadata.authority.unwrap()), 133 | &vec![], 134 | )?], 135 | OperationType::SplToken__CreateAssocAccount => vec![ 136 | spl_associated_token_account::create_associated_token_account( 137 | &source, 138 | &source, 139 | &to_pub(&metadata.mint.unwrap()), 140 | ), 141 | ], 142 | 143 | OperationType::SplToken__TransferChecked => vec![spl_token::instruction::transfer_checked( 144 | &spl_token::id(), 145 | &source, 146 | &to_pub(&metadata.mint.unwrap()), 147 | &to_pub(&metadata.destination.unwrap()), 148 | &to_pub(&metadata.authority.unwrap()), 149 | &vec![], 150 | metadata.amount.unwrap(), 151 | metadata.decimals.unwrap(), 152 | )?], 153 | OperationType::SplToken__Transfer => vec![spl_token::instruction::transfer( 154 | &spl_token::id(), 155 | &source, 156 | &to_pub(&metadata.destination.unwrap()), 157 | &to_pub(&metadata.authority.unwrap()), 158 | &vec![], 159 | metadata.amount.unwrap(), 160 | )?], 161 | _ => { 162 | return Err(ApiError::BadOperations("Invalid Operation".to_string())); 163 | } 164 | }; 165 | Ok(instruction) 166 | } 167 | -------------------------------------------------------------------------------- /src/features.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | 3 | use crate::{ 4 | error::ApiError, 5 | is_bad_network, 6 | types::Feature, 7 | types::{AcceptanceCriteria, FeaturesRequest, FeaturesResponse}, 8 | utils::to_pub, 9 | Options, 10 | }; 11 | use rocket_contrib::json::Json; 12 | use solana_sdk::program_pack::Pack; 13 | use solana_sdk::{feature_set::FEATURE_NAMES, signature::Signature}; 14 | use solana_transaction_status::{ 15 | EncodedConfirmedTransaction, EncodedTransaction, UiInstruction, UiMessage, 16 | UiTransactionEncoding, 17 | }; 18 | use spl_feature_proposal::instruction::FeatureProposalInstruction; 19 | 20 | pub fn get_features( 21 | req: FeaturesRequest, 22 | options: &Options, 23 | ) -> Result, ApiError> { 24 | is_bad_network(&options, &req.network_identifier)?; 25 | let feature_program_id = spl_feature_proposal::id(); 26 | let accounts = options.rpc.get_program_accounts(&feature_program_id)?; 27 | let features = accounts 28 | .iter() 29 | .map(|(x, _)| { 30 | let tx = options 31 | .rpc 32 | .get_confirmed_signatures_for_address2(x) 33 | .unwrap(); 34 | //tx sig of proposal 35 | let parsed_tx = options 36 | .rpc 37 | .get_confirmed_transaction( 38 | &Signature::from_str(&tx.last().unwrap().signature).unwrap(), 39 | UiTransactionEncoding::Json, 40 | ) 41 | .unwrap(); 42 | println!("{:?}", parsed_tx); 43 | let feature = if let EncodedTransaction::Json(x) = parsed_tx.transaction.transaction { 44 | if let UiMessage::Raw(x) = x.message { 45 | let funding_address = x.account_keys[0].clone(); 46 | let feature_proposal_address = x.account_keys[1].clone(); 47 | let mint_address = x.account_keys[2].clone(); 48 | let distributor_token_address = x.account_keys[3].clone(); 49 | let acceptance_token_address = x.account_keys[4].clone(); 50 | let feature_id_address = x.account_keys[5].clone(); 51 | let name = FEATURE_NAMES 52 | .get(&to_pub(&feature_id_address)) 53 | .unwrap_or(&feature_id_address.as_str()) 54 | .to_string(); 55 | 56 | let mut ttm = 0; 57 | let mut ac = AcceptanceCriteria::default(); 58 | let decoded_data = bs58::decode(x.instructions[0].data.to_string()) 59 | .into_vec() 60 | .unwrap(); 61 | let proposal_instruction = 62 | FeatureProposalInstruction::unpack_from_slice(&decoded_data).unwrap(); 63 | if let FeatureProposalInstruction::Propose { 64 | tokens_to_mint, 65 | acceptance_criteria, 66 | } = proposal_instruction 67 | { 68 | ttm = tokens_to_mint; 69 | ac = AcceptanceCriteria { 70 | tokens_required: acceptance_criteria.tokens_required, 71 | deadline: acceptance_criteria.deadline, 72 | } 73 | } 74 | let f = Feature { 75 | name, 76 | funding_address, 77 | feature_proposal_address, 78 | mint_address, 79 | distributor_token_address, 80 | acceptance_token_address, 81 | feature_id_address, 82 | tokens_to_mint: ttm, 83 | acceptance_criteria: ac, 84 | }; 85 | Some(f) 86 | } else { 87 | None 88 | } 89 | } else { 90 | None 91 | }; 92 | feature 93 | }) 94 | .filter_map(|x| x) 95 | .collect::>(); 96 | Ok(Json(FeaturesResponse { features })) 97 | } 98 | //EncodedConfirmedTransaction { slot: 36265038, transaction: EncodedTransactionWithStatusMeta { transaction: Json(UiTransaction { signatures: ["rCFXW6KBwqeubDqSggyDM8YxU4NianBDzfeHwbmB36Eaw9n5qxfuqvtFLqkkDZuLRJyshH1qyZSxCUsvLKP69nK", "3AK9zmZmTDz5BLXdDwibcSzmXVfqcMWgdpm7TmF5omBAu1gfxvUm3TvKbaBDAHXGVHmbZ5yNsAb3MUSZBeKLow7w"], message: Raw(UiRawMessage { header: MessageHeader { num_required_signatures: 2, num_readonly_signed_accounts: 0, num_readonly_unsigned_accounts: 4 }, account_keys: ["HJGPMwVuqrbm7BDMeA3shLkqdHUru337fgytM7HzqTnH", "BnSuisc6EfQ6JYCpVppYGPXweJbWwGFo5rP1urReHqd", "DQzjTELf9udq4xErkwU53YQMwWpy5bYecMbziuBVLcjv", "DE9f1grJTYk1epgq86ioeNGymYv24isSCJrgnnwmeR8c", "CSxXC756mZYrAquQNs7EJ4u9xLZC6YGTnvvQqTeDom3L", "3sfjf5r6iTuLVuwa92td6YN1ytfXVSTiJDECrQqNYzHW", "11111111111111111111111111111111", "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", "SysvarRent111111111111111111111111111111111", "Feat1YXHhH6t1juaWF74WLcfv4XoNocjXA6sPWHNgAse"], recent_blockhash: "DsrkdRiw8rf1wKGfPmDPEwjGNe8Q1dYnyQcKwyXUo2Zp", instructions: [UiCompiledInstruction { program_id_index: 9, accounts: [0, 1, 2, 3, 4, 5, 6, 7, 8], data: "19ZzoL2fCKATvq3fcagV6fxt4zxmfefipP" }] }) }), meta: Some(UiTransactionStatusMeta { err: None, status: Ok(()), fee: 10000, pre_balances: [5382919120, 0, 0, 0, 0, 0, 1, 1118555520, 1, 1332533760], post_balances: [5375406240, 1009200, 1461600, 2039280, 2039280, 953520, 1, 1118555520, 1, 1332533760], inner_instructions: None, log_messages: Some([]), pre_token_balances: Some([]), post_token_balances: Some([]) }) } } 99 | 100 | //EncodedConfirmedTransaction { slot: 36265038, transaction: EncodedTransactionWithStatusMeta { transaction: Json(UiTransaction { signatures: ["rCFXW6KBwqeubDqSggyDM8YxU4NianBDzfeHwbmB36Eaw9n5qxfuqvtFLqkkDZuLRJyshH1qyZSxCUsvLKP69nK", "3AK9zmZmTDz5BLXdDwibcSzmXVfqcMWgdpm7TmF5omBAu1gfxvUm3TvKbaBDAHXGVHmbZ5yNsAb3MUSZBeKLow7w"], message: Parsed(UiParsedMessage { account_keys: [ParsedAccount { pubkey: "HJGPMwVuqrbm7BDMeA3shLkqdHUru337fgytM7HzqTnH", writable: true, signer: true }, ParsedAccount { pubkey: "BnSuisc6EfQ6JYCpVppYGPXweJbWwGFo5rP1urReHqd", writable: true, signer: true }, ParsedAccount { pubkey: "DQzjTELf9udq4xErkwU53YQMwWpy5bYecMbziuBVLcjv", writable: true, signer: false }, ParsedAccount { pubkey: "DE9f1grJTYk1epgq86ioeNGymYv24isSCJrgnnwmeR8c", writable: true, signer: false }, ParsedAccount { pubkey: "CSxXC756mZYrAquQNs7EJ4u9xLZC6YGTnvvQqTeDom3L", writable: true, signer: false }, ParsedAccount { pubkey: "3sfjf5r6iTuLVuwa92td6YN1ytfXVSTiJDECrQqNYzHW", writable: true, signer: false }, ParsedAccount { pubkey: "11111111111111111111111111111111", writable: false, signer: false }, ParsedAccount { pubkey: "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", writable: false, signer: false }, ParsedAccount { pubkey: "SysvarRent111111111111111111111111111111111", writable: false, signer: false }, ParsedAccount { pubkey: "Feat1YXHhH6t1juaWF74WLcfv4XoNocjXA6sPWHNgAse", writable: false, signer: false }], recent_blockhash: "DsrkdRiw8rf1wKGfPmDPEwjGNe8Q1dYnyQcKwyXUo2Zp", instructions: [Parsed(PartiallyDecoded(UiPartiallyDecodedInstruction { program_id: "Feat1YXHhH6t1juaWF74WLcfv4XoNocjXA6sPWHNgAse", accounts: ["HJGPMwVuqrbm7BDMeA3shLkqdHUru337fgytM7HzqTnH", "BnSuisc6EfQ6JYCpVppYGPXweJbWwGFo5rP1urReHqd", "DQzjTELf9udq4xErkwU53YQMwWpy5bYecMbziuBVLcjv", "DE9f1grJTYk1epgq86ioeNGymYv24isSCJrgnnwmeR8c", "CSxXC756mZYrAquQNs7EJ4u9xLZC6YGTnvvQqTeDom3L", "3sfjf5r6iTuLVuwa92td6YN1ytfXVSTiJDECrQqNYzHW", "11111111111111111111111111111111", "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", "SysvarRent111111111111111111111111111111111"], data: "19ZzoL2fCKATvq3fcagV6fxt4zxmfefipP" }))] }) }), meta: Some(UiTransactionStatusMeta { err: None, status: Ok(()), fee: 10000, pre_balances: [5382919120, 0, 0, 0, 0, 0, 1, 1118555520, 1, 1332533760], post_balances: [5375406240, 1009200, 1461600, 2039280, 2039280, 953520, 1, 1118555520, 1, 1332533760], inner_instructions: None, log_messages: Some([]), pre_token_balances: Some([]), post_token_balances: Some([]) }) } }, 101 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use std::io::Cursor; 2 | 3 | use crate::types::{self, ErrorDetails}; 4 | use rocket::http::{hyper::StatusCode, Status}; 5 | use solana_client::client_error::ClientError; 6 | use solana_sdk::{ 7 | program_error::ProgramError, pubkey::ParsePubkeyError, signature::ParseSignatureError, 8 | }; 9 | use thiserror::Error; 10 | 11 | #[derive(Debug, Error)] 12 | pub enum ApiError { 13 | #[error("{0}")] 14 | //PlaceHolder for any errors 15 | PlaceHolderError(String), 16 | #[error("bad request")] 17 | BadRequest, 18 | #[error("programerror {0}")] 19 | ProgramError(#[from] ProgramError), 20 | #[error("curve not supported")] 21 | UnsupportedCurve, 22 | #[error("invalid signed transaction")] 23 | InvalidSignedTransaction, 24 | #[error("bad network")] 25 | BadNetwork, 26 | #[error("deserialization failed: {0}")] 27 | DeserializationFailed(String), 28 | //#[error("serialization failed: {0:?}")] 29 | //SerializationFailed(#[from] bcs::Error), 30 | #[error("bad operations {0}")] 31 | BadOperations(String), 32 | #[error("account not found")] 33 | AccountNotFound, 34 | #[error("system time error: {0:?}")] 35 | SystemTimeError(#[from] std::time::SystemTimeError), 36 | #[error("hex decoding failed: {0:?}")] 37 | HexDecodingFailed(#[from] hex::FromHexError), 38 | #[error("bad signature")] 39 | BadSignature, 40 | #[error("bad signature type")] 41 | BadSignatureType, 42 | #[error("bad transaction script")] 43 | BadTransactionScript, 44 | #[error("bad transaction payload")] 45 | BadTransactionPayload, 46 | #[error("bad coin")] 47 | BadCoin, 48 | #[error("bad siganture count")] 49 | BadSignatureCount, 50 | #[error("historic balances unsupported")] 51 | HistoricBalancesUnsupported, 52 | #[error("ParsePubkeyError")] 53 | ParsePubkeyError(#[from] ParsePubkeyError), 54 | #[error(transparent)] 55 | RpcClientError(#[from] ClientError), 56 | #[error("ParseSignatureError")] 57 | ParseSignatureError(#[from] ParseSignatureError), 58 | #[error("Base64DecodeError")] 59 | Base64DecodeError(#[from] base64::DecodeError), 60 | } 61 | impl ApiError { 62 | pub fn code(&self) -> u64 { 63 | match self { 64 | ApiError::PlaceHolderError(_) => 19, 65 | ApiError::BadRequest => 20, 66 | ApiError::UnsupportedCurve => 21, 67 | ApiError::InvalidSignedTransaction => 22, 68 | ApiError::BadNetwork => 40, 69 | ApiError::DeserializationFailed(_) => 50, 70 | //ApiError::SerializationFailed(_) => 60, 71 | ApiError::BadOperations(_) => 70, 72 | ApiError::AccountNotFound => 80, 73 | ApiError::SystemTimeError(_) => 90, 74 | ApiError::HexDecodingFailed(_) => 100, 75 | ApiError::BadSignature => 110, 76 | ApiError::BadSignatureType => 120, 77 | ApiError::BadTransactionScript => 130, 78 | ApiError::BadTransactionPayload => 140, 79 | ApiError::BadCoin => 150, 80 | ApiError::BadSignatureCount => 160, 81 | ApiError::HistoricBalancesUnsupported => 170, 82 | ApiError::RpcClientError(_) => 180, 83 | ApiError::ParsePubkeyError(_) => 190, 84 | ApiError::ParseSignatureError(_) => 200, 85 | ApiError::Base64DecodeError(_) => 210, 86 | ApiError::ProgramError(_) => 220, 87 | } 88 | } 89 | 90 | pub fn retriable(&self) -> bool { 91 | match self { 92 | ApiError::PlaceHolderError(_) => false, 93 | ApiError::BadRequest => false, 94 | ApiError::UnsupportedCurve => false, 95 | ApiError::InvalidSignedTransaction => false, 96 | ApiError::BadNetwork => false, 97 | ApiError::DeserializationFailed(_) => false, 98 | //ApiError::SerializationFailed(_) => false, 99 | ApiError::BadOperations(_) => false, 100 | ApiError::AccountNotFound => true, 101 | ApiError::SystemTimeError(_) => true, 102 | ApiError::HexDecodingFailed(_) => false, 103 | ApiError::BadSignature => false, 104 | ApiError::BadSignatureType => false, 105 | ApiError::BadTransactionScript => false, 106 | ApiError::BadTransactionPayload => false, 107 | ApiError::BadCoin => false, 108 | ApiError::BadSignatureCount => false, 109 | ApiError::HistoricBalancesUnsupported => false, 110 | ApiError::RpcClientError(_) => false, 111 | ApiError::ParsePubkeyError(_) => false, 112 | ApiError::ParseSignatureError(_) => false, 113 | ApiError::Base64DecodeError(_) => false, 114 | ApiError::ProgramError(_) => false, 115 | } 116 | } 117 | 118 | pub fn status_code(&self) -> StatusCode { 119 | match self { 120 | ApiError::PlaceHolderError(_) => StatusCode::InternalServerError, 121 | ApiError::BadRequest => StatusCode::BadRequest, 122 | ApiError::UnsupportedCurve => StatusCode::InternalServerError, 123 | ApiError::InvalidSignedTransaction => StatusCode::InternalServerError, 124 | ApiError::BadNetwork => StatusCode::BadRequest, 125 | ApiError::DeserializationFailed(_) => StatusCode::BadRequest, 126 | //ApiError::SerializationFailed(_) => StatusCode::BadRequest, 127 | ApiError::BadOperations(_) => StatusCode::BadRequest, 128 | ApiError::AccountNotFound => StatusCode::NotFound, 129 | ApiError::SystemTimeError(_) => StatusCode::InternalServerError, 130 | ApiError::HexDecodingFailed(_) => StatusCode::BadRequest, 131 | ApiError::BadSignature => StatusCode::BadRequest, 132 | ApiError::BadSignatureType => StatusCode::BadRequest, 133 | ApiError::BadTransactionScript => StatusCode::BadRequest, 134 | ApiError::BadTransactionPayload => StatusCode::BadRequest, 135 | ApiError::BadCoin => StatusCode::BadRequest, 136 | ApiError::BadSignatureCount => StatusCode::BadRequest, 137 | ApiError::HistoricBalancesUnsupported => StatusCode::BadRequest, 138 | ApiError::RpcClientError(_) => StatusCode::InternalServerError, 139 | ApiError::ParsePubkeyError(_) => StatusCode::InternalServerError, 140 | ApiError::ParseSignatureError(_) => StatusCode::InternalServerError, 141 | ApiError::Base64DecodeError(_) => StatusCode::InternalServerError, 142 | ApiError::ProgramError(_) => StatusCode::InternalServerError, 143 | } 144 | } 145 | 146 | pub fn message(&self) -> String { 147 | let full = format!("{}", self); 148 | let parts: Vec<_> = full.split(":").collect(); 149 | parts[0].to_string() 150 | } 151 | 152 | pub(crate) fn details(&self) -> ErrorDetails { 153 | let error = format!("{}", self); 154 | ErrorDetails { error } 155 | } 156 | 157 | pub fn deserialization_failed(type_: &str) -> ApiError { 158 | ApiError::DeserializationFailed(type_.to_string()) 159 | } 160 | 161 | pub(crate) fn all_errors() -> Vec { 162 | vec![ 163 | types::Error { 164 | message: "bad block request".to_string(), 165 | code: 20, 166 | retriable: false, 167 | details: None, 168 | }, 169 | types::Error { 170 | message: "bad network".to_string(), 171 | code: 40, 172 | retriable: false, 173 | details: None, 174 | }, 175 | types::Error { 176 | message: "deserialization failed".to_string(), 177 | code: 50, 178 | retriable: false, 179 | details: None, 180 | }, 181 | types::Error { 182 | message: "serialization failed".to_string(), 183 | code: 60, 184 | retriable: false, 185 | details: None, 186 | }, 187 | types::Error { 188 | message: "bad transfer operations".to_string(), 189 | code: 70, 190 | retriable: false, 191 | details: None, 192 | }, 193 | types::Error { 194 | message: "account not found".to_string(), 195 | code: 80, 196 | retriable: false, 197 | details: None, 198 | }, 199 | types::Error { 200 | message: "system time error".to_string(), 201 | code: 90, 202 | retriable: true, 203 | details: None, 204 | }, 205 | types::Error { 206 | message: "hex decoding failed".to_string(), 207 | code: 100, 208 | retriable: false, 209 | details: None, 210 | }, 211 | types::Error { 212 | message: "bad signature".to_string(), 213 | code: 110, 214 | retriable: false, 215 | details: None, 216 | }, 217 | types::Error { 218 | message: "bad signature type".to_string(), 219 | code: 120, 220 | retriable: false, 221 | details: None, 222 | }, 223 | types::Error { 224 | message: "bad transaction script".to_string(), 225 | code: 130, 226 | retriable: false, 227 | details: None, 228 | }, 229 | types::Error { 230 | message: "bad transaction payload".to_string(), 231 | code: 140, 232 | retriable: false, 233 | details: None, 234 | }, 235 | types::Error { 236 | message: "bad coin".to_string(), 237 | code: 150, 238 | retriable: false, 239 | details: None, 240 | }, 241 | types::Error { 242 | message: "bad signature count".to_string(), 243 | code: 160, 244 | retriable: false, 245 | details: None, 246 | }, 247 | types::Error { 248 | message: "historic balances unsupported".to_string(), 249 | code: 170, 250 | retriable: false, 251 | details: None, 252 | }, 253 | ] 254 | } 255 | 256 | pub fn into_error(self) -> types::Error { 257 | types::Error { 258 | message: self.message(), 259 | code: self.code(), 260 | retriable: self.retriable(), 261 | details: Some(self.details()), 262 | } 263 | } 264 | } 265 | 266 | impl<'r> rocket::response::Responder<'r> for ApiError { 267 | fn respond_to(self, request: &rocket::Request) -> Result, Status> { 268 | Ok(rocket::Response::build() 269 | .header(rocket::http::ContentType::JSON) 270 | .status(Status::InternalServerError) 271 | .sized_body(Cursor::new(format!( 272 | "{}", 273 | serde_json::to_string(&self.into_error()).unwrap() 274 | ))) 275 | .finalize()) 276 | } 277 | } 278 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2020 Coinbase, Inc. 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /src/operations/utils.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | consts, 3 | types::AccountIdentifier, 4 | types::{Amount, Currency, Operation, OperationIdentifier, OperationStatusType, OperationType}, 5 | utils::get_operation_type, 6 | utils::get_operation_type_with_program, 7 | }; 8 | 9 | use solana_transaction_status::{ 10 | parse_instruction::ParsedInstructionEnum, EncodedTransaction, UiInstruction, UiMessage, 11 | UiParsedInstruction, 12 | }; 13 | 14 | use serde::{Deserialize, Serialize}; 15 | 16 | #[derive(Clone, Debug, Serialize, Deserialize, Default)] 17 | pub struct OpMetaTokenAmount { 18 | #[serde(skip_serializing_if = "Option::is_none")] 19 | amount: Option, 20 | #[serde(skip_serializing_if = "Option::is_none")] 21 | decimals: Option, 22 | #[serde(skip_serializing_if = "Option::is_none")] 23 | uiAmount: Option, 24 | } 25 | #[derive(Clone, Debug, Serialize, Deserialize, Default)] 26 | struct OpMeta { 27 | ///owner of sender address 28 | #[serde(skip_serializing_if = "Option::is_none", alias = "custodian")] 29 | authority: Option, 30 | #[serde(skip_serializing_if = "Option::is_none")] 31 | new_authority: Option, 32 | ///sender token wallet address 33 | #[serde( 34 | skip_serializing_if = "Option::is_none", 35 | alias = "nonceAccount", 36 | alias = "stakeAccount", 37 | alias = "voteAccount" 38 | )] 39 | source: Option, 40 | ///destination token wallet address 41 | #[serde( 42 | skip_serializing_if = "Option::is_none", 43 | alias = "newAccount", 44 | alias = "newSplitAccount" 45 | )] 46 | destination: Option, 47 | /// owner of source address 48 | #[serde(skip_serializing_if = "Option::is_none")] 49 | mint: Option, 50 | /// decimals 51 | #[serde(skip_serializing_if = "Option::is_none")] 52 | decimals: Option, 53 | #[serde(skip_serializing_if = "Option::is_none", rename = "tokenAmount")] 54 | token_amount: Option, 55 | 56 | #[serde(skip_serializing_if = "Option::is_none")] 57 | amount: Option, 58 | 59 | #[serde(skip_serializing_if = "Option::is_none")] 60 | lamports: Option, 61 | 62 | #[serde(skip_serializing_if = "Option::is_none")] 63 | space: Option, 64 | 65 | #[serde(skip_serializing_if = "Option::is_none")] 66 | owner: Option, 67 | #[serde(skip_serializing_if = "Option::is_none", alias = "voter")] 68 | staker: Option, 69 | #[serde(skip_serializing_if = "Option::is_none")] 70 | withdrawer: Option, 71 | #[serde(skip_serializing_if = "Option::is_none")] 72 | vote_pubkey: Option, 73 | #[serde(skip_serializing_if = "Option::is_none")] 74 | custodian: Option, 75 | #[serde(skip_serializing_if = "Option::is_none")] 76 | comission: Option, 77 | } 78 | impl OpMeta { 79 | fn amount_str(&self) -> String { 80 | if let Some(x) = &self.lamports { 81 | x.to_string() 82 | } else if let Some(x) = &self.amount { 83 | x.to_string() 84 | } else if let Some(x) = &self.token_amount { 85 | x.amount.clone().unwrap() 86 | } else { 87 | "0".to_string() 88 | } 89 | } 90 | fn amount_u64(&self) -> u64 { 91 | if let Some(x) = &self.lamports { 92 | *x 93 | } else if let Some(x) = &self.amount { 94 | x.parse::().unwrap() 95 | } else if let Some(x) = &self.token_amount { 96 | x.amount.clone().unwrap().parse::().unwrap() 97 | } else { 98 | 0 as u64 99 | } 100 | } 101 | } 102 | impl From<&Option> for OpMeta { 103 | fn from(meta: &Option) -> Self { 104 | let op = serde_json::from_value::(meta.clone().unwrap()).unwrap(); 105 | op 106 | } 107 | } 108 | 109 | #[macro_export] 110 | macro_rules! set_meta { 111 | ($metadata:expr, $metastruct:ident) => { 112 | if let Some(x) = $metadata { 113 | serde_json::from_value::<$metastruct>(x).unwrap() 114 | } else { 115 | $metastruct::default() 116 | } 117 | }; 118 | } 119 | #[macro_export] 120 | macro_rules! merge_meta { 121 | ($metadata:expr, $defmeta:expr, $i:expr, $enum:ident) => { 122 | if let Some(x) = $defmeta { 123 | if let Some(y) = &x[$i] { 124 | match y { 125 | InternalOperationMetadata::$enum(x) => { 126 | $metadata.merge(x.clone()); 127 | } 128 | _ => {} 129 | } 130 | } 131 | }; 132 | }; 133 | } 134 | //Convert tx recieved from json rpc to rosetta style operations 135 | //TODO: find All balance change scenarios and should be converted to -ve +ve balance change operations 136 | 137 | pub fn get_operations_from_encoded_tx( 138 | transaction: &EncodedTransaction, 139 | status: Option, 140 | ) -> (Vec, String) { 141 | let mut operations = vec![]; 142 | let mut tx_hash = String::from(""); 143 | 144 | let mut op_index = 0 as u64; 145 | if let EncodedTransaction::Json(t) = &transaction { 146 | tx_hash = t.signatures[0].to_string(); 147 | if let UiMessage::Parsed(m) = &t.message { 148 | m.instructions.iter().for_each(|instruction| { 149 | let oi = OperationIdentifier { 150 | index: op_index as u64, 151 | network_index: None, 152 | }; 153 | if let UiInstruction::Parsed(ui_parsed_instruction) = &instruction { 154 | match &ui_parsed_instruction { 155 | UiParsedInstruction::Parsed(parsed_instruction) => { 156 | let parsed_instruction_enum: ParsedInstructionEnum = 157 | serde_json::from_value(parsed_instruction.parsed.clone()) 158 | .unwrap_or(ParsedInstructionEnum { 159 | instruction_type: "Unknown".to_string(), 160 | info: serde_json::Value::Null, 161 | }); 162 | let optype = get_operation_type_with_program( 163 | &parsed_instruction.program, 164 | &parsed_instruction_enum.instruction_type, 165 | ); 166 | let metadata = parsed_instruction_enum.info; 167 | if optype.is_balance_changing() { 168 | let parsed_meta = OpMeta::from(&Some(metadata.clone())); 169 | let mut parsed_meta_cloned = parsed_meta.clone(); 170 | let currency = Currency { 171 | symbol: parsed_meta 172 | .mint 173 | .unwrap_or(consts::NATIVE_SYMBOL.to_string()), 174 | decimals: parsed_meta 175 | .decimals 176 | .unwrap_or(consts::NATIVE_DECIMALS), 177 | metadata: None, 178 | }; 179 | let sender = Some(AccountIdentifier { 180 | address: parsed_meta.source.unwrap(), 181 | sub_account: None, 182 | }); 183 | let sender_amt = Some(Amount { 184 | value: format!("-{}", parsed_meta_cloned.amount_str()), 185 | currency: currency.clone(), 186 | }); 187 | let receiver = Some(AccountIdentifier { 188 | address: parsed_meta.destination.unwrap(), 189 | sub_account: None, 190 | }); 191 | let receiver_amt = Some(Amount { 192 | value: parsed_meta_cloned.amount_str(), 193 | currency: currency, 194 | }); 195 | 196 | op_index += 1; 197 | let oi2 = OperationIdentifier { 198 | index: (op_index) as u64, 199 | network_index: None, 200 | }; 201 | 202 | //for construction test 203 | parsed_meta_cloned.amount = None; 204 | parsed_meta_cloned.lamports = None; 205 | parsed_meta_cloned.source = None; 206 | parsed_meta_cloned.destination = None; 207 | 208 | let res = serde_json::to_value(parsed_meta_cloned).unwrap(); 209 | let (metasend, metarec) = if res.as_object().unwrap().is_empty() { 210 | (None, None) 211 | } else { 212 | (Some(res.clone()), Some(res)) 213 | }; 214 | //sender push 215 | operations.push(Operation { 216 | operation_identifier: oi.clone(), 217 | related_operations: None, 218 | type_: optype.clone(), 219 | status: status.clone(), 220 | account: sender, 221 | amount: sender_amt, 222 | metadata: metasend, 223 | }); 224 | //receiver push 225 | operations.push(Operation { 226 | operation_identifier: oi2, 227 | related_operations: None, 228 | type_: optype, 229 | status: status.clone(), 230 | account: receiver, 231 | amount: receiver_amt, 232 | metadata: metarec, 233 | }); 234 | } else { 235 | //TODO: See metadata in other op types and put here accordingly 236 | operations.push(Operation { 237 | operation_identifier: oi, 238 | related_operations: None, 239 | type_: optype, 240 | status: status.clone(), 241 | account: None, 242 | amount: None, 243 | metadata: Some(metadata), 244 | }); 245 | } 246 | } 247 | UiParsedInstruction::PartiallyDecoded(partially_decoded_instruction) => { 248 | operations.push(Operation { 249 | operation_identifier: oi, 250 | related_operations: None, 251 | type_: get_operation_type("Unknown"), 252 | status: status.clone(), 253 | account: None, 254 | amount: None, 255 | metadata: Some( 256 | serde_json::to_value(&partially_decoded_instruction).unwrap(), 257 | ), 258 | }); 259 | } 260 | } 261 | } 262 | op_index += 1; 263 | }) 264 | } 265 | } 266 | (operations, tx_hash) 267 | } 268 | -------------------------------------------------------------------------------- /src/operations/matcher.rs: -------------------------------------------------------------------------------- 1 | use merge::Merge; 2 | use serde_json::Value; 3 | use solana_sdk::instruction::Instruction; 4 | 5 | use super::stake::*; 6 | use super::system::*; 7 | use super::vote::*; 8 | use super::{spltoken, spltoken::*, stake, system, vote}; 9 | use crate::{ 10 | error::ApiError, 11 | merge_meta, set_meta, 12 | types::Operation, 13 | types::{OperationType, OptionalInternalOperationMetadatas}, 14 | }; 15 | use serde::{Deserialize, Serialize}; 16 | #[derive(Clone, Debug, Deserialize, Serialize)] 17 | pub enum InternalOperationMetadata { 18 | System(SystemOperationMetadata), 19 | Vote(VoteOperationMetadata), 20 | Stake(StakeOperationMetadata), 21 | SplToken(SplTokenOperationMetadata), 22 | } 23 | #[derive(Clone, Debug, Deserialize, Serialize)] 24 | pub struct InternalOperation { 25 | pub metadata: InternalOperationMetadata, 26 | pub type_: OperationType, 27 | } 28 | impl InternalOperation { 29 | fn to_instruction(self) -> Result, ApiError> { 30 | Ok(match self.metadata { 31 | InternalOperationMetadata::System(x) => system::to_instruction(self.type_, x)?, 32 | InternalOperationMetadata::Vote(x) => vote::to_instruction(self.type_, x)?, 33 | InternalOperationMetadata::Stake(x) => stake::to_instruction(self.type_, x)?, 34 | InternalOperationMetadata::SplToken(x) => spltoken::to_instruction(self.type_, x)?, 35 | }) 36 | } 37 | } 38 | pub struct Matcher<'a> { 39 | checked_indexes: Vec, 40 | operations: &'a Vec, 41 | meta: OptionalInternalOperationMetadatas, 42 | } 43 | 44 | impl<'a> Matcher<'a> { 45 | pub fn new(operations: &Vec, meta: OptionalInternalOperationMetadatas) -> Matcher { 46 | Matcher { 47 | operations, 48 | checked_indexes: vec![], 49 | meta, 50 | } 51 | } 52 | pub fn to_instructions(&mut self) -> Result, ApiError> { 53 | let combined = self.combine()?; 54 | Ok(combined 55 | .into_iter() 56 | .map(|x| x.to_instruction().unwrap()) 57 | .flatten() 58 | .collect()) 59 | } 60 | 61 | pub fn combine(&mut self) -> Result, ApiError> { 62 | let mut internal_operations = vec![]; 63 | for i in 0..self.operations.len() { 64 | let operation = self.operations[i].clone(); 65 | if !self 66 | .checked_indexes 67 | .contains(&operation.operation_identifier.index) 68 | { 69 | let mut meta_clone = operation.metadata.clone(); 70 | if let Some(ref acc) = operation.account { 71 | if let Some(ref amt) = operation.amount { 72 | let clean_amt = amt.value.replace("-", ""); 73 | let matched_operation = self.operations.iter().find(|sub_op| { 74 | if let Some(sub_op_amt) = &sub_op.amount { 75 | if sub_op_amt.value.replace("-", "") == clean_amt 76 | && sub_op.type_ == operation.type_ 77 | && sub_op_amt.currency.symbol == amt.currency.symbol 78 | && sub_op_amt.currency.decimals == amt.currency.decimals 79 | && sub_op.operation_identifier.index 80 | != operation.operation_identifier.index 81 | && !self 82 | .checked_indexes 83 | .contains(&sub_op.operation_identifier.index) 84 | { 85 | true 86 | } else { 87 | false 88 | } 89 | } else { 90 | false 91 | } 92 | }); 93 | if let Some(matched_op) = matched_operation { 94 | let x = &matched_op.operation_identifier; 95 | let index = x.index; 96 | let main_amount = amt.value.parse::().unwrap(); 97 | let main_address = acc.address.clone(); 98 | 99 | let amount = matched_op.amount(); 100 | let address = matched_op.address(); 101 | 102 | //if subamt is -ve then subamt is sender is source 103 | let (source, destination, lamports) = if amount < 0.0 { 104 | //negative = this is sender 105 | (address, main_address, main_amount as u64) 106 | } else { 107 | (main_address, address, amount as u64) 108 | }; 109 | if meta_clone.is_none() { 110 | meta_clone = Some(serde_json::json!({})); 111 | } 112 | if let Some(ref mut m) = meta_clone { 113 | if let Value::Object(ref mut map) = m { 114 | map.insert( 115 | "source".to_string(), 116 | serde_json::Value::String(source.clone()), 117 | ); 118 | map.insert( 119 | "destination".to_string(), 120 | serde_json::Value::String(destination), 121 | ); 122 | map.insert( 123 | "lamports".to_string(), 124 | serde_json::Value::Number(serde_json::Number::from( 125 | lamports, 126 | )), 127 | ); 128 | map.insert( 129 | "amount".to_string(), 130 | serde_json::Value::Number(serde_json::Number::from( 131 | lamports, 132 | )), 133 | ); 134 | } 135 | } 136 | self.checked_indexes.push(index); 137 | } 138 | } else { 139 | if meta_clone.is_none() { 140 | meta_clone = Some(serde_json::json!({})); 141 | } 142 | if let Some(ref mut m) = meta_clone { 143 | if let Value::Object(ref mut map) = m { 144 | map.insert( 145 | "source".to_string(), 146 | serde_json::Value::String(acc.address.clone()), 147 | ); 148 | if map.get("authority").is_none() { 149 | map.insert( 150 | "authority".to_string(), 151 | serde_json::Value::String(acc.address.clone()), 152 | ); 153 | } 154 | } 155 | } 156 | } 157 | } 158 | if let Some(ref mut m) = meta_clone { 159 | if let Value::Object(ref mut map) = m { 160 | if map.get("authority").is_none() { 161 | map.insert("authority".to_string(), map.get("source").unwrap().clone()); 162 | } 163 | } 164 | } 165 | match operation.type_ { 166 | OperationType::System__Assign 167 | | OperationType::System__CreateAccount 168 | | OperationType::System__Transfer 169 | | OperationType::System__Allocate 170 | | OperationType::System__CreateNonceAccount 171 | | OperationType::System__AdvanceNonce 172 | | OperationType::System__WithdrawFromNonce 173 | | OperationType::System__AuthorizeNonce => { 174 | let mut new_metadata = set_meta!(meta_clone, SystemOperationMetadata); 175 | merge_meta!(new_metadata, &self.meta, internal_operations.len(), System); 176 | 177 | internal_operations.push(InternalOperation { 178 | type_: operation.type_, 179 | metadata: InternalOperationMetadata::System(new_metadata), 180 | }) 181 | } 182 | OperationType::SplToken__InitializeMint 183 | | OperationType::SplToken__InitializeAccount 184 | | OperationType::SplToken__CreateToken 185 | | OperationType::SplToken__CreateAccount 186 | | OperationType::SplToken__Transfer 187 | | OperationType::SplToken__Approve 188 | | OperationType::SplToken__Revoke 189 | | OperationType::SplToken__MintTo 190 | | OperationType::SplToken__Burn 191 | | OperationType::SplToken__CloseAccount 192 | | OperationType::SplToken__FreezeAccount 193 | | OperationType::SplToken__ThawAccount 194 | | OperationType::SplToken__TransferChecked 195 | | OperationType::SplToken__CreateAssocAccount => { 196 | let mut new_metadata = set_meta!(meta_clone, SplTokenOperationMetadata); 197 | merge_meta!( 198 | new_metadata, 199 | &self.meta, 200 | internal_operations.len(), 201 | SplToken 202 | ); 203 | 204 | internal_operations.push(InternalOperation { 205 | type_: operation.type_, 206 | metadata: InternalOperationMetadata::SplToken(new_metadata), 207 | }) 208 | } 209 | OperationType::Stake__CreateAccount 210 | | OperationType::Stake__Delegate 211 | | OperationType::Stake__Split 212 | | OperationType::Stake__Merge 213 | | OperationType::Stake__Authorize 214 | | OperationType::Stake__Withdraw 215 | | OperationType::Stake__Deactivate 216 | | OperationType::Stake__SetLockup => { 217 | let mut new_metadata = set_meta!(meta_clone, StakeOperationMetadata); 218 | merge_meta!(new_metadata, &self.meta, internal_operations.len(), Stake); 219 | internal_operations.push(InternalOperation { 220 | type_: operation.type_, 221 | metadata: InternalOperationMetadata::Stake(new_metadata), 222 | }) 223 | } 224 | OperationType::Vote__CreateAccount 225 | | OperationType::Vote__Authorize 226 | | OperationType::Vote__Withdraw 227 | | OperationType::Vote__UpdateValidatorIdentity 228 | | OperationType::Vote__UpdateCommission => { 229 | let mut new_metadata = set_meta!(meta_clone, VoteOperationMetadata); 230 | merge_meta!(new_metadata, &self.meta, internal_operations.len(), Vote); 231 | internal_operations.push(InternalOperation { 232 | type_: operation.type_, 233 | metadata: InternalOperationMetadata::Vote(new_metadata), 234 | }) 235 | } 236 | OperationType::Unknown => {} 237 | }; 238 | } 239 | } 240 | Ok(internal_operations) 241 | } 242 | } 243 | 244 | #[cfg(test)] 245 | mod tests { 246 | 247 | use crate::types::*; 248 | use serde_json::json; 249 | 250 | use super::Matcher; 251 | 252 | #[test] 253 | fn convert_op_test() { 254 | let operations = &vec![ 255 | Operation { 256 | operation_identifier: OperationIdentifier { 257 | index: 0, 258 | network_index: None, 259 | }, 260 | related_operations: None, 261 | status: None, 262 | account: Some(AccountIdentifier { 263 | address: "SenderAddress".to_string(), 264 | sub_account: None, 265 | }), 266 | amount: Some(Amount { 267 | value: "-1000".to_string(), 268 | currency: Currency { 269 | symbol: "TEST".to_string(), 270 | decimals: 10, 271 | metadata: None, 272 | }, 273 | }), 274 | type_: OperationType::System__Transfer, 275 | metadata: None, 276 | }, 277 | Operation { 278 | operation_identifier: OperationIdentifier { 279 | index: 1, 280 | network_index: None, 281 | }, 282 | related_operations: None, 283 | status: None, 284 | account: Some(AccountIdentifier { 285 | address: "DestinationAddress".to_string(), 286 | sub_account: None, 287 | }), 288 | amount: Some(Amount { 289 | value: "1000".to_string(), 290 | currency: Currency { 291 | symbol: "TEST".to_string(), 292 | decimals: 10, 293 | metadata: None, 294 | }, 295 | }), 296 | type_: OperationType::System__Transfer, 297 | metadata: None, 298 | }, 299 | //unrelated operation 300 | Operation { 301 | operation_identifier: OperationIdentifier { 302 | index: 5, 303 | network_index: None, 304 | }, 305 | related_operations: None, 306 | status: None, 307 | account: None, 308 | amount: None, 309 | type_: OperationType::System__Transfer, 310 | metadata: Some(json!({ 311 | "source": "SomeUnrelatedSender", 312 | "destination": "SomeUnrelatedDest", 313 | "lamports": 10000, 314 | })), 315 | }, 316 | Operation { 317 | operation_identifier: OperationIdentifier { 318 | index: 10, 319 | network_index: None, 320 | }, 321 | related_operations: None, 322 | status: None, 323 | account: Some(AccountIdentifier { 324 | address: "SS".to_string(), 325 | sub_account: None, 326 | }), 327 | amount: Some(Amount { 328 | value: "-10".to_string(), 329 | currency: Currency { 330 | symbol: "MM".to_string(), 331 | decimals: 2, 332 | metadata: None, 333 | }, 334 | }), 335 | type_: OperationType::SplToken__TransferChecked, 336 | metadata: Some(json!({ 337 | "authority": "AA", 338 | })), 339 | }, 340 | Operation { 341 | operation_identifier: OperationIdentifier { 342 | index: 11, 343 | network_index: None, 344 | }, 345 | related_operations: None, 346 | status: None, 347 | account: Some(AccountIdentifier { 348 | address: "DD".to_string(), 349 | sub_account: None, 350 | }), 351 | amount: Some(Amount { 352 | value: "10".to_string(), 353 | currency: Currency { 354 | symbol: "MM".to_string(), 355 | decimals: 2, 356 | metadata: None, 357 | }, 358 | }), 359 | type_: OperationType::SplToken__TransferChecked, 360 | metadata: Some(json!({ 361 | "authority": "AA", 362 | })), 363 | }, 364 | ]; 365 | let mut matcher = Matcher::new(&operations, None); 366 | let ops = matcher.combine().unwrap(); 367 | assert_eq!(ops.len(), 3); 368 | println!("{:?}", ops); 369 | } 370 | } 371 | -------------------------------------------------------------------------------- /src/types.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use solana_client::rpc_request::RpcRequest; 3 | use solana_sdk::{clock::UnixTimestamp, fee_calculator::FeeCalculator, hash::Hash}; 4 | use solana_transaction_status::UiTransactionStatusMeta; 5 | use spl_feature_proposal::instruction::FeatureProposalInstruction; 6 | 7 | use crate::operations::matcher::{InternalOperation, InternalOperationMetadata}; 8 | 9 | // Objects 10 | 11 | #[derive(Clone, Debug, Deserialize, Serialize)] 12 | pub struct CallRequest { 13 | pub network_identifier: NetworkIdentifier, 14 | pub method: RpcRequestInternal, 15 | pub parameters: serde_json::Value, 16 | } 17 | #[derive(Clone, Debug, Deserialize, Serialize)] 18 | pub struct CallResponse { 19 | pub result: serde_json::Value, 20 | pub idempotent: bool, 21 | } 22 | 23 | //TODO: orphan rule cant impl deserialize for RpcRequest 24 | #[derive(Clone, Debug, Deserialize, Serialize, strum::EnumIter, strum_macros::EnumString)] 25 | pub enum RpcRequestInternal { 26 | DeregisterNode, 27 | ValidatorExit, 28 | GetAccountInfo, 29 | GetBalance, 30 | GetBlockTime, 31 | GetClusterNodes, 32 | GetConfirmedBlock, 33 | GetConfirmedBlocks, 34 | GetConfirmedBlocksWithLimit, 35 | GetConfirmedSignaturesForAddress, 36 | GetConfirmedSignaturesForAddress2, 37 | GetConfirmedTransaction, 38 | GetEpochInfo, 39 | GetEpochSchedule, 40 | GetFeeCalculatorForBlockhash, 41 | GetFeeRateGovernor, 42 | GetFees, 43 | GetFirstAvailableBlock, 44 | GetGenesisHash, 45 | GetHealth, 46 | GetIdentity, 47 | GetInflationGovernor, 48 | GetInflationRate, 49 | GetLargestAccounts, 50 | GetLeaderSchedule, 51 | GetMinimumBalanceForRentExemption, 52 | GetMultipleAccounts, 53 | GetProgramAccounts, 54 | GetRecentBlockhash, 55 | GetSnapshotSlot, 56 | GetSignatureStatuses, 57 | GetSlot, 58 | GetSlotLeader, 59 | GetStorageTurn, 60 | GetStorageTurnRate, 61 | GetSlotsPerSegment, 62 | GetStoragePubkeysForSlot, 63 | GetSupply, 64 | GetTokenAccountBalance, 65 | GetTokenAccountsByDelegate, 66 | GetTokenAccountsByOwner, 67 | GetTokenSupply, 68 | GetTotalSupply, 69 | GetTransactionCount, 70 | GetVersion, 71 | GetVoteAccounts, 72 | MinimumLedgerSlot, 73 | RegisterNode, 74 | RequestAirdrop, 75 | SendTransaction, 76 | SimulateTransaction, 77 | SignVote, 78 | } 79 | 80 | impl From for RpcRequest { 81 | fn from(r: RpcRequestInternal) -> Self { 82 | match r { 83 | RpcRequestInternal::DeregisterNode => RpcRequest::DeregisterNode, 84 | RpcRequestInternal::ValidatorExit => RpcRequest::ValidatorExit, 85 | RpcRequestInternal::GetAccountInfo => RpcRequest::GetAccountInfo, 86 | RpcRequestInternal::GetBalance => RpcRequest::GetBalance, 87 | RpcRequestInternal::GetBlockTime => RpcRequest::GetBlockTime, 88 | RpcRequestInternal::GetClusterNodes => RpcRequest::GetClusterNodes, 89 | RpcRequestInternal::GetConfirmedBlock => RpcRequest::GetConfirmedBlock, 90 | RpcRequestInternal::GetConfirmedBlocks => RpcRequest::GetConfirmedBlocks, 91 | RpcRequestInternal::GetConfirmedBlocksWithLimit => { 92 | RpcRequest::GetConfirmedBlocksWithLimit 93 | } 94 | RpcRequestInternal::GetConfirmedSignaturesForAddress => { 95 | RpcRequest::GetConfirmedSignaturesForAddress 96 | } 97 | RpcRequestInternal::GetConfirmedSignaturesForAddress2 => { 98 | RpcRequest::GetConfirmedSignaturesForAddress2 99 | } 100 | RpcRequestInternal::GetConfirmedTransaction => RpcRequest::GetConfirmedTransaction, 101 | RpcRequestInternal::GetEpochInfo => RpcRequest::GetEpochInfo, 102 | RpcRequestInternal::GetEpochSchedule => RpcRequest::GetEpochSchedule, 103 | RpcRequestInternal::GetFeeCalculatorForBlockhash => { 104 | RpcRequest::GetFeeCalculatorForBlockhash 105 | } 106 | RpcRequestInternal::GetFeeRateGovernor => RpcRequest::GetFeeRateGovernor, 107 | RpcRequestInternal::GetFees => RpcRequest::GetFees, 108 | RpcRequestInternal::GetFirstAvailableBlock => RpcRequest::GetFirstAvailableBlock, 109 | RpcRequestInternal::GetGenesisHash => RpcRequest::GetGenesisHash, 110 | RpcRequestInternal::GetHealth => RpcRequest::GetHealth, 111 | RpcRequestInternal::GetIdentity => RpcRequest::GetIdentity, 112 | RpcRequestInternal::GetInflationGovernor => RpcRequest::GetInflationGovernor, 113 | RpcRequestInternal::GetInflationRate => RpcRequest::GetInflationRate, 114 | RpcRequestInternal::GetLargestAccounts => RpcRequest::GetLargestAccounts, 115 | RpcRequestInternal::GetLeaderSchedule => RpcRequest::GetLeaderSchedule, 116 | RpcRequestInternal::GetMinimumBalanceForRentExemption => { 117 | RpcRequest::GetMinimumBalanceForRentExemption 118 | } 119 | RpcRequestInternal::GetMultipleAccounts => RpcRequest::GetMultipleAccounts, 120 | RpcRequestInternal::GetProgramAccounts => RpcRequest::GetProgramAccounts, 121 | RpcRequestInternal::GetRecentBlockhash => RpcRequest::GetRecentBlockhash, 122 | RpcRequestInternal::GetSnapshotSlot => RpcRequest::GetSnapshotSlot, 123 | RpcRequestInternal::GetSignatureStatuses => RpcRequest::GetSignatureStatuses, 124 | RpcRequestInternal::GetSlot => RpcRequest::GetSlot, 125 | RpcRequestInternal::GetSlotLeader => RpcRequest::GetSlotLeader, 126 | RpcRequestInternal::GetStorageTurn => RpcRequest::GetStorageTurn, 127 | RpcRequestInternal::GetStorageTurnRate => RpcRequest::GetStorageTurnRate, 128 | RpcRequestInternal::GetSlotsPerSegment => RpcRequest::GetSlotsPerSegment, 129 | RpcRequestInternal::GetStoragePubkeysForSlot => RpcRequest::GetStoragePubkeysForSlot, 130 | RpcRequestInternal::GetSupply => RpcRequest::GetSupply, 131 | RpcRequestInternal::GetTokenAccountBalance => RpcRequest::GetTokenAccountBalance, 132 | RpcRequestInternal::GetTokenAccountsByDelegate => { 133 | RpcRequest::GetTokenAccountsByDelegate 134 | } 135 | RpcRequestInternal::GetTokenAccountsByOwner => RpcRequest::GetTokenAccountsByOwner, 136 | RpcRequestInternal::GetTokenSupply => RpcRequest::GetTokenSupply, 137 | RpcRequestInternal::GetTotalSupply => RpcRequest::GetTotalSupply, 138 | RpcRequestInternal::GetTransactionCount => RpcRequest::GetTransactionCount, 139 | RpcRequestInternal::GetVersion => RpcRequest::GetVersion, 140 | RpcRequestInternal::GetVoteAccounts => RpcRequest::GetVoteAccounts, 141 | RpcRequestInternal::MinimumLedgerSlot => RpcRequest::MinimumLedgerSlot, 142 | RpcRequestInternal::RegisterNode => RpcRequest::RegisterNode, 143 | RpcRequestInternal::RequestAirdrop => RpcRequest::RequestAirdrop, 144 | RpcRequestInternal::SendTransaction => RpcRequest::SendTransaction, 145 | RpcRequestInternal::SimulateTransaction => RpcRequest::SimulateTransaction, 146 | RpcRequestInternal::SignVote => RpcRequest::SignVote, 147 | } 148 | } 149 | } 150 | #[derive(Clone, Debug, Deserialize, Serialize)] 151 | pub struct Allow { 152 | pub operation_statuses: Vec, 153 | pub operation_types: Vec, 154 | pub errors: Vec, 155 | pub historical_balance_lookup: bool, 156 | #[serde(skip_serializing_if = "Option::is_none")] 157 | pub timestamp_start_index: Option, 158 | pub call_methods: Vec, 159 | pub balance_exemptions: Vec, 160 | } 161 | 162 | #[derive(Clone, Debug, Deserialize, Serialize)] 163 | pub struct BalanceExemption { 164 | #[serde(skip_serializing_if = "Option::is_none")] 165 | pub sub_account_address: Option, 166 | #[serde(skip_serializing_if = "Option::is_none")] 167 | pub currency: Option, 168 | #[serde(skip_serializing_if = "Option::is_none")] 169 | pub exemption_type: Option, 170 | } 171 | 172 | #[derive(Clone, Debug, Deserialize, Serialize)] 173 | pub enum ExemptionType { 174 | #[serde(rename = "greater_or_equal")] 175 | GreaterOrEqual, 176 | #[serde(rename = "less_or_equal")] 177 | LessOrEqual, 178 | #[serde(rename = "dynamic")] 179 | Dynanic, 180 | } 181 | 182 | #[derive(Clone, Debug, Deserialize, Serialize)] 183 | pub struct Amount { 184 | pub value: String, 185 | pub currency: Currency, 186 | } 187 | 188 | #[derive(Clone, Debug, Deserialize, Serialize)] 189 | pub struct Block { 190 | pub block_identifier: BlockIdentifier, 191 | pub parent_block_identifier: BlockIdentifier, 192 | pub timestamp: u64, 193 | pub transactions: Vec, 194 | } 195 | 196 | #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] 197 | pub struct Currency { 198 | pub symbol: String, 199 | pub decimals: u8, 200 | #[serde(skip_serializing_if = "Option::is_none")] 201 | pub metadata: Option, 202 | } 203 | 204 | #[derive(Clone, Debug, Deserialize, Serialize)] 205 | pub struct Error { 206 | pub code: u64, 207 | pub message: String, 208 | pub retriable: bool, 209 | #[serde(skip_serializing_if = "Option::is_none")] 210 | pub details: Option, 211 | } 212 | 213 | #[derive(Clone, Debug, Deserialize, Serialize)] 214 | pub struct Operation { 215 | pub operation_identifier: OperationIdentifier, 216 | #[serde(skip_serializing_if = "Option::is_none")] 217 | pub related_operations: Option>, 218 | #[serde(rename = "type")] 219 | pub type_: OperationType, 220 | #[serde(skip_serializing_if = "Option::is_none")] 221 | pub status: Option, //TODO: sucess/faliure for now 222 | #[serde(skip_serializing_if = "Option::is_none")] 223 | pub account: Option, 224 | #[serde(skip_serializing_if = "Option::is_none")] 225 | pub amount: Option, 226 | #[serde(skip_serializing_if = "Option::is_none")] 227 | pub metadata: Option, 228 | } 229 | 230 | #[derive( 231 | Clone, 232 | Debug, 233 | Deserialize, 234 | Serialize, 235 | PartialEq, 236 | strum::EnumIter, 237 | strum::AsStaticStr, 238 | strum_macros::EnumString, 239 | )] 240 | pub enum OperationType { 241 | System__CreateAccount, 242 | System__Assign, 243 | System__Transfer, 244 | // System__CreateAccountWithSeed, 245 | System__CreateNonceAccount, 246 | System__AdvanceNonce, 247 | System__WithdrawFromNonce, 248 | System__AuthorizeNonce, 249 | System__Allocate, 250 | //System__AllocateWithSeed, 251 | //System__AssignWithSeed, 252 | //System__TransferWithSeed, 253 | SplToken__InitializeMint, 254 | SplToken__InitializeAccount, 255 | SplToken__CreateToken, 256 | SplToken__CreateAccount, 257 | SplToken__Transfer, 258 | SplToken__Approve, 259 | SplToken__Revoke, 260 | //SplToken__SetAuthority, 261 | SplToken__MintTo, 262 | SplToken__Burn, 263 | SplToken__CloseAccount, 264 | SplToken__FreezeAccount, 265 | SplToken__ThawAccount, 266 | SplToken__TransferChecked, 267 | SplToken__CreateAssocAccount, 268 | 269 | Stake__CreateAccount, 270 | Stake__Delegate, 271 | Stake__Split, 272 | Stake__Merge, 273 | Stake__Authorize, 274 | Stake__Withdraw, 275 | Stake__Deactivate, 276 | Stake__SetLockup, 277 | 278 | Vote__CreateAccount, 279 | Vote__Authorize, 280 | //Vote__Vote, 281 | Vote__Withdraw, 282 | Vote__UpdateValidatorIdentity, 283 | Vote__UpdateCommission, 284 | //Vote__VoteSwitch, 285 | Unknown, 286 | } 287 | //TODO: Add all balance changing oepration types 288 | //make sure the the source and destination json names of them are added as alias in OpMeta struct or this wont work 289 | //e.g nonceAccount returned as source in WithdrawFromNonce Operation from json rpc 290 | //see transaction-status parse_x for names 291 | //operations wheres theres no equal negative and positive are not eligible for balance changing. e.g mint or burn where theres nobody on sending or receving end 292 | impl OperationType { 293 | pub fn is_balance_changing(&self) -> bool { 294 | match &self { 295 | OperationType::System__CreateAccount 296 | | OperationType::System__WithdrawFromNonce 297 | | OperationType::System__Transfer 298 | | OperationType::SplToken__Transfer 299 | | OperationType::SplToken__TransferChecked 300 | | OperationType::Stake__Split 301 | | OperationType::Stake__Merge 302 | | OperationType::Stake__Withdraw 303 | | OperationType::Vote__Withdraw => true, 304 | _ => false, 305 | } 306 | } 307 | } 308 | #[derive( 309 | Clone, Debug, Deserialize, Serialize, PartialEq, strum::EnumIter, strum_macros::EnumString, 310 | )] 311 | pub enum OperationStatusType { 312 | Success, 313 | Faliure, 314 | } 315 | 316 | #[derive(Clone, Debug, Deserialize, Serialize)] 317 | pub struct SigningPayload { 318 | #[serde(skip_serializing_if = "Option::is_none")] 319 | pub account_identifier: Option, 320 | pub hex_bytes: String, 321 | #[serde(skip_serializing_if = "Option::is_none")] 322 | pub signature_type: Option, 323 | } 324 | 325 | #[derive(Clone, Debug, Deserialize, Serialize)] 326 | pub struct PublicKey { 327 | pub hex_bytes: String, 328 | pub curve_type: CurveType, 329 | } 330 | 331 | #[derive(Clone, Debug, Deserialize, Serialize)] 332 | pub struct Signature { 333 | pub signing_payload: SigningPayload, 334 | pub public_key: PublicKey, 335 | pub signature_type: SignatureType, 336 | pub hex_bytes: String, 337 | } 338 | 339 | #[derive(Clone, Debug, Deserialize, Serialize)] 340 | pub struct Transaction { 341 | pub transaction_identifier: TransactionIdentifier, 342 | pub operations: Vec, 343 | #[serde(skip_serializing_if = "Option::is_none")] 344 | pub metadata: Option, 345 | } 346 | 347 | // Identifiers 348 | 349 | #[derive(Clone, Debug, Deserialize, Serialize)] 350 | pub struct AccountIdentifier { 351 | pub address: String, 352 | #[serde(skip_serializing_if = "Option::is_none")] 353 | pub sub_account: Option, 354 | } 355 | 356 | #[derive(Clone, Debug, Deserialize, Serialize)] 357 | pub struct BlockIdentifier { 358 | pub index: u64, 359 | pub hash: String, 360 | } 361 | 362 | #[derive(Clone, Debug, Deserialize, Serialize)] 363 | pub struct NetworkIdentifier { 364 | pub blockchain: String, 365 | pub network: String, 366 | #[serde(skip_serializing_if = "Option::is_none")] 367 | pub sub_network_identifier: Option, 368 | } 369 | 370 | #[derive(Clone, Debug, Deserialize, Serialize)] 371 | pub struct OperationIdentifier { 372 | pub index: u64, 373 | #[serde(skip_serializing_if = "Option::is_none")] 374 | pub network_index: Option, 375 | } 376 | 377 | #[derive(Clone, Debug, Deserialize, Serialize)] 378 | pub struct PartialBlockIdentifier { 379 | #[serde(skip_serializing_if = "Option::is_none")] 380 | pub index: Option, 381 | #[serde(skip_serializing_if = "Option::is_none")] 382 | pub hash: Option, 383 | } 384 | 385 | #[derive(Clone, Debug, Deserialize, Serialize)] 386 | pub struct SubAccountIdentifier { 387 | pub address: String, 388 | } 389 | 390 | #[derive(Clone, Debug, Deserialize, Serialize)] 391 | pub struct SubNetworkIdentifier { 392 | pub network: String, 393 | } 394 | 395 | #[derive(Clone, Debug, Deserialize, Serialize)] 396 | pub struct TransactionIdentifier { 397 | pub hash: String, 398 | } 399 | 400 | // Requests and Rseponses 401 | 402 | #[derive(Clone, Debug, Deserialize, Serialize)] 403 | pub struct AccountBalanceRequest { 404 | pub network_identifier: NetworkIdentifier, 405 | pub account_identifier: AccountIdentifier, 406 | #[serde(skip_serializing_if = "Option::is_none")] 407 | pub block_identifier: Option, 408 | #[serde(skip_serializing_if = "Option::is_none")] 409 | pub currencies: Option>, 410 | } 411 | 412 | #[derive(Clone, Debug, Deserialize, Serialize)] 413 | pub struct AccountBalanceResponse { 414 | pub block_identifier: BlockIdentifier, 415 | pub balances: Vec, 416 | } 417 | 418 | #[derive(Clone, Debug, Deserialize, Serialize)] 419 | pub struct BlockRequest { 420 | pub network_identifier: NetworkIdentifier, 421 | pub block_identifier: PartialBlockIdentifier, 422 | } 423 | 424 | #[derive(Clone, Debug, Deserialize, Serialize)] 425 | pub struct BlockResponse { 426 | #[serde(skip_serializing_if = "Option::is_none")] 427 | pub block: Option, 428 | //pub other_transactions: Vec 429 | } 430 | 431 | #[derive(Clone, Debug, Deserialize, Serialize)] 432 | pub struct BlockTransactionRequest { 433 | pub network_identifier: NetworkIdentifier, 434 | pub block_identifier: BlockIdentifier, 435 | pub transaction_identifier: TransactionIdentifier, 436 | } 437 | 438 | #[derive(Clone, Debug, Deserialize, Serialize)] 439 | pub struct BlockTransactionResponse { 440 | pub transaction: Transaction, 441 | } 442 | 443 | #[derive(Clone, Debug, Deserialize, Serialize)] 444 | pub struct ConstructionCombineRequest { 445 | pub network_identifier: NetworkIdentifier, 446 | pub unsigned_transaction: String, 447 | pub signatures: Vec, 448 | } 449 | 450 | #[derive(Clone, Debug, Deserialize, Serialize)] 451 | pub struct ConstructionCombineResponse { 452 | pub signed_transaction: String, 453 | } 454 | 455 | #[derive(Clone, Debug, Deserialize, Serialize)] 456 | pub struct ConstructionDeriveRequest { 457 | pub network_identifier: NetworkIdentifier, 458 | pub public_key: PublicKey, 459 | } 460 | 461 | #[derive(Clone, Debug, Deserialize, Serialize)] 462 | pub struct ConstructionDeriveResponse { 463 | pub account_identifier: AccountIdentifier, 464 | } 465 | 466 | #[derive(Clone, Debug, Deserialize, Serialize)] 467 | pub struct ConstructionHashRequest { 468 | pub network_identifier: NetworkIdentifier, 469 | pub signed_transaction: String, 470 | } 471 | 472 | #[derive(Clone, Debug, Deserialize, Serialize)] 473 | pub struct ConstructionMetadataRequest { 474 | pub network_identifier: NetworkIdentifier, 475 | #[serde(skip_serializing_if = "Option::is_none")] 476 | pub options: Option, 477 | } 478 | 479 | #[derive(Clone, Debug, Deserialize, Serialize)] 480 | pub struct ConstructionMetadataResponse { 481 | pub metadata: ConstructionMetadata, 482 | } 483 | 484 | #[derive(Clone, Debug, Deserialize, Serialize)] 485 | pub struct ConstructionParseRequest { 486 | pub network_identifier: NetworkIdentifier, 487 | pub signed: bool, 488 | pub transaction: String, 489 | } 490 | 491 | #[derive(Clone, Debug, Deserialize, Serialize)] 492 | pub struct ConstructionParseResponse { 493 | pub operations: Vec, 494 | #[serde(skip_serializing_if = "Option::is_none")] 495 | pub account_identifier_signers: Option>, 496 | } 497 | 498 | #[derive(Clone, Debug, Deserialize, Serialize)] 499 | pub struct ConstructionPayloadsRequest { 500 | pub network_identifier: NetworkIdentifier, 501 | pub operations: Vec, 502 | #[serde(skip_serializing_if = "Option::is_none")] 503 | pub metadata: Option, 504 | } 505 | 506 | #[derive(Clone, Debug, Deserialize, Serialize)] 507 | pub struct ConstructionPayloadsResponse { 508 | pub unsigned_transaction: String, 509 | pub payloads: Vec>, 510 | } 511 | 512 | #[derive(Clone, Debug, Deserialize, Serialize)] 513 | pub struct ConstructionPreprocessRequest { 514 | pub network_identifier: NetworkIdentifier, 515 | pub operations: Vec, 516 | #[serde(skip_serializing_if = "Option::is_none")] 517 | pub metadata: Option, 518 | } 519 | #[derive(Clone, Debug, Deserialize, Serialize)] 520 | pub struct ConstructionPreprocessRequestMetadata { 521 | #[serde(skip_serializing_if = "Option::is_none")] 522 | pub with_nonce: Option, 523 | } 524 | #[derive(Clone, Debug, Deserialize, Serialize)] 525 | pub struct ConstructionPreprocessResponse { 526 | #[serde(skip_serializing_if = "Option::is_none")] 527 | pub options: Option, 528 | } 529 | 530 | #[derive(Clone, Debug, Deserialize, Serialize)] 531 | pub struct ConstructionSubmitRequest { 532 | pub network_identifier: NetworkIdentifier, 533 | pub signed_transaction: String, 534 | } 535 | 536 | #[derive(Clone, Debug, Deserialize, Serialize)] 537 | pub struct ConstructionSubmitResponse { 538 | pub transaction_identifier: TransactionIdentifier, 539 | } 540 | 541 | #[derive(Clone, Debug, Deserialize, Serialize)] 542 | pub struct MempoolRequest { 543 | pub network_identifier: NetworkIdentifier, 544 | } 545 | 546 | #[derive(Clone, Debug, Deserialize, Serialize)] 547 | pub struct MempoolResponse { 548 | pub transaction_identifiers: Vec, 549 | } 550 | 551 | #[derive(Clone, Debug, Deserialize, Serialize)] 552 | pub struct MempoolTransactionRequest { 553 | pub network_identifier: NetworkIdentifier, 554 | pub transaction_identifier: TransactionIdentifier, 555 | } 556 | 557 | #[derive(Clone, Debug, Deserialize, Serialize)] 558 | pub struct MempoolTransactionResponse { 559 | pub transaction: Transaction, 560 | } 561 | 562 | #[derive(Clone, Debug, Deserialize, Serialize)] 563 | pub struct MetadataRequest {} 564 | 565 | #[derive(Clone, Debug, Deserialize, Serialize)] 566 | pub struct NetworkListResponse { 567 | pub network_identifiers: Vec, 568 | } 569 | 570 | #[derive(Clone, Debug, Deserialize, Serialize)] 571 | pub struct NetworkOptionsResponse { 572 | pub version: Version, 573 | pub allow: Allow, 574 | } 575 | 576 | #[derive(Clone, Debug, Deserialize, Serialize)] 577 | pub struct NetworkRequest { 578 | pub network_identifier: NetworkIdentifier, 579 | } 580 | 581 | #[derive(Clone, Debug, Deserialize, Serialize)] 582 | pub struct NetworkStatusResponse { 583 | pub current_block_identifier: BlockIdentifier, 584 | pub current_block_timestamp: u64, 585 | pub genesis_block_identifier: BlockIdentifier, 586 | pub peers: Vec, 587 | } 588 | 589 | #[derive(Clone, Debug, Deserialize, Serialize)] 590 | pub struct TransactionIdentifierResponse { 591 | pub transaction_identifier: TransactionIdentifier, 592 | } 593 | 594 | // Miscellaneous 595 | 596 | #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] 597 | pub enum CurveType { 598 | #[serde(rename = "secp256k1")] 599 | Secp256k1, 600 | #[serde(rename = "secp256r1")] 601 | Secp256r1, 602 | #[serde(rename = "edwards25519")] 603 | Edwards25519, 604 | #[serde(rename = "tweedle")] 605 | Tweedle, 606 | } 607 | 608 | #[derive(Clone, Debug, Deserialize, Serialize)] 609 | pub struct OperationStatus { 610 | pub status: OperationStatusType, 611 | pub successful: bool, 612 | } 613 | 614 | #[derive(Clone, Debug, Deserialize, Serialize)] 615 | pub struct Peer { 616 | pub peer_id: String, 617 | } 618 | 619 | #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] 620 | pub enum SignatureType { 621 | #[serde(rename = "ecdsa")] 622 | ECDSA, 623 | #[serde(rename = "ecdsa_recovery")] 624 | ECDSARecovery, 625 | #[serde(rename = "ed25519")] 626 | Ed25519, 627 | } 628 | 629 | #[derive(Clone, Debug, Deserialize, Serialize)] 630 | pub struct Version { 631 | pub rosetta_version: String, 632 | pub node_version: String, 633 | pub middleware_version: String, 634 | } 635 | 636 | #[derive(Clone, Debug, Deserialize, Serialize)] 637 | pub struct MetadataOptions { 638 | pub internal_operations: Vec, 639 | pub with_nonce: Option, 640 | } 641 | 642 | #[derive(Clone, Debug, Deserialize, Serialize)] 643 | pub struct ErrorDetails { 644 | /// The detailed error 645 | pub error: String, 646 | } 647 | 648 | pub type OptionalInternalOperationMetadatas = Option>>; 649 | #[derive(Clone, Debug, Deserialize, Serialize)] 650 | pub struct ConstructionMetadata { 651 | pub blockhash: String, 652 | pub fee_calculator: FeeCalculator, 653 | pub internal_meta: OptionalInternalOperationMetadatas, 654 | pub with_nonce: Option, 655 | } 656 | #[derive(Clone, Debug, Deserialize, Serialize)] 657 | pub struct WithNonce { 658 | pub account: String, 659 | #[serde(skip_serializing_if = "Option::is_none")] 660 | pub authority: Option, 661 | } 662 | 663 | #[derive(Clone, Debug, Deserialize, Serialize)] 664 | pub struct FeaturesRequest { 665 | pub network_identifier: NetworkIdentifier, 666 | } 667 | #[derive(Clone, Debug, Deserialize, Serialize)] 668 | pub struct FeaturesResponse { 669 | pub features: Vec, 670 | } 671 | #[derive(Clone, Debug, Deserialize, Serialize)] 672 | pub struct Feature { 673 | pub name: String, 674 | pub funding_address: String, 675 | pub feature_proposal_address: String, 676 | pub mint_address: String, 677 | pub distributor_token_address: String, 678 | pub acceptance_token_address: String, 679 | pub feature_id_address: String, 680 | pub tokens_to_mint: u64, 681 | pub acceptance_criteria: AcceptanceCriteria, 682 | } 683 | #[derive(Clone, Default, Debug, Deserialize, Serialize)] 684 | pub struct AcceptanceCriteria { 685 | /// The balance of the feature proposal's token account must be greater than this amount, and 686 | /// tallied before the deadline for the feature to be accepted. 687 | pub tokens_required: u64, 688 | 689 | /// If the required tokens are not tallied by this deadline then the proposal will expire. 690 | pub deadline: UnixTimestamp, 691 | } 692 | -------------------------------------------------------------------------------- /src/construction.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | 3 | use crate::{ 4 | error::ApiError, 5 | is_bad_network, 6 | operations::get_tx_from_str, 7 | operations::matcher::InternalOperationMetadata, 8 | operations::matcher::Matcher, 9 | operations::spltoken::SplTokenOperationMetadata, 10 | types::{OperationType, WithNonce}, 11 | utils::to_pub, 12 | Options, 13 | }; 14 | use crate::{ 15 | operations::utils::get_operations_from_encoded_tx, 16 | types::{ 17 | AccountIdentifier, ConstructionCombineRequest, ConstructionCombineResponse, 18 | ConstructionDeriveRequest, ConstructionDeriveResponse, ConstructionHashRequest, 19 | ConstructionMetadata, ConstructionMetadataRequest, ConstructionMetadataResponse, 20 | ConstructionParseRequest, ConstructionParseResponse, ConstructionPayloadsRequest, 21 | ConstructionPayloadsResponse, ConstructionPreprocessRequest, 22 | ConstructionPreprocessResponse, ConstructionSubmitRequest, ConstructionSubmitResponse, 23 | CurveType, MetadataOptions, SignatureType, SigningPayload, TransactionIdentifier, 24 | TransactionIdentifierResponse, 25 | }, 26 | }; 27 | use rocket_contrib::json::Json; 28 | use serde::{Deserialize, Serialize}; 29 | use solana_account_decoder::{UiAccount, UiAccountData, UiAccountEncoding, UiFeeCalculator}; 30 | use solana_sdk::{ 31 | fee_calculator::FeeCalculator, hash::Hash, message::Message, program_pack::Pack, 32 | pubkey::Pubkey, signature::Signature, transaction::Transaction, 33 | }; 34 | use solana_transaction_status::{EncodedTransaction, UiMessage, UiTransactionEncoding}; 35 | use spl_token::state::{Account, Mint}; 36 | 37 | pub fn construction_derive( 38 | construction_derive_request: ConstructionDeriveRequest, 39 | options: &Options, 40 | ) -> Result, ApiError> { 41 | is_bad_network(&options, &construction_derive_request.network_identifier)?; 42 | 43 | if construction_derive_request.public_key.curve_type != CurveType::Edwards25519 { 44 | return Err(ApiError::UnsupportedCurve); 45 | }; 46 | let hex_pubkey = hex::decode(&construction_derive_request.public_key.hex_bytes)?; 47 | let bs58_pubkey = bs58::encode(hex_pubkey).into_string(); 48 | 49 | let response = ConstructionDeriveResponse { 50 | account_identifier: AccountIdentifier { 51 | address: bs58_pubkey, 52 | sub_account: None, 53 | }, 54 | }; 55 | Ok(Json(response)) 56 | } 57 | pub fn construction_hash( 58 | construction_hash_request: ConstructionHashRequest, 59 | options: &Options, 60 | ) -> Result, ApiError> { 61 | is_bad_network(&options, &construction_hash_request.network_identifier)?; 62 | 63 | let tx = get_tx_from_str(&construction_hash_request.signed_transaction)?; 64 | let response = TransactionIdentifierResponse { 65 | transaction_identifier: TransactionIdentifier { 66 | hash: tx.signatures[0].to_string(), 67 | }, 68 | }; 69 | Ok(Json(response)) 70 | } 71 | //Create Metadata Request to send to construction/metadata 72 | pub fn construction_preprocess( 73 | construction_preprocess_request: ConstructionPreprocessRequest, 74 | options: &Options, 75 | ) -> Result, ApiError> { 76 | is_bad_network( 77 | &options, 78 | &construction_preprocess_request.network_identifier, 79 | )?; 80 | 81 | let mut matcher = Matcher::new(&construction_preprocess_request.operations, None); 82 | let internal_operations = matcher.combine()?; 83 | 84 | let with_nonce = if let Some(x) = construction_preprocess_request.metadata { 85 | x.with_nonce 86 | } else { 87 | None 88 | }; 89 | let response = ConstructionPreprocessResponse { 90 | options: Some(MetadataOptions { 91 | internal_operations, 92 | with_nonce, 93 | }), 94 | }; 95 | Ok(Json(response)) 96 | } 97 | //Get recent blockhash and other metadata 98 | 99 | pub fn construction_metadata( 100 | construction_metadata_request: ConstructionMetadataRequest, 101 | options: &Options, 102 | ) -> Result, ApiError> { 103 | is_bad_network(&options, &construction_metadata_request.network_identifier)?; 104 | #[serde(rename_all = "camelCase")] 105 | #[derive(Default, Serialize, Deserialize, Clone, Debug)] 106 | struct Info { 107 | authority: String, 108 | blockhash: String, 109 | fee_calculator: UiFeeCalculator, 110 | } 111 | #[derive(Default, Serialize, Deserialize, Clone, Debug)] 112 | struct Parsed { 113 | info: Info, 114 | } 115 | //optional metadata for some special types 116 | let mut with_nonce = None; 117 | let mut parsed = Parsed::default(); 118 | let internal_meta = if let Some(x) = &construction_metadata_request.options { 119 | if let Some(n) = &x.with_nonce { 120 | let pubkey = &to_pub(&n.account); 121 | let acc = options.rpc.get_account(pubkey)?; 122 | let uiacc = UiAccount::encode(pubkey, acc, UiAccountEncoding::JsonParsed, None, None); 123 | if let UiAccountData::Json(parsed_acc) = uiacc.data { 124 | parsed = serde_json::from_value::(parsed_acc.parsed).unwrap(); 125 | with_nonce = Some(WithNonce { 126 | account: n.account.clone(), 127 | authority: Some(parsed.info.authority), 128 | }); 129 | } 130 | }; 131 | let ops = x 132 | .internal_operations 133 | .iter() 134 | .map(|x| match x.type_ { 135 | //TODO: Add more metadata as required 136 | OperationType::SplToken__CreateAccount => { 137 | let rent = options 138 | .rpc 139 | .get_minimum_balance_for_rent_exemption(Account::LEN) 140 | .unwrap(); 141 | Some(InternalOperationMetadata::SplToken( 142 | SplTokenOperationMetadata { 143 | amount: Some(rent), 144 | ..Default::default() 145 | }, 146 | )) 147 | } 148 | OperationType::SplToken__CreateToken => { 149 | let rent = options 150 | .rpc 151 | .get_minimum_balance_for_rent_exemption(Mint::LEN) 152 | .unwrap(); 153 | Some(InternalOperationMetadata::SplToken( 154 | SplTokenOperationMetadata { 155 | amount: Some(rent), 156 | ..Default::default() 157 | }, 158 | )) 159 | } 160 | _ => None, 161 | }) 162 | .collect::>>(); 163 | Some(ops) 164 | } else { 165 | None 166 | }; 167 | //required metadata 168 | let (hash, fee_calculator) = if with_nonce.is_none() { 169 | options.rpc.get_recent_blockhash()? 170 | } else { 171 | ( 172 | Hash::from_str(&parsed.info.blockhash).unwrap(), 173 | FeeCalculator::new( 174 | parsed 175 | .info 176 | .fee_calculator 177 | .lamports_per_signature 178 | .parse::() 179 | .unwrap(), 180 | ), 181 | ) 182 | }; 183 | let response = ConstructionMetadataResponse { 184 | metadata: ConstructionMetadata { 185 | blockhash: hash.to_string(), 186 | fee_calculator, 187 | internal_meta, 188 | with_nonce, 189 | }, 190 | }; 191 | Ok(Json(response)) 192 | } 193 | //Construct Payloads to Sign 194 | 195 | pub fn construction_payloads( 196 | construction_payloads_request: ConstructionPayloadsRequest, 197 | options: &Options, 198 | ) -> Result, ApiError> { 199 | is_bad_network(&options, &construction_payloads_request.network_identifier)?; 200 | 201 | let mut with_nonce = None; 202 | let meta = if let Some(x) = &construction_payloads_request.metadata { 203 | with_nonce = x.with_nonce.clone(); 204 | if let Some(x) = &x.internal_meta { 205 | Some(x.clone()) 206 | } else { 207 | None 208 | } 209 | } else { 210 | None 211 | }; 212 | let mut matcher = Matcher::new(&construction_payloads_request.operations, meta); 213 | let instructions = matcher.to_instructions()?; 214 | let mut fee_payer = None; 215 | let mut fee_payer_pub = None; 216 | instructions.iter().for_each(|x| { 217 | if let Some(y) = x.accounts.iter().find(|a| a.is_signer) { 218 | fee_payer_pub = Some(y.pubkey.clone()); 219 | } 220 | }); 221 | if let Some(x) = &fee_payer_pub { 222 | fee_payer = Some(x); 223 | }; 224 | 225 | let msg = if let Some(x) = with_nonce { 226 | Message::new_with_nonce( 227 | instructions, 228 | fee_payer, 229 | &to_pub(&x.account), 230 | &to_pub(&x.authority.unwrap()), 231 | ) 232 | } else { 233 | Message::new(&instructions, fee_payer) 234 | }; 235 | let mut tx = Transaction::new_unsigned(msg); 236 | //recent_blockhash is required as metadata 237 | if let Some(x) = &construction_payloads_request.metadata { 238 | let h = bs58::decode(&x.blockhash).into_vec().unwrap(); 239 | tx.message.recent_blockhash = Hash::new(&h); 240 | } else { 241 | return Err(ApiError::BadTransactionPayload); 242 | } 243 | 244 | let v = bincode::serialize(&tx); 245 | if v.is_err() { 246 | return Err(ApiError::BadTransactionPayload); 247 | } 248 | let unsigned_transaction = bs58::encode(v.unwrap()).into_string(); 249 | let to_be_signed = hex::encode(tx.message.serialize()); 250 | let signing_payloads = tx 251 | .message 252 | .account_keys 253 | .iter() 254 | .enumerate() 255 | .map(|(i, pubk)| { 256 | if tx.message.is_signer(i) { 257 | Some(SigningPayload { 258 | account_identifier: Some(AccountIdentifier { 259 | address: bs58::encode(pubk.to_bytes()).into_string(), 260 | sub_account: None, 261 | }), 262 | hex_bytes: to_be_signed.clone(), 263 | signature_type: Some(SignatureType::Ed25519), 264 | }) 265 | } else { 266 | None 267 | } 268 | }) 269 | .take_while(|e| e.is_some()) 270 | .collect::>>(); 271 | 272 | let response = ConstructionPayloadsResponse { 273 | unsigned_transaction, 274 | payloads: signing_payloads, 275 | }; 276 | Ok(Json(response)) 277 | } 278 | 279 | //Parse Unsigned Transaction to to Confirm Correctness 280 | 281 | pub fn construction_parse( 282 | construction_parse_request: ConstructionParseRequest, 283 | options: &Options, 284 | ) -> Result, ApiError> { 285 | is_bad_network(&options, &construction_parse_request.network_identifier)?; 286 | 287 | let tx = get_tx_from_str(&construction_parse_request.transaction)?; 288 | let encoded_tx = EncodedTransaction::encode(tx, UiTransactionEncoding::JsonParsed); 289 | let mut signers: Vec = vec![]; 290 | if construction_parse_request.signed { 291 | if let EncodedTransaction::Json(t) = &encoded_tx { 292 | if let UiMessage::Parsed(m) = &t.message { 293 | m.account_keys.iter().for_each(|x| { 294 | if x.signer == true { 295 | signers.push(AccountIdentifier { 296 | address: x.pubkey.to_string(), 297 | sub_account: None, 298 | }); 299 | } 300 | }); 301 | } 302 | } 303 | } 304 | let account_identifier_signers = if signers.len() == 0 { 305 | None 306 | } else { 307 | Some(signers) 308 | }; 309 | let (operations, _) = get_operations_from_encoded_tx(&encoded_tx, None); 310 | let response = ConstructionParseResponse { 311 | operations: operations, 312 | account_identifier_signers, 313 | }; 314 | Ok(Json(response)) 315 | } 316 | 317 | //combine sign 318 | 319 | pub fn construction_combine( 320 | construction_combine_request: ConstructionCombineRequest, 321 | options: &Options, 322 | ) -> Result, ApiError> { 323 | is_bad_network(&options, &construction_combine_request.network_identifier)?; 324 | 325 | let mut tx = get_tx_from_str(&construction_combine_request.unsigned_transaction)?; 326 | let pubkeys = construction_combine_request 327 | .signatures 328 | .iter() 329 | .map(|x| { 330 | let p = hex::decode(&x.public_key.hex_bytes).unwrap(); 331 | Pubkey::new(&p) 332 | }) 333 | .collect::>(); 334 | let positions = tx 335 | .get_signing_keypair_positions(pubkeys.as_slice()) 336 | .unwrap(); 337 | for i in 0..positions.len() { 338 | tx.signatures[positions[i].unwrap()] = Signature::new(&hex::decode( 339 | &construction_combine_request.signatures[i].hex_bytes, 340 | )?); 341 | } 342 | let v = bincode::serialize(&tx); 343 | if v.is_err() { 344 | return Err(ApiError::BadTransactionPayload); 345 | } 346 | let response = ConstructionCombineResponse { 347 | signed_transaction: bs58::encode(v.unwrap()).into_string(), 348 | }; 349 | Ok(Json(response)) 350 | } 351 | 352 | //broadcast signed tx 353 | 354 | pub fn construction_submit( 355 | construction_submit_request: ConstructionSubmitRequest, 356 | options: &Options, 357 | ) -> Result, ApiError> { 358 | is_bad_network(&options, &construction_submit_request.network_identifier)?; 359 | let tx = get_tx_from_str(&construction_submit_request.signed_transaction)?; 360 | let signatureres = options.rpc.send_transaction(&tx); 361 | let signature = signatureres?; 362 | 363 | let response = ConstructionSubmitResponse { 364 | transaction_identifier: TransactionIdentifier { 365 | hash: signature.to_string(), 366 | }, 367 | }; 368 | Ok(Json(response)) 369 | } 370 | #[cfg(test)] 371 | mod tests { 372 | use std::{thread, time::Duration}; 373 | 374 | use ed25519_dalek::*; 375 | use serde_json::json; 376 | 377 | use crate::{consts, create_rpc_client, types::*}; 378 | 379 | //live debug tests on devnet 380 | //TODO: remove hardcoded keys 381 | 382 | use super::*; 383 | 384 | fn source() -> String { 385 | "HJGPMwVuqrbm7BDMeA3shLkqdHUru337fgytM7HzqTnH".to_string() 386 | } 387 | fn dest() -> String { 388 | "CgVKbBwogjaqtGtPLkMBSkhwtkTMLVdSdHM5cWzyxT5n".to_string() 389 | } 390 | 391 | fn main_account_keypair() -> Keypair { 392 | let privkey = 393 | hex::decode("cb1a134c296fbf309d78fe9378c18bc129e5045fbe92d2ad8577ccc84689d4ef") 394 | .unwrap(); 395 | let public = 396 | hex::decode("f22742d48ce6eeb0c062237b04a5b7f57bfeb8803e9287cd8a112320860e307a") 397 | .unwrap(); 398 | 399 | let secret = ed25519_dalek::SecretKey::from_bytes(&privkey).unwrap(); 400 | let pubkey = ed25519_dalek::PublicKey::from_bytes(&public).unwrap(); 401 | let keypair = ed25519_dalek::Keypair { 402 | secret: secret, 403 | public: pubkey, 404 | }; 405 | keypair 406 | } 407 | 408 | #[test] 409 | #[ignore] 410 | fn test_token_bulk() { 411 | let (k, p) = new_throwaway_signer(); 412 | let (k2, p2) = new_throwaway_signer(); 413 | 414 | let parsed = constructions_pipe( 415 | vec![ 416 | Operation { 417 | operation_identifier: OperationIdentifier { 418 | index: 0, 419 | network_index: None, 420 | }, 421 | related_operations: None, 422 | status: None, 423 | account: None, 424 | amount: None, 425 | type_: OperationType::SplToken__CreateToken, 426 | metadata: Some(json!({ 427 | "mint": p.to_string(), 428 | "source": source() 429 | })), 430 | }, 431 | Operation { 432 | operation_identifier: OperationIdentifier { 433 | index: 1, 434 | network_index: None, 435 | }, 436 | related_operations: None, 437 | status: None, 438 | account: None, 439 | amount: None, 440 | type_: OperationType::SplToken__CreateAccount, 441 | metadata: Some(json!({ 442 | "mint": p.to_string(), 443 | "source": source(), 444 | "destination": p2.to_string(), 445 | })), 446 | }, 447 | Operation { 448 | operation_identifier: OperationIdentifier { 449 | index: 2, 450 | network_index: None, 451 | }, 452 | related_operations: None, 453 | status: None, 454 | account: None, 455 | amount: None, 456 | type_: OperationType::SplToken__MintTo, 457 | metadata: Some(json!({ 458 | "mint": p.to_string(), 459 | "source": p2.to_string(), 460 | "authority": source(), 461 | "amount": 1000, 462 | })), 463 | }, 464 | ], 465 | vec![&main_account_keypair(), &k, &k2], 466 | None, 467 | ); 468 | } 469 | #[test] 470 | #[ignore] 471 | fn test_construction_transfer() { 472 | let parsed = constructions_pipe( 473 | vec![ 474 | Operation { 475 | operation_identifier: OperationIdentifier { 476 | index: 0, 477 | network_index: None, 478 | }, 479 | related_operations: None, 480 | status: None, 481 | account: None, 482 | amount: None, 483 | type_: OperationType::System__Transfer, 484 | metadata: Some(json!({ 485 | "source": source(), 486 | "destination": dest(), 487 | "lamports": 10000, 488 | })), 489 | }, 490 | Operation { 491 | operation_identifier: OperationIdentifier { 492 | index: 1, 493 | network_index: None, 494 | }, 495 | related_operations: None, 496 | status: None, 497 | account: None, 498 | amount: None, 499 | type_: OperationType::System__Transfer, 500 | metadata: Some(json!({ 501 | "source": source(), 502 | "destination": dest(), 503 | "lamports": 10000, 504 | })), 505 | }, 506 | ], 507 | vec![&main_account_keypair()], 508 | None, 509 | ); 510 | } 511 | #[test] 512 | #[ignore] 513 | fn test_token_transfer_rosetta_style() { 514 | let rpc = create_rpc_client("https://devnet.solana.com".to_string()); 515 | let parsed = constructions_pipe( 516 | vec![ 517 | Operation { 518 | operation_identifier: OperationIdentifier { 519 | index: 10, 520 | network_index: None, 521 | }, 522 | related_operations: None, 523 | status: None, 524 | account: Some(AccountIdentifier { 525 | address: "95Dq3sXa3omVjiyxBSD6UMrzPYdmyu6CFCw5wS4rhqgV".to_string(), 526 | sub_account: None, 527 | }), 528 | amount: Some(Amount { 529 | value: "-0.01".to_string(), 530 | currency: Currency { 531 | symbol: "3fJRYbtSYZo9SYhwgUBn2zjG98ASy3kuUEnZeHJXqREr".to_string(), 532 | decimals: 2, 533 | metadata: None, 534 | }, 535 | }), 536 | type_: OperationType::SplToken__Transfer, 537 | metadata: Some(json!({ 538 | "authority": source(), 539 | })), 540 | }, 541 | Operation { 542 | operation_identifier: OperationIdentifier { 543 | index: 11, 544 | network_index: None, 545 | }, 546 | related_operations: None, 547 | status: None, 548 | account: Some(AccountIdentifier { 549 | address: "GyUjMMeZH3PVXp4tk5sR8LgnVaLTvCPipQ3dQY74k75L".to_string(), 550 | sub_account: None, 551 | }), 552 | amount: Some(Amount { 553 | value: "0.01".to_string(), 554 | currency: Currency { 555 | symbol: "3fJRYbtSYZo9SYhwgUBn2zjG98ASy3kuUEnZeHJXqREr".to_string(), 556 | decimals: 2, 557 | metadata: None, 558 | }, 559 | }), 560 | type_: OperationType::SplToken__Transfer, 561 | metadata: Some(json!({ 562 | "authority": source(), 563 | })), 564 | }, 565 | ], 566 | vec![&main_account_keypair()], 567 | None, 568 | ); 569 | } 570 | 571 | #[test] 572 | #[ignore] 573 | fn test_nonce_accounts() { 574 | let (k, p) = new_throwaway_signer(); 575 | let parsed = constructions_pipe( 576 | vec![Operation { 577 | operation_identifier: OperationIdentifier { 578 | index: 0, 579 | network_index: None, 580 | }, 581 | related_operations: None, 582 | status: None, 583 | account: None, 584 | amount: None, 585 | type_: OperationType::System__CreateNonceAccount, 586 | metadata: Some(json!({ 587 | "source": source(), 588 | "destination": p.to_string() 589 | })), 590 | }], 591 | vec![&main_account_keypair(), &k], 592 | None, 593 | ); 594 | thread::sleep(Duration::from_secs(20)); 595 | let parsed = constructions_pipe( 596 | vec![Operation { 597 | operation_identifier: OperationIdentifier { 598 | index: 1, 599 | network_index: None, 600 | }, 601 | related_operations: None, 602 | status: None, 603 | account: None, 604 | amount: None, 605 | type_: OperationType::System__Transfer, 606 | metadata: Some(json!({ 607 | "source": source(), 608 | "destination": dest(), 609 | "lamports": 1000, 610 | })), 611 | }], 612 | vec![&main_account_keypair()], 613 | Some(p.to_string()), 614 | ); 615 | thread::sleep(Duration::from_secs(20)); 616 | let parsed = constructions_pipe( 617 | vec![Operation { 618 | operation_identifier: OperationIdentifier { 619 | index: 1, 620 | network_index: None, 621 | }, 622 | related_operations: None, 623 | status: None, 624 | account: None, 625 | amount: None, 626 | type_: OperationType::System__WithdrawFromNonce, 627 | metadata: Some(json!({ 628 | "source": p.to_string(), 629 | "destination": source(), 630 | "lamports": 1000, 631 | })), 632 | }], 633 | vec![&main_account_keypair(), &k], 634 | Some(p.to_string()), 635 | ); 636 | } 637 | 638 | #[test] 639 | #[ignore] 640 | fn test_stake_accounts() { 641 | let (k, p) = new_throwaway_signer(); 642 | let (k2, p2) = new_throwaway_signer(); 643 | 644 | let parsed = constructions_pipe( 645 | vec![ 646 | Operation { 647 | operation_identifier: OperationIdentifier { 648 | index: 1, 649 | network_index: None, 650 | }, 651 | related_operations: None, 652 | status: None, 653 | account: None, 654 | amount: None, 655 | type_: OperationType::Stake__CreateAccount, 656 | metadata: Some(json!({ 657 | "source": source(), 658 | "lamports": 1000000000, 659 | "lockup": { 660 | "epoch": 0, 661 | "unix_timestamp": 0, 662 | "custodian": source(), 663 | }, 664 | "destination": p.to_string() 665 | })), 666 | }, 667 | Operation { 668 | operation_identifier: OperationIdentifier { 669 | index: 2, 670 | network_index: None, 671 | }, 672 | related_operations: None, 673 | status: None, 674 | account: None, 675 | amount: None, 676 | type_: OperationType::Stake__Delegate, 677 | metadata: Some(json!({ 678 | "source": source(), 679 | "destination": p.to_string(), 680 | "vote_pubkey": "5MMCR4NbTZqjthjLGywmeT66iwE9J9f7kjtxzJjwfUx2".to_string() 681 | })), 682 | }, 683 | Operation { 684 | operation_identifier: OperationIdentifier { 685 | index: 3, 686 | network_index: None, 687 | }, 688 | related_operations: None, 689 | status: None, 690 | account: None, 691 | amount: None, 692 | type_: OperationType::Stake__Split, 693 | metadata: Some(json!({ 694 | "source": p.to_string(), 695 | "authority": source(), 696 | "lamports": 500000000, 697 | "destination": p2.to_string() 698 | })), 699 | }, 700 | Operation { 701 | operation_identifier: OperationIdentifier { 702 | index: 4, 703 | network_index: None, 704 | }, 705 | related_operations: None, 706 | status: None, 707 | account: None, 708 | amount: None, 709 | type_: OperationType::Stake__Merge, 710 | metadata: Some(json!({ 711 | "source": p2.to_string(), 712 | "authority": source(), 713 | "destination": p.to_string() 714 | })), 715 | }, 716 | Operation { 717 | operation_identifier: OperationIdentifier { 718 | index: 5, 719 | network_index: None, 720 | }, 721 | related_operations: None, 722 | status: None, 723 | account: None, 724 | amount: None, 725 | type_: OperationType::Stake__SetLockup, 726 | metadata: Some(json!({ 727 | "stake_pubkey": p.to_string(), 728 | "source": source(), 729 | "lockup": { 730 | "epoch": 420, 731 | } 732 | })), 733 | }, 734 | Operation { 735 | operation_identifier: OperationIdentifier { 736 | index: 5, 737 | network_index: None, 738 | }, 739 | related_operations: None, 740 | status: None, 741 | account: None, 742 | amount: None, 743 | type_: OperationType::Stake__Authorize, 744 | metadata: Some(json!({ 745 | "staker": p2.to_string(), 746 | "withdrawer": p2.to_string(), 747 | "source": source(), 748 | "stake_pubkey": p.to_string() 749 | })), 750 | }, 751 | ], 752 | vec![&main_account_keypair(), &k, &k2], 753 | None, 754 | ); 755 | } 756 | #[test] 757 | #[ignore] 758 | fn stake_withdraw_deactivate() { 759 | let parsed = constructions_pipe( 760 | vec![ 761 | /* 762 | Operation { 763 | operation_identifier: OperationIdentifier { 764 | index: 6, 765 | network_index: None, 766 | }, 767 | related_operations: None, 768 | status: None, 769 | account: None, 770 | amount: None, 771 | type_: OperationType::Stake__Deactivate, 772 | metadata: Some(json!({ 773 | "source": source(), 774 | "destination": "7pLKwSRmAR3pN3PkBnssm142Pg4Daj86WkWrnGC3Uh7h".to_string() 775 | })), 776 | },*/ 777 | Operation { 778 | operation_identifier: OperationIdentifier { 779 | index: 6, 780 | network_index: None, 781 | }, 782 | related_operations: None, 783 | status: None, 784 | account: None, 785 | amount: None, 786 | type_: OperationType::Stake__Withdraw, 787 | metadata: Some(json!({ 788 | "source": "7pLKwSRmAR3pN3PkBnssm142Pg4Daj86WkWrnGC3Uh7h".to_string(), 789 | "withdrawer": source(), 790 | "destination": source(), 791 | "lamports": 350000 792 | })), 793 | }, 794 | ], 795 | vec![&main_account_keypair()], 796 | None, 797 | ); 798 | } 799 | 800 | #[test] 801 | #[ignore] 802 | fn stake_setlockup() { 803 | let parsed = constructions_pipe( 804 | vec![Operation { 805 | operation_identifier: OperationIdentifier { 806 | index: 6, 807 | network_index: None, 808 | }, 809 | related_operations: None, 810 | status: None, 811 | account: None, 812 | amount: None, 813 | type_: OperationType::Stake__SetLockup, 814 | metadata: Some(json!({ 815 | "destination": "7pLKwSRmAR3pN3PkBnssm142Pg4Daj86WkWrnGC3Uh7h".to_string(), 816 | "source": source(), 817 | "lockup": { 818 | "epoch": 0, 819 | "unix_timestamp": 100, 820 | "custodian": source(), 821 | } 822 | })), 823 | }], 824 | vec![&main_account_keypair()], 825 | None, 826 | ); 827 | } 828 | #[test] 829 | #[ignore] 830 | fn test_token_transfer() { 831 | let rpc = create_rpc_client("https://devnet.solana.com".to_string()); 832 | let parsed = constructions_pipe( 833 | vec![Operation { 834 | operation_identifier: OperationIdentifier { 835 | index: 0, 836 | network_index: None, 837 | }, 838 | related_operations: None, 839 | status: None, 840 | account: None, 841 | amount: None, 842 | type_: OperationType::SplToken__Transfer, 843 | metadata: Some(json!({ 844 | "authority": source(), 845 | "source": "95Dq3sXa3omVjiyxBSD6UMrzPYdmyu6CFCw5wS4rhqgV", 846 | "destination": "GyUjMMeZH3PVXp4tk5sR8LgnVaLTvCPipQ3dQY74k75L", 847 | "amount": "10", 848 | "decimals": 2, 849 | "mint": "3fJRYbtSYZo9SYhwgUBn2zjG98ASy3kuUEnZeHJXqREr", 850 | })), 851 | }], 852 | vec![&main_account_keypair()], 853 | None, 854 | ); 855 | } 856 | 857 | fn new_throwaway_signer() -> (Keypair, solana_sdk::pubkey::Pubkey) { 858 | let keypair = solana_sdk::signature::Keypair::new(); 859 | let pubkey = solana_sdk::signature::Signer::pubkey(&keypair); 860 | ( 861 | ed25519_dalek::Keypair::from_bytes(&keypair.to_bytes()).unwrap(), 862 | pubkey, 863 | ) 864 | } 865 | #[test] 866 | #[ignore] 867 | fn test_construction_create_assoc_acc() { 868 | //wont create anymore coz already created change mint address 869 | 870 | let parsed = constructions_pipe( 871 | vec![Operation { 872 | operation_identifier: OperationIdentifier { 873 | index: 0, 874 | network_index: None, 875 | }, 876 | related_operations: None, 877 | status: None, 878 | account: None, 879 | amount: None, 880 | type_: OperationType::SplToken__CreateAssocAccount, 881 | metadata: Some(json!({ 882 | "source": source(), 883 | "mint": "3fJRYbtSYZo9SYhwgUBn2zjG98ASy3kuUEnZeHJXqREr".to_string(), 884 | })), 885 | }], 886 | vec![], 887 | None, 888 | ); 889 | } 890 | 891 | fn constructions_pipe( 892 | operations: Vec, 893 | mut keypairs: Vec<&Keypair>, 894 | nonce: Option, 895 | ) -> ConstructionParseResponse { 896 | let rpc = create_rpc_client("https://devnet.solana.com".to_string()); 897 | 898 | let options = Options { 899 | rpc: rpc, 900 | network: "devnet".to_string(), 901 | }; 902 | let network_identifier = NetworkIdentifier { 903 | blockchain: consts::BLOCKCHAIN.to_string(), 904 | network: "devnet".to_string(), 905 | sub_network_identifier: None, 906 | }; 907 | let prepmeta = if let Some(x) = nonce { 908 | Some(ConstructionPreprocessRequestMetadata { 909 | with_nonce: Some(WithNonce { 910 | account: x, 911 | authority: None, 912 | }), 913 | }) 914 | } else { 915 | None 916 | }; 917 | let preproess = construction_preprocess( 918 | ConstructionPreprocessRequest { 919 | network_identifier: network_identifier.clone(), 920 | operations: operations.clone(), 921 | metadata: prepmeta, 922 | }, 923 | &options, 924 | ) 925 | .unwrap(); 926 | println!("Preproess {:?} \n\n", &preproess.options); 927 | 928 | let metadata = construction_metadata( 929 | ConstructionMetadataRequest { 930 | network_identifier: network_identifier.clone(), 931 | options: preproess.into_inner().options, 932 | }, 933 | &options, 934 | ) 935 | .unwrap(); 936 | println!("Metadata {:?} \n\n", metadata); 937 | let payloads = construction_payloads( 938 | ConstructionPayloadsRequest { 939 | network_identifier: network_identifier.clone(), 940 | operations: operations, 941 | metadata: Some(metadata.into_inner().metadata), 942 | }, 943 | &options, 944 | ) 945 | .unwrap(); 946 | println!("Payloads {:?} \n\n", payloads); 947 | let parsed = construction_parse( 948 | ConstructionParseRequest { 949 | network_identifier: network_identifier.clone(), 950 | signed: false, 951 | transaction: payloads.clone().unsigned_transaction, 952 | }, 953 | &options, 954 | ) 955 | .unwrap(); 956 | println!("Parsed {:?} \n\n", parsed); 957 | let signatures = payloads 958 | .clone() 959 | .payloads 960 | .iter() 961 | .enumerate() 962 | .map(|(i, y)| { 963 | let x = y.clone().unwrap(); 964 | crate::types::Signature { 965 | signing_payload: SigningPayload { 966 | hex_bytes: x.hex_bytes.clone(), 967 | account_identifier: None, 968 | signature_type: Some(SignatureType::Ed25519), 969 | }, 970 | public_key: crate::types::PublicKey { 971 | hex_bytes: hex::encode(&keypairs[i].public.as_bytes()), 972 | curve_type: CurveType::Edwards25519, 973 | }, 974 | signature_type: SignatureType::Ed25519, 975 | hex_bytes: sign_msg(&keypairs[i], &x.hex_bytes), 976 | } 977 | }) 978 | .collect::>(); 979 | println!("Signatures {:?} \n\n", signatures); 980 | let combined = construction_combine( 981 | ConstructionCombineRequest { 982 | network_identifier: network_identifier.clone(), 983 | unsigned_transaction: payloads.clone().unsigned_transaction, 984 | signatures: signatures, 985 | }, 986 | &options, 987 | ) 988 | .unwrap(); 989 | println!("Signed TX: {:?} \n\n", combined.signed_transaction.clone()); 990 | 991 | let submited = construction_submit( 992 | ConstructionSubmitRequest { 993 | network_identifier: network_identifier.clone(), 994 | signed_transaction: combined.signed_transaction.clone(), 995 | }, 996 | &options, 997 | ); 998 | println!( 999 | "Broadcasted TX Hash: {:?} \n\n", 1000 | submited.unwrap().clone().transaction_identifier.hash 1001 | ); 1002 | return parsed.into_inner(); 1003 | } 1004 | fn sign_msg(keypair: &Keypair, s: &str) -> String { 1005 | let msg = hex::decode(s).unwrap(); 1006 | let signature = keypair.sign(&msg); 1007 | hex::encode(signature.to_bytes()) 1008 | } 1009 | } 1010 | --------------------------------------------------------------------------------