Skip to content

Commit

Permalink
[Solana]: Add utils to get/set compute unit price/limit (#4085)
Browse files Browse the repository at this point in the history
* [Solana]: Add `SolanaTransaction` functions to get/set compute unit price/limit

* [Solana]: Add Rust FFI functions

* Fix `VersionedMessage::address_table_lookups()` method

* [Solana]: Add set priority tests

* [Solana]: Add C++ bindings

* [Solana]: Add Android, iOS tests
  • Loading branch information
satoshiotomakan authored Oct 31, 2024
1 parent 63ef219 commit f6d8a10
Show file tree
Hide file tree
Showing 17 changed files with 867 additions and 11 deletions.
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
package com.trustwallet.core.app.blockchains.solana

import com.google.protobuf.ByteString
import com.trustwallet.core.app.utils.toHex
import com.trustwallet.core.app.utils.toHexByteArray
import org.junit.Assert.assertEquals
import org.junit.Test
import wallet.core.jni.Base58
import wallet.core.java.AnySigner
import wallet.core.jni.Base64
import wallet.core.jni.CoinType
import wallet.core.jni.CoinType.SOLANA
import wallet.core.jni.SolanaTransaction
import wallet.core.jni.DataVector
Expand All @@ -18,6 +15,7 @@ import wallet.core.jni.proto.Solana
import wallet.core.jni.proto.Solana.DecodingTransactionOutput
import wallet.core.jni.proto.Solana.SigningInput
import wallet.core.jni.proto.Solana.SigningOutput
import wallet.core.jni.proto.Solana.Encoding

class TestSolanaTransaction {

Expand Down Expand Up @@ -81,4 +79,43 @@ class TestSolanaTransaction {
val expectedString = "Ajzc/Tke0CG8Cew5qFa6xZI/7Ya3DN0M8Ige6tKPsGzhg8Bw9DqL18KUrEZZ1F4YqZBo4Rv+FsDT8A7Nss7p4A6BNVZzzGprCJqYQeNg0EVIbmPc6mDitNniHXGeKgPZ6QZbM4FElw9O7IOFTpOBPvQFeqy0vZf/aayncL8EK/UEAgACBssq8Im1alV3N7wXGODL8jLPWwLhTuCqfGZ1Iz9fb5tXlMOJD6jUvASrKmdtLK/qXNyJns2Vqcvlk+nfJYdZaFpIWiT/tAcEYbttfxyLdYxrLckAKdVRtf1OrNgtZeMCII4SAn6SYaaidrX/AN3s/aVn/zrlEKW0cEUIatHVDKtXO0Qss5EhV/E6kz0BNCgtAytf/s0Botvxt3kGCN8ALqcG3fbh12Whk9nL4UbO63msHLSF7V9bN5E6jPWFfv8AqbHiki6ThNH3auuyZPQpJntnN0mA//56nMpK/6HIuu8xAQUEAgQDAQoMoA8AAAAAAAAG"
assertEquals(output.encoded, expectedString)
}

@Test
fun testSetPriorityFee() {
val privateKey = ByteString.copyFrom("baf2b2dbbbad7ca96c1fa199c686f3d8fbd2c7b352f307e37e04f33df6741f18".toHexByteArray())
val originalTx = "AX43+Ir2EDqf2zLEvgzFrCZKRjdr3wCdp8CnvYh6N0G/s86IueX9BbiNUl16iLRGvwREDfi2Srb0hmLNBFw1BwABAAEDODI+iWe7g68B9iwCy8bFkJKvsIEj350oSOpcv4gNnv/st+6qmqipl9lwMK6toB9TiL7LrJVfij+pKwr+pUKxfwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAG6GdPcA92ORzVJe2jfG8KQqqMHr9YTLu30oM4i7MFEoBAgIAAQwCAAAA6AMAAAAAAAA="

// Step 1 - Check if there are no price and limit instructions in the original transaction.
assertEquals(SolanaTransaction.getComputeUnitPrice(originalTx), null)
assertEquals(SolanaTransaction.getComputeUnitLimit(originalTx), null)

// Step 2 - Set price and limit instructions.
val txWithPrice = SolanaTransaction.setComputeUnitPrice(originalTx, "1000")
val updatedTx = SolanaTransaction.setComputeUnitLimit(txWithPrice, "10000")

assertEquals(updatedTx, "AX43+Ir2EDqf2zLEvgzFrCZKRjdr3wCdp8CnvYh6N0G/s86IueX9BbiNUl16iLRGvwREDfi2Srb0hmLNBFw1BwABAAIEODI+iWe7g68B9iwCy8bFkJKvsIEj350oSOpcv4gNnv/st+6qmqipl9lwMK6toB9TiL7LrJVfij+pKwr+pUKxfwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwZGb+UhFzL/7K26csOb57yM5bvF9xJrLEObOkAAAAAboZ09wD3Y5HNUl7aN8bwpCqowev1hMu7fSgziLswUSgMDAAUCECcAAAICAAEMAgAAAOgDAAAAAAAAAwAJA+gDAAAAAAAA")

// Step 3 - Check if price and limit instructions are set successfully.
assertEquals(SolanaTransaction.getComputeUnitPrice(updatedTx), "1000")
assertEquals(SolanaTransaction.getComputeUnitLimit(updatedTx), "10000")

// Step 4 - Decode transaction into a `RawMessage` Protobuf.
val updatedTxData = Base64.decode(updatedTx)
val decodedData = TransactionDecoder.decode(SOLANA, updatedTxData)
val decodedOutput = DecodingTransactionOutput.parseFrom(decodedData)

assertEquals(decodedOutput.error, SigningError.OK)

// Step 5 - Sign the decoded `RawMessage` transaction.
val signingInput = SigningInput.newBuilder()
.setPrivateKey(privateKey)
.setRawMessage(decodedOutput.transaction)
.setTxEncoding(Encoding.Base64)
.build()
val output = AnySigner.sign(signingInput, SOLANA, SigningOutput.parser())
// Successfully broadcasted tx:
// https://explorer.solana.com/tx/2ho7wZUXbDNz12xGfsXg2kcNMqkBAQjv7YNXNcVcuCmbC4p9FZe9ELeM2gMjq9MKQPpmE3nBW5pbdgwVCfNLr1h8
val expectedString = "AVUye82Mv+/aWeU2G+B6Nes365mUU2m8iqcGZn/8kFJvw4wY6AgKGG+vJHaknHlCDwE1yi1SIMVUUtNCOm3kHg8BAAIEODI+iWe7g68B9iwCy8bFkJKvsIEj350oSOpcv4gNnv/st+6qmqipl9lwMK6toB9TiL7LrJVfij+pKwr+pUKxfwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwZGb+UhFzL/7K26csOb57yM5bvF9xJrLEObOkAAAAAboZ09wD3Y5HNUl7aN8bwpCqowev1hMu7fSgziLswUSgMDAAUCECcAAAICAAEMAgAAAOgDAAAAAAAAAwAJA+gDAAAAAAAA"
assertEquals(output.encoded, expectedString)
}
}
34 changes: 34 additions & 0 deletions include/TrustWalletCore/TWSolanaTransaction.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,38 @@ TWData *_Nonnull TWSolanaTransactionUpdateBlockhashAndSign(TWString *_Nonnull en
TWString *_Nonnull recentBlockhash,
const struct TWDataVector *_Nonnull privateKeys);

/// Try to find a `ComputeBudgetInstruction::SetComputeUnitPrice` instruction in the given transaction,
/// and returns the specified Unit Price.
///
/// \param encodedTx base64 encoded Solana transaction.
/// \return nullable Unit Price as a decimal string. Null if no instruction found.
TW_EXPORT_STATIC_METHOD
TWString *_Nullable TWSolanaTransactionGetComputeUnitPrice(TWString *_Nonnull encodedTx);

/// Try to find a `ComputeBudgetInstruction::SetComputeUnitLimit` instruction in the given transaction,
/// and returns the specified Unit Limit.
///
/// \param encodedTx base64 encoded Solana transaction.
/// \return nullable Unit Limit as a decimal string. Null if no instruction found.
TW_EXPORT_STATIC_METHOD
TWString *_Nullable TWSolanaTransactionGetComputeUnitLimit(TWString *_Nonnull encodedTx);

/// Adds or updates a `ComputeBudgetInstruction::SetComputeUnitPrice` instruction of the given transaction,
/// and returns the updated transaction.
///
/// \param encodedTx base64 encoded Solana transaction.
/// \price Unit Price as a decimal string.
/// \return base64 encoded Solana transaction. Null if an error occurred.
TW_EXPORT_STATIC_METHOD
TWString *_Nullable TWSolanaTransactionSetComputeUnitPrice(TWString *_Nonnull encodedTx, TWString *_Nonnull price);

/// Adds or updates a `ComputeBudgetInstruction::SetComputeUnitLimit` instruction of the given transaction,
/// and returns the updated transaction.
///
/// \param encodedTx base64 encoded Solana transaction.
/// \limit Unit Limit as a decimal string.
/// \return base64 encoded Solana transaction. Null if an error occurred.
TW_EXPORT_STATIC_METHOD
TWString *_Nullable TWSolanaTransactionSetComputeUnitLimit(TWString *_Nonnull encodedTx, TWString *_Nonnull limit);

TW_EXTERN_C_END
10 changes: 6 additions & 4 deletions rust/chains/tw_solana/src/modules/compiled_keys.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ use std::collections::hash_map::Entry;
use std::collections::HashMap;
use tw_coin_entry::error::prelude::*;

pub fn try_into_u8(num: usize) -> SigningResult<u8> {
u8::try_from(num)
.tw_err(|_| SigningErrorType::Error_tx_too_big)
.context("There are too many accounts in the transaction")
}

#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
struct CompiledKeyMeta {
is_signer: bool,
Expand Down Expand Up @@ -67,10 +73,6 @@ impl CompiledKeys {
}

pub fn try_into_message_components(self) -> SigningResult<(MessageHeader, Vec<SolanaAddress>)> {
let try_into_u8 = |num: usize| -> SigningResult<u8> {
u8::try_from(num).tw_err(|_| SigningErrorType::Error_tx_too_big)
};

let Self {
ordered_keys,
key_meta_map,
Expand Down
102 changes: 102 additions & 0 deletions rust/chains/tw_solana/src/modules/insert_instruction.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// SPDX-License-Identifier: Apache-2.0
//
// Copyright © 2017 Trust Wallet.

use crate::address::SolanaAddress;
use crate::modules::compiled_keys::try_into_u8;
use crate::transaction::v0::MessageAddressTableLookup;
use crate::transaction::{CompiledInstruction, MessageHeader};
use std::iter;
use tw_coin_entry::error::prelude::*;
use tw_memory::Data;

pub trait InsertInstruction {
/// Pushes a simple instruction that doesn't have accounts.
fn push_simple_instruction(
&mut self,
program_id: SolanaAddress,
data: Data,
) -> SigningResult<()> {
let insert_at = self.instructions_mut().len();
self.insert_simple_instruction(insert_at, program_id, data)
}

/// Inserts a simple instruction that doesn't have accounts at the given `insert_at` index.
fn insert_simple_instruction(
&mut self,
insert_at: usize,
program_id: SolanaAddress,
data: Data,
) -> SigningResult<()> {
if insert_at > self.instructions_mut().len() {
return SigningError::err(SigningErrorType::Error_internal)
.context(format!("Unable to add '{program_id}' instruction at the '{insert_at}' index. Number of existing instructions: {}", self.instructions_mut().len()));
}

// Step 1 - find or add the `program_id` in the accounts list.
let program_id_index = match self
.account_keys_mut()
.iter()
.position(|acc| *acc == program_id)
{
Some(pos) => try_into_u8(pos)?,
None => self.push_readonly_unsigned_account(program_id)?,
};

// Step 2 - Create a `CompiledInstruction` based on the `program_id` index and instruction `data`.
let new_compiled_ix = CompiledInstruction {
program_id_index,
accounts: Vec::default(),
data,
};

// Step 3 - Insert the created instruction at the given `insert_at` index.
self.instructions_mut().insert(insert_at, new_compiled_ix);

Ok(())
}

fn push_readonly_unsigned_account(&mut self, account: SolanaAddress) -> SigningResult<u8> {
debug_assert!(
!self.account_keys_mut().contains(&account),
"Account must not be in the account list yet"
);

self.account_keys_mut().push(account);
self.message_header_mut().num_readonly_unsigned_accounts += 1;

let account_added_at = try_into_u8(self.account_keys_mut().len() - 1)?;

// There is no need to update instruction account ids that point to [`Message::account_keys`] list
// as we pushed the account to the end of the list.
// But we must update it in case if there are instruction account ids that point to [`address_table_lookups`].

match self.address_table_lookups() {
Some(lookups) if !lookups.is_empty() => (),
// No address table lookups, no need to update the indexes.
_ => return Ok(account_added_at),
}

self.instructions_mut()
.iter_mut()
.flat_map(|ix| {
ix.accounts
.iter_mut()
.chain(iter::once(&mut ix.program_id_index))
})
// Update every instruction account id that points to the address table lookups.
.filter(|ix_account_id| **ix_account_id >= account_added_at)
.for_each(|ix_account_id| *ix_account_id += 1);

Ok(account_added_at)
}

/// Returns ALT (Address Lookup Tables) if supported by the message version.
fn address_table_lookups(&self) -> Option<&[MessageAddressTableLookup]>;

fn account_keys_mut(&mut self) -> &mut Vec<SolanaAddress>;

fn message_header_mut(&mut self) -> &mut MessageHeader;

fn instructions_mut(&mut self) -> &mut Vec<CompiledInstruction>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use crate::defined_addresses::COMPUTE_BUDGET_ADDRESS;
use crate::instruction::Instruction;
use borsh::{BorshDeserialize, BorshSerialize};
use tw_encoding::{EncodingError, EncodingResult};

pub type UnitLimit = u32;
pub type UnitPrice = u64;
Expand All @@ -18,6 +19,12 @@ pub enum ComputeBudgetInstruction {
SetLoadedAccountsDataSizeLimit(u32),
}

impl ComputeBudgetInstruction {
pub fn try_from_borsh(data: &[u8]) -> EncodingResult<Self> {
borsh::from_slice(data).map_err(|_| EncodingError::InvalidInput)
}
}

pub struct ComputeBudgetInstructionBuilder;

impl ComputeBudgetInstructionBuilder {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use crate::address::SolanaAddress;
use crate::defined_addresses::*;
use crate::instruction::{AccountMeta, Instruction};
use serde::{Deserialize, Serialize};
use tw_encoding::{EncodingError, EncodingResult};

/// An instruction to the system program.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
Expand Down Expand Up @@ -181,6 +182,12 @@ pub enum SystemInstruction {
UpgradeNonceAccount,
}

impl SystemInstruction {
pub fn try_from_bincode(data: &[u8]) -> EncodingResult<Self> {
bincode::deserialize(data).map_err(|_| EncodingError::InvalidInput)
}
}

pub struct SystemInstructionBuilder;

impl SystemInstructionBuilder {
Expand Down
60 changes: 60 additions & 0 deletions rust/chains/tw_solana/src/modules/message_decompiler.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// SPDX-License-Identifier: Apache-2.0
//
// Copyright © 2017 Trust Wallet.

use crate::address::SolanaAddress;
use crate::transaction::versioned::VersionedMessage;
use crate::transaction::CompiledInstruction;
use tw_coin_entry::error::prelude::{OrTWError, ResultContext, SigningErrorType, SigningResult};
use tw_memory::Data;

/// [`Instruction`] without `accounts` field.
pub struct InstructionWithoutAccounts {
/// Pubkey of the program that executes this instruction.
pub program_id: SolanaAddress,
/// Opaque data passed to the program for its own interpretation.
pub data: Data,
}

pub struct MessageDecompiler;

impl MessageDecompiler {
pub fn decompile_partly(
message: &VersionedMessage,
) -> SigningResult<Vec<InstructionWithoutAccounts>> {
match message {
VersionedMessage::Legacy(legacy) => {
Self::decompile_partly_impl(&legacy.instructions, &legacy.account_keys)
},
VersionedMessage::V0(v0) => {
Self::decompile_partly_impl(&v0.instructions, &v0.account_keys)
},
}
}

fn decompile_partly_impl(
instructions: &[CompiledInstruction],
account_keys: &[SolanaAddress],
) -> SigningResult<Vec<InstructionWithoutAccounts>> {
instructions
.iter()
.map(|ix| Self::decompile_instruction_partly(ix, account_keys))
.collect()
}

fn decompile_instruction_partly(
ix: &CompiledInstruction,
account_keys: &[SolanaAddress],
) -> SigningResult<InstructionWithoutAccounts> {
// Program ID should always be in the transaction's accounts list even if AddressLookupTable is used:
// https://solana.stackexchange.com/questions/16122/using-program-ids-in-address-lookup-tables-missing-documentation-about-luts
let program_id = *account_keys
.get(ix.program_id_index as usize)
.or_tw_err(SigningErrorType::Error_invalid_params)
.context("Program ID not found in the accounts list")?;
Ok(InstructionWithoutAccounts {
program_id,
data: ix.data.clone(),
})
}
}
2 changes: 2 additions & 0 deletions rust/chains/tw_solana/src/modules/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ use tw_keypair::ed25519;

pub mod compiled_instructions;
pub mod compiled_keys;
pub mod insert_instruction;
pub mod instruction_builder;
pub mod message_builder;
pub mod message_decompiler;
pub mod proto_builder;
pub mod transaction_decoder;
pub mod transaction_util;
Expand Down
Loading

0 comments on commit f6d8a10

Please sign in to comment.