solana_program_client/
versioned_tx.rs

1use std::str::FromStr;
2
3use base64::{engine::general_purpose, Engine as _};
4#[allow(unused_imports)]
5pub use borsh::{BorshDeserialize, BorshSerialize};
6pub use solana_address_lookup_table_program::state::AddressLookupTable;
7pub use solana_client::{
8    rpc_client::RpcClient, rpc_config::RpcSendTransactionConfig, rpc_request::RpcRequest,
9};
10pub use solana_sdk::instruction::AccountMeta;
11#[allow(unused_imports)]
12pub use solana_sdk::{
13    address_lookup_table::AddressLookupTableAccount,
14    commitment_config::{CommitmentConfig, CommitmentLevel},
15    instruction::Instruction,
16    message::VersionedMessage,
17    pubkey::Pubkey,
18    signature::Signature,
19    signature::{Keypair, Signer},
20    signer::EncodableKey,
21    transaction::{Transaction, VersionedTransaction},
22};
23pub use solana_transaction_status::UiTransactionEncoding;
24
25use crate::legacy_tx::get_discriminant;
26
27/// Sign and submit a legacy transaction.
28///
29/// This method fully signs a transaction with all required signers, which
30/// must be present in the `keypairs` slice.
31///
32/// # Panics
33///
34/// Panics when signing or signature verification fails.
35/// 
36/// # Examples
37///
38/// This example uses the [`solana_program_client`] crate.
39///
40/// ```
41/// use solana_program_client::versioned_tx::*;
42
43/// #[derive(BorshSerialize, BorshDeserialize)]
44/// #[borsh(crate = "borsh")]
45/// pub struct UpdateBlob {
46///     pub data: Vec<u8>,
47/// }
48
49/// fn call_with_lookup_table() {
50///     let connection = RpcClient::new("https://api.devnet.solana.com");
51///     let program_id = blob::ID;
52///     let instruction_name = "update_blob";
53///     let instruction_data = UpdateBlob {
54///         data: "another data".as_bytes().to_vec(),
55///     };
56///     let payer: Keypair = Keypair::read_from_file("~/.config/solana/id.json").unwrap();
57///     let signers = &[&payer];
58/// 
59///     // create lookup table
60///     let latest_blockhash = connection
61///         .get_latest_blockhash()
62///         .expect("latest block hash");
63///     let table_pk = create_lookup_table(&connection, &payer, latest_blockhash).unwrap();
64
65///     // add accounts to lookup table
66///     let new_accounts = vec![program_id, payer.pubkey()];
67///     update_lookup_table(
68///         &connection,
69///         &payer,
70///         latest_blockhash,
71///         table_pk,
72///         new_accounts,
73///     )
74///     .unwrap();
75
76///     // set up accounts
77///     let (blob_account, _) = Pubkey::find_program_address(&[&b"blob"[..]], &program_id);
78///     let accounts = vec![
79///         AccountMeta::new(blob_account, false),
80///         AccountMeta::new(payer.pubkey(), true),
81///     ];
82
83///     // call program with lookup table
84///     let _tx_signature = call_with_lookup_table(
85///         connection,
86///         program_id,
87///         instruction_name,
88///         instruction_data,
89///         &table_pk,
90///         &payer,
91///         signers,
92///         accounts,
93///     )
94///     .unwrap();
95/// }
96/// ```
97pub fn call_with_lookup_table<T>(
98    connection: &RpcClient,
99    program_id: &Pubkey,
100    instruction_name: &str,
101    instruction_data: T,
102    lookup_table_key: &Pubkey,
103    payer: &Keypair,
104    signers: &[&Keypair],
105    accounts: Vec<AccountMeta>,
106) -> Result<Signature, Box<dyn std::error::Error>>
107where
108    T: BorshSerialize,
109{
110    // get lookup table addresses from lookup table key
111    let lookup_table_account = connection.get_account(lookup_table_key)?;
112    let address_lookup_table = AddressLookupTable::deserialize(&lookup_table_account.data)?;
113    let address_lookup_table_account = AddressLookupTableAccount {
114        key: lookup_table_key.clone(),
115        addresses: address_lookup_table.addresses.to_vec(),
116    };
117
118    // construct instruction
119    let instruction_discriminant = get_discriminant("global", instruction_name);
120    let ix = Instruction::new_with_borsh(
121        program_id.clone(),
122        &(instruction_discriminant, instruction_data),
123        accounts,
124    );
125
126    // create versioned transaction with lookup table
127    let blockhash = connection.get_latest_blockhash()?;
128    let tx = VersionedTransaction::try_new(
129        VersionedMessage::V0(solana_sdk::message::v0::Message::try_compile(
130            &payer.pubkey(),
131            &[ix],
132            &[address_lookup_table_account],
133            blockhash,
134        )?),
135        signers,
136    )?;
137
138    // serialize and encode transaction
139    let serialized_tx = bincode::serialize(&tx)?;
140    let serialized_encoded_tx = general_purpose::STANDARD.encode(serialized_tx);
141
142    // construct transaction pre-execution configuration
143    let config = RpcSendTransactionConfig {
144        skip_preflight: false,
145        preflight_commitment: Some(CommitmentLevel::Confirmed),
146        encoding: Some(UiTransactionEncoding::Base64),
147        ..RpcSendTransactionConfig::default()
148    };
149
150    // submit transaction and retrieve transaction signature
151    let signature = connection.send::<String>(
152        RpcRequest::SendTransaction,
153        serde_json::json!([serialized_encoded_tx, config]),
154    )?;
155
156    // verify transaction execution
157    connection.confirm_transaction_with_commitment(
158        &Signature::from_str(signature.as_str())?,
159        CommitmentConfig::finalized(),
160    )?;
161
162    Ok(Signature::from_str(&signature)?)
163}
164
165/// create a lookup table with an authority account.
166///
167/// This method submit transaction that creates a 
168/// lookup table. Returns lookup table account public key.
169///
170/// # Panics
171///
172/// Panics when signature verification fails.
173/// 
174/// # Examples
175///
176/// This example uses the [`solana_program_client`] crate.
177///
178/// ```
179/// use solana_program_client::versioned_tx::*;
180/// 
181/// fn create_lookup_table() {
182///     let connection = RpcClient::new("https://api.devnet.solana.com");
183///     let payer: Keypair =
184///         Keypair::read_from_file("/Users/cenwadike/.config/solana/solfate-dev.json").unwrap();
185
186///     let latest_blockhash = connection
187///         .get_latest_blockhash()
188///         .expect("latest block hash");
189
190///     let lookup_table_pk = create_lookup_table(&connection, &payer, latest_blockhash);
191/// }
192/// ```
193pub fn create_lookup_table(
194    connection: &RpcClient,
195    payer: &Keypair,
196    latest_blockhash: solana_sdk::hash::Hash,
197) -> Result<Pubkey, Box<dyn std::error::Error>> {
198    let recent_slot = connection.get_slot()?;
199    let (create_ix, table_pk) =
200        solana_address_lookup_table_program::instruction::create_lookup_table(
201            payer.pubkey(),
202            payer.pubkey(),
203            recent_slot,
204        );
205
206    connection.send_and_confirm_transaction(&Transaction::new_signed_with_payer(
207        &[create_ix],
208        Some(&payer.pubkey()),
209        &[&payer],
210        latest_blockhash,
211    ))?;
212
213    Ok(table_pk)
214}
215
216/// extend a lookup table.
217///
218/// This method submit transaction that extends a 
219/// lookup table. Returns lookup table account public key.
220///
221/// # Panics
222///
223/// Panics when signature verification fails.
224/// 
225/// # Examples
226///
227/// This example uses the [`solana_program_client`] crate.
228///
229/// ```
230/// use solana_program_client::versioned_tx::*;
231/// 
232/// fn test_update_lookup_table() {
233///     let connection = RpcClient::new("https://api.devnet.solana.com");
234///     let payer: Keypair =
235///         Keypair::read_from_file("/Users/cenwadike/.config/solana/solfate-dev.json").unwrap();
236
237///     let latest_blockhash = connection
238///         .get_latest_blockhash()
239///         .expect("latest block hash");
240
241///     let table_pk = create_lookup_table(&connection, &payer, latest_blockhash).unwrap();
242///     let new_accounts = vec![Pubkey::new_unique()];
243///     let res = extend_lookup_table(
244///         &connection,
245///         &payer,
246///         latest_blockhash,
247///         table_pk,
248///         new_accounts,
249///     )
250///     .unwrap();
251/// }  
252/// ```
253pub fn extend_lookup_table(
254    connection: &RpcClient,
255    payer: &Keypair,
256    latest_blockhash: solana_sdk::hash::Hash,
257    table_pk: Pubkey,
258    new_accounts: Vec<Pubkey>,
259) -> Result<bool, Box<dyn std::error::Error>> {
260    // add accounts to look up table
261    let extend_ix = solana_address_lookup_table_program::instruction::extend_lookup_table(
262        table_pk,
263        payer.pubkey(),
264        Some(payer.pubkey()),
265        new_accounts,
266    );
267
268    let signature =
269        connection.send_and_confirm_transaction(&Transaction::new_signed_with_payer(
270            &[extend_ix],
271            Some(&payer.pubkey()),
272            &[&payer],
273            latest_blockhash,
274        ))?;
275
276    Ok(connection
277        .confirm_transaction_with_spinner(
278            &signature,
279            &latest_blockhash,
280            CommitmentConfig::confirmed(),
281        )
282        .is_ok())
283}
284
285#[cfg(test)]
286mod test {
287    use super::*;
288
289    #[test]
290    fn test_create_lookup_table() {
291        let connection = RpcClient::new("https://api.devnet.solana.com");
292        let payer: Keypair =
293            Keypair::read_from_file("/Users/cenwadike/.config/solana/solfate-dev.json").unwrap();
294
295        let latest_blockhash = connection
296            .get_latest_blockhash()
297            .expect("latest block hash");
298
299        let lookup_table_pk = create_lookup_table(&connection, &payer, latest_blockhash);
300        assert!(lookup_table_pk.is_ok())
301    }
302
303    #[test]
304    fn test_update_lookup_table() {
305        let connection = RpcClient::new("https://api.devnet.solana.com");
306        let payer: Keypair =
307            Keypair::read_from_file("/Users/cenwadike/.config/solana/solfate-dev.json").unwrap();
308
309        let latest_blockhash = connection
310            .get_latest_blockhash()
311            .expect("latest block hash");
312
313        let table_pk = create_lookup_table(&connection, &payer, latest_blockhash).unwrap();
314        let new_accounts = vec![Pubkey::new_unique()];
315        let res = extend_lookup_table(
316            &connection,
317            &payer,
318            latest_blockhash,
319            table_pk,
320            new_accounts,
321        )
322        .unwrap();
323        assert!(res);
324    }
325
326    #[derive(BorshSerialize, BorshDeserialize)]
327    pub struct UpdateBlob {
328        pub data: Vec<u8>,
329    }
330
331    #[test]
332    fn test_call_with_lookup_table() {
333        let connection = RpcClient::new("https://api.devnet.solana.com");
334        let program_id = blob::ID;
335        let instruction_name = "update_blob";
336        let instruction_data = UpdateBlob {
337            data: "another data".as_bytes().to_vec(),
338        };
339        let payer: Keypair =
340            Keypair::read_from_file("/Users/cenwadike/.config/solana/solfate-dev.json").unwrap();
341
342        let signers = &[&payer];
343        // create lookup table
344        let latest_blockhash = connection
345            .get_latest_blockhash()
346            .expect("latest block hash");
347        let table_pk = create_lookup_table(&connection, &payer, latest_blockhash).unwrap();
348
349        // add accounts to lookup table
350        let new_accounts = vec![program_id, payer.pubkey()];
351        extend_lookup_table(
352            &connection,
353            &payer,
354            latest_blockhash,
355            table_pk,
356            new_accounts,
357        )
358        .unwrap();
359
360        // set up accounts
361        let (blob_account, _) = Pubkey::find_program_address(&[&b"blob"[..]], &program_id);
362        let accounts = vec![
363            AccountMeta::new(blob_account, false),
364            AccountMeta::new(payer.pubkey(), true),
365        ];
366
367        let res = call_with_lookup_table(
368            &connection,
369            &program_id,
370            instruction_name,
371            instruction_data,
372            &table_pk,
373            &payer,
374            signers,
375            accounts,
376        );
377
378        assert!(res.is_ok());
379    }
380}