diff --git a/Cargo.toml b/Cargo.toml index beda717..f5ad87c 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "mai3-pumpfun-sdk" +name = "pumpfun-sdk" version = "2.4.3" edition = "2021" authors = ["William "] @@ -13,15 +13,23 @@ readme = "README.md" crate-type = ["cdylib", "rlib"] [dependencies] -solana-sdk = "2.1.13" -solana-client = "2.1.13" -solana-transaction-status = "2.1.13" -solana-rpc-client = "2.1.13" -solana-rpc-client-api = "2.1.13" -solana-program = "2.1.13" -spl-token = "7.0.0" +jito-protos = { path = "./jito_protos" } + +solana-sdk = "2.1.16" +solana-client = "2.1.16" +solana-program = "2.1.16" +solana-rpc-client = "2.1.16" +solana-rpc-client-api = "2.1.16" +solana-transaction-status = "2.1.16" +solana-account-decoder = "2.1.16" +solana-hash = "2.1.16" +solana-security-txt = "1.1.1" + +spl-token = "8.0.0" +spl-token-2022 = { version = "8.0.0", features = ["no-entrypoint"] } spl-associated-token-account = "6.0.0" mpl-token-metadata = "5.1.0" + borsh = { version = "1.5.3", features = ["derive"] } isahc = "1.7.2" serde = { version = "1.0.215", features = ["derive"] } @@ -33,12 +41,15 @@ bs58 = "0.5.1" rand = "0.9.0" bincode = "1.3.3" anyhow = "1.0.90" +yellowstone-grpc-client = { version = "6.0.0" } +yellowstone-grpc-proto = { version = "6.0.0" } reqwest = { version = "0.12.12", features = ["json", "multipart"] } -tonic = { version = "0.12.3", features = ["tls", "tls-webpki-roots", "tls-roots"] } tokio = { version = "1.42.0" , features = ["full", "rt-multi-thread"]} -yellowstone-grpc-client = { version = "5.0.0" } -yellowstone-grpc-proto = { version = "5.0.0" } -rustls = { version = "0.23.20", features = ["ring"] } +tonic = { version = "0.12.3", features = ["tls", "tls-roots", "tls-webpki-roots"] } +rustls = { version = "0.23.23", features = ["ring"] } +rustls-native-certs = "0.8.1" +tokio-rustls = "0.26.1" + dotenvy = "0.15.7" pretty_env_logger = "0.5.0" log = "0.4.22" @@ -49,5 +60,31 @@ thiserror = "2.0.11" async-trait = "0.1.86" lazy_static = "1.5.0" once_cell = "1.20.3" +prost = "0.13.5" +prost-types = "0.13.5" +arrform = { git = "https://github.com/raydium-io/arrform" } +num_enum = "0.7.3" +num-derive = "0.4.2" +num-traits = "0.2.19" +uint = "0.10.0" +clap = { version = "4.5.31", features = ["derive"] } + +hex = "0.4.3" +bytemuck = { version = "1.4.0" } +safe-transmute = "0.11.0" +enumflags2 = "0.6.4" +static_assertions = "1.1.0" +demand = "1.2.2" +arrayref = "0.3.6" +default-env = "0.1.1" + +borsh-derive = "1.5.5" +axum = { version = "0.8.1", features = ["macros"] } +tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } +tokio-tungstenite = { version = "0.26.1", features = ["native-tls"] } +indicatif = "0.17.11" +toml = "0.8.20" + + diff --git a/README.md b/README.md index e5f2a21..8da0445 100755 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ A comprehensive Rust SDK for seamless interaction with the PumpFun Solana progra ### logs subscription for token create and trade transaction ```rust -use mai3_pumpfun_sdk::common::{ +use pumpfun_sdk::common::{ logs_events::DexEvent, logs_subscribe::{tokens_subscription, stop_subscription} }; @@ -72,7 +72,7 @@ use solana_sdk::{ signer::Signer, }; -use mai3_pumpfun_sdk::{accounts::BondingCurveAccount, utils::CreateTokenMetadata, PriorityFee, PumpFun}; +use pumpfun_sdk::{accounts::BondingCurveAccount, utils::CreateTokenMetadata, PriorityFee, PumpFun}; // Create a new PumpFun client let payer: Keypair = Keypair::new(); diff --git a/jito_protos/Cargo.toml b/jito_protos/Cargo.toml new file mode 100755 index 0000000..49388cb --- /dev/null +++ b/jito_protos/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "jito-protos" +version = "0.1.0" +edition = "2021" +publish = false + +[dependencies] +bincode = "1.3.3" +bytes = "1.4.0" +prost = "0.13.5" +prost-types = "0.13.5" +solana-perf = "2.1.13" +solana-sdk = "2.1.13" +tonic = "0.12.3" + +[build-dependencies] +tonic-build = "0.12.3" diff --git a/jito_protos/README.md b/jito_protos/README.md new file mode 100755 index 0000000..a799930 --- /dev/null +++ b/jito_protos/README.md @@ -0,0 +1,56 @@ +# Schemas + +This repository contains schemas for [grpc](#grpc) and [json rpc](#json-rpc) endpoints for Jito Lab's MEV system. + +## grpc + +The below explains how to use the public protobuf definitions for Jito Lab's MEV system. + +### Usage + +Add this repo as a git submodule to your repo. Here's an example file tree in a Rust codebase: + +``` +your-rust-repo/ +├─ src/ +│ ├─ gm/ +│ │ ├─ lib.rs +│ ├─ jito-protos/ +│ │ ├─ protos/ +│ │ │ ├─ *.proto +| | |─ src/ +| | | |─ lib.rs +| | |─ build.rs +``` + +```rust +/// lib.rs + +pub mod proto_package { + tonic::include_proto!("proto_package.proto"); +} +``` + +```rust +/// build.rs + +use tonic_build::configure; + +fn main() { + configure() + .compile( + &[ + "protos/proto_package.proto", + ], + &["protos"], + ) + .unwrap(); +} + +``` + +If you are looking for inspiration, a sample client can be found at [searcher examples](https://github.com/jito-labs/searcher-examples) + +## json rpc + +[json rpc schema](json_rpc/http.md) explains how to use json rpc for Jito Lab's MEV system. diff --git a/jito_protos/build.rs b/jito_protos/build.rs new file mode 100755 index 0000000..0be6a00 --- /dev/null +++ b/jito_protos/build.rs @@ -0,0 +1,23 @@ + +fn main() { + tonic_build::configure() + .protoc_arg("--experimental_allow_proto3_optional") + .build_server(false) + .out_dir("src/grpc") + .compile_protos( + &[ + "protos/auth.proto", + "protos/block.proto", + "protos/block_engine.proto", + "protos/bundle.proto", + "protos/packet.proto", + "protos/relayer.proto", + "protos/searcher.proto", + "protos/shared.proto", + "protos/shredstream.proto", + "protos/trace_shred.proto", + ], + &["protos"], + ) + .unwrap(); +} diff --git a/jito_protos/json_rpc/http.md b/jito_protos/json_rpc/http.md new file mode 100755 index 0000000..b1b75e5 --- /dev/null +++ b/jito_protos/json_rpc/http.md @@ -0,0 +1,245 @@ +# JSON RPC HTTP Methods + +The block engine accepts HTTP requests using the [JSON-RPC 2.0](https://www.jsonrpc.org/specification) specification. + +## RPC HTTP Endpoint + +**Default port:** 443 e.g. [https://mainnet.block-engine.jito.wtf:443/api/v1/bundles](https://mainnet.block-engine.jito.wtf:443/api/v1/bundles), [https://{REGION}.mainnet.block-engine.jito.wtf:443/api/v1/bundles](https://${REGION}.mainnet.block-engine.jito.wtf:443/api/v1/bundles) + +## Request Formatting + +To make a JSON-RPC request, send an HTTP POST request with a `Content-Type: application/json` header. The JSON request data should contain 4 fields: + +- `jsonrpc`: `string` - set to "2.0" +- `id`: `number` - a unique client-generated identifying integer +- `method`: `string` - a string containing the method to be invoked +- `params`: `array` - a JSON array of ordered parameter values + +We follow the same request formatting as Solana json rpc requests. You can find the documentation [here](https://docs.solana.com/api/http#request-formatting) + +## Authorization + +In the short-term we don't require authentication to send the requests. + +If there are any changes to the authentication mechanism it would be updated in the document and communicated with all the stakeholders. + +## Definitions + +- Bundle: Bundles are a list of transactions that execute sequentially and atomically. “All or nothing” so to speak. This means that a user can send a bundle that contains multiple transactions and guarantee that they are all executed one after the other and the bundle succeeds only if all individual transactions succeed. +- Hash: A SHA-256 hash of a chunk of data. +- Pubkey: The public key of a Ed25519 key-pair. +- Tip Account: List of accounts to which a tip can be paid for processing the bundles. Clients submitting bundles must pay a tip for bundle processing. +- Transaction: A list of Solana instructions signed by a client keypair to authorize those actions. +- Signature: An Ed25519 signature of transaction's payload data including instructions. This can be used to identify transactions. + +## State Commitment + +The commitment describes how finalized a block is at that point in time. + +In descending order of commitment (most finalized to least finalized), these are the commitment levels: + +- `"finalized"` - the node will query the most recent block confirmed by supermajority + of the cluster as having reached maximum lockout, meaning the cluster has + recognized this block as finalized +- `"confirmed"` - the node will query the most recent block that has been voted on by supermajority of the cluster. + - It incorporates votes from gossip and replay. + - It does not count votes on descendants of a block, only direct votes on that block. + - This confirmation level also upholds "optimistic confirmation" guarantees in + release 1.3 and onwards. +- `"processed"` - the node will query its most recent block. Note that the block + may still be skipped by the cluster. + +Please refer to [configuring-state-commitment](https://docs.solana.com/api/http#configuring-state-commitment) for more details. + +#### RpcResponse Structure + +Many methods that take a commitment parameter return an RpcResponse JSON object comprised of two parts: + +- `context` : An RpcResponseContext JSON structure including a `slot` field at which the operation was evaluated. example: + + ```json + "context": { + "slot": 1 + }, + ``` + +- `value` : The value returned by the operation itself. example: + +```json + "value": [ + { + "bundle": { + "signatures": [ + "3Eq21vXNB5s86c62bVuUfTeaMif1N2kUqRPBmGRJhyTA", + "2nBhEBYYvfaAe16UMNqRHre4YNSskvuYgx3M6E4JP1oDYvZEJHvoPzyUidNgNX5r9sTyN1J9UxtbCXy2rqYcuyuv" + ] + }, + "slot": 1234, + "confirmationStatus": "finalized", + "err": "null" + } + ] +``` + +## JSON RPC API Reference + +## getTipAccounts + +Returns the tip accounts for tip payment for the bundles. + +### Parameters + +None + +### Result + +The result field will be a JSON object with the following fields: + +- `result`: `` - Tip accounts as a list of `strings` + +### Code sample + +#### Request + +```bash +curl https://mainnet.block-engine.jito.wtf:443/api/v1/bundles -X POST -H "Content-Type: application/json" -d ' +{ + "jsonrpc": "2.0", + "id": 1, + "method": "getTipAccounts", + "params": [] +} +' +``` + +#### Response + +```json +{ + "jsonrpc": "2.0", + "result": [ + "9n3d1K5YD2vECAbRFhFFGYNNjiXtHXJWn9F31t89vsAV", + "aTtUk2DHgLhKZRDjePq6eiHRKC1XXFMBiSUfQ2JNDbN", + "B1mrQSpdeMU9gCvkJ6VsXVVoYjRGkNA7TtjMyqxrhecH", + "9ttgPBBhRYFuQccdR1DSnb7hydsWANoDsV3P9kaGMCEh", + "4xgEmT58RwTNsF5xm2RMYCnR1EVukdK8a1i2qFjnJFu3", + "EoW3SUQap7ZeynXQ2QJ847aerhxbPVr843uMeTfc9dxM", + "E2eSqe33tuhAHKTrwky5uEjaVqnb2T9ns6nHHUrN8588", + "ARTtviJkLLt6cHGQDydfo1Wyk6M4VGZdKZ2ZhdnJL336" + ], + "id": 1 +} +``` + +## sendBundle + +Submits a bundled list of signed transaction(s) (base-58 encoded string) to the cluster for processing. The transactions will be atomically processed in order, meaning if any of the transactions fail, the entire bundle won’t be processed (all or nothing). This method does not alter the transaction in any way; it relays the bundle created by clients to the leader as-is. If the bundle is not set to expire before the next upcoming Jito-Solana leader, this method will immediately return a success response acknowledging that the bundle has been received with a bundle_id. This does not guarantee the bundle is processed or landed on-chain. For the bundle status regarding whether it landed or not, getBundleStatuses should be used with the bundle id + +Please note that a tip is necessary for the bundle to considered. A tip can be any instruction, top-level or CPI, that transfers SOL to one of the 8 tip accounts. Clients should make sure they have balance and state assertions that allow the tip to only go thru conditionally, especially if tipping as a separate tx. If the tip is low, there is a chance that the bundle does not get selected during the auction. You can get the tip accounts using [getTipAccounts](#gettipaccounts). Ideally select one of the accounts in random to reduce contention. + +### Parameters + +``: `required` Fully-signed Transactions, as encoded string (base-58) upto a maximum of 5. Please note that at this point, we don't support base-64 encoded transactions + +### Result + +The result field will be a JSON object with the following fields: + +- `result`: `` - A bundle id, used to identify the bundle. This is the Sha256 hashes of the bundle's tx signatures. + +### Code sample + +#### Request + +```bash +curl https://mainnet.block-engine.jito.wtf:443/api/v1/bundles -X POST -H "Content-Type: application/json" -d ' +{ + "jsonrpc": "2.0", + "id": 1, + "method": "sendBundle", + "params": [ + [ + "4VbvoRYXFaXzDBUYfMXP1irhMZ9XRE6F1keS8GbYzKxgdpEasZtRv6GXxbygPp3yBVeSR4wN9JEauSTnVTKjuq3ktM3JpMebYpdGxZWUttJv9N2DzxBm4vhySdq2hbu1LQX7WxS2xsHG6vNwVCjP33Z2ZLP7S5dZujcan1Xq5Z2HibbbK3M3LD59QVuczyK44Fe3k27kVQ43oRH5L7KgpUS1vBoqTd9ZTzC32H62WPHJeLrQiNkmSB668FivXBAfMg13Svgiu9E", + "6HZu11s3SDBz5ytDj1tyBuoeUnwa1wPoKvq6ffivmfhTGahe3xvGpizJkofHCeDn1UgPN8sLABueKE326aGLXkn5yQyrrpuRF9q1TPZqqBMzcDvoJS1khPBprxnXcxNhMUbV78cS2R8LrCU29wjYk5b4JpVtF23ys4ZBZoNZKmPekAW9odcPVXb9HoMnWvx8xwqd7GsVB56R343vAX6HGUMoiB1WgR9jznG655WiXQTff5gPsCP3QJFTXC7iYEYtrcA3dUeZ3q4YK9ipdYZsgAS9H46i9dhDP2Zx3" + ] + ] +} +' +``` + +#### Response + +```json +{ + "jsonrpc": "2.0", + "result": "2id3YC2jK9G5Wo2phDx4gJVAew8DcY5NAojnVuao8rkxwPYPe8cSwE5GzhEgJA2y8fVjDEo6iR6ykBvDxrTQrtpb", + "id": 1 +} +``` + +## getBundleStatuses + +Returns bundle statuses for submitted bundle(s). The behavior is similar to the solana rpc method [getSignatureStatuses](https://docs.solana.com/api/http#getsignaturestatuses). If the bundle_id is not found or if the bundle has not landed, we return null. If found and landed, we return the context information including the slot at which the request was made and result with the bundle_id(s) and the transactions with the slot and confirmation status. + +### Parameters + +- ``: `required` An array of bundle ids to confirm, as base-58 encoded strings (up to a maximum of 5). + +### Result + +An array of RpcResponse`` consisting of either: + +- ``: If the bundle is not found. +- ``: If the bundle is found, an array of objects with the following fields: + - `bundle_id`: `` Bundle id + - `transactions`: `` - A list of base-58 encoded signatures applied by the bundle. The list will not be empty. + - `slot`: `` The slot this bundle was processed in. + - `confirmationStatus`: `` - The bundle transaction's cluster confirmation status; Either processed, confirmed, or finalized. See [Commitment](#state-commitment) for more on optimistic confirmation. + - `err`: ``: This will show any retryable or non-retryable error encountered when getting the bundle status. If retryable, please query again + +### Code sample + +### Request + +```bash +curl https://mainnet.block-engine.jito.wtf:443/api/v1/bundles -X POST -H "Content-Type: application/json" -d ' +{ + "jsonrpc": "2.0", + "id": 1, + "method": "getBundleStatuses", + "params": [ + [ + "892b79ed49138bfb3aa5441f0df6e06ef34f9ee8f3976c15b323605bae0cf51d" + ] + ] +} +' +``` + +### Response + +```json +{ + "jsonrpc": "2.0", + "result": { + "context": { + "slot": 242806119 + }, + "value": [ + { + "bundle_id": "892b79ed49138bfb3aa5441f0df6e06ef34f9ee8f3976c15b323605bae0cf51d", + "transactions": [ + "3bC2M9fiACSjkTXZDgeNAuQ4ScTsdKGwR42ytFdhUvikqTmBheUxfsR1fDVsM5ADCMMspuwGkdm1uKbU246x5aE3", + "8t9hKYEYNbLvNqiSzP96S13XF1C2f1ro271Kdf7bkZ6EpjPLuDff1ywRy4gfaGSTubsM2FeYGDoT64ZwPm1cQUt" + ], + "slot": 242804011, + "confirmation_status": "finalized", + "err": { + "Ok": null + } + } + ] + }, + "id": 1 +} +``` diff --git a/jito_protos/protos/auth.proto b/jito_protos/protos/auth.proto new file mode 100755 index 0000000..da67b4d --- /dev/null +++ b/jito_protos/protos/auth.proto @@ -0,0 +1,76 @@ +syntax = "proto3"; + +package auth; + +import "google/protobuf/timestamp.proto"; + +enum Role { + RELAYER = 0; + SEARCHER = 1; + VALIDATOR = 2; + SHREDSTREAM_SUBSCRIBER = 3; +} + +message GenerateAuthChallengeRequest { + /// Role the client is attempting to generate tokens for. + Role role = 1; + + /// Client's 32 byte pubkey. + bytes pubkey = 2; +} + +message GenerateAuthChallengeResponse { + string challenge = 1; +} + +message GenerateAuthTokensRequest { + /// The pre-signed challenge. + string challenge = 1; + + /// The signing keypair's corresponding 32 byte pubkey. + bytes client_pubkey = 2; + + /// The 64 byte signature of the challenge signed by the client's private key. The private key must correspond to + // the pubkey passed in the [GenerateAuthChallenge] method. The client is expected to sign the challenge token + // prepended with their pubkey. For example sign(pubkey, challenge). + bytes signed_challenge = 3; +} + +message Token { + /// The token. + string value = 1; + + /// When the token will expire. + google.protobuf.Timestamp expires_at_utc = 2; +} + +message GenerateAuthTokensResponse { + /// The token granting access to resources. + Token access_token = 1; + + /// The token used to refresh the access_token. This has a longer TTL than the access_token. + Token refresh_token = 2; +} + +message RefreshAccessTokenRequest { + /// Non-expired refresh token obtained from the [GenerateAuthTokens] method. + string refresh_token = 1; +} + +message RefreshAccessTokenResponse { + /// Fresh access_token. + Token access_token = 1; +} + +/// This service is responsible for issuing auth tokens to clients for API access. +service AuthService { + /// Returns a challenge, client is expected to sign this challenge with an appropriate keypair in order to obtain access tokens. + rpc GenerateAuthChallenge(GenerateAuthChallengeRequest) returns (GenerateAuthChallengeResponse) {} + + /// Provides the client with the initial pair of auth tokens for API access. + rpc GenerateAuthTokens(GenerateAuthTokensRequest) returns (GenerateAuthTokensResponse) {} + + /// Call this method with a non-expired refresh token to obtain a new access token. + rpc RefreshAccessToken(RefreshAccessTokenRequest) returns (RefreshAccessTokenResponse) {} +} + diff --git a/jito_protos/protos/block.proto b/jito_protos/protos/block.proto new file mode 100755 index 0000000..398c494 --- /dev/null +++ b/jito_protos/protos/block.proto @@ -0,0 +1,15 @@ +syntax = "proto3"; + +package block; +import "shared.proto"; + +// Condensed block helpful for getting data around efficiently internal to our system. +message CondensedBlock { + shared.Header header = 1; + string previous_blockhash = 2; + string blockhash = 3; + uint64 parent_slot = 4; + repeated bytes versioned_transactions = 5; + uint64 slot = 6; + string commitment = 7; +} diff --git a/jito_protos/protos/block_engine.proto b/jito_protos/protos/block_engine.proto new file mode 100755 index 0000000..466adea --- /dev/null +++ b/jito_protos/protos/block_engine.proto @@ -0,0 +1,98 @@ +syntax = "proto3"; + +import "packet.proto"; +import "shared.proto"; +import "bundle.proto"; + +package block_engine; + +message SubscribePacketsRequest {} +message SubscribePacketsResponse { + shared.Header header = 1; + packet.PacketBatch batch = 2; +} + +message SubscribeBundlesRequest {} +message SubscribeBundlesResponse { + repeated bundle.BundleUuid bundles = 1; +} + +message BlockBuilderFeeInfoRequest {} +message BlockBuilderFeeInfoResponse { + string pubkey = 1; + + // commission (0-100) + uint64 commission = 2; +} + +message AccountsOfInterest { + // use * for all accounts + repeated string accounts = 1; +} + +message AccountsOfInterestRequest {} +message AccountsOfInterestUpdate { + repeated string accounts = 1; +} + +message ProgramsOfInterestRequest {} +message ProgramsOfInterestUpdate { + repeated string programs = 1; +} + +// A series of packets with an expiration attached to them. +// The header contains a timestamp for when this packet was generated. +// The expiry is how long the packet batches have before they expire and are forwarded to the validator. +// This provides a more censorship resistant method to MEV than block engines receiving packets directly. +message ExpiringPacketBatch { + shared.Header header = 1; + packet.PacketBatch batch = 2; + uint32 expiry_ms = 3; +} + +// Packets and heartbeats are sent over the same stream. +// ExpiringPacketBatches have an expiration attached to them so the block engine can track +// how long it has until the relayer forwards the packets to the validator. +// Heartbeats contain a timestamp from the system and is used as a simple and naive time-sync mechanism +// so the block engine has some idea on how far their clocks are apart. +message PacketBatchUpdate { + oneof msg { + ExpiringPacketBatch batches = 1; + shared.Heartbeat heartbeat = 2; + } +} + +message StartExpiringPacketStreamResponse { + shared.Heartbeat heartbeat = 1; +} + +/// Validators can connect to Block Engines to receive packets and bundles. +service BlockEngineValidator { + /// Validators can subscribe to the block engine to receive a stream of packets + rpc SubscribePackets (SubscribePacketsRequest) returns (stream SubscribePacketsResponse) {} + + /// Validators can subscribe to the block engine to receive a stream of simulated and profitable bundles + rpc SubscribeBundles (SubscribeBundlesRequest) returns (stream SubscribeBundlesResponse) {} + + // Block builders can optionally collect fees. This returns fee information if a block builder wants to + // collect one. + rpc GetBlockBuilderFeeInfo (BlockBuilderFeeInfoRequest) returns (BlockBuilderFeeInfoResponse) {} +} + +/// Relayers can forward packets to Block Engines. +/// Block Engines provide an AccountsOfInterest field to only send transactions that are of interest. +service BlockEngineRelayer { + /// The block engine feeds accounts of interest (AOI) updates to the relayer periodically. + /// For all transactions the relayer receives, it forwards transactions to the block engine which write-lock + /// any of the accounts in the AOI. + rpc SubscribeAccountsOfInterest (AccountsOfInterestRequest) returns (stream AccountsOfInterestUpdate) {} + + rpc SubscribeProgramsOfInterest (ProgramsOfInterestRequest) returns (stream ProgramsOfInterestUpdate) {} + + // Validators can subscribe to packets from the relayer and receive a multiplexed signal that contains a mixture + // of packets and heartbeats. + // NOTE: This is a bi-directional stream due to a bug with how Envoy handles half closed client-side streams. + // The issue is being tracked here: https://github.com/envoyproxy/envoy/issues/22748. In the meantime, the + // server will stream heartbeats to clients at some reasonable cadence. + rpc StartExpiringPacketStream (stream PacketBatchUpdate) returns (stream StartExpiringPacketStreamResponse) {} +} diff --git a/jito_protos/protos/bundle.proto b/jito_protos/protos/bundle.proto new file mode 100755 index 0000000..7b0f954 --- /dev/null +++ b/jito_protos/protos/bundle.proto @@ -0,0 +1,111 @@ +syntax = "proto3"; + +import "packet.proto"; +import "shared.proto"; + +package bundle; + +message Bundle { + shared.Header header = 2; + repeated packet.Packet packets = 3; +} + +message BundleUuid { + bundle.Bundle bundle = 1; + string uuid = 2; +} + +/* Bundle Result Types */ + +// Indicates the bundle was accepted and forwarded to a validator. +// NOTE: A single bundle may have multiple events emitted if forwarded to many validators. +message Accepted { + // Slot at which bundle was forwarded. + uint64 slot = 1; + + // Validator identity bundle was forwarded to. + string validator_identity = 2; +} + +// Indicates the bundle was dropped and therefore not forwarded to any validator. +message Rejected { + oneof reason { + StateAuctionBidRejected state_auction_bid_rejected = 1; + WinningBatchBidRejected winning_batch_bid_rejected = 2; + SimulationFailure simulation_failure = 3; + InternalError internal_error = 4; + DroppedBundle dropped_bundle = 5; + } +} + +// Indicates the bundle's bid was high enough to win its state auction. +// However, not high enough relative to other state auction winners and therefore excluded from being forwarded. +message WinningBatchBidRejected { + // Auction's unique identifier. + string auction_id = 1; + // Bundle's simulated bid. + uint64 simulated_bid_lamports = 2; + optional string msg = 3; +} + +// Indicates the bundle's bid was __not__ high enough to be included in its state auction's set of winners. +message StateAuctionBidRejected { + // Auction's unique identifier. + string auction_id = 1; + // Bundle's simulated bid. + uint64 simulated_bid_lamports = 2; + optional string msg = 3; +} + +// Bundle dropped due to simulation failure. +message SimulationFailure { + // Signature of the offending transaction. + string tx_signature = 1; + optional string msg = 2; +} + +// Bundle dropped due to an internal error. +message InternalError { + string msg = 1; +} + +// Bundle dropped (e.g. because no leader upcoming) +message DroppedBundle { + string msg = 1; +} + +message Finalized {} +message Processed { + string validator_identity = 1; + uint64 slot = 2; + /// Index within the block. + uint64 bundle_index = 3; +} +message Dropped { + DroppedReason reason = 1; +} +enum DroppedReason { + BlockhashExpired = 0; + // One or more transactions in the bundle landed on-chain, invalidating the bundle. + PartiallyProcessed = 1; + // This indicates bundle was processed but not finalized. This could occur during forks. + NotFinalized = 2; +} + +message BundleResult { + // Bundle's Uuid. + string bundle_id = 1; + + oneof result { + // Indicated accepted by the block-engine and forwarded to a jito-solana validator. + Accepted accepted = 2; + // Rejected by the block-engine. + Rejected rejected = 3; + // Reached finalized commitment level. + Finalized finalized = 4; + // Reached a processed commitment level. + Processed processed = 5; + // Was accepted and forwarded by the block-engine but never landed on-chain. + Dropped dropped = 6; + } +} diff --git a/jito_protos/protos/packet.proto b/jito_protos/protos/packet.proto new file mode 100755 index 0000000..23042a3 --- /dev/null +++ b/jito_protos/protos/packet.proto @@ -0,0 +1,30 @@ +syntax = "proto3"; + +package packet; + +message PacketBatch { + repeated Packet packets = 1; +} + +message Packet { + bytes data = 1; + Meta meta = 2; +} + +message Meta { + uint64 size = 1; + string addr = 2; + uint32 port = 3; + PacketFlags flags = 4; + uint64 sender_stake = 5; +} + +message PacketFlags { + bool discard = 1; + bool forwarded = 2; + bool repair = 3; + bool simple_vote_tx = 4; + bool tracer_packet = 5; + bool from_staked_node = 6; +} + diff --git a/jito_protos/protos/relayer.proto b/jito_protos/protos/relayer.proto new file mode 100755 index 0000000..5058d1f --- /dev/null +++ b/jito_protos/protos/relayer.proto @@ -0,0 +1,35 @@ +syntax = "proto3"; + +import "packet.proto"; +import "shared.proto"; + +package relayer; + +message GetTpuConfigsRequest {} +message GetTpuConfigsResponse { + shared.Socket tpu = 1; + shared.Socket tpu_forward = 2; +} + +message SubscribePacketsRequest {} +message SubscribePacketsResponse { + shared.Header header = 1; + oneof msg { + shared.Heartbeat heartbeat = 2; + packet.PacketBatch batch = 3; + } +} + +/// Relayers offer a TPU and TPU forward proxy for Solana validators. +/// Validators can connect and fetch the TPU configuration for the relayer and start to advertise the +/// relayer's information in gossip. +/// They can also subscribe to packets which arrived on the TPU ports at the relayer +service Relayer { + // The relayer has TPU and TPU forward sockets that validators can leverage. + // A validator can fetch this config and change its TPU and TPU forward port in gossip. + rpc GetTpuConfigs (GetTpuConfigsRequest) returns (GetTpuConfigsResponse) {} + + // Validators can subscribe to packets from the relayer and receive a multiplexed signal that contains a mixture + // of packets and heartbeats + rpc SubscribePackets (SubscribePacketsRequest) returns (stream SubscribePacketsResponse) {} +} diff --git a/jito_protos/protos/searcher.proto b/jito_protos/protos/searcher.proto new file mode 100755 index 0000000..06a9882 --- /dev/null +++ b/jito_protos/protos/searcher.proto @@ -0,0 +1,92 @@ +syntax = "proto3"; + +package searcher; + +import "bundle.proto"; + +message SlotList { + repeated uint64 slots = 1; +} + +message ConnectedLeadersResponse { + // Mapping of validator pubkey to leader slots for the current epoch. + map connected_validators = 1; +} + +message SendBundleRequest { + bundle.Bundle bundle = 1; +} + +message SendBundleResponse { + // server uuid for the bundle + string uuid = 1; +} + +message NextScheduledLeaderRequest { + // Defaults to the currently connected region if no region provided. + repeated string regions = 1; +} + +message NextScheduledLeaderResponse { + // the current slot the backend is on + uint64 current_slot = 1; + + // the slot of the next leader + uint64 next_leader_slot = 2; + // the identity pubkey (base58) of the next leader + string next_leader_identity = 3; + // the block engine region of the next leader + string next_leader_region = 4; +} + +message ConnectedLeadersRequest {} + +message ConnectedLeadersRegionedRequest { + // Defaults to the currently connected region if no region provided. + repeated string regions = 1; +} +message ConnectedLeadersRegionedResponse { + map connected_validators = 1; +} + + +message GetTipAccountsRequest {} + +message GetTipAccountsResponse { + repeated string accounts = 1; +} + +message SubscribeBundleResultsRequest {} + +message GetRegionsRequest {} +message GetRegionsResponse { + // The region the client is currently connected to + string current_region = 1; + + // Regions that are online and ready for connections + // All regions: https://jito-labs.gitbook.io/mev/systems/connecting/mainnet + repeated string available_regions = 2; +} + +service SearcherService { + // Searchers can invoke this endpoint to subscribe to their respective bundle results. + // A success result would indicate the bundle won its state auction and was submitted to the validator. + rpc SubscribeBundleResults (SubscribeBundleResultsRequest) returns (stream bundle.BundleResult) {} + + rpc SendBundle (SendBundleRequest) returns (SendBundleResponse) {} + + // Returns the next scheduled leader connected to the block engine. + rpc GetNextScheduledLeader (NextScheduledLeaderRequest) returns (NextScheduledLeaderResponse) {} + + // Returns leader slots for connected jito validators during the current epoch. Only returns data for this region. + rpc GetConnectedLeaders (ConnectedLeadersRequest) returns (ConnectedLeadersResponse) {} + + // Returns leader slots for connected jito validators during the current epoch. + rpc GetConnectedLeadersRegioned (ConnectedLeadersRegionedRequest) returns (ConnectedLeadersRegionedResponse) {} + + // Returns the tip accounts searchers shall transfer funds to for the leader to claim. + rpc GetTipAccounts (GetTipAccountsRequest) returns (GetTipAccountsResponse) {} + + // Returns region the client is directly connected to, along with all available regions + rpc GetRegions (GetRegionsRequest) returns (GetRegionsResponse) {} +} diff --git a/jito_protos/protos/shared.proto b/jito_protos/protos/shared.proto new file mode 100755 index 0000000..bdf391d --- /dev/null +++ b/jito_protos/protos/shared.proto @@ -0,0 +1,18 @@ +syntax = "proto3"; + +import "google/protobuf/timestamp.proto"; + +package shared; + +message Header { + google.protobuf.Timestamp ts = 1; +} + +message Heartbeat { + uint64 count = 1; +} + +message Socket { + string ip = 1; + int64 port = 2; +} diff --git a/jito_protos/protos/shredstream.proto b/jito_protos/protos/shredstream.proto new file mode 100755 index 0000000..af89449 --- /dev/null +++ b/jito_protos/protos/shredstream.proto @@ -0,0 +1,25 @@ +syntax = "proto3"; + +package shredstream; + +import "shared.proto"; + +message Heartbeat { + // don't trust IP:PORT from tcp header since it can be tampered over the wire + // `socket.ip` must match incoming packet's ip. this prevents spamming an unwitting destination + shared.Socket socket = 1; + + // regions for shredstream proxy to receive shreds from + // list of valid regions: https://jito-labs.gitbook.io/mev/systems/connecting/mainnet + repeated string regions = 2; +} + +message HeartbeatResponse { + // client must respond within `ttl_ms` to keep stream alive + uint32 ttl_ms = 1; +} + +service Shredstream { + // RPC endpoint to send heartbeats to keep shreds flowing + rpc SendHeartbeat (Heartbeat) returns (HeartbeatResponse) {} +} diff --git a/jito_protos/protos/trace_shred.proto b/jito_protos/protos/trace_shred.proto new file mode 100755 index 0000000..560d443 --- /dev/null +++ b/jito_protos/protos/trace_shred.proto @@ -0,0 +1,15 @@ +syntax = "proto3"; + +package trace_shred; + +import "google/protobuf/timestamp.proto"; + +message TraceShred { + // source region, one of: https://jito-labs.gitbook.io/mev/systems/connecting/mainnet + string region = 1; + // timestamp of creation + google.protobuf.Timestamp created_at = 2; + // monotonically increases, resets upon service restart + uint32 seq_num = 3; +} + diff --git a/jito_protos/src/convert.rs b/jito_protos/src/convert.rs new file mode 100755 index 0000000..b607716 --- /dev/null +++ b/jito_protos/src/convert.rs @@ -0,0 +1,158 @@ +use std::{ + cmp::min, + net::{AddrParseError, IpAddr, Ipv4Addr, SocketAddr}, + str::FromStr, +}; + +use bincode::serialize; +use solana_perf::packet::{Packet, PacketBatch, PACKET_DATA_SIZE}; +use solana_sdk::{ + packet::{Meta, PacketFlags}, + transaction::VersionedTransaction, +}; + +use crate::{ + packet::{ + Meta as ProtoMeta, Packet as ProtoPacket, PacketBatch as ProtoPacketBatch, + PacketFlags as ProtoPacketFlags, + }, + shared::Socket, +}; + +/// Converts a Solana packet to a protobuf packet +/// NOTE: the packet.data() function will filter packets marked for discard +pub fn packet_to_proto_packet(p: &Packet) -> Option { + Some(ProtoPacket { + data: p.data(..)?.to_vec(), + meta: Some(ProtoMeta { + size: p.meta().size as u64, + addr: p.meta().addr.to_string(), + port: p.meta().port as u32, + flags: Some(ProtoPacketFlags { + discard: p.meta().discard(), + forwarded: p.meta().forwarded(), + repair: p.meta().repair(), + simple_vote_tx: p.meta().is_simple_vote_tx(), + tracer_packet: p.meta().is_perf_track_packet(), + from_staked_node: p.meta().is_from_staked_node(), + }), + sender_stake: 0, + }), + }) +} + +pub fn packet_batches_to_proto_packets( + batches: &[PacketBatch], +) -> impl Iterator + '_ { + batches + .iter() + .flat_map(|b| b.iter().filter_map(packet_to_proto_packet)) +} + +/// converts from a protobuf packet to packet +pub fn proto_packet_to_packet(p: &ProtoPacket) -> Packet { + let mut data = [0u8; PACKET_DATA_SIZE]; + let copy_len = min(data.len(), p.data.len()); + data[..copy_len].copy_from_slice(&p.data[..copy_len]); + let mut packet = Packet::new(data, Meta::default()); + if let Some(meta) = &p.meta { + packet.meta_mut().size = meta.size as usize; + packet.meta_mut().addr = meta + .addr + .parse() + .unwrap_or(IpAddr::V4(Ipv4Addr::UNSPECIFIED)); + packet.meta_mut().port = meta.port as u16; + if let Some(flags) = &meta.flags { + if flags.simple_vote_tx { + packet.meta_mut().flags.insert(PacketFlags::SIMPLE_VOTE_TX); + } + if flags.forwarded { + packet.meta_mut().flags.insert(PacketFlags::FORWARDED); + } + if flags.tracer_packet { + packet.meta_mut().flags.insert(PacketFlags::PERF_TRACK_PACKET); + } + if flags.repair { + packet.meta_mut().flags.insert(PacketFlags::REPAIR); + } + if flags.discard { + packet.meta_mut().flags.insert(PacketFlags::DISCARD); + } + } + } + packet +} + +pub fn proto_packet_batch_to_packets( + packet_batch: ProtoPacketBatch, +) -> impl Iterator { + packet_batch + .packets + .into_iter() + .map(|proto_packet| proto_packet_to_packet(&proto_packet)) +} + +/// Converts a protobuf packet to a VersionedTransaction +pub fn versioned_tx_from_packet(p: &ProtoPacket) -> Option { + let mut data = [0; PACKET_DATA_SIZE]; + let copy_len = min(data.len(), p.data.len()); + data[..copy_len].copy_from_slice(&p.data[..copy_len]); + let mut packet = Packet::new(data, Default::default()); + if let Some(meta) = &p.meta { + packet.meta_mut().size = meta.size as usize; + } + packet.deserialize_slice(..).ok() +} + +/// Coverts a VersionedTransaction to packet +pub fn packet_from_versioned_tx(tx: VersionedTransaction) -> Packet { + let tx_data = serialize(&tx).expect("serializes"); + let mut data = [0; PACKET_DATA_SIZE]; + let copy_len = min(tx_data.len(), data.len()); + data[..copy_len].copy_from_slice(&tx_data[..copy_len]); + let mut packet = Packet::new(data, Default::default()); + packet.meta_mut().size = copy_len; + packet +} + +/// Converts a VersionedTransaction to a protobuf packet +pub fn proto_packet_from_versioned_tx(tx: &VersionedTransaction) -> ProtoPacket { + let data = serialize(tx).expect("serializes"); + let size = data.len() as u64; + ProtoPacket { + data, + meta: Some(ProtoMeta { + size, + addr: "".to_string(), + port: 0, + flags: None, + sender_stake: 0, + }), + } +} + +/// Converts a GRPC Socket to stdlib SocketAddr +impl TryFrom<&Socket> for SocketAddr { + type Error = AddrParseError; + + fn try_from(value: &Socket) -> Result { + IpAddr::from_str(&value.ip).map(|ip| SocketAddr::new(ip, value.port as u16)) + } +} + +// #[cfg(test)] +// mod tests { +// use solana_perf::test_tx::test_tx; +// use solana_sdk::transaction::VersionedTransaction; + +// use crate::convert::{proto_packet_from_versioned_tx, versioned_tx_from_packet}; + +// #[test] +// fn test_proto_to_packet() { +// let tx_before = VersionedTransaction::from(test_tx()); +// let tx_after = versioned_tx_from_packet(&proto_packet_from_versioned_tx(&tx_before)) +// .expect("tx_after"); + +// assert_eq!(tx_before, tx_after); +// } +// } diff --git a/jito_protos/src/grpc/auth.rs b/jito_protos/src/grpc/auth.rs new file mode 100755 index 0000000..127047c --- /dev/null +++ b/jito_protos/src/grpc/auth.rs @@ -0,0 +1,260 @@ +// This file is @generated by prost-build. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GenerateAuthChallengeRequest { + /// / Role the client is attempting to generate tokens for. + #[prost(enumeration = "Role", tag = "1")] + pub role: i32, + /// / Client's 32 byte pubkey. + #[prost(bytes = "vec", tag = "2")] + pub pubkey: ::prost::alloc::vec::Vec, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GenerateAuthChallengeResponse { + #[prost(string, tag = "1")] + pub challenge: ::prost::alloc::string::String, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GenerateAuthTokensRequest { + /// / The pre-signed challenge. + #[prost(string, tag = "1")] + pub challenge: ::prost::alloc::string::String, + /// / The signing keypair's corresponding 32 byte pubkey. + #[prost(bytes = "vec", tag = "2")] + pub client_pubkey: ::prost::alloc::vec::Vec, + /// / The 64 byte signature of the challenge signed by the client's private key. The private key must correspond to + /// the pubkey passed in the \[GenerateAuthChallenge\] method. The client is expected to sign the challenge token + /// prepended with their pubkey. For example sign(pubkey, challenge). + #[prost(bytes = "vec", tag = "3")] + pub signed_challenge: ::prost::alloc::vec::Vec, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Token { + /// / The token. + #[prost(string, tag = "1")] + pub value: ::prost::alloc::string::String, + /// / When the token will expire. + #[prost(message, optional, tag = "2")] + pub expires_at_utc: ::core::option::Option<::prost_types::Timestamp>, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GenerateAuthTokensResponse { + /// / The token granting access to resources. + #[prost(message, optional, tag = "1")] + pub access_token: ::core::option::Option, + /// / The token used to refresh the access_token. This has a longer TTL than the access_token. + #[prost(message, optional, tag = "2")] + pub refresh_token: ::core::option::Option, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct RefreshAccessTokenRequest { + /// / Non-expired refresh token obtained from the \[GenerateAuthTokens\] method. + #[prost(string, tag = "1")] + pub refresh_token: ::prost::alloc::string::String, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct RefreshAccessTokenResponse { + /// / Fresh access_token. + #[prost(message, optional, tag = "1")] + pub access_token: ::core::option::Option, +} +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] +#[repr(i32)] +pub enum Role { + Relayer = 0, + Searcher = 1, + Validator = 2, + ShredstreamSubscriber = 3, +} +impl Role { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + Self::Relayer => "RELAYER", + Self::Searcher => "SEARCHER", + Self::Validator => "VALIDATOR", + Self::ShredstreamSubscriber => "SHREDSTREAM_SUBSCRIBER", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "RELAYER" => Some(Self::Relayer), + "SEARCHER" => Some(Self::Searcher), + "VALIDATOR" => Some(Self::Validator), + "SHREDSTREAM_SUBSCRIBER" => Some(Self::ShredstreamSubscriber), + _ => None, + } + } +} +/// Generated client implementations. +pub mod auth_service_client { + #![allow( + unused_variables, + dead_code, + missing_docs, + clippy::wildcard_imports, + clippy::let_unit_value, + )] + use tonic::codegen::*; + use tonic::codegen::http::Uri; + /// / This service is responsible for issuing auth tokens to clients for API access. + #[derive(Debug, Clone)] + pub struct AuthServiceClient { + inner: tonic::client::Grpc, + } + impl AuthServiceClient { + /// Attempt to create a new client by connecting to a given endpoint. + pub async fn connect(dst: D) -> Result + where + D: TryInto, + D::Error: Into, + { + let conn = tonic::transport::Endpoint::new(dst)?.connect().await?; + Ok(Self::new(conn)) + } + } + impl AuthServiceClient + where + T: tonic::client::GrpcService, + T::Error: Into, + T::ResponseBody: Body + std::marker::Send + 'static, + ::Error: Into + std::marker::Send, + { + pub fn new(inner: T) -> Self { + let inner = tonic::client::Grpc::new(inner); + Self { inner } + } + pub fn with_origin(inner: T, origin: Uri) -> Self { + let inner = tonic::client::Grpc::with_origin(inner, origin); + Self { inner } + } + pub fn with_interceptor( + inner: T, + interceptor: F, + ) -> AuthServiceClient> + where + F: tonic::service::Interceptor, + T::ResponseBody: Default, + T: tonic::codegen::Service< + http::Request, + Response = http::Response< + >::ResponseBody, + >, + >, + , + >>::Error: Into + std::marker::Send + std::marker::Sync, + { + AuthServiceClient::new(InterceptedService::new(inner, interceptor)) + } + /// Compress requests with the given encoding. + /// + /// This requires the server to support it otherwise it might respond with an + /// error. + #[must_use] + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.send_compressed(encoding); + self + } + /// Enable decompressing responses. + #[must_use] + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.accept_compressed(encoding); + self + } + /// Limits the maximum size of a decoded message. + /// + /// Default: `4MB` + #[must_use] + pub fn max_decoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_decoding_message_size(limit); + self + } + /// Limits the maximum size of an encoded message. + /// + /// Default: `usize::MAX` + #[must_use] + pub fn max_encoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_encoding_message_size(limit); + self + } + /// / Returns a challenge, client is expected to sign this challenge with an appropriate keypair in order to obtain access tokens. + pub async fn generate_auth_challenge( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/auth.AuthService/GenerateAuthChallenge", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert(GrpcMethod::new("auth.AuthService", "GenerateAuthChallenge")); + self.inner.unary(req, path, codec).await + } + /// / Provides the client with the initial pair of auth tokens for API access. + pub async fn generate_auth_tokens( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/auth.AuthService/GenerateAuthTokens", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert(GrpcMethod::new("auth.AuthService", "GenerateAuthTokens")); + self.inner.unary(req, path, codec).await + } + /// / Call this method with a non-expired refresh token to obtain a new access token. + pub async fn refresh_access_token( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/auth.AuthService/RefreshAccessToken", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert(GrpcMethod::new("auth.AuthService", "RefreshAccessToken")); + self.inner.unary(req, path, codec).await + } + } +} diff --git a/jito_protos/src/grpc/block.rs b/jito_protos/src/grpc/block.rs new file mode 100755 index 0000000..0fc0434 --- /dev/null +++ b/jito_protos/src/grpc/block.rs @@ -0,0 +1,19 @@ +// This file is @generated by prost-build. +/// Condensed block helpful for getting data around efficiently internal to our system. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct CondensedBlock { + #[prost(message, optional, tag = "1")] + pub header: ::core::option::Option, + #[prost(string, tag = "2")] + pub previous_blockhash: ::prost::alloc::string::String, + #[prost(string, tag = "3")] + pub blockhash: ::prost::alloc::string::String, + #[prost(uint64, tag = "4")] + pub parent_slot: u64, + #[prost(bytes = "vec", repeated, tag = "5")] + pub versioned_transactions: ::prost::alloc::vec::Vec<::prost::alloc::vec::Vec>, + #[prost(uint64, tag = "6")] + pub slot: u64, + #[prost(string, tag = "7")] + pub commitment: ::prost::alloc::string::String, +} diff --git a/jito_protos/src/grpc/block_engine.rs b/jito_protos/src/grpc/block_engine.rs new file mode 100755 index 0000000..69c22eb --- /dev/null +++ b/jito_protos/src/grpc/block_engine.rs @@ -0,0 +1,462 @@ +// This file is @generated by prost-build. +#[derive(Clone, Copy, PartialEq, ::prost::Message)] +pub struct SubscribePacketsRequest {} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct SubscribePacketsResponse { + #[prost(message, optional, tag = "1")] + pub header: ::core::option::Option, + #[prost(message, optional, tag = "2")] + pub batch: ::core::option::Option, +} +#[derive(Clone, Copy, PartialEq, ::prost::Message)] +pub struct SubscribeBundlesRequest {} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct SubscribeBundlesResponse { + #[prost(message, repeated, tag = "1")] + pub bundles: ::prost::alloc::vec::Vec, +} +#[derive(Clone, Copy, PartialEq, ::prost::Message)] +pub struct BlockBuilderFeeInfoRequest {} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct BlockBuilderFeeInfoResponse { + #[prost(string, tag = "1")] + pub pubkey: ::prost::alloc::string::String, + /// commission (0-100) + #[prost(uint64, tag = "2")] + pub commission: u64, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct AccountsOfInterest { + /// use * for all accounts + #[prost(string, repeated, tag = "1")] + pub accounts: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, +} +#[derive(Clone, Copy, PartialEq, ::prost::Message)] +pub struct AccountsOfInterestRequest {} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct AccountsOfInterestUpdate { + #[prost(string, repeated, tag = "1")] + pub accounts: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, +} +#[derive(Clone, Copy, PartialEq, ::prost::Message)] +pub struct ProgramsOfInterestRequest {} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ProgramsOfInterestUpdate { + #[prost(string, repeated, tag = "1")] + pub programs: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, +} +/// A series of packets with an expiration attached to them. +/// The header contains a timestamp for when this packet was generated. +/// The expiry is how long the packet batches have before they expire and are forwarded to the validator. +/// This provides a more censorship resistant method to MEV than block engines receiving packets directly. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ExpiringPacketBatch { + #[prost(message, optional, tag = "1")] + pub header: ::core::option::Option, + #[prost(message, optional, tag = "2")] + pub batch: ::core::option::Option, + #[prost(uint32, tag = "3")] + pub expiry_ms: u32, +} +/// Packets and heartbeats are sent over the same stream. +/// ExpiringPacketBatches have an expiration attached to them so the block engine can track +/// how long it has until the relayer forwards the packets to the validator. +/// Heartbeats contain a timestamp from the system and is used as a simple and naive time-sync mechanism +/// so the block engine has some idea on how far their clocks are apart. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct PacketBatchUpdate { + #[prost(oneof = "packet_batch_update::Msg", tags = "1, 2")] + pub msg: ::core::option::Option, +} +/// Nested message and enum types in `PacketBatchUpdate`. +pub mod packet_batch_update { + #[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum Msg { + #[prost(message, tag = "1")] + Batches(super::ExpiringPacketBatch), + #[prost(message, tag = "2")] + Heartbeat(super::super::shared::Heartbeat), + } +} +#[derive(Clone, Copy, PartialEq, ::prost::Message)] +pub struct StartExpiringPacketStreamResponse { + #[prost(message, optional, tag = "1")] + pub heartbeat: ::core::option::Option, +} +/// Generated client implementations. +pub mod block_engine_validator_client { + #![allow( + unused_variables, + dead_code, + missing_docs, + clippy::wildcard_imports, + clippy::let_unit_value, + )] + use tonic::codegen::*; + use tonic::codegen::http::Uri; + /// / Validators can connect to Block Engines to receive packets and bundles. + #[derive(Debug, Clone)] + pub struct BlockEngineValidatorClient { + inner: tonic::client::Grpc, + } + impl BlockEngineValidatorClient { + /// Attempt to create a new client by connecting to a given endpoint. + pub async fn connect(dst: D) -> Result + where + D: TryInto, + D::Error: Into, + { + let conn = tonic::transport::Endpoint::new(dst)?.connect().await?; + Ok(Self::new(conn)) + } + } + impl BlockEngineValidatorClient + where + T: tonic::client::GrpcService, + T::Error: Into, + T::ResponseBody: Body + std::marker::Send + 'static, + ::Error: Into + std::marker::Send, + { + pub fn new(inner: T) -> Self { + let inner = tonic::client::Grpc::new(inner); + Self { inner } + } + pub fn with_origin(inner: T, origin: Uri) -> Self { + let inner = tonic::client::Grpc::with_origin(inner, origin); + Self { inner } + } + pub fn with_interceptor( + inner: T, + interceptor: F, + ) -> BlockEngineValidatorClient> + where + F: tonic::service::Interceptor, + T::ResponseBody: Default, + T: tonic::codegen::Service< + http::Request, + Response = http::Response< + >::ResponseBody, + >, + >, + , + >>::Error: Into + std::marker::Send + std::marker::Sync, + { + BlockEngineValidatorClient::new(InterceptedService::new(inner, interceptor)) + } + /// Compress requests with the given encoding. + /// + /// This requires the server to support it otherwise it might respond with an + /// error. + #[must_use] + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.send_compressed(encoding); + self + } + /// Enable decompressing responses. + #[must_use] + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.accept_compressed(encoding); + self + } + /// Limits the maximum size of a decoded message. + /// + /// Default: `4MB` + #[must_use] + pub fn max_decoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_decoding_message_size(limit); + self + } + /// Limits the maximum size of an encoded message. + /// + /// Default: `usize::MAX` + #[must_use] + pub fn max_encoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_encoding_message_size(limit); + self + } + /// / Validators can subscribe to the block engine to receive a stream of packets + pub async fn subscribe_packets( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response>, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/block_engine.BlockEngineValidator/SubscribePackets", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new( + "block_engine.BlockEngineValidator", + "SubscribePackets", + ), + ); + self.inner.server_streaming(req, path, codec).await + } + /// / Validators can subscribe to the block engine to receive a stream of simulated and profitable bundles + pub async fn subscribe_bundles( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response>, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/block_engine.BlockEngineValidator/SubscribeBundles", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new( + "block_engine.BlockEngineValidator", + "SubscribeBundles", + ), + ); + self.inner.server_streaming(req, path, codec).await + } + /// Block builders can optionally collect fees. This returns fee information if a block builder wants to + /// collect one. + pub async fn get_block_builder_fee_info( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/block_engine.BlockEngineValidator/GetBlockBuilderFeeInfo", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new( + "block_engine.BlockEngineValidator", + "GetBlockBuilderFeeInfo", + ), + ); + self.inner.unary(req, path, codec).await + } + } +} +/// Generated client implementations. +pub mod block_engine_relayer_client { + #![allow( + unused_variables, + dead_code, + missing_docs, + clippy::wildcard_imports, + clippy::let_unit_value, + )] + use tonic::codegen::*; + use tonic::codegen::http::Uri; + /// / Relayers can forward packets to Block Engines. + /// / Block Engines provide an AccountsOfInterest field to only send transactions that are of interest. + #[derive(Debug, Clone)] + pub struct BlockEngineRelayerClient { + inner: tonic::client::Grpc, + } + impl BlockEngineRelayerClient { + /// Attempt to create a new client by connecting to a given endpoint. + pub async fn connect(dst: D) -> Result + where + D: TryInto, + D::Error: Into, + { + let conn = tonic::transport::Endpoint::new(dst)?.connect().await?; + Ok(Self::new(conn)) + } + } + impl BlockEngineRelayerClient + where + T: tonic::client::GrpcService, + T::Error: Into, + T::ResponseBody: Body + std::marker::Send + 'static, + ::Error: Into + std::marker::Send, + { + pub fn new(inner: T) -> Self { + let inner = tonic::client::Grpc::new(inner); + Self { inner } + } + pub fn with_origin(inner: T, origin: Uri) -> Self { + let inner = tonic::client::Grpc::with_origin(inner, origin); + Self { inner } + } + pub fn with_interceptor( + inner: T, + interceptor: F, + ) -> BlockEngineRelayerClient> + where + F: tonic::service::Interceptor, + T::ResponseBody: Default, + T: tonic::codegen::Service< + http::Request, + Response = http::Response< + >::ResponseBody, + >, + >, + , + >>::Error: Into + std::marker::Send + std::marker::Sync, + { + BlockEngineRelayerClient::new(InterceptedService::new(inner, interceptor)) + } + /// Compress requests with the given encoding. + /// + /// This requires the server to support it otherwise it might respond with an + /// error. + #[must_use] + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.send_compressed(encoding); + self + } + /// Enable decompressing responses. + #[must_use] + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.accept_compressed(encoding); + self + } + /// Limits the maximum size of a decoded message. + /// + /// Default: `4MB` + #[must_use] + pub fn max_decoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_decoding_message_size(limit); + self + } + /// Limits the maximum size of an encoded message. + /// + /// Default: `usize::MAX` + #[must_use] + pub fn max_encoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_encoding_message_size(limit); + self + } + /// / The block engine feeds accounts of interest (AOI) updates to the relayer periodically. + /// / For all transactions the relayer receives, it forwards transactions to the block engine which write-lock + /// / any of the accounts in the AOI. + pub async fn subscribe_accounts_of_interest( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response>, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/block_engine.BlockEngineRelayer/SubscribeAccountsOfInterest", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new( + "block_engine.BlockEngineRelayer", + "SubscribeAccountsOfInterest", + ), + ); + self.inner.server_streaming(req, path, codec).await + } + pub async fn subscribe_programs_of_interest( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response>, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/block_engine.BlockEngineRelayer/SubscribeProgramsOfInterest", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new( + "block_engine.BlockEngineRelayer", + "SubscribeProgramsOfInterest", + ), + ); + self.inner.server_streaming(req, path, codec).await + } + /// Validators can subscribe to packets from the relayer and receive a multiplexed signal that contains a mixture + /// of packets and heartbeats. + /// NOTE: This is a bi-directional stream due to a bug with how Envoy handles half closed client-side streams. + /// The issue is being tracked here: https://github.com/envoyproxy/envoy/issues/22748. In the meantime, the + /// server will stream heartbeats to clients at some reasonable cadence. + pub async fn start_expiring_packet_stream( + &mut self, + request: impl tonic::IntoStreamingRequest, + ) -> std::result::Result< + tonic::Response< + tonic::codec::Streaming, + >, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/block_engine.BlockEngineRelayer/StartExpiringPacketStream", + ); + let mut req = request.into_streaming_request(); + req.extensions_mut() + .insert( + GrpcMethod::new( + "block_engine.BlockEngineRelayer", + "StartExpiringPacketStream", + ), + ); + self.inner.streaming(req, path, codec).await + } + } +} diff --git a/jito_protos/src/grpc/bundle.rs b/jito_protos/src/grpc/bundle.rs new file mode 100755 index 0000000..5409c92 --- /dev/null +++ b/jito_protos/src/grpc/bundle.rs @@ -0,0 +1,171 @@ +// This file is @generated by prost-build. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Bundle { + #[prost(message, optional, tag = "2")] + pub header: ::core::option::Option, + #[prost(message, repeated, tag = "3")] + pub packets: ::prost::alloc::vec::Vec, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct BundleUuid { + #[prost(message, optional, tag = "1")] + pub bundle: ::core::option::Option, + #[prost(string, tag = "2")] + pub uuid: ::prost::alloc::string::String, +} +/// Indicates the bundle was accepted and forwarded to a validator. +/// NOTE: A single bundle may have multiple events emitted if forwarded to many validators. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Accepted { + /// Slot at which bundle was forwarded. + #[prost(uint64, tag = "1")] + pub slot: u64, + /// Validator identity bundle was forwarded to. + #[prost(string, tag = "2")] + pub validator_identity: ::prost::alloc::string::String, +} +/// Indicates the bundle was dropped and therefore not forwarded to any validator. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Rejected { + #[prost(oneof = "rejected::Reason", tags = "1, 2, 3, 4, 5")] + pub reason: ::core::option::Option, +} +/// Nested message and enum types in `Rejected`. +pub mod rejected { + #[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum Reason { + #[prost(message, tag = "1")] + StateAuctionBidRejected(super::StateAuctionBidRejected), + #[prost(message, tag = "2")] + WinningBatchBidRejected(super::WinningBatchBidRejected), + #[prost(message, tag = "3")] + SimulationFailure(super::SimulationFailure), + #[prost(message, tag = "4")] + InternalError(super::InternalError), + #[prost(message, tag = "5")] + DroppedBundle(super::DroppedBundle), + } +} +/// Indicates the bundle's bid was high enough to win its state auction. +/// However, not high enough relative to other state auction winners and therefore excluded from being forwarded. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct WinningBatchBidRejected { + /// Auction's unique identifier. + #[prost(string, tag = "1")] + pub auction_id: ::prost::alloc::string::String, + /// Bundle's simulated bid. + #[prost(uint64, tag = "2")] + pub simulated_bid_lamports: u64, + #[prost(string, optional, tag = "3")] + pub msg: ::core::option::Option<::prost::alloc::string::String>, +} +/// Indicates the bundle's bid was __not__ high enough to be included in its state auction's set of winners. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct StateAuctionBidRejected { + /// Auction's unique identifier. + #[prost(string, tag = "1")] + pub auction_id: ::prost::alloc::string::String, + /// Bundle's simulated bid. + #[prost(uint64, tag = "2")] + pub simulated_bid_lamports: u64, + #[prost(string, optional, tag = "3")] + pub msg: ::core::option::Option<::prost::alloc::string::String>, +} +/// Bundle dropped due to simulation failure. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct SimulationFailure { + /// Signature of the offending transaction. + #[prost(string, tag = "1")] + pub tx_signature: ::prost::alloc::string::String, + #[prost(string, optional, tag = "2")] + pub msg: ::core::option::Option<::prost::alloc::string::String>, +} +/// Bundle dropped due to an internal error. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct InternalError { + #[prost(string, tag = "1")] + pub msg: ::prost::alloc::string::String, +} +/// Bundle dropped (e.g. because no leader upcoming) +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct DroppedBundle { + #[prost(string, tag = "1")] + pub msg: ::prost::alloc::string::String, +} +#[derive(Clone, Copy, PartialEq, ::prost::Message)] +pub struct Finalized {} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Processed { + #[prost(string, tag = "1")] + pub validator_identity: ::prost::alloc::string::String, + #[prost(uint64, tag = "2")] + pub slot: u64, + /// / Index within the block. + #[prost(uint64, tag = "3")] + pub bundle_index: u64, +} +#[derive(Clone, Copy, PartialEq, ::prost::Message)] +pub struct Dropped { + #[prost(enumeration = "DroppedReason", tag = "1")] + pub reason: i32, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct BundleResult { + /// Bundle's Uuid. + #[prost(string, tag = "1")] + pub bundle_id: ::prost::alloc::string::String, + #[prost(oneof = "bundle_result::Result", tags = "2, 3, 4, 5, 6")] + pub result: ::core::option::Option, +} +/// Nested message and enum types in `BundleResult`. +pub mod bundle_result { + #[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum Result { + /// Indicated accepted by the block-engine and forwarded to a jito-solana validator. + #[prost(message, tag = "2")] + Accepted(super::Accepted), + /// Rejected by the block-engine. + #[prost(message, tag = "3")] + Rejected(super::Rejected), + /// Reached finalized commitment level. + #[prost(message, tag = "4")] + Finalized(super::Finalized), + /// Reached a processed commitment level. + #[prost(message, tag = "5")] + Processed(super::Processed), + /// Was accepted and forwarded by the block-engine but never landed on-chain. + #[prost(message, tag = "6")] + Dropped(super::Dropped), + } +} +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] +#[repr(i32)] +pub enum DroppedReason { + BlockhashExpired = 0, + /// One or more transactions in the bundle landed on-chain, invalidating the bundle. + PartiallyProcessed = 1, + /// This indicates bundle was processed but not finalized. This could occur during forks. + NotFinalized = 2, +} +impl DroppedReason { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + Self::BlockhashExpired => "BlockhashExpired", + Self::PartiallyProcessed => "PartiallyProcessed", + Self::NotFinalized => "NotFinalized", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "BlockhashExpired" => Some(Self::BlockhashExpired), + "PartiallyProcessed" => Some(Self::PartiallyProcessed), + "NotFinalized" => Some(Self::NotFinalized), + _ => None, + } + } +} diff --git a/jito_protos/src/grpc/mod.rs b/jito_protos/src/grpc/mod.rs new file mode 100755 index 0000000..62ee28e --- /dev/null +++ b/jito_protos/src/grpc/mod.rs @@ -0,0 +1,10 @@ +pub mod auth; +pub mod block; +pub mod block_engine; +pub mod bundle; +pub mod packet; +pub mod relayer; +pub mod searcher; +pub mod shared; +pub mod shredstream; +pub mod trace_shred; diff --git a/jito_protos/src/grpc/packet.rs b/jito_protos/src/grpc/packet.rs new file mode 100755 index 0000000..fd20158 --- /dev/null +++ b/jito_protos/src/grpc/packet.rs @@ -0,0 +1,41 @@ +// This file is @generated by prost-build. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct PacketBatch { + #[prost(message, repeated, tag = "1")] + pub packets: ::prost::alloc::vec::Vec, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Packet { + #[prost(bytes = "vec", tag = "1")] + pub data: ::prost::alloc::vec::Vec, + #[prost(message, optional, tag = "2")] + pub meta: ::core::option::Option, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Meta { + #[prost(uint64, tag = "1")] + pub size: u64, + #[prost(string, tag = "2")] + pub addr: ::prost::alloc::string::String, + #[prost(uint32, tag = "3")] + pub port: u32, + #[prost(message, optional, tag = "4")] + pub flags: ::core::option::Option, + #[prost(uint64, tag = "5")] + pub sender_stake: u64, +} +#[derive(Clone, Copy, PartialEq, ::prost::Message)] +pub struct PacketFlags { + #[prost(bool, tag = "1")] + pub discard: bool, + #[prost(bool, tag = "2")] + pub forwarded: bool, + #[prost(bool, tag = "3")] + pub repair: bool, + #[prost(bool, tag = "4")] + pub simple_vote_tx: bool, + #[prost(bool, tag = "5")] + pub tracer_packet: bool, + #[prost(bool, tag = "6")] + pub from_staked_node: bool, +} diff --git a/jito_protos/src/grpc/relayer.rs b/jito_protos/src/grpc/relayer.rs new file mode 100755 index 0000000..c771fc5 --- /dev/null +++ b/jito_protos/src/grpc/relayer.rs @@ -0,0 +1,178 @@ +// This file is @generated by prost-build. +#[derive(Clone, Copy, PartialEq, ::prost::Message)] +pub struct GetTpuConfigsRequest {} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetTpuConfigsResponse { + #[prost(message, optional, tag = "1")] + pub tpu: ::core::option::Option, + #[prost(message, optional, tag = "2")] + pub tpu_forward: ::core::option::Option, +} +#[derive(Clone, Copy, PartialEq, ::prost::Message)] +pub struct SubscribePacketsRequest {} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct SubscribePacketsResponse { + #[prost(message, optional, tag = "1")] + pub header: ::core::option::Option, + #[prost(oneof = "subscribe_packets_response::Msg", tags = "2, 3")] + pub msg: ::core::option::Option, +} +/// Nested message and enum types in `SubscribePacketsResponse`. +pub mod subscribe_packets_response { + #[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum Msg { + #[prost(message, tag = "2")] + Heartbeat(super::super::shared::Heartbeat), + #[prost(message, tag = "3")] + Batch(super::super::packet::PacketBatch), + } +} +/// Generated client implementations. +pub mod relayer_client { + #![allow( + unused_variables, + dead_code, + missing_docs, + clippy::wildcard_imports, + clippy::let_unit_value, + )] + use tonic::codegen::*; + use tonic::codegen::http::Uri; + /// / Relayers offer a TPU and TPU forward proxy for Solana validators. + /// / Validators can connect and fetch the TPU configuration for the relayer and start to advertise the + /// / relayer's information in gossip. + /// / They can also subscribe to packets which arrived on the TPU ports at the relayer + #[derive(Debug, Clone)] + pub struct RelayerClient { + inner: tonic::client::Grpc, + } + impl RelayerClient { + /// Attempt to create a new client by connecting to a given endpoint. + pub async fn connect(dst: D) -> Result + where + D: TryInto, + D::Error: Into, + { + let conn = tonic::transport::Endpoint::new(dst)?.connect().await?; + Ok(Self::new(conn)) + } + } + impl RelayerClient + where + T: tonic::client::GrpcService, + T::Error: Into, + T::ResponseBody: Body + std::marker::Send + 'static, + ::Error: Into + std::marker::Send, + { + pub fn new(inner: T) -> Self { + let inner = tonic::client::Grpc::new(inner); + Self { inner } + } + pub fn with_origin(inner: T, origin: Uri) -> Self { + let inner = tonic::client::Grpc::with_origin(inner, origin); + Self { inner } + } + pub fn with_interceptor( + inner: T, + interceptor: F, + ) -> RelayerClient> + where + F: tonic::service::Interceptor, + T::ResponseBody: Default, + T: tonic::codegen::Service< + http::Request, + Response = http::Response< + >::ResponseBody, + >, + >, + , + >>::Error: Into + std::marker::Send + std::marker::Sync, + { + RelayerClient::new(InterceptedService::new(inner, interceptor)) + } + /// Compress requests with the given encoding. + /// + /// This requires the server to support it otherwise it might respond with an + /// error. + #[must_use] + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.send_compressed(encoding); + self + } + /// Enable decompressing responses. + #[must_use] + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.accept_compressed(encoding); + self + } + /// Limits the maximum size of a decoded message. + /// + /// Default: `4MB` + #[must_use] + pub fn max_decoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_decoding_message_size(limit); + self + } + /// Limits the maximum size of an encoded message. + /// + /// Default: `usize::MAX` + #[must_use] + pub fn max_encoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_encoding_message_size(limit); + self + } + /// The relayer has TPU and TPU forward sockets that validators can leverage. + /// A validator can fetch this config and change its TPU and TPU forward port in gossip. + pub async fn get_tpu_configs( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/relayer.Relayer/GetTpuConfigs", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert(GrpcMethod::new("relayer.Relayer", "GetTpuConfigs")); + self.inner.unary(req, path, codec).await + } + /// Validators can subscribe to packets from the relayer and receive a multiplexed signal that contains a mixture + /// of packets and heartbeats + pub async fn subscribe_packets( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response>, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/relayer.Relayer/SubscribePackets", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert(GrpcMethod::new("relayer.Relayer", "SubscribePackets")); + self.inner.server_streaming(req, path, codec).await + } + } +} diff --git a/jito_protos/src/grpc/searcher.rs b/jito_protos/src/grpc/searcher.rs new file mode 100755 index 0000000..ad922df --- /dev/null +++ b/jito_protos/src/grpc/searcher.rs @@ -0,0 +1,363 @@ +// This file is @generated by prost-build. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct SlotList { + #[prost(uint64, repeated, tag = "1")] + pub slots: ::prost::alloc::vec::Vec, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ConnectedLeadersResponse { + /// Mapping of validator pubkey to leader slots for the current epoch. + #[prost(map = "string, message", tag = "1")] + pub connected_validators: ::std::collections::HashMap< + ::prost::alloc::string::String, + SlotList, + >, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct SendBundleRequest { + #[prost(message, optional, tag = "1")] + pub bundle: ::core::option::Option, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct SendBundleResponse { + /// server uuid for the bundle + #[prost(string, tag = "1")] + pub uuid: ::prost::alloc::string::String, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct NextScheduledLeaderRequest { + /// Defaults to the currently connected region if no region provided. + #[prost(string, repeated, tag = "1")] + pub regions: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct NextScheduledLeaderResponse { + /// the current slot the backend is on + #[prost(uint64, tag = "1")] + pub current_slot: u64, + /// the slot of the next leader + #[prost(uint64, tag = "2")] + pub next_leader_slot: u64, + /// the identity pubkey (base58) of the next leader + #[prost(string, tag = "3")] + pub next_leader_identity: ::prost::alloc::string::String, + /// the block engine region of the next leader + #[prost(string, tag = "4")] + pub next_leader_region: ::prost::alloc::string::String, +} +#[derive(Clone, Copy, PartialEq, ::prost::Message)] +pub struct ConnectedLeadersRequest {} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ConnectedLeadersRegionedRequest { + /// Defaults to the currently connected region if no region provided. + #[prost(string, repeated, tag = "1")] + pub regions: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ConnectedLeadersRegionedResponse { + #[prost(map = "string, message", tag = "1")] + pub connected_validators: ::std::collections::HashMap< + ::prost::alloc::string::String, + ConnectedLeadersResponse, + >, +} +#[derive(Clone, Copy, PartialEq, ::prost::Message)] +pub struct GetTipAccountsRequest {} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetTipAccountsResponse { + #[prost(string, repeated, tag = "1")] + pub accounts: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, +} +#[derive(Clone, Copy, PartialEq, ::prost::Message)] +pub struct SubscribeBundleResultsRequest {} +#[derive(Clone, Copy, PartialEq, ::prost::Message)] +pub struct GetRegionsRequest {} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetRegionsResponse { + /// The region the client is currently connected to + #[prost(string, tag = "1")] + pub current_region: ::prost::alloc::string::String, + /// Regions that are online and ready for connections + /// All regions: + #[prost(string, repeated, tag = "2")] + pub available_regions: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, +} +/// Generated client implementations. +pub mod searcher_service_client { + #![allow( + unused_variables, + dead_code, + missing_docs, + clippy::wildcard_imports, + clippy::let_unit_value, + )] + use tonic::codegen::*; + use tonic::codegen::http::Uri; + #[derive(Debug, Clone)] + pub struct SearcherServiceClient { + inner: tonic::client::Grpc, + } + impl SearcherServiceClient { + /// Attempt to create a new client by connecting to a given endpoint. + pub async fn connect(dst: D) -> Result + where + D: TryInto, + D::Error: Into, + { + let conn = tonic::transport::Endpoint::new(dst)?.connect().await?; + Ok(Self::new(conn)) + } + } + impl SearcherServiceClient + where + T: tonic::client::GrpcService, + T::Error: Into, + T::ResponseBody: Body + std::marker::Send + 'static, + ::Error: Into + std::marker::Send, + { + pub fn new(inner: T) -> Self { + let inner = tonic::client::Grpc::new(inner); + Self { inner } + } + pub fn with_origin(inner: T, origin: Uri) -> Self { + let inner = tonic::client::Grpc::with_origin(inner, origin); + Self { inner } + } + pub fn with_interceptor( + inner: T, + interceptor: F, + ) -> SearcherServiceClient> + where + F: tonic::service::Interceptor, + T::ResponseBody: Default, + T: tonic::codegen::Service< + http::Request, + Response = http::Response< + >::ResponseBody, + >, + >, + , + >>::Error: Into + std::marker::Send + std::marker::Sync, + { + SearcherServiceClient::new(InterceptedService::new(inner, interceptor)) + } + /// Compress requests with the given encoding. + /// + /// This requires the server to support it otherwise it might respond with an + /// error. + #[must_use] + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.send_compressed(encoding); + self + } + /// Enable decompressing responses. + #[must_use] + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.accept_compressed(encoding); + self + } + /// Limits the maximum size of a decoded message. + /// + /// Default: `4MB` + #[must_use] + pub fn max_decoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_decoding_message_size(limit); + self + } + /// Limits the maximum size of an encoded message. + /// + /// Default: `usize::MAX` + #[must_use] + pub fn max_encoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_encoding_message_size(limit); + self + } + /// Searchers can invoke this endpoint to subscribe to their respective bundle results. + /// A success result would indicate the bundle won its state auction and was submitted to the validator. + pub async fn subscribe_bundle_results( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response>, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/searcher.SearcherService/SubscribeBundleResults", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new("searcher.SearcherService", "SubscribeBundleResults"), + ); + self.inner.server_streaming(req, path, codec).await + } + pub async fn send_bundle( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/searcher.SearcherService/SendBundle", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert(GrpcMethod::new("searcher.SearcherService", "SendBundle")); + self.inner.unary(req, path, codec).await + } + /// Returns the next scheduled leader connected to the block engine. + pub async fn get_next_scheduled_leader( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/searcher.SearcherService/GetNextScheduledLeader", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new("searcher.SearcherService", "GetNextScheduledLeader"), + ); + self.inner.unary(req, path, codec).await + } + /// Returns leader slots for connected jito validators during the current epoch. Only returns data for this region. + pub async fn get_connected_leaders( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/searcher.SearcherService/GetConnectedLeaders", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new("searcher.SearcherService", "GetConnectedLeaders"), + ); + self.inner.unary(req, path, codec).await + } + /// Returns leader slots for connected jito validators during the current epoch. + pub async fn get_connected_leaders_regioned( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/searcher.SearcherService/GetConnectedLeadersRegioned", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new( + "searcher.SearcherService", + "GetConnectedLeadersRegioned", + ), + ); + self.inner.unary(req, path, codec).await + } + /// Returns the tip accounts searchers shall transfer funds to for the leader to claim. + pub async fn get_tip_accounts( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/searcher.SearcherService/GetTipAccounts", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert(GrpcMethod::new("searcher.SearcherService", "GetTipAccounts")); + self.inner.unary(req, path, codec).await + } + /// Returns region the client is directly connected to, along with all available regions + pub async fn get_regions( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/searcher.SearcherService/GetRegions", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert(GrpcMethod::new("searcher.SearcherService", "GetRegions")); + self.inner.unary(req, path, codec).await + } + } +} diff --git a/jito_protos/src/grpc/shared.rs b/jito_protos/src/grpc/shared.rs new file mode 100755 index 0000000..b2b0680 --- /dev/null +++ b/jito_protos/src/grpc/shared.rs @@ -0,0 +1,18 @@ +// This file is @generated by prost-build. +#[derive(Clone, Copy, PartialEq, ::prost::Message)] +pub struct Header { + #[prost(message, optional, tag = "1")] + pub ts: ::core::option::Option<::prost_types::Timestamp>, +} +#[derive(Clone, Copy, PartialEq, ::prost::Message)] +pub struct Heartbeat { + #[prost(uint64, tag = "1")] + pub count: u64, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Socket { + #[prost(string, tag = "1")] + pub ip: ::prost::alloc::string::String, + #[prost(int64, tag = "2")] + pub port: i64, +} diff --git a/jito_protos/src/grpc/shredstream.rs b/jito_protos/src/grpc/shredstream.rs new file mode 100755 index 0000000..1ee983f --- /dev/null +++ b/jito_protos/src/grpc/shredstream.rs @@ -0,0 +1,136 @@ +// This file is @generated by prost-build. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Heartbeat { + /// don't trust IP:PORT from tcp header since it can be tampered over the wire + /// `socket.ip` must match incoming packet's ip. this prevents spamming an unwitting destination + #[prost(message, optional, tag = "1")] + pub socket: ::core::option::Option, + /// regions for shredstream proxy to receive shreds from + /// list of valid regions: + #[prost(string, repeated, tag = "2")] + pub regions: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, +} +#[derive(Clone, Copy, PartialEq, ::prost::Message)] +pub struct HeartbeatResponse { + /// client must respond within `ttl_ms` to keep stream alive + #[prost(uint32, tag = "1")] + pub ttl_ms: u32, +} +/// Generated client implementations. +pub mod shredstream_client { + #![allow( + unused_variables, + dead_code, + missing_docs, + clippy::wildcard_imports, + clippy::let_unit_value, + )] + use tonic::codegen::*; + use tonic::codegen::http::Uri; + #[derive(Debug, Clone)] + pub struct ShredstreamClient { + inner: tonic::client::Grpc, + } + impl ShredstreamClient { + /// Attempt to create a new client by connecting to a given endpoint. + pub async fn connect(dst: D) -> Result + where + D: TryInto, + D::Error: Into, + { + let conn = tonic::transport::Endpoint::new(dst)?.connect().await?; + Ok(Self::new(conn)) + } + } + impl ShredstreamClient + where + T: tonic::client::GrpcService, + T::Error: Into, + T::ResponseBody: Body + std::marker::Send + 'static, + ::Error: Into + std::marker::Send, + { + pub fn new(inner: T) -> Self { + let inner = tonic::client::Grpc::new(inner); + Self { inner } + } + pub fn with_origin(inner: T, origin: Uri) -> Self { + let inner = tonic::client::Grpc::with_origin(inner, origin); + Self { inner } + } + pub fn with_interceptor( + inner: T, + interceptor: F, + ) -> ShredstreamClient> + where + F: tonic::service::Interceptor, + T::ResponseBody: Default, + T: tonic::codegen::Service< + http::Request, + Response = http::Response< + >::ResponseBody, + >, + >, + , + >>::Error: Into + std::marker::Send + std::marker::Sync, + { + ShredstreamClient::new(InterceptedService::new(inner, interceptor)) + } + /// Compress requests with the given encoding. + /// + /// This requires the server to support it otherwise it might respond with an + /// error. + #[must_use] + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.send_compressed(encoding); + self + } + /// Enable decompressing responses. + #[must_use] + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.accept_compressed(encoding); + self + } + /// Limits the maximum size of a decoded message. + /// + /// Default: `4MB` + #[must_use] + pub fn max_decoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_decoding_message_size(limit); + self + } + /// Limits the maximum size of an encoded message. + /// + /// Default: `usize::MAX` + #[must_use] + pub fn max_encoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_encoding_message_size(limit); + self + } + /// RPC endpoint to send heartbeats to keep shreds flowing + pub async fn send_heartbeat( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/shredstream.Shredstream/SendHeartbeat", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert(GrpcMethod::new("shredstream.Shredstream", "SendHeartbeat")); + self.inner.unary(req, path, codec).await + } + } +} diff --git a/jito_protos/src/grpc/trace_shred.rs b/jito_protos/src/grpc/trace_shred.rs new file mode 100755 index 0000000..35c5558 --- /dev/null +++ b/jito_protos/src/grpc/trace_shred.rs @@ -0,0 +1,13 @@ +// This file is @generated by prost-build. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct TraceShred { + /// source region, one of: + #[prost(string, tag = "1")] + pub region: ::prost::alloc::string::String, + /// timestamp of creation + #[prost(message, optional, tag = "2")] + pub created_at: ::core::option::Option<::prost_types::Timestamp>, + /// monotonically increases, resets upon service restart + #[prost(uint32, tag = "3")] + pub seq_num: u32, +} diff --git a/jito_protos/src/lib.rs b/jito_protos/src/lib.rs new file mode 100755 index 0000000..d969c16 --- /dev/null +++ b/jito_protos/src/lib.rs @@ -0,0 +1,3 @@ +pub mod convert; +pub mod grpc; +pub use grpc::*; \ No newline at end of file diff --git a/src/accounts/global.rs b/src/accounts/global.rs index 3da2250..91aa052 100755 --- a/src/accounts/global.rs +++ b/src/accounts/global.rs @@ -25,9 +25,9 @@ use solana_sdk::pubkey::Pubkey; use borsh::{BorshDeserialize, BorshSerialize}; - +use serde::{Serialize, Deserialize}; /// Represents the global configuration account for token pricing and fees -#[derive(Debug, Clone, BorshSerialize, BorshDeserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct GlobalAccount { /// Unique identifier for the global account pub discriminator: u64, diff --git a/src/common/logs_data.rs b/src/common/logs_data.rs index 27df2c1..05ae2d6 100755 --- a/src/common/logs_data.rs +++ b/src/common/logs_data.rs @@ -13,6 +13,7 @@ pub enum DexInstruction { #[derive(Clone, Debug, Default, PartialEq, BorshDeserialize, BorshSerialize)] pub struct CreateTokenInfo { + pub slot: u64, pub name: String, pub symbol: String, pub uri: String, @@ -23,6 +24,7 @@ pub struct CreateTokenInfo { #[derive(Clone, Debug, Default, PartialEq, BorshDeserialize, BorshSerialize)] pub struct TradeInfo { + pub slot: u64, pub mint: Pubkey, pub sol_amount: u64, pub token_amount: u64, diff --git a/src/common/logs_events.rs b/src/common/logs_events.rs index 4b8be65..bb7dc13 100755 --- a/src/common/logs_events.rs +++ b/src/common/logs_events.rs @@ -8,6 +8,7 @@ pub const PROGRAM_DATA: &str = "Program data: "; #[derive(Debug)] pub enum PumpfunEvent { NewToken(CreateTokenInfo), + NewDevTrade(TradeInfo), NewUserTrade(TradeInfo), NewBotTrade(TradeInfo), Error(String), diff --git a/src/common/logs_parser.rs b/src/common/logs_parser.rs index 62ed4d9..7da29d6 100755 --- a/src/common/logs_parser.rs +++ b/src/common/logs_parser.rs @@ -94,6 +94,7 @@ pub fn parse_create_token_data(data: &str) -> ClientResult { let user = bs58::encode(&decoded[cursor..cursor+32]).into_string(); Ok(CreateTokenInfo { + slot: 0, name, symbol, uri, @@ -158,6 +159,7 @@ pub fn parse_trade_data(data: &str) -> ClientResult { let real_token_reserves = u64::from_le_bytes(decoded[cursor..cursor + 8].try_into().unwrap()); Ok(TradeInfo { + slot: 0, mint: Pubkey::from_str(&mint).unwrap(), sol_amount, token_amount, diff --git a/src/common/mod.rs b/src/common/mod.rs index 3dd3acf..af56e04 100755 --- a/src/common/mod.rs +++ b/src/common/mod.rs @@ -3,3 +3,6 @@ pub mod logs_parser; pub mod logs_filters; pub mod logs_subscribe; pub mod logs_events; +pub mod types; + +pub use types::*; diff --git a/src/common/types.rs b/src/common/types.rs new file mode 100755 index 0000000..dced0e9 --- /dev/null +++ b/src/common/types.rs @@ -0,0 +1,93 @@ +use std::sync::Arc; + +use solana_client::rpc_client::RpcClient; +use solana_sdk::{commitment_config::CommitmentConfig, signature::Keypair}; +use serde::Deserialize; +use crate::{constants::trade::{DEFAULT_BUY_TIP_FEE, DEFAULT_COMPUTE_UNIT_LIMIT, DEFAULT_COMPUTE_UNIT_PRICE, DEFAULT_SELL_TIP_FEE}, jito::FeeClient}; + +#[derive(Debug, Clone, PartialEq)] +pub enum FeeType { + Jito, + NextBlock, +} + +#[derive(Debug, Clone)] +pub struct Cluster { + pub rpc_url: String, + pub block_engine_url: String, + pub nextblock_url: String, + pub nextblock_auth_token: String, + pub zeroslot_url: String, + pub zeroslot_auth_token: String, + pub use_jito: bool, + pub use_nextblock: bool, + pub use_zeroslot: bool, + pub priority_fee: PriorityFee, + pub commitment: CommitmentConfig, +} + +impl Cluster { + pub fn new( + rpc_url: String, + block_engine_url: + String, nextblock_url: + String, nextblock_auth_token: + String, zeroslot_url: String, + zeroslot_auth_token: String, + priority_fee: PriorityFee, + commitment: CommitmentConfig, + use_jito: bool, + use_nextblock: bool, + use_zeroslot: bool + ) -> Self { + Self { + rpc_url, + block_engine_url, + nextblock_url, + nextblock_auth_token, + zeroslot_url, + zeroslot_auth_token, + priority_fee, + commitment, + use_jito, + use_nextblock, + use_zeroslot + } + } +} + +#[derive(Debug, Deserialize, Clone, Copy, PartialEq)] + +pub struct PriorityFee { + pub unit_limit: u32, + pub unit_price: u64, + pub buy_tip_fee: f64, + pub sell_tip_fee: f64, +} + +impl Default for PriorityFee { + fn default() -> Self { + Self { + unit_limit: DEFAULT_COMPUTE_UNIT_LIMIT, + unit_price: DEFAULT_COMPUTE_UNIT_PRICE, + buy_tip_fee: DEFAULT_BUY_TIP_FEE, + sell_tip_fee: DEFAULT_SELL_TIP_FEE + } + } +} + +pub type SolanaRpcClient = solana_client::nonblocking::rpc_client::RpcClient; + +pub struct MethodArgs { + pub payer: Arc, + pub rpc: Arc, + pub nonblocking_rpc: Arc, + pub jito_client: Arc, +} + +impl MethodArgs { + pub fn new(payer: Arc, rpc: Arc, nonblocking_rpc: Arc, jito_client: Arc) -> Self { + Self { payer, rpc, nonblocking_rpc, jito_client } + } +} + diff --git a/src/constants/mod.rs b/src/constants/mod.rs index fdcec09..645b3f7 100755 --- a/src/constants/mod.rs +++ b/src/constants/mod.rs @@ -27,6 +27,8 @@ pub mod seeds { /// Constants related to program accounts and authorities pub mod accounts { + use std::collections::{HashMap, HashSet}; + use solana_sdk::{pubkey, pubkey::Pubkey}; /// Public key for the Pump.fun program @@ -50,13 +52,53 @@ pub mod accounts { /// Rent Sysvar ID pub const RENT: Pubkey = pubkey!("SysvarRent111111111111111111111111111111111"); + + pub const JITO_TIP_ACCOUNTS: [&str; 8] = [ + "96gYZGLnJYVFmbjzopPSU6QiEV5fGqZNyN9nmNhvrZU5", + "HFqU5x63VTqvQss8hp11i4wVV8bD44PvwucfZ2bU7gRe", + "Cw8CFyM9FkoMi7K7Crf6HNQqf4uEMzpKw6QNghXLvLkY", + "ADaUMid9yfUytqMBgopwjb2DTLSokTSzL1zt6iGPaS49", + "DfXygSm4jCyNCybVYYK6DwvWqjKee8pbDmJGcLWNDXjh", + "ADuUkR4vqLUMWXxW9gh6D6L8pMSawimctcNZ5pGwDcEt", + "DttWaMuVvTiduZRnguLF7jNxTgiMBZ1hyAumKUiL2KRL", + "3AVi9Tg9Uo68tJfuvoKvqKNWKkC5wPdSSdeBnizKZ6jT", + ]; + + + /// Tip accounts + pub const NEXTBLOCK_TIP_ACCOUNTS: &[&str] = &[ + "NextbLoCkVtMGcV47JzewQdvBpLqT9TxQFozQkN98pE", + "NexTbLoCkWykbLuB1NkjXgFWkX9oAtcoagQegygXXA2", + "NeXTBLoCKs9F1y5PJS9CKrFNNLU1keHW71rfh7KgA1X", + "NexTBLockJYZ7QD7p2byrUa6df8ndV2WSd8GkbWqfbb", + "neXtBLock1LeC67jYd1QdAa32kbVeubsfPNTJC1V5At", + "nEXTBLockYgngeRmRrjDV31mGSekVPqZoMGhQEZtPVG", + "NEXTbLoCkB51HpLBLojQfpyVAMorm3zzKg7w9NFdqid", + "nextBLoCkPMgmG8ZgJtABeScP35qLa2AMCNKntAP7Xc" + ]; + + pub const ZEROSLOT_TIP_ACCOUNTS: &[&str] = &[ + "Eb2KpSC8uMt9GmzyAEm5Eb1AAAgTjRaXWFjKyFXHZxF3", + "FCjUJZ1qozm1e8romw216qyfQMaaWKxWsuySnumVCCNe", + "ENxTEjSQ1YabmUpXAdCgevnHQ9MHdLv8tzFiuiYJqa13", + "6rYLG55Q9RpsPGvqdPNJs4z5WTxJVatMB8zV3WJhs5EK", + "Cix2bHfqPcKcM233mzxbLk14kSggUUiz2A87fJtGivXr", + ]; + + pub const AMM_PROGRAM: Pubkey = pubkey!("675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8"); } pub mod trade { - pub const JITO_TIP_AMOUNT: f64 = 0.0001; + pub const TRADER_TIP_AMOUNT: f64 = 0.0001; pub const DEFAULT_SLIPPAGE: u64 = 3000; // 30% pub const DEFAULT_COMPUTE_UNIT_LIMIT: u32 = 78000; pub const DEFAULT_COMPUTE_UNIT_PRICE: u64 = 500000; - pub const DEFAULT_BUY_JITO_FEE: f64 = 0.0006; - pub const DEFAULT_SELL_JITO_FEE: f64 = 0.00006; + pub const DEFAULT_BUY_TIP_FEE: f64 = 0.0006; + pub const DEFAULT_SELL_TIP_FEE: f64 = 0.0001; +} + +pub struct Symbol; + +impl Symbol { + pub const SOLANA: &'static str = "solana"; } diff --git a/src/grpc/mod.rs b/src/grpc/mod.rs index 8148d6f..721927b 100755 --- a/src/grpc/mod.rs +++ b/src/grpc/mod.rs @@ -2,8 +2,10 @@ use std::{collections::HashMap, fmt, time::Duration}; use futures::{channel::mpsc, sink::Sink, Stream, StreamExt, SinkExt}; use rustls::crypto::{ring::default_provider, CryptoProvider}; +use tonic::codec::CompressionEncoding; use tonic::{transport::channel::ClientTlsConfig, Status}; use yellowstone_grpc_client::{GeyserGrpcClient, GeyserGrpcClientResult}; +use yellowstone_grpc_proto::geyser::SubscribeUpdateSlot; use yellowstone_grpc_proto::geyser::{ CommitmentLevel, SubscribeRequest, SubscribeRequestFilterTransactions, SubscribeUpdate, SubscribeUpdateTransaction, subscribe_update::UpdateOneof, SubscribeRequestPing, @@ -15,7 +17,9 @@ use solana_transaction_status::{ option_serializer::OptionSerializer, EncodedTransactionWithStatusMeta, UiTransactionEncoding, }; +use crate::common::logs_data::DexInstruction; use crate::common::logs_events::PumpfunEvent; +use crate::common::logs_filters::LogFilter; use crate::error::{ClientError, ClientResult}; type TransactionsFilterMap = HashMap; @@ -25,11 +29,13 @@ const CONNECT_TIMEOUT: u64 = 10; const REQUEST_TIMEOUT: u64 = 60; const CHANNEL_SIZE: usize = 1000; +#[derive(Clone)] pub struct TransactionPretty { pub slot: u64, pub signature: Signature, pub is_vote: bool, pub tx: EncodedTransactionWithStatusMeta, + // pub transaction: Option, } impl fmt::Debug for TransactionPretty { @@ -54,6 +60,7 @@ impl fmt::Debug for TransactionPretty { impl From for TransactionPretty { fn from(SubscribeUpdateTransaction { transaction, slot }: SubscribeUpdateTransaction) -> Self { let tx = transaction.expect("should be defined"); + // let transaction_info = tx.transaction.clone().unwrap(); Self { slot, signature: Signature::try_from(tx.signature.as_slice()).expect("valid signature"), @@ -62,10 +69,12 @@ impl From for TransactionPretty { .expect("valid tx with meta") .encode(UiTransactionEncoding::Base64, Some(u8::MAX), true) .expect("failed to encode"), + // transaction: Some(transaction_info), } } } +#[derive(Clone)] pub struct YellowstoneGrpc { endpoint: String, } @@ -109,43 +118,6 @@ impl YellowstoneGrpc { Ok(client.subscribe_with_request(Some(subscribe_request)).await) } - pub async fn subscribe_pumpfun(&self, callback: F, bot_wallet: Option) -> ClientResult<()> - where - F: Fn(PumpfunEvent) + Send + Sync + 'static, - { - let addrs = vec![PUMP_PROGRAM_ID.to_string()]; - let transactions = self.get_subscribe_request_filter(addrs, vec![], vec![]); - let (mut subscribe_tx, mut stream) = self.connect(transactions).await? - .map_err(|e| ClientError::Other(format!("Failed to subscribe: {:?}", e)))?; - let (mut tx, mut rx) = mpsc::channel::(CHANNEL_SIZE); - - let callback = Box::new(callback); - - tokio::spawn(async move { - while let Some(message) = stream.next().await { - match message { - Ok(msg) => { - if let Err(e) = Self::handle_stream_message(msg, &mut tx, &mut subscribe_tx).await { - error!("Error handling message: {:?}", e); - break; - } - } - Err(error) => { - error!("Stream error: {error:?}"); - break; - } - } - } - }); - - while let Some(transaction_pretty) = rx.next().await { - if let Err(e) = Self::process_transaction(transaction_pretty, &*callback, bot_wallet).await { - error!("Error processing transaction: {:?}", e); - } - } - Ok(()) - } - pub fn get_subscribe_request_filter( &self, account_include: Vec, @@ -167,6 +139,43 @@ impl YellowstoneGrpc { transactions } + // pub fn get_subscribe_account_updater_request_filter( + // &self, + // account_include: Vec, + // account_exclude: Vec, + // account_required: Vec, + // ) -> TransactionsFilterMap { + // let mut transactions = HashMap::new(); + // transactions.insert( + // "client".to_string(), + // SubscribeUpdateAccount { + // account: account_include, + // slot: None, + // is_startup: None, + // }, + // ); + // transactions + // } + + pub fn get_subscribe_update_slot_request_filter( + &self, + account_include: Vec, + account_exclude: Vec, + account_required: Vec, + ) -> TransactionsFilterMap { + let mut transactions = HashMap::new(); + transactions.insert( + "client".to_string(), + SubscribeUpdateSlot { + slot: 0, + parent: None, + status: None, + dead_error: None, + }, + ); + transactions + } + async fn handle_stream_message( msg: SubscribeUpdate, tx: &mut mpsc::Sender, @@ -195,10 +204,62 @@ impl YellowstoneGrpc { Ok(()) } - async fn process_transaction(transaction_pretty: TransactionPretty, callback: &F, bot_wallet: Option) -> ClientResult<()> + // pub async fn subscribe_account_updater(&self, callback: F, bot_wallet: Option) -> ClientResult<()> + // where + // F: Fn(PumpfunEvent) + Send + Sync + 'static, + // { + // let addrs = vec![PUMP_PROGRAM_ID.to_string()]; + // let transactions = self.get_subscribe_request_filter(addrs, vec![], vec![]); + // let (mut subscribe_tx, mut stream) = self.connect(transactions).await? + // .map_err(|e| ClientError::Other(format!("Failed to subscribe: {:?}", e)))?; + // let (mut tx, mut rx) = mpsc::channel::(CHANNEL_SIZE); + + // let callback = Box::new(callback); + + // } + + pub async fn subscribe_pumpfun(&self, callback: F, bot_wallet: Option) -> ClientResult<()> + where + F: Fn(PumpfunEvent) + Send + Sync + 'static, + { + let addrs = vec![PUMP_PROGRAM_ID.to_string()]; + let transactions = self.get_subscribe_request_filter(addrs, vec![], vec![]); + let (mut subscribe_tx, mut stream) = self.connect(transactions).await? + .map_err(|e| ClientError::Other(format!("Failed to subscribe: {:?}", e)))?; + let (mut tx, mut rx) = mpsc::channel::(CHANNEL_SIZE); + + let callback = Box::new(callback); + + tokio::spawn(async move { + while let Some(message) = stream.next().await { + match message { + Ok(msg) => { + if let Err(e) = Self::handle_stream_message(msg, &mut tx, &mut subscribe_tx).await { + error!("Error handling message: {:?}", e); + break; + } + } + Err(error) => { + error!("Stream error: {error:?}"); + break; + } + } + } + }); + + while let Some(transaction_pretty) = rx.next().await { + if let Err(e) = Self::process_pumpfun_transaction(transaction_pretty, &*callback, bot_wallet).await { + error!("Error processing transaction: {:?}", e); + } + } + Ok(()) + } + + async fn process_pumpfun_transaction(transaction_pretty: TransactionPretty, callback: &F, bot_wallet: Option) -> ClientResult<()> where F: Fn(PumpfunEvent) + Send + Sync, { + let slot = transaction_pretty.slot; let trade_raw = transaction_pretty.tx; let meta = trade_raw.meta.as_ref() .ok_or_else(|| ClientError::Other("Missing transaction metadata".to_string()))?; @@ -213,21 +274,31 @@ impl YellowstoneGrpc { &vec![] }; - let (create_event, trade_event) = PumpfunEvent::parse_logs(logs); - if let Some(create_event) = create_event { - callback(PumpfunEvent::NewToken(create_event)); - } - if let Some(trade_event) = trade_event { - if let Some(bot_wallet_pubkey) = bot_wallet { - if trade_event.user == bot_wallet_pubkey { - callback(PumpfunEvent::NewBotTrade(trade_event)); - } else { - callback(PumpfunEvent::NewUserTrade(trade_event)); + let mut dev_address: Option = None; + let instructions = LogFilter::parse_instruction(logs, bot_wallet).unwrap(); + for instruction in instructions { + match instruction { + DexInstruction::CreateToken(mut token_info) => { + token_info.slot = slot; + dev_address = Some(token_info.user); + callback(PumpfunEvent::NewToken(token_info)); } - } else { - callback(PumpfunEvent::NewUserTrade(trade_event)); + DexInstruction::UserTrade(mut trade_info) => { + trade_info.slot = slot; + if Some(trade_info.user) == dev_address { + callback(PumpfunEvent::NewDevTrade(trade_info)); + } else { + callback(PumpfunEvent::NewUserTrade(trade_info)); + } + } + DexInstruction::BotTrade(mut trade_info) => { + trade_info.slot = slot; + callback(PumpfunEvent::NewBotTrade(trade_info)); + } + _ => {} } } + Ok(()) } } diff --git a/src/instruction/mod.rs b/src/instruction/mod.rs index 8d205c7..c12b7af 100755 --- a/src/instruction/mod.rs +++ b/src/instruction/mod.rs @@ -10,7 +10,20 @@ //! - `buy`: Instruction to buy tokens from a bonding curve by providing SOL. //! - `sell`: Instruction to sell tokens back to the bonding curve in exchange for SOL. -use crate::{constants, trade::common::{get_bonding_curve_pda, get_global_pda, get_metadata_pda, get_mint_authority_pda}, PumpFun}; +use std::sync::Arc; + +use spl_associated_token_account::instruction::create_associated_token_account; +use spl_token::instruction::close_account; +use crate::common::SolanaRpcClient; +use crate::constants::trade::DEFAULT_SLIPPAGE; +use crate::ipfs::TokenMetadataIPFS; +use crate::pumpfun::common::{calculate_with_slippage_buy, calculate_with_slippage_sell, get_bonding_curve_account, get_buy_amount_with_slippage, get_global_account, get_initial_buy_price, get_token_balance, get_token_balance_and_ata}; +use crate::{ + constants, + pumpfun::common::{ + get_bonding_curve_pda, get_global_pda, get_metadata_pda, get_mint_authority_pda + }, +}; use spl_associated_token_account::get_associated_token_address; use solana_sdk::{ @@ -20,6 +33,7 @@ use solana_sdk::{ signer::Signer, }; +use anyhow::{anyhow, Result}; pub struct Create { pub _name: String, pub _symbol: String, @@ -205,3 +219,157 @@ pub fn sell( ], ) } + +pub async fn build_create_and_buy_instructions( + rpc: Arc, + payer: Arc, + mint: Arc, + ipfs: TokenMetadataIPFS, + amount_sol: u64, + slippage_basis_points: Option, +) -> Result, anyhow::Error> { + if amount_sol == 0 { + return Err(anyhow!("build_create_and_buy_instructions: Amount cannot be zero")); + } + + let rpc = rpc.as_ref(); + let global_account = get_global_account(&rpc).await?; + let buy_amount = global_account.get_initial_buy_price(amount_sol); + let buy_amount_with_slippage = + get_buy_amount_with_slippage(amount_sol, slippage_basis_points); + + let mut instructions = vec![]; + + instructions.push(create( + payer.as_ref(), + mint.as_ref(), + Create { + _name: ipfs.metadata.name.clone(), + _symbol: ipfs.metadata.symbol.clone(), + _uri: ipfs.metadata_uri.clone(), + }, + )); + + let ata = get_associated_token_address(&payer.pubkey(), &mint.pubkey()); + instructions.push(create_associated_token_account( + &payer.pubkey(), + &payer.pubkey(), + &mint.pubkey(), + &constants::accounts::TOKEN_PROGRAM, + )); + + instructions.push(buy( + payer.as_ref(), + &mint.pubkey(), + &global_account.fee_recipient, + Buy { + _amount: buy_amount, + _max_sol_cost: buy_amount_with_slippage, + }, + )); + + Ok(instructions) +} + +pub async fn build_buy_instructions( + rpc: Arc, + payer: Arc, + mint: Arc, + amount_sol: u64, + slippage_basis_points: Option, +) -> Result, anyhow::Error> { + if amount_sol == 0 { + return Err(anyhow!("build_buy_instructions:Amount cannot be zero")); + } + + let global_account = get_global_account(&rpc).await?; + let buy_amount = match get_bonding_curve_account(&rpc, mint.as_ref()).await { + Ok(account) => { + account.get_buy_price(amount_sol).map_err(|e| anyhow!(e))? + }, + Err(_e) => { + let initial_buy_amount = get_initial_buy_price(&global_account, amount_sol).await?; + initial_buy_amount * 80 / 100 + } + }; + + let buy_amount_with_slippage = calculate_with_slippage_buy(amount_sol, slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE)); + let mut instructions = vec![]; + // let ata = get_associated_token_address(&payer.pubkey(), &mint); + // match rpc.get_account(&ata).await { + // Ok(_) => {}, + // Err(_) => { + // instructions.push(create_associated_token_account( + // &payer.pubkey(), + // &payer.pubkey(), + // &mint, + // &constants::accounts::TOKEN_PROGRAM, + // )); + // } + // } + + instructions.push(create_associated_token_account( + &payer.pubkey(), + &payer.pubkey(), + &mint, + &constants::accounts::TOKEN_PROGRAM, + )); + + instructions.push(buy( + payer.as_ref(), + &mint, + &global_account.fee_recipient, + Buy { + _amount: buy_amount, + _max_sol_cost: buy_amount_with_slippage, + }, + )); + + Ok(instructions) +} + +pub async fn build_sell_instructions( + rpc: Arc, + payer: Arc, + mint: Arc, + amount_token: u64, + slippage_basis_points: Option, +) -> Result, anyhow::Error> { + if amount_token == 0 { + return Err(anyhow!("build_sell_instructions: Amount cannot be zero")); + } + + let ata = get_associated_token_address(&payer.pubkey(), mint.as_ref()); + let global_account = get_global_account(&rpc).await?; + let bonding_curve_account = get_bonding_curve_account(&rpc, mint.as_ref()).await?; + let min_sol_output = bonding_curve_account + .get_sell_price(amount_token, global_account.fee_basis_points) + .map_err(|e| anyhow!(e))?; + let min_sol_output_with_slippage = calculate_with_slippage_sell( + min_sol_output, + slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE), + ); + + let mut instructions = vec![]; + + instructions.push(sell( + payer.as_ref(), + &mint, + &global_account.fee_recipient, + Sell { + _amount: amount_token, + _min_sol_output: min_sol_output_with_slippage, + }, + )); + + instructions.push(close_account( + &spl_token::ID, + &ata, + &payer.pubkey(), + &payer.pubkey(), + &[&payer.pubkey()], + )?); + + Ok(instructions) +} + diff --git a/src/jito/api.rs b/src/jito/api.rs index 9cac97c..a69e66d 100755 --- a/src/jito/api.rs +++ b/src/jito/api.rs @@ -1,18 +1,462 @@ -use std::env; - -use anyhow::{Context, Result}; -use reqwest::Proxy; -use serde::{Deserialize, Serialize}; -use serde_json::{json, Value}; -use std::convert::TryFrom; - -#[derive(Debug)] -pub struct TipAccountResult { - pub accounts: Vec, +// This file is @generated by prost-build. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct PostSubmitRequest { + #[prost(message, optional, tag = "1")] + pub transaction: ::core::option::Option, + #[prost(bool, tag = "2")] + pub skip_pre_flight: bool, + #[prost(bool, optional, tag = "3")] + pub front_running_protection: ::core::option::Option, + #[prost(bool, optional, tag = "8")] + pub experimental_front_running_protection: ::core::option::Option, + #[prost(bool, optional, tag = "9")] + pub snipe_transaction: ::core::option::Option, } - -impl TipAccountResult { - pub fn from(accounts: Vec) -> Result { - Ok(TipAccountResult { accounts }) +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct PostSubmitRequestEntry { + #[prost(message, optional, tag = "1")] + pub transaction: ::core::option::Option, + #[prost(bool, tag = "2")] + pub skip_pre_flight: bool, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct PostSubmitBatchRequest { + #[prost(message, repeated, tag = "1")] + pub entries: ::prost::alloc::vec::Vec, + #[prost(enumeration = "SubmitStrategy", tag = "2")] + pub submit_strategy: i32, + #[prost(bool, optional, tag = "3")] + pub use_bundle: ::core::option::Option, + #[prost(bool, optional, tag = "4")] + pub front_running_protection: ::core::option::Option, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct PostSubmitBatchResponseEntry { + #[prost(string, tag = "1")] + pub signature: ::prost::alloc::string::String, + #[prost(string, tag = "2")] + pub error: ::prost::alloc::string::String, + #[prost(bool, tag = "3")] + pub submitted: bool, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct PostSubmitBatchResponse { + #[prost(message, repeated, tag = "1")] + pub transactions: ::prost::alloc::vec::Vec, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct PostSubmitResponse { + #[prost(string, tag = "1")] + pub signature: ::prost::alloc::string::String, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct TransactionMessage { + #[prost(string, tag = "1")] + pub content: ::prost::alloc::string::String, + #[prost(bool, tag = "2")] + pub is_cleanup: bool, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct TransactionMessageV2 { + #[prost(string, tag = "1")] + pub content: ::prost::alloc::string::String, +} +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] +#[repr(i32)] +pub enum SubmitStrategy { + PUknown = 0, + PSubmitAll = 1, + PAbortOnFirstError = 2, + PWaitForConfirmation = 3, +} +impl SubmitStrategy { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + Self::PUknown => "P_UKNOWN", + Self::PSubmitAll => "P_SUBMIT_ALL", + Self::PAbortOnFirstError => "P_ABORT_ON_FIRST_ERROR", + Self::PWaitForConfirmation => "P_WAIT_FOR_CONFIRMATION", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "P_UKNOWN" => Some(Self::PUknown), + "P_SUBMIT_ALL" => Some(Self::PSubmitAll), + "P_ABORT_ON_FIRST_ERROR" => Some(Self::PAbortOnFirstError), + "P_WAIT_FOR_CONFIRMATION" => Some(Self::PWaitForConfirmation), + _ => None, + } } } +/// Generated client implementations. +pub mod api_client { + #![allow( + unused_variables, + dead_code, + missing_docs, + clippy::wildcard_imports, + clippy::let_unit_value, + )] + use tonic::codegen::*; + use tonic::codegen::http::Uri; + #[derive(Debug, Clone)] + pub struct ApiClient { + inner: tonic::client::Grpc, + } + impl ApiClient { + /// Attempt to create a new client by connecting to a given endpoint. + pub async fn connect(dst: D) -> Result + where + D: std::convert::TryInto, + D::Error: Into, + { + let conn = tonic::transport::Endpoint::new(dst)?.connect().await?; + Ok(Self::new(conn)) + } + } + impl ApiClient + where + T: tonic::client::GrpcService, + T::Error: Into, + T::ResponseBody: Body + std::marker::Send + 'static, + ::Error: Into + std::marker::Send, + { + pub fn new(inner: T) -> Self { + let inner = tonic::client::Grpc::new(inner); + Self { inner } + } + pub fn with_origin(inner: T, origin: Uri) -> Self { + let inner = tonic::client::Grpc::with_origin(inner, origin); + Self { inner } + } + pub fn with_interceptor( + inner: T, + interceptor: F, + ) -> ApiClient> + where + F: tonic::service::Interceptor, + T::ResponseBody: Default, + T: tonic::codegen::Service< + http::Request, + Response = http::Response< + >::ResponseBody, + >, + >, + , + >>::Error: Into + std::marker::Send + std::marker::Sync, + { + ApiClient::new(InterceptedService::new(inner, interceptor)) + } + /// Compress requests with the given encoding. + /// + /// This requires the server to support it otherwise it might respond with an + /// error. + #[must_use] + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.send_compressed(encoding); + self + } + /// Enable decompressing responses. + #[must_use] + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.accept_compressed(encoding); + self + } + /// Limits the maximum size of a decoded message. + /// + /// Default: `4MB` + #[must_use] + pub fn max_decoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_decoding_message_size(limit); + self + } + /// Limits the maximum size of an encoded message. + /// + /// Default: `usize::MAX` + #[must_use] + pub fn max_encoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_encoding_message_size(limit); + self + } + pub async fn post_submit_v2( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static("/api.Api/PostSubmitV2"); + let mut req = request.into_request(); + req.extensions_mut().insert(GrpcMethod::new("api.Api", "PostSubmitV2")); + self.inner.unary(req, path, codec).await + } + pub async fn post_submit_batch_v2( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/api.Api/PostSubmitBatchV2", + ); + let mut req = request.into_request(); + req.extensions_mut().insert(GrpcMethod::new("api.Api", "PostSubmitBatchV2")); + self.inner.unary(req, path, codec).await + } + } +} +/// Generated server implementations. +pub mod api_server { + #![allow( + unused_variables, + dead_code, + missing_docs, + clippy::wildcard_imports, + clippy::let_unit_value, + )] + use tonic::codegen::*; + /// Generated trait containing gRPC methods that should be implemented for use with ApiServer. + #[async_trait] + pub trait Api: std::marker::Send + std::marker::Sync + 'static { + async fn post_submit_v2( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; + async fn post_submit_batch_v2( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; + } + #[derive(Debug)] + pub struct ApiServer { + inner: Arc, + accept_compression_encodings: EnabledCompressionEncodings, + send_compression_encodings: EnabledCompressionEncodings, + max_decoding_message_size: Option, + max_encoding_message_size: Option, + } + impl ApiServer { + pub fn new(inner: T) -> Self { + Self::from_arc(Arc::new(inner)) + } + pub fn from_arc(inner: Arc) -> Self { + Self { + inner, + accept_compression_encodings: Default::default(), + send_compression_encodings: Default::default(), + max_decoding_message_size: None, + max_encoding_message_size: None, + } + } + pub fn with_interceptor( + inner: T, + interceptor: F, + ) -> InterceptedService + where + F: tonic::service::Interceptor, + { + InterceptedService::new(Self::new(inner), interceptor) + } + /// Enable decompressing requests with the given encoding. + #[must_use] + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.accept_compression_encodings.enable(encoding); + self + } + /// Compress responses with the given encoding, if the client supports it. + #[must_use] + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.send_compression_encodings.enable(encoding); + self + } + /// Limits the maximum size of a decoded message. + /// + /// Default: `4MB` + #[must_use] + pub fn max_decoding_message_size(mut self, limit: usize) -> Self { + self.max_decoding_message_size = Some(limit); + self + } + /// Limits the maximum size of an encoded message. + /// + /// Default: `usize::MAX` + #[must_use] + pub fn max_encoding_message_size(mut self, limit: usize) -> Self { + self.max_encoding_message_size = Some(limit); + self + } + } + impl tonic::codegen::Service> for ApiServer + where + T: Api, + B: Body + std::marker::Send + 'static, + B::Error: Into + std::marker::Send + 'static, + { + type Response = http::Response; + type Error = std::convert::Infallible; + type Future = BoxFuture; + fn poll_ready( + &mut self, + _cx: &mut Context<'_>, + ) -> Poll> { + Poll::Ready(Ok(())) + } + fn call(&mut self, req: http::Request) -> Self::Future { + match req.uri().path() { + "/api.Api/PostSubmitV2" => { + #[allow(non_camel_case_types)] + struct PostSubmitV2Svc(pub Arc); + impl tonic::server::UnaryService + for PostSubmitV2Svc { + type Response = super::PostSubmitResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::post_submit_v2(&inner, request).await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let method = PostSubmitV2Svc(inner); + let codec = tonic::codec::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + "/api.Api/PostSubmitBatchV2" => { + #[allow(non_camel_case_types)] + struct PostSubmitBatchV2Svc(pub Arc); + impl< + T: Api, + > tonic::server::UnaryService + for PostSubmitBatchV2Svc { + type Response = super::PostSubmitBatchResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::post_submit_batch_v2(&inner, request).await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let method = PostSubmitBatchV2Svc(inner); + let codec = tonic::codec::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + _ => { + Box::pin(async move { + let mut response = http::Response::new(empty_body()); + let headers = response.headers_mut(); + headers + .insert( + tonic::Status::GRPC_STATUS, + (tonic::Code::Unimplemented as i32).into(), + ); + headers + .insert( + http::header::CONTENT_TYPE, + tonic::metadata::GRPC_CONTENT_TYPE, + ); + Ok(response) + }) + } + } + } + } + impl Clone for ApiServer { + fn clone(&self) -> Self { + let inner = self.inner.clone(); + Self { + inner, + accept_compression_encodings: self.accept_compression_encodings, + send_compression_encodings: self.send_compression_encodings, + max_decoding_message_size: self.max_decoding_message_size, + max_encoding_message_size: self.max_encoding_message_size, + } + } + } + /// Generated gRPC service name + pub const SERVICE_NAME: &str = "api.Api"; + impl tonic::server::NamedService for ApiServer { + const NAME: &'static str = SERVICE_NAME; + } +} \ No newline at end of file diff --git a/src/jito/client_error.rs b/src/jito/client_error.rs deleted file mode 100755 index 14cd2af..0000000 --- a/src/jito/client_error.rs +++ /dev/null @@ -1,125 +0,0 @@ -pub use reqwest; -use solana_rpc_client_api::{client_error::ErrorKind, request}; -use solana_sdk::{ - signature::SignerError, transaction::TransactionError, transport::TransportError, -}; -use thiserror::Error as ThisError; - -use crate::jito::request::RpcRequest; - -#[derive(ThisError, Debug)] -#[error("{kind}")] -pub struct Error { - pub request: Option, - - #[source] - pub kind: ErrorKind, -} - -impl Error { - pub fn new_with_request(kind: ErrorKind, request: RpcRequest) -> Self { - Self { - request: Some(request), - kind, - } - } - - pub fn into_with_request(self, request: RpcRequest) -> Self { - Self { - request: Some(request), - ..self - } - } - - pub fn request(&self) -> Option<&RpcRequest> { - self.request.as_ref() - } - - pub fn kind(&self) -> &ErrorKind { - &self.kind - } - - pub fn get_transaction_error(&self) -> Option { - self.kind.get_transaction_error() - } -} - -impl From for Error { - fn from(kind: ErrorKind) -> Self { - Self { - request: None, - kind, - } - } -} - -impl From for Error { - fn from(err: TransportError) -> Self { - Self { - request: None, - kind: err.into(), - } - } -} - -impl From for TransportError { - fn from(client_error: Error) -> Self { - client_error.kind.into() - } -} - -impl From for Error { - fn from(err: std::io::Error) -> Self { - Self { - request: None, - kind: err.into(), - } - } -} - -impl From for Error { - fn from(err: reqwest::Error) -> Self { - Self { - request: None, - kind: ErrorKind::Custom(format!("Reqwest error: {}", err)), - } - } -} - -impl From for Error { - fn from(err: request::RpcError) -> Self { - Self { - request: None, - kind: err.into(), - } - } -} - -impl From for Error { - fn from(err: serde_json::error::Error) -> Self { - Self { - request: None, - kind: err.into(), - } - } -} - -impl From for Error { - fn from(err: SignerError) -> Self { - Self { - request: None, - kind: err.into(), - } - } -} - -impl From for Error { - fn from(err: TransactionError) -> Self { - Self { - request: None, - kind: err.into(), - } - } -} - -pub type Result = std::result::Result; diff --git a/src/jito/common.rs b/src/jito/common.rs new file mode 100755 index 0000000..fcb6205 --- /dev/null +++ b/src/jito/common.rs @@ -0,0 +1,127 @@ +use bincode::serialize; +use serde_json::json; +use solana_client::rpc_client::SerializableTransaction; +use solana_sdk::signature::Signature; +use solana_sdk::transaction::Transaction; +use solana_transaction_status::{TransactionConfirmationStatus, UiTransactionEncoding}; +use std::str::FromStr; +use std::time::{Duration, Instant}; +use tokio::time::sleep; +use crate::common::types::SolanaRpcClient; +use anyhow::Result; +use base64::Engine; +use base64::engine::general_purpose::STANDARD; +use reqwest::Client; + +pub async fn poll_transaction_confirmation(rpc: &SolanaRpcClient, txt_sig: Signature) -> Result { + // 15 second timeout + let timeout: Duration = Duration::from_secs(15); + // 5 second retry interval + let interval: Duration = Duration::from_secs(5); + let start: Instant = Instant::now(); + + loop { + if start.elapsed() >= timeout { + return Err(anyhow::anyhow!("Transaction {}'s confirmation timed out", txt_sig)); + } + + let status = rpc.get_signature_statuses(&[txt_sig]).await?; + + match status.value[0].clone() { + Some(status) => { + if status.err.is_none() + && (status.confirmation_status == Some(TransactionConfirmationStatus::Confirmed) + || status.confirmation_status == Some(TransactionConfirmationStatus::Finalized)) + { + return Ok(txt_sig); + } + if status.err.is_some() { + return Err(anyhow::anyhow!(status.err.unwrap())); + } + } + None => { + sleep(interval).await; + } + } + } +} + +pub async fn send_nb_transaction(client: Client, endpoint: &str, auth_token: &str, transaction: &Transaction) -> Result { + // 序列化交易 + let serialized = bincode::serialize(transaction) + .map_err(|e| anyhow::anyhow!("序列化交易失败: {}", e))?; + + // Base64编码 + let encoded = STANDARD.encode(serialized); + + let request_data = json!({ + "transaction": { + "content": encoded + }, + "frontRunningProtection": true + }); + + let url = format!("{}/api/v2/submit", endpoint); + let response = client + .post(url) + .header("Authorization", auth_token) + .header("Content-Type", "application/json") + .json(&request_data) + .send() + .await + .map_err(|e| anyhow::anyhow!("请求失败: {}", e))?; + + let resp = response.json::().await + .map_err(|e| anyhow::anyhow!("解析响应失败: {}", e))?; + + if let Some(reason) = resp["reason"].as_str() { + return Err(anyhow::anyhow!(reason.to_string())); + } + + let signature = resp["signature"].as_str() + .ok_or_else(|| anyhow::anyhow!("响应中缺少signature字段"))?; + + let signature = Signature::from_str(signature) + .map_err(|e| anyhow::anyhow!("无效的签名: {}", e))?; + + Ok(signature) +} + +pub async fn serialize_and_encode( + transaction: &Vec, + encoding: UiTransactionEncoding, +) -> Result { + let serialized = match encoding { + UiTransactionEncoding::Base58 => bs58::encode(transaction).into_string(), + UiTransactionEncoding::Base64 => STANDARD.encode(transaction), + _ => return Err(anyhow::anyhow!("Unsupported encoding")), + }; + Ok(serialized) +} + +pub async fn serialize_transaction_and_encode( + transaction: &impl SerializableTransaction, + encoding: UiTransactionEncoding, +) -> Result { + let serialized_tx = serialize(transaction)?; + let serialized = match encoding { + UiTransactionEncoding::Base58 => bs58::encode(serialized_tx).into_string(), + UiTransactionEncoding::Base64 => STANDARD.encode(serialized_tx), + _ => return Err(anyhow::anyhow!("Unsupported encoding")), + }; + Ok(serialized) +} + +pub async fn serialize_smart_transaction_and_encode( + transaction: &impl SerializableTransaction, + encoding: UiTransactionEncoding, +) -> Result<(String, Signature)> { + let signature = transaction.get_signature(); + let serialized_tx = serialize(transaction)?; + let serialized = match encoding { + UiTransactionEncoding::Base58 => bs58::encode(serialized_tx).into_string(), + UiTransactionEncoding::Base64 => STANDARD.encode(serialized_tx), + _ => return Err(anyhow::anyhow!("Unsupported encoding")), + }; + Ok((serialized, *signature)) +} \ No newline at end of file diff --git a/src/jito/http_sender.rs b/src/jito/http_sender.rs deleted file mode 100755 index 44c8caf..0000000 --- a/src/jito/http_sender.rs +++ /dev/null @@ -1,218 +0,0 @@ -use std::{ - sync::{ - atomic::{AtomicU64, Ordering}, - Arc, RwLock, - }, - time::{Duration, Instant}, -}; - -use async_trait::async_trait; -use log::debug; -use reqwest::{ - self, - header::{CONTENT_TYPE, RETRY_AFTER}, - StatusCode, -}; -use solana_rpc_client_api::{ - custom_error, - error_object::RpcErrorObject, - request::{RpcError, RpcResponseErrorData}, - response::RpcSimulateTransactionResult, -}; -use tokio::time::sleep; - -use crate::jito::{client_error::Result, request::RpcRequest, rpc_sender::RpcSender}; - -pub struct HttpSender { - client: Arc, - url: String, - request_id: AtomicU64, - stats: RwLock, -} - -/// Nonblocking [`RpcSender`] over HTTP. -impl HttpSender { - /// Create an HTTP RPC sender. - /// - /// The URL is an HTTP URL, usually for port 8899, as in - /// "http://localhost:8899". The sender has a default timeout of 30 seconds. - pub fn new(url: U) -> Self { - Self::new_with_timeout(url, Duration::from_secs(30)) - } - - /// Create an HTTP RPC sender. - /// - /// The URL is an HTTP URL, usually for port 8899. - pub fn new_with_timeout(url: U, timeout: Duration) -> Self { - let client = Arc::new( - reqwest::Client::builder() - .timeout(timeout) - .pool_idle_timeout(timeout) - .build() - .expect("build rpc client"), - ); - - Self { - client, - url: url.to_string(), - request_id: AtomicU64::new(0), - stats: RwLock::new(solana_rpc_client::rpc_sender::RpcTransportStats::default()), - } - } -} - -struct StatsUpdater<'a> { - stats: &'a RwLock, - request_start_time: Instant, - rate_limited_time: Duration, -} - -impl<'a> StatsUpdater<'a> { - fn new(stats: &'a RwLock) -> Self { - Self { - stats, - request_start_time: Instant::now(), - rate_limited_time: Duration::default(), - } - } - - fn add_rate_limited_time(&mut self, duration: Duration) { - self.rate_limited_time += duration; - } -} - -impl<'a> Drop for StatsUpdater<'a> { - fn drop(&mut self) { - let mut stats = self.stats.write().unwrap(); - stats.request_count += 1; - stats.elapsed_time += Instant::now().duration_since(self.request_start_time); - stats.rate_limited_time += self.rate_limited_time; - } -} - -#[async_trait] -impl RpcSender for HttpSender { - fn get_transport_stats(&self) -> solana_rpc_client::rpc_sender::RpcTransportStats { - self.stats.read().unwrap().clone() - } - - async fn send( - &self, - request: RpcRequest, - params: serde_json::Value, - ) -> Result { - let mut stats_updater = StatsUpdater::new(&self.stats); - - let request_id = self.request_id.fetch_add(1, Ordering::Relaxed); - let request_json = request.build_request_json(request_id, params).to_string(); - - let mut too_many_requests_retries = 5; - loop { - let response = { - let client = self.client.clone(); - let request_json = request_json.clone(); - client - .post(&self.url) - .header(CONTENT_TYPE, "application/json") - .body(request_json) - .send() - .await - }?; - - if !response.status().is_success() { - if response.status() == StatusCode::TOO_MANY_REQUESTS - && too_many_requests_retries > 0 - { - let mut duration = Duration::from_millis(500); - if let Some(retry_after) = response.headers().get(RETRY_AFTER) { - if let Ok(retry_after) = retry_after.to_str() { - if let Ok(retry_after) = retry_after.parse::() { - if retry_after < 120 { - duration = Duration::from_secs(retry_after); - } - } - } - } - - too_many_requests_retries -= 1; - debug!( - "Too many requests: server responded with {:?}, {} retries left, pausing for {:?}", - response, too_many_requests_retries, duration - ); - - sleep(duration).await; - stats_updater.add_rate_limited_time(duration); - continue; - } - return Err(response.error_for_status().unwrap_err().into()); - } - - let mut json = response.json::().await?; - if json["error"].is_object() { - return match serde_json::from_value::(json["error"].clone()) { - Ok(rpc_error_object) => { - let data = match rpc_error_object.code { - solana_rpc_client_api::custom_error::JSON_RPC_SERVER_ERROR_SEND_TRANSACTION_PREFLIGHT_FAILURE => { - match serde_json::from_value::(json["error"]["data"].clone()) { - Ok(data) => RpcResponseErrorData::SendTransactionPreflightFailure(data), - Err(err) => { - debug!("Failed to deserialize RpcSimulateTransactionResult: {:?}", err); - RpcResponseErrorData::Empty - } - } - }, - custom_error::JSON_RPC_SERVER_ERROR_NODE_UNHEALTHY => { - match serde_json::from_value::(json["error"]["data"].clone()) { - Ok(custom_error::NodeUnhealthyErrorData {num_slots_behind}) => RpcResponseErrorData::NodeUnhealthy {num_slots_behind}, - Err(_err) => { - RpcResponseErrorData::Empty - } - } - }, - _ => RpcResponseErrorData::Empty - }; - - Err(RpcError::RpcResponseError { - code: rpc_error_object.code, - message: rpc_error_object.message, - data, - } - .into()) - } - Err(err) => Err(RpcError::RpcRequestError(format!( - "Failed to deserialize RPC error response: {} [{}]", - serde_json::to_string(&json["error"]).unwrap(), - err - )) - .into()), - }; - } - return Ok(json["result"].take()); - } - } - - fn url(&self) -> String { - self.url.clone() - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[tokio::test(flavor = "multi_thread")] - async fn http_sender_on_tokio_multi_thread() { - let http_sender = HttpSender::new("http://localhost:1234".to_string()); - let _ = http_sender - .send(RpcRequest::GetTipAccounts, serde_json::Value::Null) - .await; - } - - #[tokio::test(flavor = "current_thread")] - async fn http_sender_on_tokio_current_thread() { - let http_sender = HttpSender::new("http://localhost:1234".to_string()); - let _ = http_sender - .send(RpcRequest::GetTipAccounts, serde_json::Value::Null) - .await; - } -} diff --git a/src/jito/mod.rs b/src/jito/mod.rs index cee2175..c227e96 100755 --- a/src/jito/mod.rs +++ b/src/jito/mod.rs @@ -1,105 +1,349 @@ +use api::api_client::ApiClient; +use common::{poll_transaction_confirmation, serialize_smart_transaction_and_encode}; +use jito_protos::{searcher::searcher_service_client::SearcherServiceClient, shredstream::shredstream_client::ShredstreamClient}; +use reqwest::Client; +use searcher_client::{get_searcher_client_no_auth, send_bundle_with_confirmation}; +use serde_json::json; +use tonic::transport::Channel; +use tracing::instrument::WithSubscriber; +use yellowstone_grpc_client::Interceptor; +use std::{sync::Arc, time::Instant}; +use tokio::sync::{Mutex, RwLock}; + +use solana_sdk::signature::Signature; + use std::str::FromStr; +use rustls::crypto::{ring::default_provider, CryptoProvider}; + +use tonic::{service::interceptor::InterceptedService, transport::Uri, Status}; +use std::time::Duration; +use solana_transaction_status::UiTransactionEncoding; +use tonic::transport::ClientTlsConfig; use anyhow::{anyhow, Result}; -use api::TipAccountResult; -use rand::seq::IteratorRandom; -use solana_sdk::{ - pubkey::Pubkey, - transaction::{Transaction, VersionedTransaction}, -}; -use tokio::sync::RwLock; -use tracing::error; +use rand::{rng, seq::{IndexedRandom, IteratorRandom}}; +use solana_sdk::transaction::VersionedTransaction; +use crate::{common::SolanaRpcClient, constants::accounts::{JITO_TIP_ACCOUNTS, NEXTBLOCK_TIP_ACCOUNTS, ZEROSLOT_TIP_ACCOUNTS}}; + +pub mod common; +pub mod searcher_client; pub mod api; -pub mod client_error; -pub mod http_sender; -pub mod request; -pub mod rpc_client; -pub mod rpc_sender; -use crate::jito::rpc_client::RpcClient; - -pub struct JitoClient { - base_url: String, - tip_accounts: RwLock>, - client: RpcClient, +lazy_static::lazy_static! { + static ref TIP_ACCOUNT_CACHE: RwLock> = RwLock::new(Vec::new()); } -impl Clone for JitoClient { - fn clone(&self) -> Self { - Self { - base_url: self.base_url.clone(), - tip_accounts: RwLock::new(Vec::new()), - client: RpcClient::new(self.base_url.clone()), +#[derive(Debug, Clone, Copy)] +pub enum ClientType { + Jito, + NextBlock, + ZeroSlot, +} + +pub type FeeClient = dyn FeeClientTrait + Send + Sync + 'static; + +#[async_trait::async_trait] +pub trait FeeClientTrait { + async fn send_transaction(&self, transaction: &VersionedTransaction) -> Result; + async fn send_transactions(&self, transactions: &Vec) -> Result>; + async fn get_tip_account(&self) -> Result; + async fn get_client_type(&self) -> ClientType; +} + +pub struct JitoClient { + pub rpc_client: Arc, + pub searcher_client: Arc>>, +} + +#[async_trait::async_trait] +impl FeeClientTrait for JitoClient { + async fn send_transaction(&self, transaction: &VersionedTransaction) -> Result { + self.send_bundle_with_confirmation(&vec![transaction.clone()]).await?.first().cloned().ok_or(anyhow!("Failed to send transaction")) + } + + async fn send_transactions(&self, transactions: &Vec) -> Result, anyhow::Error> { + self.send_bundle_with_confirmation(transactions).await + } + + async fn get_tip_account(&self) -> Result { + if let Some(acc) = JITO_TIP_ACCOUNTS.iter().choose(&mut rng()) { + Ok(acc.to_string()) + } else { + Err(anyhow!("no valid tip accounts found")) } } + + async fn get_client_type(&self) -> ClientType { + ClientType::Jito + } } impl JitoClient { - pub fn new(jito_url: &str, _uuid: Option) -> Self { - Self { - base_url: jito_url.to_string(), - tip_accounts: RwLock::new(vec![]), - client: RpcClient::new(jito_url.to_string()), + pub async fn new(rpc_url: String, block_engine_url: String) -> Result { + let rpc_client = SolanaRpcClient::new(rpc_url); + let searcher_client = get_searcher_client_no_auth(block_engine_url.as_str()).await?; + Ok(Self { rpc_client: Arc::new(rpc_client), searcher_client: Arc::new(Mutex::new(searcher_client)) }) + } + + pub async fn send_bundle_with_confirmation( + &self, + transactions: &Vec, + ) -> Result, anyhow::Error> { + send_bundle_with_confirmation(self.rpc_client.clone(), &transactions, self.searcher_client.clone()).await + } + + pub async fn send_bundle_no_wait( + &self, + transactions: &Vec, + ) -> Result, anyhow::Error> { + searcher_client::send_bundle_no_wait(&transactions, self.searcher_client.clone()).await + } + + // pub async fn get_tip_accounts(&self) -> Result, anyhow::Error> { + // let client = ShredstreamClient::connect("dst").await?; + // // let subscriber = Dispatch::new(tracing_subscriber::fmt::Subscriber::builder().finish()); + // let subscriber = tracing::subscriber::set_global_default(tracing_subscriber::fmt::Subscriber::builder().finish()).unwrap(); + // let aaa = client.with_subscriber(subscriber); + + // let mut stream = client.subscribe_accounts_of_interest(tonic::Request::new(())); + // let mut accounts = Vec::new(); + // while let Some(Ok(response)) = stream.next().await { + // accounts.extend(response.accounts); + // } + // Ok(accounts) + // } +} + +#[derive(Clone)] +pub struct MyInterceptor { + auth_token: String, +} + +impl MyInterceptor { + pub fn new(auth_token: String) -> Self { + Self { auth_token } + } +} + +impl Interceptor for MyInterceptor { + fn call(&mut self, mut request: tonic::Request<()>) -> Result, Status> { + request.metadata_mut().insert( + "authorization", + tonic::metadata::MetadataValue::from_str(&self.auth_token) + .map_err(|_| Status::invalid_argument("Invalid auth token"))? + ); + Ok(request) + } +} + +#[derive(Clone)] +pub struct NextBlockClient { + pub rpc_client: Arc, + pub client: ApiClient>, +} + +#[async_trait::async_trait] +impl FeeClientTrait for NextBlockClient { + async fn send_transaction(&self, transaction: &VersionedTransaction) -> Result { + self.send_transaction(transaction).await + } + + async fn send_transactions(&self, transactions: &Vec) -> Result, anyhow::Error> { + self.send_transactions(transactions).await + } + + async fn get_tip_account(&self) -> Result { + let tip_account = self.get_tip_account().await?; + Ok(tip_account) + } + + async fn get_client_type(&self) -> ClientType { + ClientType::NextBlock + } +} + +impl NextBlockClient { + pub fn new(rpc_url: String, endpoint: String, auth_token: String) -> Self { + if CryptoProvider::get_default().is_none() { + let _ = default_provider() + .install_default() + .map_err(|e| anyhow::anyhow!("Failed to install crypto provider: {:?}", e)); } + + let endpoint = endpoint.parse::().unwrap(); + let tls = ClientTlsConfig::new().with_native_roots(); + let channel = Channel::builder(endpoint) + .tls_config(tls).expect("Failed to create TLS config") + .tcp_keepalive(Some(Duration::from_secs(60))) + .http2_keep_alive_interval(Duration::from_secs(30)) + .keep_alive_while_idle(true) + .timeout(Duration::from_secs(30)) + .connect_timeout(Duration::from_secs(10)) + .connect_lazy(); + + let client = ApiClient::with_interceptor(channel, MyInterceptor::new(auth_token)); + let rpc_client = SolanaRpcClient::new(rpc_url); + Self { rpc_client: Arc::new(rpc_client), client } } - pub async fn get_tip_accounts(&self) -> Result { - let result = self.client.get_tip_accounts().await?; - TipAccountResult::from(result).map_err(|e| anyhow!(e)) + pub async fn send_transaction(&self, transaction: &VersionedTransaction) -> Result { + let (content, signature) = serialize_smart_transaction_and_encode(transaction, UiTransactionEncoding::Base64).await?; + + self.client.clone().post_submit_v2(api::PostSubmitRequest { + transaction: Some(api::TransactionMessage { + content, + is_cleanup: false, + }), + skip_pre_flight: true, + front_running_protection: Some(true), + experimental_front_running_protection: Some(true), + snipe_transaction: Some(true), + }).await?; + + let timeout: Duration = Duration::from_secs(10); + let start_time: Instant = Instant::now(); + while Instant::now().duration_since(start_time) < timeout { + match poll_transaction_confirmation(&self.rpc_client, signature).await { + Ok(sig) => return Ok(sig), + Err(_) => continue, + } + } + + Ok(signature) } - pub async fn init_tip_accounts(&self) -> Result<()> { - let accounts = self.get_tip_accounts().await?; - let mut tip_accounts = self.tip_accounts.write().await; - *tip_accounts = accounts.accounts.iter().map(|a| a.to_string()).collect(); - Ok(()) - } + pub async fn send_transactions(&self, transactions: &Vec) -> Result, anyhow::Error> { + let mut entries = Vec::new(); + let encoding = UiTransactionEncoding::Base64; + + let mut signatures = Vec::new(); + for transaction in transactions { + let (content, signature) = serialize_smart_transaction_and_encode(transaction, encoding).await?; + entries.push(api::PostSubmitRequestEntry { + transaction: Some(api::TransactionMessage { + content, + is_cleanup: false, + }), + skip_pre_flight: true, + }); + signatures.push(signature); + } - pub async fn get_tip_account(&self) -> Result { - { - let accounts = self.tip_accounts.read().await; - if !accounts.is_empty() { - if let Some(acc) = accounts.iter().choose(&mut rand::rng()) { - return Pubkey::from_str(acc) - .map_err(|err| { - error!("jito: failed to parse Pubkey: {:?}", err); - anyhow!("Invalid pubkey format") - }); + self.client.clone().post_submit_batch_v2(api::PostSubmitBatchRequest { + entries, + submit_strategy: api::SubmitStrategy::PSubmitAll as i32, + use_bundle: Some(true), + front_running_protection: Some(true), + }).await?; + + let timeout: Duration = Duration::from_secs(10); + let start_time: Instant = Instant::now(); + while Instant::now().duration_since(start_time) < timeout { + for signature in signatures.clone() { + match poll_transaction_confirmation(&self.rpc_client, signature).await { + Ok(sig) => signatures.push(sig), + Err(_) => continue, } } } - self.init_tip_accounts().await?; - - let accounts = self.tip_accounts.read().await; - accounts - .iter() - .choose(&mut rand::rng()) - .ok_or_else(|| anyhow!("jito: no tip accounts available")) - .and_then(|acc| { - Pubkey::from_str(acc).map_err(|err| { - error!("jito: failed to parse Pubkey: {:?}", err); - anyhow!("Invalid pubkey format") - }) - }) + Ok(signatures) } - pub async fn send_transaction( - &self, - transaction: &Transaction, - ) -> Result { - let bundles = vec![VersionedTransaction::from(transaction.clone())]; - Ok(self.client.send_bundle(&bundles).await?) - } - - pub async fn send_transactions( - &self, - transactions: &Vec, - ) -> Result { - let bundles: Vec = transactions.iter() - .map(|t| VersionedTransaction::from(t.clone())) - .collect(); // 显式指定类型 - Ok(self.client.send_bundle(&bundles).await?) + async fn get_tip_account(&self) -> Result { + let tip_account = *NEXTBLOCK_TIP_ACCOUNTS.choose(&mut rand::rng()).or_else(|| NEXTBLOCK_TIP_ACCOUNTS.first()).unwrap(); + Ok(tip_account.to_string()) } } + +#[derive(Clone)] +pub struct ZeroSlotClient { + pub endpoint: String, + pub auth_token: String, + pub rpc_client: Arc, +} + +#[async_trait::async_trait] +impl FeeClientTrait for ZeroSlotClient { + async fn send_transaction(&self, transaction: &VersionedTransaction) -> Result { + self.send_transaction(transaction).await + } + + async fn send_transactions(&self, transactions: &Vec) -> Result, anyhow::Error> { + self.send_transactions(transactions).await + } + + async fn get_tip_account(&self) -> Result { + let tip_account = self.get_tip_account().await?; + Ok(tip_account) + } + + async fn get_client_type(&self) -> ClientType { + ClientType::ZeroSlot + } +} + +impl ZeroSlotClient { + pub fn new(rpc_url: String, endpoint: String, auth_token: String) -> Self { + let rpc_client = SolanaRpcClient::new(rpc_url); + Self { rpc_client: Arc::new(rpc_client), endpoint, auth_token } + } + + pub async fn send_transaction(&self, transaction: &VersionedTransaction) -> Result { + let (content, signature) = serialize_smart_transaction_and_encode(transaction, UiTransactionEncoding::Base64).await?; + + let client = Client::new(); + let request_body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "sendTransaction", + "params": [ + content, + { + "encoding": "base64", + "skipPreflight": true, + } + ] + }); + + // Send the request + let response = client.post(format!("{}/?api-key={}", self.endpoint, self.auth_token)) + .json(&request_body) + .send() + .await?; + + // Parse the response + let response_json: serde_json::Value = response.json().await?; + if let Some(result) = response_json.get("result") { + println!("Transaction sent successfully: {}", result); + } else if let Some(error) = response_json.get("error") { + eprintln!("Failed to send transaction: {}", error); + } + + let timeout: Duration = Duration::from_secs(10); + let start_time: Instant = Instant::now(); + while Instant::now().duration_since(start_time) < timeout { + match poll_transaction_confirmation(&self.rpc_client, signature).await { + Ok(sig) => return Ok(sig), + Err(_) => continue, + } + } + + Ok(signature) + } + + pub async fn send_transactions(&self, transactions: &Vec) -> Result, anyhow::Error> { + let mut signatures = Vec::new(); + for transaction in transactions { + let signature = self.send_transaction(transaction).await?; + signatures.push(signature); + } + Ok(signatures) + } + + async fn get_tip_account(&self) -> Result { + let tip_account = *ZEROSLOT_TIP_ACCOUNTS.choose(&mut rand::rng()).or_else(|| NEXTBLOCK_TIP_ACCOUNTS.first()).unwrap(); + Ok(tip_account.to_string()) + } +} \ No newline at end of file diff --git a/src/jito/request.rs b/src/jito/request.rs deleted file mode 100755 index 6bbbfa0..0000000 --- a/src/jito/request.rs +++ /dev/null @@ -1,55 +0,0 @@ -use std::fmt; - -use serde_json::{json, Value}; - -#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)] -pub enum RpcRequest { - Custom { method: &'static str }, - GetBundlesStatuses, - GetTipAccounts, - SendBundle, -} - -impl fmt::Display for RpcRequest { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let method = match self { - RpcRequest::Custom { method } => method, - RpcRequest::GetBundlesStatuses => "getBundleStatuses", - RpcRequest::GetTipAccounts => "getTipAccounts", - RpcRequest::SendBundle => "sendBundle", - }; - - write!(f, "{method}") - } -} - -impl RpcRequest { - pub fn build_request_json(self, id: u64, params: Value) -> Value { - let jsonrpc = "2.0"; - json!({ - "jsonrpc": jsonrpc, - "id": id, - "method": format!("{self}"), - "params": params, - }) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_build_request_json() { - let test_request = RpcRequest::GetTipAccounts; - let request = test_request.build_request_json(1, json!([])); - assert_eq!(request["method"], "getTipAccounts"); - assert_eq!(request["params"], json!([])); - - let test_request = RpcRequest::GetBundlesStatuses; - let addr = json!("deadbeefXjn8o3yroDHxUtKsZZgoy4GPkPPXfouKNHhx"); - let request = test_request.build_request_json(1, json!([addr])); - assert_eq!(request["method"], "getBundleStatuses"); - assert_eq!(request["params"], json!([addr])); - } -} diff --git a/src/jito/rpc_client.rs b/src/jito/rpc_client.rs deleted file mode 100755 index a43c170..0000000 --- a/src/jito/rpc_client.rs +++ /dev/null @@ -1,201 +0,0 @@ -use std::time::Duration; - -use bincode::serialize; -use log::*; -use serde_json::{json, Value}; -use solana_rpc_client::{ - rpc_client::{RpcClientConfig, SerializableTransaction}, - rpc_sender::RpcTransportStats, -}; -use solana_rpc_client_api::{ - client_error::ErrorKind as ClientErrorKind, request::RpcError, response::Response, -}; -use solana_sdk::{bs58, commitment_config::CommitmentConfig}; -use solana_transaction_status::UiTransactionEncoding; - -use crate::jito::{ - client_error, - client_error::{Error as ClientError, Result as ClientResult}, - http_sender::HttpSender, - request::RpcRequest, - rpc_sender::*, -}; - -pub type RpcResult = client_error::Result>; - -pub struct RpcClient { - sender: Box, - config: RpcClientConfig, -} - -impl RpcClient { - pub fn new_sender( - sender: T, - config: RpcClientConfig, - ) -> Self { - Self { - sender: Box::new(sender), - config, - } - } - - pub fn new(url: String) -> Self { - Self::new_with_commitment(url, CommitmentConfig::default()) - } - - fn new_with_commitment(url: String, commitment_config: CommitmentConfig) -> Self { - Self::new_sender( - HttpSender::new(url), - RpcClientConfig::with_commitment(commitment_config), - ) - } - - pub fn new_with_timeout(url: String, timeout: Duration) -> Self { - Self::new_sender( - HttpSender::new_with_timeout(url, timeout), - RpcClientConfig::with_commitment(CommitmentConfig::default()), - ) - } - - pub fn url(&self) -> String { - self.sender.url() - } - - pub fn commitment(&self) -> CommitmentConfig { - self.config.commitment_config - } - - pub async fn send_bundle( - &self, - transactions: &[impl SerializableTransaction], - ) -> ClientResult { - let mut serialized_encoded: Vec = Vec::with_capacity(transactions.len()); - for transaction in transactions { - let encoding = self.default_cluster_transaction_encoding().await?; - serialized_encoded.push(serialize_and_encode(transaction, encoding)?); - } - match self - .send(RpcRequest::SendBundle, json!([serialized_encoded])) - .await - { - Ok(signature_base58_str) => ClientResult::Ok(signature_base58_str), - Err(err) => { - if let ClientErrorKind::RpcError(RpcError::RpcResponseError { - code, message, .. - }) = &err.kind - { - debug!("{} {}", code, message); - } - Err(err) - } - } - } - - async fn default_cluster_transaction_encoding( - &self, - ) -> Result { - Ok(UiTransactionEncoding::Base58) - } - - pub async fn get_bundle_statuses( - &self, - signatures: &[String], - ) -> RpcResult> { - self.send(RpcRequest::GetBundlesStatuses, json!([signatures])) - .await - } - - pub async fn get_tip_accounts(&self) -> ClientResult> { - self.send(RpcRequest::GetTipAccounts, Value::Null).await - } - - pub async fn send(&self, request: RpcRequest, params: Value) -> ClientResult - where - T: serde::de::DeserializeOwned, - { - assert!(params.is_array() || params.is_null()); - - let response = self - .sender - .send(request, params) - .await - .map_err(|err| err.into_with_request(request))?; - serde_json::from_value(response) - .map_err(|err| ClientError::new_with_request(err.into(), request)) - } - - pub fn get_transport_stats(&self) -> RpcTransportStats { - self.sender.get_transport_stats() - } -} - -fn serialize_and_encode(input: &T, encoding: UiTransactionEncoding) -> ClientResult -where - T: serde::ser::Serialize, -{ - let serialized = serialize(input) - .map_err(|e| ClientErrorKind::Custom(format!("Serialization failed: {e}")))?; - let encoded = match encoding { - UiTransactionEncoding::Base58 => bs58::encode(serialized).into_string(), - _ => { - return Err(ClientErrorKind::Custom(format!( - "unsupported encoding: {encoding}. Supported encodings: base58" - )) - .into()) - } - }; - Ok(encoded) -} - -#[cfg(test)] -mod rpc_client_tests { - use solana_program::hash::Hash; - use solana_sdk::{ - pubkey::Pubkey, signature::Signer, signer::keypair::Keypair, system_transaction, - transaction::VersionedTransaction, - }; - - use crate::jito::rpc_client::RpcClient; - - const SERVER_URL: &str = "http://0.0.0.0:8080/api/v1/bundles"; - - #[tokio::test] - pub async fn get_tip_accounts() { - let rpc_client = RpcClient::new(SERVER_URL.to_owned()); - let tip_accounts = rpc_client.get_tip_accounts().await; - println!("{:?}", tip_accounts); - } - - #[tokio::test] - pub async fn send_bundle() { - let rpc_client = RpcClient::new(SERVER_URL.to_owned()); - let signer_keypair = Keypair::new(); - let recent_blockhash = Hash::new_unique(); - let tip_account = Pubkey::try_from("DCN82qDxJAQuSqHhv2BJuAgi41SPeKZB5ioBCTMNDrCC").unwrap(); - - let mut bundle: Vec<_> = vec![VersionedTransaction::from(system_transaction::transfer( - &signer_keypair, - &signer_keypair.pubkey(), - 10000, - recent_blockhash, - ))]; - - bundle.push(VersionedTransaction::from(system_transaction::transfer( - &signer_keypair, - &tip_account, - 10000, - recent_blockhash, - ))); - let response = rpc_client.send_bundle(&bundle).await; - println!("{:?}", response); - } - - #[tokio::test] - pub async fn get_bundle_statuses() { - let rpc_client = RpcClient::new(SERVER_URL.to_owned()); - let bundle_id = - "6e4b90284778a40633b56e4289202ea79e62d2296bb3d45398bb93f6c9ec083d".to_owned(); - let response = rpc_client.get_bundle_statuses(&[bundle_id]).await; - println!("{:?}", response); - } -} diff --git a/src/jito/rpc_sender.rs b/src/jito/rpc_sender.rs deleted file mode 100755 index 228a789..0000000 --- a/src/jito/rpc_sender.rs +++ /dev/null @@ -1,21 +0,0 @@ -use async_trait::async_trait; -use solana_rpc_client::rpc_sender::RpcTransportStats; - -use crate::jito::{client_error::Result, request::RpcRequest}; - -/// A transport for RPC calls. -/// -/// `RpcSender` implements the underlying transport of requests to, and -/// responses from, a Solana node, and is used primarily by [`RpcClient`]. -/// -/// [`RpcClient`]: crate::rpc_client::RpcClient -#[async_trait] -pub trait RpcSender { - async fn send( - &self, - request: RpcRequest, - params: serde_json::Value, - ) -> Result; - fn get_transport_stats(&self) -> RpcTransportStats; - fn url(&self) -> String; -} diff --git a/src/jito/searcher_client.rs b/src/jito/searcher_client.rs new file mode 100755 index 0000000..2fdedf4 --- /dev/null +++ b/src/jito/searcher_client.rs @@ -0,0 +1,128 @@ +use std::{ + sync::Arc, + time::{Duration, Instant}, +}; + +use jito_protos::{ + bundle::{ + Bundle, BundleResult, + }, + convert::proto_packet_from_versioned_tx, + searcher::{ + searcher_service_client::SearcherServiceClient, SendBundleRequest, SubscribeBundleResultsRequest, + }, +}; +use solana_sdk::{ + signature::Signature, + transaction::VersionedTransaction, +}; +use thiserror::Error; +use tokio::sync::Mutex; +use tonic::{ + codec::CompressionEncoding, transport::{self, Channel, Endpoint}, Status +}; +use yellowstone_grpc_client::ClientTlsConfig; + +use crate::jito::common::poll_transaction_confirmation; +use crate::common::SolanaRpcClient; + +#[derive(Debug, Error)] +pub enum BlockEngineConnectionError { + #[error("transport error {0}")] + TransportError(#[from] transport::Error), + #[error("client error {0}")] + ClientError(#[from] Status), +} + +#[derive(Debug, Error)] +pub enum BundleRejectionError { + #[error("bundle lost state auction, auction: {0}, tip {1} lamports")] + StateAuctionBidRejected(String, u64), + #[error("bundle won state auction but failed global auction, auction {0}, tip {1} lamports")] + WinningBatchBidRejected(String, u64), + #[error("bundle simulation failure on tx {0}, message: {1:?}")] + SimulationFailure(String, Option), + #[error("internal error {0}")] + InternalError(String), +} + +pub type BlockEngineConnectionResult = Result; + +pub async fn get_searcher_client_no_auth( + block_engine_url: &str, +) -> BlockEngineConnectionResult> { + let searcher_channel = create_grpc_channel(block_engine_url).await?; + let searcher_client = SearcherServiceClient::new(searcher_channel); + Ok(searcher_client) +} + +pub async fn create_grpc_channel(url: &str) -> BlockEngineConnectionResult { + let mut endpoint = Endpoint::from_shared(url.to_string()).expect("invalid url"); + if url.starts_with("https") { + endpoint = endpoint.tls_config(ClientTlsConfig::new().with_native_roots())?; + } + + endpoint = endpoint.tcp_nodelay(true); + endpoint = endpoint.tcp_keepalive(Some(Duration::from_secs(10))); + endpoint = endpoint.connect_timeout(Duration::from_secs(20)); + endpoint = endpoint.http2_keep_alive_interval(Duration::from_secs(10)); + + Ok(endpoint.connect().await?) +} + +pub async fn subscribe_bundle_results( + searcher_client: Arc>>, + request: impl tonic::IntoRequest, +) -> std::result::Result< + tonic::Response>, + tonic::Status, +> { + let mut searcher = searcher_client.lock().await; + searcher.subscribe_bundle_results(request).await +} + +pub async fn send_bundle_with_confirmation( + rpc: Arc, + transactions: &Vec, + searcher_client: Arc>>, +) -> Result, anyhow::Error> { + let mut signatures = send_bundle_no_wait(transactions, searcher_client).await?; + + let timeout: Duration = Duration::from_secs(10); + let start_time: Instant = Instant::now(); + while Instant::now().duration_since(start_time) < timeout { + for signature in signatures.clone() { + match poll_transaction_confirmation(&rpc, signature).await { + Ok(sig) => signatures.push(sig), + Err(_) => continue, + } + } + } + + Ok(signatures) +} + +pub async fn send_bundle_no_wait( + transactions: &Vec, + searcher_client: Arc>>, +) -> Result, anyhow::Error> { + let mut packets = vec![]; + let mut signatures = vec![]; + for transaction in transactions { + let packet = proto_packet_from_versioned_tx(transaction); + packets.push(packet); + signatures.push(transaction.signatures[0]); + } + + let mut searcher = searcher_client.lock().await; + searcher + .send_bundle(SendBundleRequest { + bundle: Some(Bundle { + header: None, + packets, + }), + }) + .await?; + + Ok(signatures) +} diff --git a/src/jito/token_authenticator.rs b/src/jito/token_authenticator.rs new file mode 100755 index 0000000..917efb9 --- /dev/null +++ b/src/jito/token_authenticator.rs @@ -0,0 +1,167 @@ +use std::{ + sync::{Arc, RwLock}, + time::{Duration, SystemTime}, +}; + +use jito_protos::auth::{ + auth_service_client::AuthServiceClient, GenerateAuthChallengeRequest, + GenerateAuthTokensRequest, RefreshAccessTokenRequest, Role, Token, +}; +use prost_types::Timestamp; +use solana_metrics::datapoint_info; +use solana_sdk::signature::{Keypair, Signer}; +use tokio::{task::JoinHandle, time::sleep}; +use tonic::{service::Interceptor, transport::Channel, Request, Status}; + +use super::searcher_client::BlockEngineConnectionResult; + +const AUTHORIZATION_HEADER: &str = "authorization"; +const BEARER: &str = "Bearer "; + +/// Adds the token to each requests' authorization header. +/// Manages refreshing the token in a separate thread. +#[derive(Clone)] +pub struct ClientInterceptor { + /// The token added to each request header. + bearer_token: Arc>, +} + +impl ClientInterceptor { + pub async fn new( + mut auth_service_client: AuthServiceClient, + keypair: &Arc, + role: Role, + ) -> BlockEngineConnectionResult { + let (access_token, refresh_token) = + Self::auth(&mut auth_service_client, keypair, role).await?; + + let bearer_token = Arc::new(RwLock::new(access_token.value.clone())); + + let _refresh_token_thread = Self::spawn_token_refresh_thread( + auth_service_client, + bearer_token.clone(), + refresh_token, + access_token.expires_at_utc.unwrap(), + keypair.clone(), + role, + ); + + Ok(Self { bearer_token }) + } + + async fn auth( + auth_service_client: &mut AuthServiceClient, + keypair: &Keypair, + role: Role, + ) -> BlockEngineConnectionResult<(Token, Token)> { + let challenge_resp = auth_service_client + .generate_auth_challenge(GenerateAuthChallengeRequest { + role: role as i32, + pubkey: keypair.pubkey().as_ref().to_vec(), + }) + .await? + .into_inner(); + let challenge = format!("{}-{}", keypair.pubkey(), challenge_resp.challenge); + let signed_challenge = keypair.sign_message(challenge.as_bytes()).as_ref().to_vec(); + + let tokens = auth_service_client + .generate_auth_tokens(GenerateAuthTokensRequest { + challenge, + client_pubkey: keypair.pubkey().as_ref().to_vec(), + signed_challenge, + }) + .await? + .into_inner(); + + Ok((tokens.access_token.unwrap(), tokens.refresh_token.unwrap())) + } + + fn spawn_token_refresh_thread( + mut auth_service_client: AuthServiceClient, + bearer_token: Arc>, + refresh_token: Token, + access_token_expiration: Timestamp, + keypair: Arc, + role: Role, + ) -> JoinHandle> { + tokio::spawn(async move { + let mut refresh_token = refresh_token; + let mut access_token_expiration = access_token_expiration; + + loop { + let access_token_ttl = SystemTime::try_from(access_token_expiration.clone()) + .unwrap() + .duration_since(SystemTime::now()) + .unwrap_or_else(|_| Duration::from_secs(0)); + let refresh_token_ttl = + SystemTime::try_from(refresh_token.expires_at_utc.as_ref().unwrap().clone()) + .unwrap() + .duration_since(SystemTime::now()) + .unwrap_or_else(|_| Duration::from_secs(0)); + + let does_access_token_expire_soon = access_token_ttl < Duration::from_secs(5 * 60); + let does_refresh_token_expire_soon = + refresh_token_ttl < Duration::from_secs(5 * 60); + + match ( + does_refresh_token_expire_soon, + does_access_token_expire_soon, + ) { + // re-run entire auth workflow is refresh token expiring soon + (true, _) => { + let is_error = { + if let Ok((new_access_token, new_refresh_token)) = + Self::auth(&mut auth_service_client, &keypair, role).await + { + *bearer_token.write().unwrap() = new_access_token.value.clone(); + access_token_expiration = new_access_token.expires_at_utc.unwrap(); + refresh_token = new_refresh_token; + false + } else { + true + } + }; + datapoint_info!("searcher-full-auth", ("is_error", is_error, bool)); + } + // re-up the access token if it expires soon + (_, true) => { + let is_error = { + if let Ok(refresh_resp) = auth_service_client + .refresh_access_token(RefreshAccessTokenRequest { + refresh_token: refresh_token.value.clone(), + }) + .await + { + let access_token = refresh_resp.into_inner().access_token.unwrap(); + *bearer_token.write().unwrap() = access_token.value.clone(); + access_token_expiration = access_token.expires_at_utc.unwrap(); + false + } else { + true + } + }; + + datapoint_info!("searcher-refresh-auth", ("is_error", is_error, bool)); + } + _ => { + sleep(Duration::from_secs(60)).await; + } + } + } + }) + } +} + +impl Interceptor for ClientInterceptor { + fn call(&mut self, mut request: Request<()>) -> Result, Status> { + let l_token = self.bearer_token.read().unwrap(); + if !l_token.is_empty() { + request.metadata_mut().insert( + AUTHORIZATION_HEADER, + format!("{BEARER}{l_token}").parse().unwrap(), + ); + } + + Ok(request) + } +} diff --git a/src/lib.rs b/src/lib.rs index d241463..79a90b6 100755 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,254 +2,257 @@ pub mod accounts; pub mod constants; pub mod error; pub mod instruction; -pub mod jito; pub mod grpc; pub mod common; pub mod ipfs; pub mod trade; +pub mod jito; +pub mod pumpfun; use std::sync::Arc; -use anyhow::anyhow; -use solana_client::rpc_client::RpcClient; +use jito::{FeeClient, JitoClient, NextBlockClient, ZeroSlotClient}; +use rustls::crypto::{ring::default_provider, CryptoProvider}; use solana_sdk::{ commitment_config::CommitmentConfig, pubkey::Pubkey, - signature::{Keypair, Signer, Signature}, + signature::{Keypair, Signer}, }; -use common::{logs_data::TradeInfo, logs_events::PumpfunEvent, logs_subscribe}; +use common::{logs_data::TradeInfo, logs_events::PumpfunEvent, logs_subscribe, Cluster, PriorityFee, SolanaRpcClient}; use common::logs_subscribe::SubscriptionHandle; use ipfs::TokenMetadataIPFS; -use crate::jito::JitoClient; -use crate::trade::common::PriorityFee; - -#[derive(Clone)] pub struct PumpFun { pub payer: Arc, - pub rpc: Arc, - pub jito_client: Arc, + pub rpc: Arc, + pub fee_clients: Vec>, pub priority_fee: PriorityFee, + pub cluster: Cluster, +} + +impl Clone for PumpFun { + fn clone(&self) -> Self { + Self { + payer: self.payer.clone(), + rpc: self.rpc.clone(), + fee_clients: self.fee_clients.clone(), + priority_fee: self.priority_fee.clone(), + cluster: self.cluster.clone(), + } + } } impl PumpFun { #[inline] - pub fn new( + pub async fn new( payer: Arc, - rpc_url: String, - jito_url: String, - commitment: CommitmentConfig, - priority_fee: PriorityFee, + cluster: &Cluster, ) -> Self { - let rpc = Arc::new(RpcClient::new_with_commitment( - rpc_url, - commitment - )); + if CryptoProvider::get_default().is_none() { + let _ = default_provider() + .install_default() + .map_err(|e| anyhow::anyhow!("Failed to install crypto provider: {:?}", e)); + } - let jito_client = Arc::new(JitoClient::new(&jito_url, None)); + let rpc = SolanaRpcClient::new_with_commitment( + cluster.clone().rpc_url, + cluster.clone().commitment + ); + + let mut fee_clients: Vec> = vec![]; + if cluster.clone().use_jito { + let jito_client = JitoClient::new( + cluster.clone().rpc_url, + cluster.clone().block_engine_url + ).await.expect("Failed to create Jito client"); + + fee_clients.push(Arc::new(jito_client)); + } + + if cluster.clone().use_zeroslot { + let zeroslot_client = ZeroSlotClient::new( + cluster.clone().rpc_url, + cluster.clone().zeroslot_url, + cluster.clone().zeroslot_auth_token + ); + + fee_clients.push(Arc::new(zeroslot_client)); + } + + if cluster.clone().use_nextblock { + let nextblock_client = NextBlockClient::new( + cluster.clone().rpc_url, + cluster.clone().nextblock_url, + cluster.clone().nextblock_auth_token + ); + + fee_clients.push(Arc::new(nextblock_client)); + } Self { payer, - rpc, - jito_client, - priority_fee, + rpc: Arc::new(rpc), + fee_clients, + priority_fee: cluster.clone().priority_fee, + cluster: cluster.clone(), } } /// Create a new token pub async fn create( &self, - mint: &Keypair, + mint: Keypair, ipfs: TokenMetadataIPFS, - ) -> Result { - trade::create::create( - &self.rpc, - &self.payer, + ) -> Result<(), anyhow::Error> { + pumpfun::create::create( + self.rpc.clone(), + self.payer.clone(), mint, ipfs, - self.priority_fee, + self.priority_fee.clone(), ).await } pub async fn create_and_buy( &self, - mint: &Keypair, + mint: Keypair, ipfs: TokenMetadataIPFS, amount_sol: u64, slippage_basis_points: Option, - ) -> Result { - trade::create::create_and_buy( - &self.rpc, - &self.payer, + ) -> Result<(), anyhow::Error> { + pumpfun::create::create_and_buy( + self.rpc.clone(), + self.payer.clone(), mint, ipfs, amount_sol, slippage_basis_points, - self.priority_fee, + self.priority_fee.clone(), ).await } - pub async fn create_and_buy_list_with_jito( + pub async fn create_and_buy_with_tip( &self, - payers: Vec<&Keypair>, - mint: &Keypair, - ipfs: TokenMetadataIPFS, - amount_sols: Vec, - slippage_basis_points: Option, - ) -> Result { - trade::create::create_and_buy_list_with_jito( - &self.rpc, - &self.jito_client, - payers, - mint, - ipfs, - amount_sols, - slippage_basis_points, - self.priority_fee, - ).await - } - - pub async fn create_and_buy_with_jito( - &self, - payer: &Keypair, - mint: &Keypair, + payer: Arc, + mint: Keypair, ipfs: TokenMetadataIPFS, amount_sol: u64, slippage_basis_points: Option, - ) -> Result { - trade::create::create_and_buy_with_jito( - &self.rpc, - &self.jito_client, + ) -> Result<(), anyhow::Error> { + pumpfun::create::create_and_buy_with_tip( + self.rpc.clone(), + self.fee_clients.clone(), payer, mint, ipfs, amount_sol, slippage_basis_points, - self.priority_fee, + self.priority_fee.clone(), ).await } + /// Buy tokens pub async fn buy( &self, - mint: &Pubkey, + mint: Pubkey, amount_sol: u64, slippage_basis_points: Option, - ) -> Result { - trade::buy::buy( - &self.rpc, - &self.payer, + ) -> Result<(), anyhow::Error> { + pumpfun::buy::buy( + self.rpc.clone(), + self.payer.clone(), mint, amount_sol, slippage_basis_points, - self.priority_fee, + self.priority_fee.clone(), ).await } /// Buy tokens using Jito - pub async fn buy_with_jito( + pub async fn buy_with_tip( &self, - mint: &Pubkey, + mint: Pubkey, amount_sol: u64, slippage_basis_points: Option, - ) -> Result { - trade::buy::buy_with_jito( - &self.rpc, - &self.jito_client, - &self.payer, + ) -> Result<(), anyhow::Error> { + pumpfun::buy::buy_with_tip( + self.rpc.clone(), + self.fee_clients.clone(), + self.payer.clone(), mint, amount_sol, slippage_basis_points, - self.priority_fee, - ).await - } - - pub async fn buy_list_with_jito( - &self, - payers: Vec<&Keypair>, - mint: &Pubkey, - amount_sols: Vec, - slippage_basis_points: Option, - ) -> Result { - trade::buy::buy_list_with_jito( - &self.rpc, - &self.jito_client, - payers, - mint, - amount_sols, - slippage_basis_points, - self.priority_fee, + self.priority_fee.clone(), ).await } /// Sell tokens pub async fn sell( &self, - mint: &Pubkey, + mint: Pubkey, amount_token: Option, slippage_basis_points: Option, - ) -> Result { - trade::sell::sell( - &self.rpc, - &self.payer, - mint, + ) -> Result<(), anyhow::Error> { + pumpfun::sell::sell( + self.rpc.clone(), + self.payer.clone(), + mint.clone(), amount_token, slippage_basis_points, - self.priority_fee, + self.priority_fee.clone(), ).await } /// Sell tokens by percentage pub async fn sell_by_percent( &self, - mint: &Pubkey, + mint: Pubkey, percent: u64, slippage_basis_points: Option, - ) -> Result { - trade::sell::sell_by_percent( - &self.rpc, - &self.payer, - mint, + ) -> Result<(), anyhow::Error> { + pumpfun::sell::sell_by_percent( + self.rpc.clone(), + self.payer.clone(), + mint.clone(), percent, slippage_basis_points, - self.priority_fee, + self.priority_fee.clone(), ).await } pub async fn sell_by_percent_with_jito( &self, - mint: &Pubkey, + mint: Pubkey, percent: u64, slippage_basis_points: Option, - ) -> Result { - trade::sell::sell_by_percent_with_jito( - &self.rpc, - &self.payer, - &self.jito_client, + ) -> Result<(), anyhow::Error> { + pumpfun::sell::sell_by_percent_with_jito( + self.rpc.clone(), + self.fee_clients.clone(), + self.payer.clone(), mint, percent, slippage_basis_points, - self.priority_fee, + self.priority_fee.clone(), ).await } /// Sell tokens using Jito pub async fn sell_with_jito( &self, - mint: &Pubkey, + mint: Pubkey, amount_token: Option, slippage_basis_points: Option, - ) -> Result { - let jito_client = self.jito_client.as_ref(); - - trade::sell::sell_with_jito( - &self.rpc, - &self.payer, - jito_client, + ) -> Result<(), anyhow::Error> { + pumpfun::sell::sell_with_jito( + self.rpc.clone(), + self.fee_clients.clone(), + self.payer.clone(), mint, amount_token, slippage_basis_points, - self.priority_fee, + self.priority_fee.clone(), ).await } @@ -273,23 +276,24 @@ impl PumpFun { } #[inline] - pub fn get_sol_balance(&self, payer: &Pubkey) -> Result { - trade::common::get_sol_balance(&self.rpc, payer) + pub async fn get_sol_balance(&self, payer: &Pubkey) -> Result { + pumpfun::common::get_sol_balance(&self.rpc, payer).await } #[inline] - pub fn get_payer_sol_balance(&self) -> Result { - trade::common::get_sol_balance(&self.rpc, &self.payer.pubkey()) + pub async fn get_payer_sol_balance(&self) -> Result { + pumpfun::common::get_sol_balance(&self.rpc, &self.payer.pubkey()).await } #[inline] - pub fn get_token_balance(&self, payer: &Pubkey, mint: &Pubkey) -> Result { - trade::common::get_token_balance(&self.rpc, payer, mint) + pub async fn get_token_balance(&self, payer: &Pubkey, mint: &Pubkey) -> Result { + println!("get_token_balance payer: {}, mint: {}, cluster: {}", payer, mint, self.cluster.rpc_url); + pumpfun::common::get_token_balance(&self.rpc, payer, mint).await } #[inline] - pub fn get_payer_token_balance(&self, mint: &Pubkey) -> Result { - trade::common::get_token_balance(&self.rpc, &self.payer.pubkey(), mint) + pub async fn get_payer_token_balance(&self, mint: &Pubkey) -> Result { + pumpfun::common::get_token_balance(&self.rpc, &self.payer.pubkey(), mint).await } #[inline] @@ -304,16 +308,16 @@ impl PumpFun { #[inline] pub fn get_token_price(&self,virtual_sol_reserves: u64, virtual_token_reserves: u64) -> f64 { - trade::common::get_token_price(virtual_sol_reserves, virtual_token_reserves) + pumpfun::common::get_token_price(virtual_sol_reserves, virtual_token_reserves) } #[inline] pub fn get_buy_price(&self, amount: u64, trade_info: &TradeInfo) -> u64 { - trade::common::get_buy_price(amount, trade_info) + pumpfun::common::get_buy_price(amount, trade_info) } #[inline] pub async fn transfer_sol(&self, payer: &Keypair, receive_wallet: &Pubkey, amount: u64) -> Result<(), anyhow::Error> { - trade::common::transfer_sol(&self.rpc, payer, receive_wallet, amount).await + pumpfun::common::transfer_sol(&self.rpc, payer, receive_wallet, amount).await } } diff --git a/src/main.rs b/src/main.rs index c03f7b9..0d7bc57 100755 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,4 @@ -use mai3_pumpfun_sdk::common::{ +use pumpfun_sdk::common::{ logs_events::PumpfunEvent, logs_subscribe::{tokens_subscription, stop_subscription} }; @@ -16,6 +16,9 @@ async fn main() -> Result<(), Box> { // Define callback function let callback = |event: PumpfunEvent| { match event { + PumpfunEvent::NewDevTrade(trade_info) => { + println!("Received new dev trade event: {:?}", trade_info); + }, PumpfunEvent::NewToken(token_info) => { println!("Received new token event: {:?}", token_info); }, diff --git a/src/pumpfun/buy.rs b/src/pumpfun/buy.rs new file mode 100755 index 0000000..f4a5c61 --- /dev/null +++ b/src/pumpfun/buy.rs @@ -0,0 +1,179 @@ +use anyhow::anyhow; +use solana_sdk::{ + compute_budget::ComputeBudgetInstruction, instruction::Instruction, message::{v0, VersionedMessage}, native_token::sol_to_lamports, pubkey::Pubkey, signature::Keypair, signer::Signer, system_instruction, transaction::{Transaction, VersionedTransaction} +}; +use solana_hash::Hash; +use spl_associated_token_account::instruction::create_associated_token_account; +use tokio::task::JoinHandle; +use std::{str::FromStr, time::Instant, sync::Arc}; + +use crate::{common::{PriorityFee, SolanaRpcClient}, constants::{self, trade::DEFAULT_SLIPPAGE}, instruction, jito::FeeClient}; + +const MAX_LOADED_ACCOUNTS_DATA_SIZE_LIMIT: u32 = 250000; + +use super::common::{calculate_with_slippage_buy, get_bonding_curve_account, get_global_account, get_initial_buy_price}; + +pub async fn buy( + rpc: Arc, + payer: Arc, + mint: Pubkey, + amount_sol: u64, + slippage_basis_points: Option, + priority_fee: PriorityFee, +) -> Result<(), anyhow::Error> { + let transaction = build_buy_transaction(rpc.clone(), payer.clone(), mint.clone(), amount_sol, slippage_basis_points, priority_fee.clone()).await?; + rpc.send_and_confirm_transaction(&transaction).await?; + Ok(()) +} + +/// Buy tokens using Jito +pub async fn buy_with_tip( + rpc: Arc, + fee_clients: Vec>, + payer: Arc, + mint: Pubkey, + amount_sol: u64, + slippage_basis_points: Option, + priority_fee: PriorityFee, +) -> Result<(), anyhow::Error> { + let start_time = Instant::now(); + + let mint = Arc::new(mint.clone()); + let instructions = build_buy_instructions(rpc.clone(), payer.clone(), mint.clone(), amount_sol, slippage_basis_points).await?; + + let mut transactions = vec![]; + let recent_blockhash = rpc.get_latest_blockhash().await?; + for fee_client in fee_clients.clone() { + let payer = payer.clone(); + let priority_fee = priority_fee.clone(); + let tip_account = fee_client.get_tip_account().await.map_err(|e| anyhow!(e.to_string()))?; + let tip_account = Arc::new(Pubkey::from_str(&tip_account).map_err(|e| anyhow!(e))?); + + let transaction = build_buy_transaction_with_tip(tip_account, payer, priority_fee, instructions.clone(), recent_blockhash).await?; + transactions.push(transaction); + } + + let mut handles: Vec>> = vec![]; + for i in 0..fee_clients.len() { + let fee_client = fee_clients[i].clone(); + let transactions = transactions.clone(); + let start_time = start_time.clone(); + let transaction = transactions[i].clone(); + let handle = tokio::spawn(async move { + fee_client.send_transaction(&transaction).await?; + println!("index: {}, Total Jito buy operation time: {:?}ms", i, start_time.elapsed().as_millis()); + Ok::<(), anyhow::Error>(()) + }); + + handles.push(handle); + } + + for handle in handles { + match handle.await { + Ok(Ok(_)) => (), + Ok(Err(e)) => println!("Error in task: {}", e), + Err(e) => println!("Task join error: {}", e), + } + } + + Ok(()) +} + +pub async fn build_buy_transaction( + rpc: Arc, + payer: Arc, + mint: Pubkey, + amount_sol: u64, + slippage_basis_points: Option, + priority_fee: PriorityFee, +) -> Result { + let mut instructions = vec![ + ComputeBudgetInstruction::set_loaded_accounts_data_size_limit(MAX_LOADED_ACCOUNTS_DATA_SIZE_LIMIT), + ComputeBudgetInstruction::set_compute_unit_price(priority_fee.unit_price), + ComputeBudgetInstruction::set_compute_unit_limit(priority_fee.unit_limit), + ]; + + let build_instructions = build_buy_instructions(rpc.clone(), payer.clone(), Arc::new(mint), amount_sol, slippage_basis_points).await?; + instructions.extend(build_instructions); + + let recent_blockhash = rpc.get_latest_blockhash().await?; + let transaction = Transaction::new_signed_with_payer( + &instructions, + Some(&payer.pubkey()), + &[payer], + recent_blockhash, + ); + + Ok(transaction) +} + +pub async fn build_buy_transaction_with_tip( + tip_account: Arc, + payer: Arc, + priority_fee: PriorityFee, + build_instructions: Vec, + blockhash: Hash, +) -> Result { + let mut instructions = vec![ + ComputeBudgetInstruction::set_loaded_accounts_data_size_limit(MAX_LOADED_ACCOUNTS_DATA_SIZE_LIMIT), + ComputeBudgetInstruction::set_compute_unit_price(priority_fee.unit_price), + ComputeBudgetInstruction::set_compute_unit_limit(priority_fee.unit_limit), + system_instruction::transfer( + &payer.pubkey(), + &tip_account, + sol_to_lamports(priority_fee.buy_tip_fee), + ), + ]; + + instructions.extend(build_instructions); + + let v0_message: v0::Message = + v0::Message::try_compile(&payer.pubkey(), &instructions, &[], blockhash)?; + let versioned_message: VersionedMessage = VersionedMessage::V0(v0_message); + let transaction = VersionedTransaction::try_new(versioned_message, &[&payer])?; + + Ok(transaction) +} + +pub async fn build_buy_instructions( + rpc: Arc, + payer: Arc, + mint: Arc, + amount_sol: u64, + slippage_basis_points: Option, +) -> Result, anyhow::Error> { + if amount_sol == 0 { + return Err(anyhow!("Amount cannot be zero")); + } + + let rpc = rpc.as_ref(); + let global_account = get_global_account(rpc).await?; + let buy_amount = match get_bonding_curve_account(rpc, mint.as_ref()).await { + Ok(account) => account.get_buy_price(amount_sol).map_err(|e| anyhow!(e))?, + Err(_e) => { + println!("Bonding curve account not found, using initial buy price: {}", _e); + let initial_buy_amount = get_initial_buy_price(&global_account, amount_sol).await?; + initial_buy_amount * 80 / 100 + } + }; + let buy_amount_with_slippage = calculate_with_slippage_buy(amount_sol, slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE)); + let mut instructions = vec![]; + instructions.push(create_associated_token_account( + &payer.pubkey(), + &payer.pubkey(), + &mint, + &constants::accounts::TOKEN_PROGRAM, + )); + + instructions.push(instruction::buy( + payer.as_ref(), + &mint, + &global_account.fee_recipient, + instruction::Buy { + _amount: buy_amount, + _max_sol_cost: buy_amount_with_slippage, + }, + )); + + Ok(instructions) +} \ No newline at end of file diff --git a/src/trade/common.rs b/src/pumpfun/common.rs similarity index 62% rename from src/trade/common.rs rename to src/pumpfun/common.rs index 26683e9..523a050 100755 --- a/src/trade/common.rs +++ b/src/pumpfun/common.rs @@ -1,45 +1,24 @@ use anyhow::anyhow; -use serde::Deserialize; +use spl_token::state::Account; use tokio::sync::RwLock; use std::{collections::HashMap, sync::Arc}; -use solana_client::rpc_client::RpcClient; use solana_sdk::{ - compute_budget::ComputeBudgetInstruction, instruction::Instruction, native_token::sol_to_lamports, pubkey::Pubkey, signature::Keypair, signer::Signer, system_instruction, transaction::Transaction + commitment_config::CommitmentConfig, compute_budget::ComputeBudgetInstruction, instruction::Instruction, program_pack::Pack, pubkey::Pubkey, signature::Keypair, signer::Signer, system_instruction, transaction::Transaction }; use spl_associated_token_account::get_associated_token_address; -use crate::{accounts, common::logs_data::TradeInfo, constants::{self, trade::{DEFAULT_BUY_JITO_FEE, DEFAULT_COMPUTE_UNIT_LIMIT, DEFAULT_COMPUTE_UNIT_PRICE, DEFAULT_SELL_JITO_FEE, DEFAULT_SLIPPAGE}}}; +use crate::{accounts, common::{logs_data::TradeInfo, PriorityFee, SolanaRpcClient}, constants::{self, trade::DEFAULT_SLIPPAGE}}; use borsh::BorshDeserialize; lazy_static::lazy_static! { static ref ACCOUNT_CACHE: RwLock>> = RwLock::new(HashMap::new()); } -#[derive(Debug, Deserialize, Clone, Copy, PartialEq)] - -pub struct PriorityFee { - pub unit_limit: u32, - pub unit_price: u64, - pub buy_jito_fee: f64, - pub sell_jito_fee: f64, -} - -impl Default for PriorityFee { - fn default() -> Self { - Self { - unit_limit: DEFAULT_COMPUTE_UNIT_LIMIT, - unit_price: DEFAULT_COMPUTE_UNIT_PRICE, - buy_jito_fee: DEFAULT_BUY_JITO_FEE, - sell_jito_fee: DEFAULT_SELL_JITO_FEE - } - } -} - -pub async fn transfer_sol(rpc: &RpcClient, payer: &Keypair, receive_wallet: &Pubkey, amount: u64) -> Result<(), anyhow::Error> { +pub async fn transfer_sol(rpc: &SolanaRpcClient, payer: &Keypair, receive_wallet: &Pubkey, amount: u64) -> Result<(), anyhow::Error> { if amount == 0 { - return Err(anyhow!("Amount cannot be zero")); + return Err(anyhow!("transfer_sol: Amount cannot be zero")); } - let balance = get_sol_balance(rpc, &payer.pubkey())?; + let balance = get_sol_balance(rpc, &payer.pubkey()).await?; if balance < amount { return Err(anyhow!("Insufficient balance")); } @@ -50,7 +29,7 @@ pub async fn transfer_sol(rpc: &RpcClient, payer: &Keypair, receive_wallet: &Pub amount, ); - let recent_blockhash = rpc.get_latest_blockhash()?; + let recent_blockhash = rpc.get_latest_blockhash().await?; let transaction = Transaction::new_signed_with_payer( &[transfer_instruction], @@ -59,7 +38,7 @@ pub async fn transfer_sol(rpc: &RpcClient, payer: &Keypair, receive_wallet: &Pub recent_blockhash, ); - rpc.send_and_confirm_transaction(&transaction)?; + rpc.send_and_confirm_transaction(&transaction).await?; Ok(()) } @@ -73,19 +52,46 @@ pub fn create_priority_fee_instructions(priority_fee: PriorityFee) -> Vec Result { - let ata = get_associated_token_address(account, mint); - if rpc.get_account(&ata).is_err() { - return Ok(0); - } +// #[inline] +pub async fn get_token_balance(rpc: &SolanaRpcClient, payer: &Pubkey, mint: &Pubkey) -> Result { + let ata = get_associated_token_address(payer, mint); + // let account_data = rpc.get_account_data(&ata).await?; + // let token_account = Account::unpack(&account_data.as_slice())?; - let balance = rpc.get_token_account_balance(&ata)?; - balance.amount.parse::() - .map_err(|_| anyhow!("Failed to parse token balance")) + // Ok(token_account.amount) + + // println!("get_token_balance ata: {}", ata); + let balance = rpc.get_token_account_balance(&ata).await?; + let balance_u64 = balance.amount.parse::() + .map_err(|_| anyhow!("Failed to parse token balance"))?; + Ok(balance_u64) } -pub fn get_sol_balance(rpc: &RpcClient, account: &Pubkey) -> Result { - rpc.get_balance(account).map_err(|_| anyhow!("Failed to get SOL balance")) +#[inline] +pub async fn get_token_balance_and_ata(rpc: &SolanaRpcClient, payer: &Keypair, mint: &Pubkey) -> Result<(u64, Pubkey), anyhow::Error> { + let ata = get_associated_token_address(&payer.pubkey(), mint); + // let account_data = rpc.get_account_data(&ata).await?; + // let token_account = Account::unpack(&account_data)?; + + // Ok((token_account.amount, ata)) + + let balance = rpc.get_token_account_balance(&ata).await?; + let balance_u64 = balance.amount.parse::() + .map_err(|_| anyhow!("Failed to parse token balance"))?; + + if balance_u64 == 0 { + return Err(anyhow!("Balance is 0")); + } + + Ok((balance_u64, ata)) +} + +#[inline] +pub async fn get_sol_balance(rpc: &SolanaRpcClient, account: &Pubkey) -> Result { + println!("get_sol_balance account: {}", account); + let balance = rpc.get_balance(account).await?; + println!("get_sol_balance balance: {}", balance); + Ok(balance) } #[inline] @@ -125,21 +131,17 @@ pub fn get_metadata_pda(mint: &Pubkey) -> Pubkey { } #[inline] -pub async fn get_global_account(rpc: &RpcClient) -> Result, anyhow::Error> { +pub async fn get_global_account(rpc: &SolanaRpcClient) -> Result, anyhow::Error> { let global = get_global_pda(); - - // Try cache first if let Some(account) = ACCOUNT_CACHE.read().await.get(&global) { return Ok(account.clone()); } - // Cache miss, fetch from RPC - let account = rpc.get_account(&global)?; - let global_account = Arc::new(accounts::GlobalAccount::try_from_slice(&account.data)?); - - // Update cache + let account = rpc.get_account(&global).await?; + let global_account = bincode::deserialize::(&account.data)?; + let global_account = Arc::new(global_account); + ACCOUNT_CACHE.write().await.insert(global, global_account.clone()); - Ok(global_account) } @@ -151,17 +153,17 @@ pub async fn get_initial_buy_price(global_account: &Arc #[inline] pub async fn get_bonding_curve_account( - rpc: &RpcClient, + rpc: &SolanaRpcClient, mint: &Pubkey, ) -> Result, anyhow::Error> { let bonding_curve_pda = get_bonding_curve_pda(mint) .ok_or(anyhow!("Bonding curve not found"))?; - - if rpc.get_account(&bonding_curve_pda).is_err() { + + let account = rpc.get_account(&bonding_curve_pda).await?; + if account.data.is_empty() { return Err(anyhow!("Bonding curve not found")); } - let account = rpc.get_account(&bonding_curve_pda)?; let bonding_curve = Arc::new(accounts::BondingCurveAccount::try_from_slice(&account.data)?); Ok(bonding_curve) } @@ -172,12 +174,14 @@ pub fn get_buy_amount_with_slippage(amount_sol: u64, slippage_basis_points: Opti amount_sol + (amount_sol * slippage / 10000) } +#[inline] pub fn get_token_price(virtual_sol_reserves: u64, virtual_token_reserves: u64) -> f64 { let v_sol = virtual_sol_reserves as f64 / 100_000_000.0; let v_tokens = virtual_token_reserves as f64 / 100_000.0; v_sol / v_tokens } +#[inline] pub fn get_buy_price(amount: u64, trade_info: &TradeInfo) -> u64 { if amount == 0 { return 0; diff --git a/src/pumpfun/create.rs b/src/pumpfun/create.rs new file mode 100755 index 0000000..dceabc9 --- /dev/null +++ b/src/pumpfun/create.rs @@ -0,0 +1,322 @@ +use std::{str::FromStr, time::Instant, sync::Arc}; + +use anyhow::anyhow; +use solana_client::rpc_config::RpcSimulateTransactionConfig; +use solana_sdk::{ + commitment_config::CommitmentConfig, compute_budget::ComputeBudgetInstruction, instruction::Instruction, message::{v0, VersionedMessage}, native_token::sol_to_lamports, pubkey::Pubkey, signature::{Keypair, Signature}, signer::Signer, system_instruction, transaction::{Transaction, VersionedTransaction} +}; +use spl_associated_token_account::{ + instruction::create_associated_token_account, +}; + +use crate::{ + common::{PriorityFee, SolanaRpcClient}, constants, instruction, + ipfs::TokenMetadataIPFS, jito::FeeClient, + pumpfun::buy::build_buy_transaction_with_tip +}; + +use crate::pumpfun::common::{ + create_priority_fee_instructions, + get_buy_amount_with_slippage, get_global_account +}; + +/// Create a new token +pub async fn create( + rpc: Arc, + payer: Arc, + mint: Keypair, + ipfs: TokenMetadataIPFS, + priority_fee: PriorityFee, +) -> Result<(), anyhow::Error> { + let mut instructions = create_priority_fee_instructions(priority_fee); + + instructions.push(instruction::create( + payer.as_ref(), + &mint, + instruction::Create { + _name: ipfs.metadata.name, + _symbol: ipfs.metadata.symbol, + _uri: ipfs.metadata_uri, + }, + )); + + let recent_blockhash = rpc.get_latest_blockhash().await?; + let transaction = Transaction::new_signed_with_payer( + &instructions, + Some(&payer.pubkey()), + &[payer.as_ref(), &mint], + recent_blockhash, + ); + + rpc.send_and_confirm_transaction(&transaction).await?; + + Ok(()) +} + +/// Create and buy tokens in one transaction +pub async fn create_and_buy( + rpc: Arc, + payer: Arc, + mint: Keypair, + ipfs: TokenMetadataIPFS, + amount_sol: u64, + slippage_basis_points: Option, + priority_fee: PriorityFee, +) -> Result<(), anyhow::Error> { + if amount_sol == 0 { + return Err(anyhow!("Amount cannot be zero")); + } + + let mint = Arc::new(mint); + let transaction = build_create_and_buy_transaction(rpc.clone(), payer.clone(), mint.clone(), ipfs, amount_sol, slippage_basis_points, priority_fee.clone()).await?; + rpc.send_and_confirm_transaction(&transaction).await?; + + Ok(()) +} + +pub async fn create_and_buy_with_tip( + rpc: Arc, + fee_clients: Vec>, + payer: Arc, + mint: Keypair, + ipfs: TokenMetadataIPFS, + amount_sol: u64, + slippage_basis_points: Option, + priority_fee: PriorityFee, +) -> Result<(), anyhow::Error> { + let start_time = Instant::now(); + let mint = Arc::new(mint); + let build_instructions = build_create_and_buy_instructions(rpc.clone(), payer.clone(), mint.clone(), ipfs.clone(), amount_sol, slippage_basis_points, priority_fee.clone()).await?; + let mut handles = vec![]; + for fee_client in fee_clients { + let rpc = rpc.clone(); + let payer = payer.clone(); + let mint = mint.clone(); + let priority_fee = priority_fee.clone(); + let tip_account = fee_client.get_tip_account().await.map_err(|e| anyhow!(e.to_string()))?; + let tip_account = Arc::new(Pubkey::from_str(&tip_account).map_err(|e| anyhow!(e))?); + let build_instructions = build_instructions.clone(); + + let handle = tokio::spawn(async move { + let transaction = build_create_and_buy_transaction_with_tip(rpc, tip_account, payer, mint, priority_fee, build_instructions).await?; + fee_client.send_transaction(&transaction).await.map_err(|e| anyhow!(e.to_string()))?; + println!("Total Jito create and buy operation time: {:?}ms", start_time.elapsed().as_millis()); + Ok::<(), anyhow::Error>(()) + }); + + handles.push(handle); + } + + for handle in handles { + match handle.await { + Ok(Ok(_)) => (), + Ok(Err(e)) => println!("Error in task: {}", e), + Err(e) => println!("Task join error: {}", e), + } + } + + Ok(()) +} + +pub async fn build_create_and_buy_transaction( + rpc: Arc, + payer: Arc, + mint: Arc, + ipfs: TokenMetadataIPFS, + amount_sol: u64, + slippage_basis_points: Option, + priority_fee: PriorityFee, +) -> Result { + let mut instructions = vec![ + ComputeBudgetInstruction::set_compute_unit_price(priority_fee.unit_price), + ComputeBudgetInstruction::set_compute_unit_limit(priority_fee.unit_limit), + ]; + + let build_instructions = build_create_and_buy_instructions(rpc.clone(), payer.clone(), mint.clone(), ipfs, amount_sol, slippage_basis_points, priority_fee.clone()).await?; + instructions.extend(build_instructions); + + let recent_blockhash = rpc.get_latest_blockhash().await?; + let transaction = Transaction::new_signed_with_payer( + &instructions, + Some(&payer.pubkey()), + &[payer.as_ref(), mint.as_ref()], + recent_blockhash, + ); + + Ok(transaction) +} + +pub async fn build_create_and_buy_transaction_with_tip( + rpc: Arc, + tip_account: Arc, + payer: Arc, + mint: Arc, + priority_fee: PriorityFee, + build_instructions: Vec, +) -> Result { + let mut instructions = vec![ + ComputeBudgetInstruction::set_compute_unit_price(priority_fee.unit_price), + ComputeBudgetInstruction::set_compute_unit_limit(priority_fee.unit_limit), + system_instruction::transfer( + &payer.pubkey(), + &tip_account, + sol_to_lamports(priority_fee.buy_tip_fee), + ), + ]; + instructions.extend(build_instructions); + + let recent_blockhash = rpc.get_latest_blockhash().await?; + let v0_message: v0::Message = + v0::Message::try_compile(&payer.pubkey(), &instructions, &[], recent_blockhash)?; + + let versioned_message: VersionedMessage = VersionedMessage::V0(v0_message); + let transaction = VersionedTransaction::try_new(versioned_message, &[&payer])?; + + Ok(transaction) +} + +// pub async fn build_create_and_buy_instructions( +// rpc: Arc, +// payer: Arc, +// mint: Arc, +// ipfs: TokenMetadataIPFS, +// amount_sol: u64, +// slippage_basis_points: Option, +// priority_fee: PriorityFee, +// ) -> Result, anyhow::Error> { +// if amount_sol == 0 { +// return Err(anyhow!("Amount cannot be zero")); +// } + +// let rpc = rpc.as_ref(); +// let global_account = get_global_account(rpc).await?; +// let buy_amount = global_account.get_initial_buy_price(amount_sol); +// let buy_amount_with_slippage = +// get_buy_amount_with_slippage(amount_sol, slippage_basis_points); + +// let mut instructions = vec![ +// ComputeBudgetInstruction::set_compute_unit_limit(1_400_000), +// ComputeBudgetInstruction::set_compute_unit_price(0), +// ]; + +// instructions.push(instruction::create( +// payer.as_ref(), +// mint.as_ref(), +// instruction::Create { +// _name: ipfs.metadata.name, +// _symbol: ipfs.metadata.symbol, +// _uri: ipfs.metadata_uri, +// }, +// )); + +// instructions.push(create_associated_token_account( +// &payer.pubkey(), +// &payer.pubkey(), +// &mint.pubkey(), +// &constants::accounts::TOKEN_PROGRAM, +// )); + + +// instructions.push(instruction::buy( +// payer.as_ref(), +// &mint.pubkey(), +// &global_account.fee_recipient, +// instruction::Buy { +// _amount: buy_amount, +// _max_sol_cost: buy_amount_with_slippage, +// }, +// )); + +// let commitment_config = CommitmentConfig::confirmed(); +// let recent_blockhash = rpc.get_latest_blockhash_with_commitment(commitment_config).await?.0; + +// let simulate_tx = Transaction::new_signed_with_payer( +// &instructions, +// Some(&payer.pubkey()), +// &[payer.as_ref(), mint.as_ref()], +// recent_blockhash, +// ); + +// let config = RpcSimulateTransactionConfig { +// sig_verify: true, +// commitment: Some(commitment_config), +// ..RpcSimulateTransactionConfig::default() +// }; + +// let result = rpc.simulate_transaction_with_config(&simulate_tx, config).await?.value; + +// if result.logs.as_ref().map_or(true, |logs| logs.is_empty()) { +// return Err(anyhow!("Simulation failed: {:?}", result.err)); +// } + +// let result_cu = result.units_consumed.ok_or_else(|| anyhow!("No compute units consumed"))?; +// let fees = rpc.get_recent_prioritization_fees(&[]).await?; +// let average_fees = if fees.is_empty() { +// priority_fee.unit_price +// } else { +// fees.iter() +// .map(|fee| fee.prioritization_fee) +// .sum::() / fees.len() as u64 +// }; + +// let unit_price = if average_fees == 0 { priority_fee.unit_price } else { average_fees }; + +// instructions[0] = ComputeBudgetInstruction::set_compute_unit_limit(result_cu as u32); +// instructions[1] = ComputeBudgetInstruction::set_compute_unit_price(unit_price); + +// Ok(instructions) +// } + +pub async fn build_create_and_buy_instructions( + rpc: Arc, + payer: Arc, + mint: Arc, + ipfs: TokenMetadataIPFS, + amount_sol: u64, + slippage_basis_points: Option, + priority_fee: PriorityFee, +) -> Result, anyhow::Error> { + if amount_sol == 0 { + return Err(anyhow!("Amount cannot be zero")); + } + + let rpc = rpc.as_ref(); + let global_account = get_global_account(rpc).await?; + let buy_amount = global_account.get_initial_buy_price(amount_sol); + let buy_amount_with_slippage = + get_buy_amount_with_slippage(amount_sol, slippage_basis_points); + + let mut instructions = vec![ + ComputeBudgetInstruction::set_compute_unit_price(priority_fee.unit_price), + ComputeBudgetInstruction::set_compute_unit_limit(priority_fee.unit_limit), + ]; + + instructions.push(instruction::create( + payer.as_ref(), + mint.as_ref(), + instruction::Create { + _name: ipfs.metadata.name.clone(), + _symbol: ipfs.metadata.symbol.clone(), + _uri: ipfs.metadata_uri.clone(), + }, + )); + + instructions.push(create_associated_token_account( + &payer.pubkey(), + &payer.pubkey(), + &mint.pubkey(), + &constants::accounts::TOKEN_PROGRAM, + )); + + instructions.push(instruction::buy( + payer.as_ref(), + &mint.pubkey(), + &global_account.fee_recipient, + instruction::Buy { + _amount: buy_amount, + _max_sol_cost: buy_amount_with_slippage, + }, + )); + + Ok(instructions) +} diff --git a/src/pumpfun/mod.rs b/src/pumpfun/mod.rs new file mode 100755 index 0000000..8126389 --- /dev/null +++ b/src/pumpfun/mod.rs @@ -0,0 +1,4 @@ +pub mod buy; +pub mod create; +pub mod sell; +pub mod common; \ No newline at end of file diff --git a/src/pumpfun/sell.rs b/src/pumpfun/sell.rs new file mode 100755 index 0000000..4933ec9 --- /dev/null +++ b/src/pumpfun/sell.rs @@ -0,0 +1,229 @@ +use anyhow::anyhow; +use solana_client::rpc_config::RpcSimulateTransactionConfig; +use solana_sdk::{ + commitment_config::CommitmentConfig, compute_budget::ComputeBudgetInstruction, instruction::Instruction, message::{v0, VersionedMessage}, native_token::sol_to_lamports, pubkey::Pubkey, signature::{Keypair, Signature}, signer::Signer, system_instruction, transaction::{Transaction, VersionedTransaction} +}; +use solana_hash::Hash; +use spl_associated_token_account::get_associated_token_address; +use spl_token::instruction::close_account; +use tokio::task::JoinHandle; + +use std::{str::FromStr, time::Instant, sync::Arc}; + +use crate::{common::{PriorityFee, SolanaRpcClient}, constants::trade::{DEFAULT_COMPUTE_UNIT_PRICE, DEFAULT_SLIPPAGE}, instruction, jito::FeeClient}; + +use super::common::{calculate_with_slippage_sell, get_bonding_curve_account, get_global_account}; + +async fn get_token_balance(rpc: &SolanaRpcClient, payer: &Keypair, mint: &Pubkey) -> Result<(u64, Pubkey), anyhow::Error> { + let ata = get_associated_token_address(&payer.pubkey(), mint); + let balance = rpc.get_token_account_balance(&ata).await?; + let balance_u64 = balance.amount.parse::() + .map_err(|_| anyhow!("Failed to parse token balance"))?; + + if balance_u64 == 0 { + return Err(anyhow!("Balance is 0")); + } + + Ok((balance_u64, ata)) +} + +pub async fn sell( + rpc: Arc, + payer: Arc, + mint: Pubkey, + amount_token: Option, + slippage_basis_points: Option, + priority_fee: PriorityFee, +) -> Result<(), anyhow::Error> { + let instructions = build_sell_instructions(rpc.clone(), payer.clone(), mint.clone(), amount_token, slippage_basis_points).await?; + let transaction = build_sell_transaction(rpc.clone(), payer.clone(), priority_fee, instructions).await?; + rpc.send_and_confirm_transaction(&transaction).await?; + + Ok(()) +} + +/// Sell tokens by percentage +pub async fn sell_by_percent( + rpc: Arc, + payer: Arc, + mint: Pubkey, + percent: u64, + slippage_basis_points: Option, + priority_fee: PriorityFee, +) -> Result<(), anyhow::Error> { + if percent == 0 || percent > 100 { + return Err(anyhow!("Percentage must be between 1 and 100")); + } + + let (balance_u64, _) = get_token_balance(rpc.as_ref(), payer.as_ref(), &mint).await?; + let amount = balance_u64 * percent / 100; + sell(rpc, payer, mint, Some(amount), slippage_basis_points, priority_fee).await +} + +pub async fn sell_by_percent_with_jito( + rpc: Arc, + fee_clients: Vec>, + payer: Arc, + mint: Pubkey, + percent: u64, + slippage_basis_points: Option, + priority_fee: PriorityFee, +) -> Result<(), anyhow::Error> { + if percent == 0 || percent > 100 { + return Err(anyhow!("Percentage must be between 1 and 100")); + } + + let (balance_u64, _) = get_token_balance(rpc.as_ref(), payer.as_ref(), &mint).await?; + let amount = balance_u64 * percent / 100; + sell_with_jito(rpc, fee_clients, payer, mint, Some(amount), slippage_basis_points, priority_fee).await +} + +/// Sell tokens using Jito +pub async fn sell_with_jito( + rpc: Arc, + fee_clients: Vec>, + payer: Arc, + mint: Pubkey, + amount_token: Option, + slippage_basis_points: Option, + priority_fee: PriorityFee, +) -> Result<(), anyhow::Error> { + let start_time = Instant::now(); + + let mut transactions = vec![]; + let instructions = build_sell_instructions(rpc.clone(), payer.clone(), mint.clone(), amount_token, slippage_basis_points).await?; + + let recent_blockhash = rpc.get_latest_blockhash().await?; + for fee_client in fee_clients.clone() { + let payer = payer.clone(); + let priority_fee = priority_fee.clone(); + let tip_account = fee_client.get_tip_account().await.map_err(|e| anyhow!(e.to_string()))?; + let tip_account = Arc::new(Pubkey::from_str(&tip_account).map_err(|e| anyhow!(e))?); + + let transaction = build_sell_transaction_with_tip(tip_account, payer, priority_fee, instructions.clone(), recent_blockhash).await?; + transactions.push(transaction); + } + + let mut handles = vec![]; + for i in 0..fee_clients.len() { + let fee_client = fee_clients[i].clone(); + let transaction = transactions[i].clone(); + let handle: JoinHandle> = tokio::spawn(async move { + fee_client.send_transaction(&transaction).await?; + println!("index: {}, Total Jito sell operation time: {:?}ms", i, start_time.elapsed().as_millis()); + Ok(()) + }); + + handles.push(handle); + } + + for handle in handles { + match handle.await { + Ok(Ok(_)) => (), + Ok(Err(e)) => println!("Error in task: {}", e), + Err(e) => println!("Task join error: {}", e), + } + } + + println!("Total Jito sell operation time: {:?}ms", start_time.elapsed().as_millis()); + Ok(()) +} + +pub async fn build_sell_transaction( + rpc: Arc, + payer: Arc, + priority_fee: PriorityFee, + build_instructions: Vec +) -> Result { + let mut instructions = vec![ + ComputeBudgetInstruction::set_compute_unit_price(priority_fee.unit_price), + ComputeBudgetInstruction::set_compute_unit_limit(priority_fee.unit_limit), + ]; + + instructions.extend(build_instructions); + + let recent_blockhash = rpc.get_latest_blockhash().await?; + let transaction = Transaction::new_signed_with_payer( + &instructions, + Some(&payer.pubkey()), + &[payer.as_ref()], + recent_blockhash, + ); + + Ok(transaction) +} + +pub async fn build_sell_transaction_with_tip( + tip_account: Arc, + payer: Arc, + priority_fee: PriorityFee, + build_instructions: Vec, + blockhash: Hash, +) -> Result { + let mut instructions = vec![ + ComputeBudgetInstruction::set_compute_unit_price(priority_fee.unit_price), + ComputeBudgetInstruction::set_compute_unit_limit(priority_fee.unit_limit), + system_instruction::transfer( + &payer.pubkey(), + &tip_account, + sol_to_lamports(priority_fee.sell_tip_fee), + ), + ]; + + instructions.extend(build_instructions); + + let v0_message: v0::Message = + v0::Message::try_compile(&payer.pubkey(), &instructions, &[], blockhash)?; + let versioned_message: VersionedMessage = VersionedMessage::V0(v0_message); + + let transaction = VersionedTransaction::try_new(versioned_message, &[&payer])?; + + Ok(transaction) +} + +pub async fn build_sell_instructions( + rpc: Arc, + payer: Arc, + mint: Pubkey, + amount_token: Option, + slippage_basis_points: Option, +) -> Result, anyhow::Error> { + let (balance_u64, ata) = get_token_balance(rpc.as_ref(), payer.as_ref(), &mint).await?; + let amount = amount_token.unwrap_or(balance_u64); + + if amount == 0 { + return Err(anyhow!("Amount cannot be zero")); + } + + let global_account = get_global_account(rpc.as_ref()).await?; + let bonding_curve_account = get_bonding_curve_account(rpc.as_ref(), &mint).await?; + let min_sol_output = bonding_curve_account + .get_sell_price(amount, global_account.fee_basis_points) + .map_err(|e| anyhow!(e))?; + let min_sol_output_with_slippage = calculate_with_slippage_sell( + min_sol_output, + slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE), + ); + + let instructions = vec![ + instruction::sell( + payer.as_ref(), + &mint, + &global_account.fee_recipient, + instruction::Sell { + _amount: amount, + _min_sol_output: min_sol_output_with_slippage, + }, + ), + + close_account( + &spl_token::ID, + &ata, + &payer.pubkey(), + &payer.pubkey(), + &[&payer.pubkey()], + )? + ]; + + Ok(instructions) +} diff --git a/src/trade/buy.rs b/src/trade/buy.rs deleted file mode 100755 index 7005894..0000000 --- a/src/trade/buy.rs +++ /dev/null @@ -1,263 +0,0 @@ -use anyhow::anyhow; -use solana_client::{rpc_client::RpcClient, rpc_config::RpcSimulateTransactionConfig}; -use solana_sdk::{ - commitment_config::CommitmentConfig, compute_budget::ComputeBudgetInstruction, instruction::Instruction, native_token::sol_to_lamports, pubkey::Pubkey, signature::{Keypair, Signature}, signer::Signer, system_instruction, transaction::Transaction -}; -use spl_associated_token_account::{ - get_associated_token_address, - instruction::create_associated_token_account, -}; -use std::time::Instant; - -use crate::{constants::{self, trade::{DEFAULT_COMPUTE_UNIT_PRICE, DEFAULT_SLIPPAGE, JITO_TIP_AMOUNT}}, instruction, jito::JitoClient}; - -use super::common::{calculate_with_slippage_buy, get_bonding_curve_account, get_global_account, get_initial_buy_price, PriorityFee}; - -pub async fn buy( - rpc: &RpcClient, - payer: &Keypair, - mint: &Pubkey, - amount_sol: u64, - slippage_basis_points: Option, - priority_fee: PriorityFee, -) -> Result { - let transaction = build_buy_transaction(rpc, payer, mint, amount_sol, slippage_basis_points, priority_fee).await?; - let signature = rpc.send_transaction(&transaction)?; - Ok(signature) -} - -/// Buy tokens using Jito -pub async fn buy_with_jito( - rpc: &RpcClient, - jito_client: &JitoClient, - payer: &Keypair, - mint: &Pubkey, - amount_sol: u64, - slippage_basis_points: Option, - priority_fee: PriorityFee, -) -> Result { - let start_time = Instant::now(); - - let transaction = build_buy_transaction_with_jito(rpc, jito_client, payer, mint, amount_sol, slippage_basis_points, priority_fee).await?; - let signature = jito_client.send_transaction(&transaction).await?; - - println!("Total Jito buy operation time: {:?}ms", start_time.elapsed().as_millis()); - - Ok(signature) -} - -pub async fn buy_list_with_jito( - rpc: &RpcClient, - jito_client: &JitoClient, - payers: Vec<&Keypair>, - mint: &Pubkey, - amount_sols: Vec, - slippage_basis_points: Option, - priority_fee: PriorityFee, -) -> Result { - let start_time = Instant::now(); - - let mut transactions = vec![]; - for (i, payer) in payers.iter().enumerate() { - let transaction = build_buy_transaction_with_jito(rpc, jito_client, payer, mint, amount_sols[i], slippage_basis_points, priority_fee).await?; - transactions.push(transaction); - } - - let signature = jito_client.send_transactions(&transactions).await?; - - println!("Total Jito buy operation time: {:?}ms", start_time.elapsed().as_millis()); - - Ok(signature) -} - -pub async fn build_buy_transaction( - rpc: &RpcClient, - payer: &Keypair, - mint: &Pubkey, - amount_sol: u64, - slippage_basis_points: Option, - priority_fee: PriorityFee, -) -> Result { - let instructions = build_buy_instructions(rpc, payer, mint, amount_sol, slippage_basis_points, priority_fee).await?; - - let recent_blockhash = rpc.get_latest_blockhash()?; - let transaction = Transaction::new_signed_with_payer( - &instructions, - Some(&payer.pubkey()), - &[payer], - recent_blockhash, - ); - - Ok(transaction) -} - -pub async fn build_buy_transaction_with_jito( - rpc: &RpcClient, - jito_client: &JitoClient, - payer: &Keypair, - mint: &Pubkey, - amount_sol: u64, - slippage_basis_points: Option, - priority_fee: PriorityFee, -) -> Result { - let instructions = build_buy_instructions_with_jito(rpc, jito_client, payer, mint, amount_sol, slippage_basis_points, priority_fee).await?; - let recent_blockhash = rpc.get_latest_blockhash()?; - let transaction = Transaction::new_signed_with_payer( - &instructions, - Some(&payer.pubkey()), - &[payer], - recent_blockhash, - ); - - Ok(transaction) -} - -pub async fn build_buy_instructions( - rpc: &RpcClient, - payer: &Keypair, - mint: &Pubkey, - amount_sol: u64, - slippage_basis_points: Option, - priority_fee: PriorityFee, -) -> Result, anyhow::Error> { - if amount_sol == 0 { - return Err(anyhow!("Amount cannot be zero")); - } - - let global_account = get_global_account(rpc).await?; - let bonding_curve_account = get_bonding_curve_account(rpc, mint).await?; - let buy_amount = bonding_curve_account - .get_buy_price(amount_sol) - .map_err(|e| anyhow!(e))?; - let buy_amount_with_slippage = calculate_with_slippage_buy(amount_sol, slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE)); - - let mut instructions = vec![ - ComputeBudgetInstruction::set_compute_unit_limit(1_400_000), - ComputeBudgetInstruction::set_compute_unit_price(0), - ]; - - let ata = get_associated_token_address(&payer.pubkey(), mint); - if rpc.get_account(&ata).is_err() { - instructions.push(create_associated_token_account( - &payer.pubkey(), - &payer.pubkey(), - mint, - &constants::accounts::TOKEN_PROGRAM, - )); - } - - instructions.push(instruction::buy( - payer, - mint, - &global_account.fee_recipient, - instruction::Buy { - _amount: buy_amount, - _max_sol_cost: buy_amount_with_slippage, - }, - )); - - let commitment_config = CommitmentConfig::confirmed(); - let recent_blockhash = rpc.get_latest_blockhash_with_commitment(commitment_config)? - .0; - - let simulate_tx = Transaction::new_signed_with_payer( - &instructions, - Some(&payer.pubkey()), - &[payer], - recent_blockhash, - ); - - let config = RpcSimulateTransactionConfig { - sig_verify: true, - commitment: Some(commitment_config), - ..RpcSimulateTransactionConfig::default() - }; - - let result = rpc.simulate_transaction_with_config(&simulate_tx, config)? - .value; - - if result.logs.as_ref().map_or(true, |logs| logs.is_empty()) { - return Err(anyhow!("Simulation failed: {:?}", result.err)); - } - - let result_cu = result.units_consumed.ok_or_else(|| anyhow!("No compute units consumed"))?; - let fees = rpc.get_recent_prioritization_fees(&[])?; - let average_fees = if fees.is_empty() { - DEFAULT_COMPUTE_UNIT_PRICE - } else { - fees.iter() - .map(|fee| fee.prioritization_fee) - .sum::() / fees.len() as u64 - }; - - - let unit_price = if average_fees == 0 { priority_fee.unit_price } else { average_fees }; - instructions[0] = ComputeBudgetInstruction::set_compute_unit_limit(result_cu as u32); - instructions[1] = ComputeBudgetInstruction::set_compute_unit_price(unit_price); - - Ok(instructions) - -} - -pub async fn build_buy_instructions_with_jito( - rpc: &RpcClient, - jito_client: &JitoClient, - payer: &Keypair, - mint: &Pubkey, - amount_sol: u64, - slippage_basis_points: Option, - priority_fee: PriorityFee, -) -> Result, anyhow::Error> { - if amount_sol == 0 { - return Err(anyhow!("Amount cannot be zero")); - } - - let global_account = get_global_account(rpc).await?; - let buy_amount = match get_bonding_curve_account(rpc, mint).await { - Ok(account) => account.get_buy_price(amount_sol).map_err(|e| anyhow!(e))?, - Err(_e) => { - let initial_buy_amount = get_initial_buy_price(&global_account, amount_sol).await?; - initial_buy_amount * 80 / 100 - } - }; - - let buy_amount_with_slippage = calculate_with_slippage_buy(amount_sol, slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE)); - - let mut instructions = vec![ - ComputeBudgetInstruction::set_compute_unit_price(priority_fee.unit_price), - ComputeBudgetInstruction::set_compute_unit_limit(priority_fee.unit_limit), - ]; - - let ata = get_associated_token_address(&payer.pubkey(), mint); - if rpc.get_account(&ata).is_err() { - instructions.push(create_associated_token_account( - &payer.pubkey(), - &payer.pubkey(), - mint, - &constants::accounts::TOKEN_PROGRAM, - )); - } - - instructions.push(instruction::buy( - payer, - mint, - &global_account.fee_recipient, - instruction::Buy { - _amount: buy_amount, - _max_sol_cost: buy_amount_with_slippage, - }, - )); - - let tip_account = jito_client.get_tip_account().await.map_err(|e| anyhow!(e))?; - - let jito_fee = priority_fee.buy_jito_fee; - instructions.push( - system_instruction::transfer( - &payer.pubkey(), - &tip_account, - sol_to_lamports(jito_fee), - ), - ); - - Ok(instructions) -} diff --git a/src/trade/create.rs b/src/trade/create.rs deleted file mode 100755 index 3935cbc..0000000 --- a/src/trade/create.rs +++ /dev/null @@ -1,326 +0,0 @@ -use std::time::Instant; - -use anyhow::anyhow; -use solana_client::{rpc_client::RpcClient, rpc_config::RpcSimulateTransactionConfig}; -use solana_sdk::{ - commitment_config::CommitmentConfig, compute_budget::ComputeBudgetInstruction, instruction::Instruction, native_token::sol_to_lamports, signature::{Keypair, Signature}, signer::Signer, system_instruction, transaction::Transaction -}; -use spl_associated_token_account::{ - get_associated_token_address, - instruction::create_associated_token_account, -}; - -use crate::{constants, instruction, ipfs::TokenMetadataIPFS, jito::JitoClient, trade::buy::build_buy_transaction_with_jito}; - -use super::common::{create_priority_fee_instructions, get_buy_amount_with_slippage, get_global_account, PriorityFee}; - -/// Create a new token -pub async fn create( - rpc: &RpcClient, - payer: &Keypair, - mint: &Keypair, - ipfs: TokenMetadataIPFS, - priority_fee: PriorityFee, -) -> Result { - let mut instructions = create_priority_fee_instructions(priority_fee); - - instructions.push(instruction::create( - payer, - mint, - instruction::Create { - _name: ipfs.metadata.name, - _symbol: ipfs.metadata.symbol, - _uri: ipfs.metadata_uri, - }, - )); - - let recent_blockhash = rpc.get_latest_blockhash()?; - let transaction = Transaction::new_signed_with_payer( - &instructions, - Some(&payer.pubkey()), - &[payer, mint], - recent_blockhash, - ); - - let signature = rpc.send_and_confirm_transaction(&transaction)?; - - Ok(signature) -} - -/// Create and buy tokens in one transaction -pub async fn create_and_buy( - rpc: &RpcClient, - payer: &Keypair, - mint: &Keypair, - ipfs: TokenMetadataIPFS, - amount_sol: u64, - slippage_basis_points: Option, - priority_fee: PriorityFee, -) -> Result { - if amount_sol == 0 { - return Err(anyhow!("Amount cannot be zero")); - } - - let transaction = build_create_and_buy_transaction(rpc, payer, mint, ipfs, amount_sol, slippage_basis_points, priority_fee).await?; - let signature = rpc.send_and_confirm_transaction(&transaction)?; - - Ok(signature) -} - -pub async fn create_and_buy_list_with_jito( - rpc: &RpcClient, - jito_client: &JitoClient, - payers: Vec<&Keypair>, - mint: &Keypair, - ipfs: TokenMetadataIPFS, - amount_sols: Vec, - slippage_basis_points: Option, - priority_fee: PriorityFee, -) -> Result { - - let start_time = Instant::now(); - - let mut transactions = Vec::new(); - let transaction = build_create_and_buy_transaction_with_jito(rpc, jito_client, payers[0], mint, ipfs, amount_sols[0], slippage_basis_points, priority_fee).await?; - transactions.push(transaction); - - for (i, payer) in payers.iter().skip(1).enumerate() { - println!("Creating and buying token index: {}", i); - let buy_transaction = build_buy_transaction_with_jito(rpc, jito_client, payer, &mint.pubkey(), amount_sols[i], slippage_basis_points, priority_fee).await?; - transactions.push(buy_transaction); - } - - let signatures = jito_client.send_transactions(&transactions).await?; - - println!("Total Jito create and buy operation time: {:?}ms", start_time.elapsed().as_millis()); - - Ok(signatures) -} - -pub async fn create_and_buy_with_jito( - rpc: &RpcClient, - jito_client: &JitoClient, - payer: &Keypair, - mint: &Keypair, - ipfs: TokenMetadataIPFS, - amount_sol: u64, - slippage_basis_points: Option, - priority_fee: PriorityFee, -) -> Result { - - let start_time = Instant::now(); - - let transaction = build_create_and_buy_transaction_with_jito(rpc, jito_client, payer, mint, ipfs, amount_sol, slippage_basis_points, priority_fee).await?; - - let signature = jito_client.send_transaction(&transaction).await?; - - println!("Total Jito create and buy operation time: {:?}ms, signature: {}", start_time.elapsed().as_millis(), signature); - - Ok(signature) -} - -pub async fn build_create_and_buy_transaction( - rpc: &RpcClient, - payer: &Keypair, - mint: &Keypair, - ipfs: TokenMetadataIPFS, - amount_sol: u64, - slippage_basis_points: Option, - priority_fee: PriorityFee, -) -> Result { - let instructions = build_create_and_buy_instructions(rpc, payer, mint, ipfs, amount_sol, slippage_basis_points, priority_fee).await?; - let recent_blockhash = rpc.get_latest_blockhash()?; - let transaction = Transaction::new_signed_with_payer( - &instructions, - Some(&payer.pubkey()), - &[payer, mint], - recent_blockhash, - ); - - Ok(transaction) -} - -pub async fn build_create_and_buy_transaction_with_jito( - rpc: &RpcClient, - jito_client: &JitoClient, - payer: &Keypair, - mint: &Keypair, - ipfs: TokenMetadataIPFS, - amount_sol: u64, - slippage_basis_points: Option, - priority_fee: PriorityFee, -) -> Result { - let instructions = build_create_and_buy_instructions_with_jito(rpc, jito_client, payer, mint, ipfs, amount_sol, slippage_basis_points, priority_fee).await?; - let recent_blockhash = rpc.get_latest_blockhash()?; - let transaction = Transaction::new_signed_with_payer( - &instructions, - Some(&payer.pubkey()), - &[payer, mint], - recent_blockhash, - ); - - Ok(transaction) -} - -pub async fn build_create_and_buy_instructions( - rpc: &RpcClient, - payer: &Keypair, - mint: &Keypair, - ipfs: TokenMetadataIPFS, - amount_sol: u64, - slippage_basis_points: Option, - priority_fee: PriorityFee, -) -> Result, anyhow::Error> { - if amount_sol == 0 { - return Err(anyhow!("Amount cannot be zero")); - } - - let global_account = get_global_account(rpc).await?; - let buy_amount = global_account.get_initial_buy_price(amount_sol); - let buy_amount_with_slippage = - get_buy_amount_with_slippage(amount_sol, slippage_basis_points); - - let mut instructions = vec![ - ComputeBudgetInstruction::set_compute_unit_limit(1_400_000), - ComputeBudgetInstruction::set_compute_unit_price(0), - ]; - - instructions.push(instruction::create( - payer, - mint, - instruction::Create { - _name: ipfs.metadata.name, - _symbol: ipfs.metadata.symbol, - _uri: ipfs.metadata_uri, - }, - )); - - let ata = get_associated_token_address(&payer.pubkey(), &mint.pubkey()); - if rpc.get_account(&ata).is_err() { - instructions.push(create_associated_token_account( - &payer.pubkey(), - &payer.pubkey(), - &mint.pubkey(), - &constants::accounts::TOKEN_PROGRAM, - )); - } - - instructions.push(instruction::buy( - payer, - &mint.pubkey(), - &global_account.fee_recipient, - instruction::Buy { - _amount: buy_amount, - _max_sol_cost: buy_amount_with_slippage, - }, - )); - - let commitment_config = CommitmentConfig::confirmed(); - let recent_blockhash = rpc.get_latest_blockhash_with_commitment(commitment_config)? - .0; - - let simulate_tx = Transaction::new_signed_with_payer( - &instructions, - Some(&payer.pubkey()), - &[payer, mint], - recent_blockhash, - ); - - let config = RpcSimulateTransactionConfig { - sig_verify: true, - commitment: Some(commitment_config), - ..RpcSimulateTransactionConfig::default() - }; - - let result = rpc.simulate_transaction_with_config(&simulate_tx, config)? - .value; - - if result.logs.as_ref().map_or(true, |logs| logs.is_empty()) { - return Err(anyhow!("Simulation failed: {:?}", result.err)); - } - - let result_cu = result.units_consumed.ok_or_else(|| anyhow!("No compute units consumed"))?; - let fees = rpc.get_recent_prioritization_fees(&[])?; - let average_fees = if fees.is_empty() { - priority_fee.unit_price - } else { - fees.iter() - .map(|fee| fee.prioritization_fee) - .sum::() / fees.len() as u64 - }; - - - let unit_price = if average_fees == 0 { priority_fee.unit_price } else { average_fees }; - - instructions[0] = ComputeBudgetInstruction::set_compute_unit_limit(result_cu as u32); - instructions[1] = ComputeBudgetInstruction::set_compute_unit_price(unit_price); - - Ok(instructions) -} - -pub async fn build_create_and_buy_instructions_with_jito( - rpc: &RpcClient, - jito_client: &JitoClient, - payer: &Keypair, - mint: &Keypair, - ipfs: TokenMetadataIPFS, - amount_sol: u64, - slippage_basis_points: Option, - priority_fee: PriorityFee, -) -> Result, anyhow::Error> { - if amount_sol == 0 { - return Err(anyhow!("Amount cannot be zero")); - } - - let global_account = get_global_account(rpc).await?; - let buy_amount = global_account.get_initial_buy_price(amount_sol); - let buy_amount_with_slippage = - get_buy_amount_with_slippage(amount_sol, slippage_basis_points); - - let mut instructions = vec![ - ComputeBudgetInstruction::set_compute_unit_price(priority_fee.unit_price), - ComputeBudgetInstruction::set_compute_unit_limit(priority_fee.unit_limit), - ]; - - instructions.push(instruction::create( - payer, - mint, - instruction::Create { - _name: ipfs.metadata.name, - _symbol: ipfs.metadata.symbol, - _uri: ipfs.metadata_uri, - }, - )); - - let ata = get_associated_token_address(&payer.pubkey(), &mint.pubkey()); - if rpc.get_account(&ata).is_err() { - instructions.push(create_associated_token_account( - &payer.pubkey(), - &payer.pubkey(), - &mint.pubkey(), - &constants::accounts::TOKEN_PROGRAM, - )); - } - - instructions.push(instruction::buy( - payer, - &mint.pubkey(), - &global_account.fee_recipient, - instruction::Buy { - _amount: buy_amount, - _max_sol_cost: buy_amount_with_slippage, - }, - )); - - let tip_account = jito_client.get_tip_account().await.map_err(|e| anyhow!(e))?; - let jito_fee = priority_fee.buy_jito_fee; - instructions.push( - system_instruction::transfer( - &payer.pubkey(), - &tip_account, - sol_to_lamports(jito_fee * 2.0), - ), - ); - - Ok(instructions) -} diff --git a/src/trade/mod.rs b/src/trade/mod.rs index 8126389..e69de29 100755 --- a/src/trade/mod.rs +++ b/src/trade/mod.rs @@ -1,4 +0,0 @@ -pub mod buy; -pub mod create; -pub mod sell; -pub mod common; \ No newline at end of file diff --git a/src/trade/sell.rs b/src/trade/sell.rs deleted file mode 100755 index 7385136..0000000 --- a/src/trade/sell.rs +++ /dev/null @@ -1,289 +0,0 @@ -use anyhow::anyhow; -use solana_client::{rpc_client::RpcClient, rpc_config::RpcSimulateTransactionConfig}; -use solana_sdk::{ - commitment_config::CommitmentConfig, compute_budget::ComputeBudgetInstruction, instruction::Instruction, native_token::sol_to_lamports, pubkey::Pubkey, signature::{Keypair, Signature}, signer::Signer, system_instruction, transaction::Transaction -}; -use spl_associated_token_account::get_associated_token_address; -use spl_token::instruction::close_account; - -use std::time::Instant; - -use crate::{constants::trade::{DEFAULT_COMPUTE_UNIT_PRICE, DEFAULT_SLIPPAGE}, instruction, jito::JitoClient}; - -use super::common::{calculate_with_slippage_sell, get_bonding_curve_account, get_global_account, PriorityFee}; - -async fn get_token_balance(rpc: &RpcClient, payer: &Keypair, mint: &Pubkey) -> Result<(u64, Pubkey), anyhow::Error> { - let ata = get_associated_token_address(&payer.pubkey(), mint); - let balance = rpc.get_token_account_balance(&ata)?; - let balance_u64 = balance.amount.parse::() - .map_err(|_| anyhow!("Failed to parse token balance"))?; - - if balance_u64 == 0 { - return Err(anyhow!("Balance is 0")); - } - - Ok((balance_u64, ata)) -} - -pub async fn sell( - rpc: &RpcClient, - payer: &Keypair, - mint: &Pubkey, - amount_token: Option, - slippage_basis_points: Option, - priority_fee: PriorityFee, -) -> Result { - - let transaction = build_sell_transaction(rpc, payer, mint, amount_token, slippage_basis_points, priority_fee).await?; - let signature = rpc.send_and_confirm_transaction(&transaction)?; - - Ok(signature) -} - -/// Sell tokens by percentage -pub async fn sell_by_percent( - rpc: &RpcClient, - payer: &Keypair, - mint: &Pubkey, - percent: u64, - slippage_basis_points: Option, - priority_fee: PriorityFee, -) -> Result { - if percent == 0 || percent > 100 { - return Err(anyhow!("Percentage must be between 1 and 100")); - } - - let (balance_u64, _) = get_token_balance(rpc, payer, mint).await?; - let amount = balance_u64 * percent / 100; - sell(rpc, payer, mint, Some(amount), slippage_basis_points, priority_fee).await -} - -pub async fn sell_by_percent_with_jito( - rpc: &RpcClient, - payer: &Keypair, - jito_client: &JitoClient, - mint: &Pubkey, - percent: u64, - slippage_basis_points: Option, - priority_fee: PriorityFee, -) -> Result { - if percent == 0 || percent > 100 { - return Err(anyhow!("Percentage must be between 1 and 100")); - } - - let (balance_u64, _) = get_token_balance(rpc, payer, mint).await?; - let amount = balance_u64 * percent / 100; - sell_with_jito(rpc, payer, jito_client, mint, Some(amount), slippage_basis_points, priority_fee).await -} - -/// Sell tokens using Jito -pub async fn sell_with_jito( - rpc: &RpcClient, - payer: &Keypair, - jito_client: &JitoClient, - mint: &Pubkey, - amount_token: Option, - slippage_basis_points: Option, - priority_fee: PriorityFee, -) -> Result { - let start_time = Instant::now(); - - let transaction = build_sell_transaction_with_jito(rpc, jito_client, payer, mint, amount_token, slippage_basis_points, priority_fee).await?; - let signature = jito_client.send_transaction(&transaction).await?; - - println!("Total Jito sell operation time: {:?}ms, signature: {}", start_time.elapsed().as_millis(), signature); - - Ok(signature) -} - -pub async fn build_sell_transaction( - rpc: &RpcClient, - payer: &Keypair, - mint: &Pubkey, - amount_token: Option, - slippage_basis_points: Option, - priority_fee: PriorityFee, -) -> Result { - let instructions = build_sell_instructions(rpc, payer, mint, amount_token, slippage_basis_points, priority_fee).await?; - let recent_blockhash = rpc.get_latest_blockhash()?; - let transaction = Transaction::new_signed_with_payer( - &instructions, - Some(&payer.pubkey()), - &[payer], - recent_blockhash, - ); - - Ok(transaction) -} - -pub async fn build_sell_transaction_with_jito( - rpc: &RpcClient, - jito_client: &JitoClient, - payer: &Keypair, - mint: &Pubkey, - amount_token: Option, - slippage_basis_points: Option, - priority_fee: PriorityFee, -) -> Result { - let instructions = build_sell_instructions_with_jito(rpc, jito_client, payer, mint, amount_token, slippage_basis_points, priority_fee).await?; - let recent_blockhash = rpc.get_latest_blockhash()?; - let transaction = Transaction::new_signed_with_payer( - &instructions, - Some(&payer.pubkey()), - &[payer], - recent_blockhash, - ); - - Ok(transaction) -} - -pub async fn build_sell_instructions( - rpc: &RpcClient, - payer: &Keypair, - mint: &Pubkey, - amount_token: Option, - slippage_basis_points: Option, - priority_fee: PriorityFee, -) -> Result, anyhow::Error> { - let (balance_u64, ata) = get_token_balance(rpc, payer, mint).await?; - let amount = amount_token.unwrap_or(balance_u64); - - if amount == 0 { - return Err(anyhow!("Amount cannot be zero")); - } - - let global_account = get_global_account(rpc).await?; - let bonding_curve_account = get_bonding_curve_account(rpc, mint).await?; - let min_sol_output = bonding_curve_account - .get_sell_price(amount, global_account.fee_basis_points) - .map_err(|e| anyhow!(e))?; - let min_sol_output_with_slippage = calculate_with_slippage_sell( - min_sol_output, - slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE), - ); - - let mut instructions = vec![ - ComputeBudgetInstruction::set_compute_unit_limit(1_400_000), - ComputeBudgetInstruction::set_compute_unit_price(0), - ]; - - instructions.push(instruction::sell( - payer, - mint, - &global_account.fee_recipient, - instruction::Sell { - _amount: amount, - _min_sol_output: min_sol_output_with_slippage, - }, - )); - - instructions.push(close_account( - &spl_token::ID, - &ata, - &payer.pubkey(), - &payer.pubkey(), - &[&payer.pubkey()], - )?); - - let commitment_config = CommitmentConfig::confirmed(); - let recent_blockhash = rpc.get_latest_blockhash_with_commitment(commitment_config)? - .0; - - let simulate_tx = Transaction::new_signed_with_payer( - &instructions, - Some(&payer.pubkey()), - &[payer], - recent_blockhash, - ); - - let config = RpcSimulateTransactionConfig { - sig_verify: true, - commitment: Some(commitment_config), - ..RpcSimulateTransactionConfig::default() - }; - - let result = rpc.simulate_transaction_with_config(&simulate_tx, config)? - .value; - - if result.logs.as_ref().map_or(true, |logs| logs.is_empty()) { - return Err(anyhow!("Simulation failed: {:?}", result.err)); - } - - let result_cu = result.units_consumed.ok_or_else(|| anyhow!("No compute units consumed"))?; - let fees = rpc.get_recent_prioritization_fees(&[])?; - let average_fees = if fees.is_empty() { - DEFAULT_COMPUTE_UNIT_PRICE - } else { - fees.iter() - .map(|fee| fee.prioritization_fee) - .sum::() / fees.len() as u64 - }; - - let unit_price = if average_fees == 0 { priority_fee.unit_price } else { average_fees }; - instructions[0] = ComputeBudgetInstruction::set_compute_unit_limit(result_cu as u32); - instructions[1] = ComputeBudgetInstruction::set_compute_unit_price(unit_price); - - Ok(instructions) -} - -pub async fn build_sell_instructions_with_jito( - rpc: &RpcClient, - jito_client: &JitoClient, - payer: &Keypair, - mint: &Pubkey, - amount_token: Option, - slippage_basis_points: Option, - priority_fee: PriorityFee, -) -> Result, anyhow::Error> { - let (balance_u64, ata) = get_token_balance(rpc, payer, mint).await?; - let amount = amount_token.unwrap_or(balance_u64); - - if amount == 0 { - return Err(anyhow!("Amount cannot be zero")); - } - - let global_account = get_global_account(rpc).await?; - let bonding_curve_account = get_bonding_curve_account(rpc, mint).await?; - let min_sol_output = bonding_curve_account - .get_sell_price(amount, global_account.fee_basis_points) - .map_err(|e| anyhow!(e))?; - let min_sol_output_with_slippage = calculate_with_slippage_sell( - min_sol_output, - slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE), - ); - - let mut instructions = vec![ - ComputeBudgetInstruction::set_compute_unit_price(priority_fee.unit_price), - ComputeBudgetInstruction::set_compute_unit_limit(priority_fee.unit_limit), - ]; - - instructions.push(instruction::sell( - payer, - mint, - &global_account.fee_recipient, - instruction::Sell { - _amount: amount, - _min_sol_output: min_sol_output_with_slippage, - }, - )); - - instructions.push(close_account( - &spl_token::ID, - &ata, - &payer.pubkey(), - &payer.pubkey(), - &[&payer.pubkey()], - )?); - - let tip_account = jito_client.get_tip_account().await.map_err(|e| anyhow!(e))?; - let jito_fee = priority_fee.sell_jito_fee; - instructions.push( - system_instruction::transfer( - &payer.pubkey(), - &tip_account, - sol_to_lamports(jito_fee), - ), - ); - - Ok(instructions) -}