RPC: Support base64 encoded transactions
Defaults to base58
This commit is contained in:
parent
7f67d36777
commit
e35889542b
7
Cargo.lock
generated
7
Cargo.lock
generated
@ -217,6 +217,12 @@ version = "0.12.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff"
|
checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "base64"
|
||||||
|
version = "0.13.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bincode"
|
name = "bincode"
|
||||||
version = "1.3.1"
|
version = "1.3.1"
|
||||||
@ -3614,6 +3620,7 @@ name = "solana-client"
|
|||||||
version = "1.4.0"
|
version = "1.4.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"assert_matches",
|
"assert_matches",
|
||||||
|
"base64 0.13.0",
|
||||||
"bincode",
|
"bincode",
|
||||||
"bs58",
|
"bs58",
|
||||||
"clap",
|
"clap",
|
||||||
|
@ -9,6 +9,7 @@ license = "Apache-2.0"
|
|||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
base64 = "0.13.0"
|
||||||
bincode = "1.3.1"
|
bincode = "1.3.1"
|
||||||
bs58 = "0.3.1"
|
bs58 = "0.3.1"
|
||||||
clap = "2.33.0"
|
clap = "2.33.0"
|
||||||
|
@ -5,7 +5,8 @@ use crate::{
|
|||||||
rpc_config::RpcAccountInfoConfig,
|
rpc_config::RpcAccountInfoConfig,
|
||||||
rpc_config::{
|
rpc_config::{
|
||||||
RpcGetConfirmedSignaturesForAddress2Config, RpcLargestAccountsConfig,
|
RpcGetConfirmedSignaturesForAddress2Config, RpcLargestAccountsConfig,
|
||||||
RpcProgramAccountsConfig, RpcSendTransactionConfig, RpcTokenAccountsFilter,
|
RpcProgramAccountsConfig, RpcSendTransactionConfig, RpcSimulateTransactionConfig,
|
||||||
|
RpcTokenAccountsFilter,
|
||||||
},
|
},
|
||||||
rpc_request::{RpcError, RpcRequest, TokenAccountsFilter},
|
rpc_request::{RpcError, RpcRequest, TokenAccountsFilter},
|
||||||
rpc_response::*,
|
rpc_response::*,
|
||||||
@ -48,6 +49,26 @@ pub struct RpcClient {
|
|||||||
sender: Box<dyn RpcSender + Send + Sync + 'static>,
|
sender: Box<dyn RpcSender + Send + Sync + 'static>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn serialize_encode_transaction(
|
||||||
|
transaction: &Transaction,
|
||||||
|
encoding: UiTransactionEncoding,
|
||||||
|
) -> ClientResult<String> {
|
||||||
|
let serialized = serialize(transaction)
|
||||||
|
.map_err(|e| ClientErrorKind::Custom(format!("transaction serialization failed: {}", e)))?;
|
||||||
|
let encoded = match encoding {
|
||||||
|
UiTransactionEncoding::Base58 => bs58::encode(serialized).into_string(),
|
||||||
|
UiTransactionEncoding::Base64 => base64::encode(serialized),
|
||||||
|
_ => {
|
||||||
|
return Err(ClientErrorKind::Custom(format!(
|
||||||
|
"unsupported transaction encoding: {}. Supported encodings: base58, base64",
|
||||||
|
encoding
|
||||||
|
))
|
||||||
|
.into())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(encoded)
|
||||||
|
}
|
||||||
|
|
||||||
impl RpcClient {
|
impl RpcClient {
|
||||||
pub fn new_sender<T: RpcSender + Send + Sync + 'static>(sender: T) -> Self {
|
pub fn new_sender<T: RpcSender + Send + Sync + 'static>(sender: T) -> Self {
|
||||||
Self {
|
Self {
|
||||||
@ -112,8 +133,8 @@ impl RpcClient {
|
|||||||
transaction: &Transaction,
|
transaction: &Transaction,
|
||||||
config: RpcSendTransactionConfig,
|
config: RpcSendTransactionConfig,
|
||||||
) -> ClientResult<Signature> {
|
) -> ClientResult<Signature> {
|
||||||
let serialized_encoded = bs58::encode(serialize(transaction).unwrap()).into_string();
|
let encoding = config.encoding.unwrap_or(UiTransactionEncoding::Base58);
|
||||||
|
let serialized_encoded = serialize_encode_transaction(transaction, encoding)?;
|
||||||
let signature_base58_str: String = self.send(
|
let signature_base58_str: String = self.send(
|
||||||
RpcRequest::SendTransaction,
|
RpcRequest::SendTransaction,
|
||||||
json!([serialized_encoded, config]),
|
json!([serialized_encoded, config]),
|
||||||
@ -142,7 +163,21 @@ impl RpcClient {
|
|||||||
transaction: &Transaction,
|
transaction: &Transaction,
|
||||||
sig_verify: bool,
|
sig_verify: bool,
|
||||||
) -> RpcResult<RpcSimulateTransactionResult> {
|
) -> RpcResult<RpcSimulateTransactionResult> {
|
||||||
let serialized_encoded = bs58::encode(serialize(transaction).unwrap()).into_string();
|
self.simulate_transaction_with_config(
|
||||||
|
transaction,
|
||||||
|
sig_verify,
|
||||||
|
RpcSimulateTransactionConfig::default(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn simulate_transaction_with_config(
|
||||||
|
&self,
|
||||||
|
transaction: &Transaction,
|
||||||
|
sig_verify: bool,
|
||||||
|
config: RpcSimulateTransactionConfig,
|
||||||
|
) -> RpcResult<RpcSimulateTransactionResult> {
|
||||||
|
let encoding = config.encoding.unwrap_or(UiTransactionEncoding::Base58);
|
||||||
|
let serialized_encoded = serialize_encode_transaction(transaction, encoding)?;
|
||||||
self.send(
|
self.send(
|
||||||
RpcRequest::SimulateTransaction,
|
RpcRequest::SimulateTransaction,
|
||||||
json!([serialized_encoded, { "sigVerify": sig_verify }]),
|
json!([serialized_encoded, { "sigVerify": sig_verify }]),
|
||||||
|
@ -4,6 +4,7 @@ use solana_sdk::{
|
|||||||
clock::Epoch,
|
clock::Epoch,
|
||||||
commitment_config::{CommitmentConfig, CommitmentLevel},
|
commitment_config::{CommitmentConfig, CommitmentLevel},
|
||||||
};
|
};
|
||||||
|
use solana_transaction_status::UiTransactionEncoding;
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
@ -17,6 +18,7 @@ pub struct RpcSendTransactionConfig {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub skip_preflight: bool,
|
pub skip_preflight: bool,
|
||||||
pub preflight_commitment: Option<CommitmentLevel>,
|
pub preflight_commitment: Option<CommitmentLevel>,
|
||||||
|
pub encoding: Option<UiTransactionEncoding>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
@ -26,6 +28,7 @@ pub struct RpcSimulateTransactionConfig {
|
|||||||
pub sig_verify: bool,
|
pub sig_verify: bool,
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
pub commitment: Option<CommitmentConfig>,
|
pub commitment: Option<CommitmentConfig>,
|
||||||
|
pub encoding: Option<UiTransactionEncoding>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
@ -14,6 +14,7 @@ edition = "2018"
|
|||||||
codecov = { repository = "solana-labs/solana", branch = "master", service = "github" }
|
codecov = { repository = "solana-labs/solana", branch = "master", service = "github" }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
base64 = "0.12.3"
|
||||||
bincode = "1.3.1"
|
bincode = "1.3.1"
|
||||||
bv = { version = "0.11.1", features = ["serde"] }
|
bv = { version = "0.11.1", features = ["serde"] }
|
||||||
bs58 = "0.3.1"
|
bs58 = "0.3.1"
|
||||||
@ -78,7 +79,6 @@ solana-rayon-threadlimit = { path = "../rayon-threadlimit", version = "1.4.0" }
|
|||||||
trees = "0.2.1"
|
trees = "0.2.1"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
base64 = "0.12.3"
|
|
||||||
matches = "0.1.6"
|
matches = "0.1.6"
|
||||||
reqwest = { version = "0.10.6", default-features = false, features = ["blocking", "rustls-tls", "json"] }
|
reqwest = { version = "0.10.6", default-features = false, features = ["blocking", "rustls-tls", "json"] }
|
||||||
serial_test = "0.4.0"
|
serial_test = "0.4.0"
|
||||||
|
@ -2268,7 +2268,8 @@ impl RpcSol for RpcSolImpl {
|
|||||||
) -> Result<String> {
|
) -> Result<String> {
|
||||||
debug!("send_transaction rpc request received");
|
debug!("send_transaction rpc request received");
|
||||||
let config = config.unwrap_or_default();
|
let config = config.unwrap_or_default();
|
||||||
let (wire_transaction, transaction) = deserialize_bs58_transaction(data)?;
|
let encoding = config.encoding.unwrap_or(UiTransactionEncoding::Base58);
|
||||||
|
let (wire_transaction, transaction) = deserialize_transaction(data, encoding)?;
|
||||||
let bank = &*meta.bank(None);
|
let bank = &*meta.bank(None);
|
||||||
let last_valid_slot = bank
|
let last_valid_slot = bank
|
||||||
.get_blockhash_last_valid_slot(&transaction.message.recent_blockhash)
|
.get_blockhash_last_valid_slot(&transaction.message.recent_blockhash)
|
||||||
@ -2313,8 +2314,9 @@ impl RpcSol for RpcSolImpl {
|
|||||||
config: Option<RpcSimulateTransactionConfig>,
|
config: Option<RpcSimulateTransactionConfig>,
|
||||||
) -> Result<RpcResponse<RpcSimulateTransactionResult>> {
|
) -> Result<RpcResponse<RpcSimulateTransactionResult>> {
|
||||||
debug!("simulate_transaction rpc request received");
|
debug!("simulate_transaction rpc request received");
|
||||||
let (_, transaction) = deserialize_bs58_transaction(data)?;
|
|
||||||
let config = config.unwrap_or_default();
|
let config = config.unwrap_or_default();
|
||||||
|
let encoding = config.encoding.unwrap_or(UiTransactionEncoding::Base58);
|
||||||
|
let (_, transaction) = deserialize_transaction(data, encoding)?;
|
||||||
|
|
||||||
let mut result = if config.sig_verify {
|
let mut result = if config.sig_verify {
|
||||||
transaction.verify()
|
transaction.verify()
|
||||||
@ -2600,18 +2602,44 @@ impl RpcSol for RpcSolImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const WORST_CASE_BASE58_TX: usize = 1683; // Golden, bump if PACKET_DATA_SIZE changes
|
const WORST_CASE_BASE58_TX: usize = 1683; // Golden, bump if PACKET_DATA_SIZE changes
|
||||||
fn deserialize_bs58_transaction(bs58_transaction: String) -> Result<(Vec<u8>, Transaction)> {
|
const WORST_CASE_BASE64_TX: usize = 1644; // Golden, bump if PACKET_DATA_SIZE changes
|
||||||
if bs58_transaction.len() > WORST_CASE_BASE58_TX {
|
fn deserialize_transaction(
|
||||||
|
encoded_transaction: String,
|
||||||
|
encoding: UiTransactionEncoding,
|
||||||
|
) -> Result<(Vec<u8>, Transaction)> {
|
||||||
|
let wire_transaction = match encoding {
|
||||||
|
UiTransactionEncoding::Base58 => {
|
||||||
|
if encoded_transaction.len() > WORST_CASE_BASE58_TX {
|
||||||
return Err(Error::invalid_params(format!(
|
return Err(Error::invalid_params(format!(
|
||||||
"encoded transaction too large: {} bytes (max: encoded/raw {}/{})",
|
"encoded transaction too large: {} bytes (max: encoded/raw {}/{})",
|
||||||
bs58_transaction.len(),
|
encoded_transaction.len(),
|
||||||
WORST_CASE_BASE58_TX,
|
WORST_CASE_BASE58_TX,
|
||||||
PACKET_DATA_SIZE,
|
PACKET_DATA_SIZE,
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
let wire_transaction = bs58::decode(bs58_transaction)
|
bs58::decode(encoded_transaction)
|
||||||
.into_vec()
|
.into_vec()
|
||||||
.map_err(|e| Error::invalid_params(format!("{:?}", e)))?;
|
.map_err(|e| Error::invalid_params(format!("{:?}", e)))?
|
||||||
|
}
|
||||||
|
UiTransactionEncoding::Base64 => {
|
||||||
|
if encoded_transaction.len() > WORST_CASE_BASE64_TX {
|
||||||
|
return Err(Error::invalid_params(format!(
|
||||||
|
"encoded transaction too large: {} bytes (max: encoded/raw {}/{})",
|
||||||
|
encoded_transaction.len(),
|
||||||
|
WORST_CASE_BASE64_TX,
|
||||||
|
PACKET_DATA_SIZE,
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
base64::decode(encoded_transaction)
|
||||||
|
.map_err(|e| Error::invalid_params(format!("{:?}", e)))?
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
return Err(Error::invalid_params(format!(
|
||||||
|
"unsupported transaction encoding: {}. Supported encodings: base58, base64",
|
||||||
|
encoding
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
};
|
||||||
if wire_transaction.len() > PACKET_DATA_SIZE {
|
if wire_transaction.len() > PACKET_DATA_SIZE {
|
||||||
let err = format!(
|
let err = format!(
|
||||||
"transaction too large: {} bytes (max: {} bytes)",
|
"transaction too large: {} bytes (max: {} bytes)",
|
||||||
@ -5789,7 +5817,52 @@ pub mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_worst_case_encoded_tx_goldens() {
|
fn test_worst_case_encoded_tx_goldens() {
|
||||||
let ff_tx = vec![0xffu8; PACKET_DATA_SIZE];
|
let ff_tx = vec![0xffu8; PACKET_DATA_SIZE];
|
||||||
let tx58 = bs58::encode(ff_tx).into_string();
|
let tx58 = bs58::encode(&ff_tx).into_string();
|
||||||
assert_eq!(tx58.len(), WORST_CASE_BASE58_TX);
|
assert_eq!(tx58.len(), WORST_CASE_BASE58_TX);
|
||||||
|
let tx64 = base64::encode(&ff_tx);
|
||||||
|
assert_eq!(tx64.len(), WORST_CASE_BASE64_TX);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_deserialize_transacion_too_large_payloads_fail() {
|
||||||
|
// +2 because +1 still fits in base64 encoded worst-case
|
||||||
|
let too_big = PACKET_DATA_SIZE + 2;
|
||||||
|
let tx_ser = vec![0xffu8; too_big];
|
||||||
|
let tx58 = bs58::encode(&tx_ser).into_string();
|
||||||
|
let tx58_len = tx58.len();
|
||||||
|
let expect58 = Error::invalid_params(format!(
|
||||||
|
"encoded transaction too large: {} bytes (max: encoded/raw {}/{})",
|
||||||
|
tx58_len, WORST_CASE_BASE58_TX, PACKET_DATA_SIZE,
|
||||||
|
));
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_transaction(tx58, UiTransactionEncoding::Base58).unwrap_err(),
|
||||||
|
expect58
|
||||||
|
);
|
||||||
|
let tx64 = base64::encode(&tx_ser);
|
||||||
|
let tx64_len = tx64.len();
|
||||||
|
let expect64 = Error::invalid_params(format!(
|
||||||
|
"encoded transaction too large: {} bytes (max: encoded/raw {}/{})",
|
||||||
|
tx64_len, WORST_CASE_BASE64_TX, PACKET_DATA_SIZE,
|
||||||
|
));
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_transaction(tx64, UiTransactionEncoding::Base64).unwrap_err(),
|
||||||
|
expect64
|
||||||
|
);
|
||||||
|
let too_big = PACKET_DATA_SIZE + 1;
|
||||||
|
let tx_ser = vec![0x00u8; too_big];
|
||||||
|
let tx58 = bs58::encode(&tx_ser).into_string();
|
||||||
|
let expect = Error::invalid_params(format!(
|
||||||
|
"transaction too large: {} bytes (max: {} bytes)",
|
||||||
|
too_big, PACKET_DATA_SIZE
|
||||||
|
));
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_transaction(tx58, UiTransactionEncoding::Base58).unwrap_err(),
|
||||||
|
expect
|
||||||
|
);
|
||||||
|
let tx64 = base64::encode(&tx_ser);
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_transaction(tx64, UiTransactionEncoding::Base64).unwrap_err(),
|
||||||
|
expect
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1470,10 +1470,11 @@ Before submitting, the following preflight checks are performed:
|
|||||||
|
|
||||||
#### Parameters:
|
#### Parameters:
|
||||||
|
|
||||||
- `<string>` - fully-signed Transaction, as base-58 encoded string
|
- `<string>` - fully-signed Transaction, as encoded string
|
||||||
- `<object>` - (optional) Configuration object containing the following field:
|
- `<object>` - (optional) Configuration object containing the following field:
|
||||||
- `skipPreflight: <bool>` - if true, skip the preflight transaction checks (default: false)
|
- `skipPreflight: <bool>` - if true, skip the preflight transaction checks (default: false)
|
||||||
- `preflightCommitment: <string>` - (optional) [Commitment](jsonrpc-api.md#configuring-state-commitment) level to use for preflight (default: `"max"`).
|
- `preflightCommitment: <string>` - (optional) [Commitment](jsonrpc-api.md#configuring-state-commitment) level to use for preflight (default: `"max"`).
|
||||||
|
- `encoding: <string>` - (optional) Encoding used for the transaction data. Either `"base58"` (*slow*, **DEPRECATED**), or `"base64"`. (default: `"base58"`).
|
||||||
|
|
||||||
#### Results:
|
#### Results:
|
||||||
|
|
||||||
@ -1495,10 +1496,11 @@ Simulate sending a transaction
|
|||||||
|
|
||||||
#### Parameters:
|
#### Parameters:
|
||||||
|
|
||||||
- `<string>` - Transaction, as base-58 encoded string. The transaction must have a valid blockhash, but is not required to be signed.
|
- `<string>` - Transaction, as an encoded string. The transaction must have a valid blockhash, but is not required to be signed.
|
||||||
- `<object>` - (optional) Configuration object containing the following field:
|
- `<object>` - (optional) Configuration object containing the following field:
|
||||||
- `sigVerify: <bool>` - if true the transaction signatures will be verified (default: false)
|
- `sigVerify: <bool>` - if true the transaction signatures will be verified (default: false)
|
||||||
- `commitment: <string>` - (optional) [Commitment](jsonrpc-api.md#configuring-state-commitment) level to simulate the transaction at (default: `"max"`).
|
- `commitment: <string>` - (optional) [Commitment](jsonrpc-api.md#configuring-state-commitment) level to simulate the transaction at (default: `"max"`).
|
||||||
|
- `encoding: <string>` - (optional) Encoding used for the transaction data. Either `"base58"` (*slow*, **DEPRECATED**), or `"base64"`. (default: `"base58"`).
|
||||||
|
|
||||||
#### Results:
|
#### Results:
|
||||||
|
|
||||||
|
@ -21,6 +21,7 @@ use solana_sdk::{
|
|||||||
signature::Signature,
|
signature::Signature,
|
||||||
transaction::{Result, Transaction, TransactionError},
|
transaction::{Result, Transaction, TransactionError},
|
||||||
};
|
};
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
/// A duplicate representation of an Instruction for pretty JSON serialization
|
/// A duplicate representation of an Instruction for pretty JSON serialization
|
||||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||||
@ -380,6 +381,14 @@ pub enum UiTransactionEncoding {
|
|||||||
JsonParsed,
|
JsonParsed,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for UiTransactionEncoding {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
let v = serde_json::to_value(self).map_err(|_| fmt::Error)?;
|
||||||
|
let s = v.as_str().ok_or(fmt::Error)?;
|
||||||
|
write!(f, "{}", s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase", untagged)]
|
#[serde(rename_all = "camelCase", untagged)]
|
||||||
pub enum EncodedTransaction {
|
pub enum EncodedTransaction {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user