Skip to main content

arch_sdk/helper/
async_program_deployment.rs

1use crate::arch_program::pubkey::Pubkey;
2use crate::arch_program::system_instruction;
3use crate::build_and_sign_transaction;
4use crate::client::ArchRpcClient;
5use crate::program_deployment::ProgramDeployerError;
6use crate::sign_message_bip322;
7use crate::Config;
8use crate::MAX_TX_BATCH_SIZE;
9use crate::RUNTIME_TX_SIZE_LIMIT;
10use crate::{
11    types::{RuntimeTransaction, Signature},
12    Status,
13};
14use arch_program::bpf_loader::{LoaderState, BPF_LOADER_ID};
15use arch_program::hash::Hash;
16use arch_program::loader_instruction;
17use arch_program::sanitized::ArchMessage;
18use bitcoin::key::Keypair;
19use indicatif::{ProgressBar, ProgressStyle};
20use std::fs;
21use tracing::{debug, info, warn};
22
23pub struct ProgramDeployer {
24    client: ArchRpcClient,
25}
26
27impl ProgramDeployer {
28    pub fn new(config: &Config) -> Self {
29        Self {
30            client: ArchRpcClient::new(config),
31        }
32    }
33
34    pub async fn try_deploy_program(
35        &self,
36        program_name: String,
37        program_keypair: Keypair,
38        authority_keypair: Keypair,
39        elf_path: &String,
40    ) -> Result<Pubkey, ProgramDeployerError> {
41        info!("Starting program deployment: {}", program_name);
42
43        let elf = fs::read(elf_path).map_err(|source| ProgramDeployerError::ElfReadError {
44            path: elf_path.clone(),
45            source,
46        })?;
47
48        let program_pubkey = Pubkey::from_slice(&program_keypair.x_only_public_key().0.serialize());
49        let authority_pubkey =
50            Pubkey::from_slice(&authority_keypair.x_only_public_key().0.serialize());
51
52        if let Some(pubkey) = self
53            .ensure_account_exists(
54                program_pubkey,
55                authority_pubkey,
56                program_keypair,
57                authority_keypair,
58                &elf,
59            )
60            .await?
61        {
62            return Ok(pubkey);
63        }
64
65        self.write_program_elf(program_keypair, authority_keypair, &elf)
66            .await?;
67
68        self.verify_elf_deployed(program_pubkey, &elf).await?;
69
70        info!(program = %program_pubkey, "Step 2/3: ELF file sent and verified");
71
72        self.ensure_executable(program_pubkey, authority_pubkey, authority_keypair)
73            .await?;
74
75        self.verify_executable(program_pubkey).await?;
76
77        info!(
78            program = %program_pubkey,
79            "Program deployment complete: {}",
80            program_name
81        );
82
83        Ok(program_pubkey)
84    }
85
86    /// Step 1: Ensure the program account exists on-chain.
87    ///
88    /// Returns `Ok(Some(pubkey))` if the same ELF is already deployed (early return),
89    /// or `Ok(None)` to continue with the deployment.
90    async fn ensure_account_exists(
91        &self,
92        program_pubkey: Pubkey,
93        authority_pubkey: Pubkey,
94        program_keypair: Keypair,
95        authority_keypair: Keypair,
96        elf: &[u8],
97    ) -> Result<Option<Pubkey>, ProgramDeployerError> {
98        if let Ok(account_info) = self.client.read_account_info(program_pubkey).await {
99            info!(program = %program_pubkey, "Step 1/3: Account already exists, skipping creation");
100
101            if account_info.data.len() < LoaderState::program_data_offset() {
102                warn!(program = %program_pubkey, "Account is not initialized, redeploying");
103            } else if account_info.data[LoaderState::program_data_offset()..] == *elf {
104                info!(program = %program_pubkey, "Same program already deployed, skipping");
105
106                if !account_info.is_executable {
107                    self.make_program_executable(
108                        program_pubkey,
109                        authority_pubkey,
110                        authority_keypair,
111                    )
112                    .await?;
113                }
114
115                return Ok(Some(program_pubkey));
116            } else {
117                warn!(program = %program_pubkey, "ELF mismatch with on-chain content, redeploying");
118            }
119        } else {
120            self.create_program_account(
121                program_pubkey,
122                authority_pubkey,
123                program_keypair,
124                authority_keypair,
125                elf.len(),
126            )
127            .await?;
128        }
129
130        Ok(None)
131    }
132
133    async fn create_program_account(
134        &self,
135        program_pubkey: Pubkey,
136        authority_pubkey: Pubkey,
137        program_keypair: Keypair,
138        authority_keypair: Keypair,
139        elf_len: usize,
140    ) -> Result<(), ProgramDeployerError> {
141        let recent_blockhash = self.client.get_best_finalized_block_hash().await?;
142
143        let create_account_tx = build_and_sign_transaction(
144            ArchMessage::new(
145                &[system_instruction::create_account(
146                    &authority_pubkey,
147                    &program_pubkey,
148                    arch_program::rent::minimum_rent(LoaderState::program_data_offset() + elf_len),
149                    0,
150                    &BPF_LOADER_ID,
151                )],
152                Some(authority_pubkey),
153                recent_blockhash,
154            ),
155            vec![authority_keypair, program_keypair],
156            self.client.config.network,
157        )?;
158
159        let txid = self.client.send_transaction(create_account_tx).await?;
160        let tx = self.client.wait_for_processed_transaction(&txid).await?;
161
162        if let Status::Failed(reason) = tx.status {
163            return Err(ProgramDeployerError::AccountCreationFailed { txid, reason });
164        }
165
166        info!(program = %program_pubkey, tx = %txid, "Step 1/3: Program account created");
167        Ok(())
168    }
169
170    async fn verify_elf_deployed(
171        &self,
172        program_pubkey: Pubkey,
173        elf: &[u8],
174    ) -> Result<(), ProgramDeployerError> {
175        let account_info = self.client.read_account_info(program_pubkey).await?;
176
177        if account_info.data[LoaderState::program_data_offset()..] != *elf {
178            return Err(ProgramDeployerError::ElfMismatch {
179                program: program_pubkey,
180            });
181        }
182
183        debug!(
184            program = %program_pubkey,
185            owner = %account_info.owner,
186            data_len = account_info.data.len(),
187            utxo = %account_info.utxo,
188            executable = account_info.is_executable,
189            "Program account state after ELF upload"
190        );
191
192        Ok(())
193    }
194
195    async fn ensure_executable(
196        &self,
197        program_pubkey: Pubkey,
198        authority_pubkey: Pubkey,
199        authority_keypair: Keypair,
200    ) -> Result<(), ProgramDeployerError> {
201        let account_info = self.client.read_account_info(program_pubkey).await?;
202
203        if account_info.is_executable {
204            info!(program = %program_pubkey, "Step 3/3: Program account is already executable");
205        } else {
206            self.make_program_executable(program_pubkey, authority_pubkey, authority_keypair)
207                .await?;
208        }
209
210        Ok(())
211    }
212
213    async fn verify_executable(&self, program_pubkey: Pubkey) -> Result<(), ProgramDeployerError> {
214        let account_info = self.client.read_account_info(program_pubkey).await?;
215
216        if !account_info.is_executable {
217            return Err(ProgramDeployerError::NotExecutable {
218                program: program_pubkey,
219            });
220        }
221
222        debug!(
223            program = %program_pubkey,
224            owner = %account_info.owner,
225            data_len = account_info.data.len(),
226            utxo = %account_info.utxo,
227            executable = account_info.is_executable,
228            "Final program account state"
229        );
230
231        Ok(())
232    }
233
234    async fn make_program_executable(
235        &self,
236        program_pubkey: Pubkey,
237        authority_pubkey: Pubkey,
238        authority_keypair: Keypair,
239    ) -> Result<(), ProgramDeployerError> {
240        let recent_blockhash = self.client.get_best_finalized_block_hash().await?;
241        let executability_tx = build_and_sign_transaction(
242            ArchMessage::new(
243                &[loader_instruction::deploy(program_pubkey, authority_pubkey)],
244                Some(authority_pubkey),
245                recent_blockhash,
246            ),
247            vec![authority_keypair],
248            self.client.config.network,
249        )?;
250
251        let txid = self.client.send_transaction(executability_tx).await?;
252        let tx = self.client.wait_for_processed_transaction(&txid).await?;
253
254        if let Status::Failed(reason) = tx.status {
255            return Err(ProgramDeployerError::MakeExecutableFailed { txid, reason });
256        }
257
258        info!(program = %program_pubkey, tx = %txid, "Step 3/3: Made program account executable");
259        Ok(())
260    }
261
262    async fn write_program_elf(
263        &self,
264        program_keypair: Keypair,
265        authority_keypair: Keypair,
266        elf: &[u8],
267    ) -> Result<(), ProgramDeployerError> {
268        let program_pubkey = Pubkey::from_slice(&program_keypair.x_only_public_key().0.serialize());
269        let authority_pubkey =
270            Pubkey::from_slice(&authority_keypair.x_only_public_key().0.serialize());
271
272        let account_info = self.client.read_account_info(program_pubkey).await?;
273
274        debug!(
275            program = %program_pubkey,
276            executable = account_info.is_executable,
277            data_len = account_info.data.len(),
278            utxo = %account_info.utxo,
279            owner = %account_info.owner,
280            "Account state before ELF write"
281        );
282
283        if account_info.is_executable {
284            let recent_blockhash = self.client.get_best_finalized_block_hash().await?;
285            let retract_tx = build_and_sign_transaction(
286                ArchMessage::new(
287                    &[loader_instruction::retract(
288                        program_pubkey,
289                        authority_pubkey,
290                    )],
291                    Some(authority_pubkey),
292                    recent_blockhash,
293                ),
294                vec![authority_keypair],
295                self.client.config.network,
296            )?;
297
298            let retract_txid = self.client.send_transaction(retract_tx).await?;
299            self.client
300                .wait_for_processed_transaction(&retract_txid)
301                .await?;
302        }
303
304        if account_info.data.len() != LoaderState::program_data_offset() + elf.len() {
305            self.resize_program_account(
306                program_pubkey,
307                authority_pubkey,
308                program_keypair,
309                authority_keypair,
310                &account_info,
311                elf.len(),
312            )
313            .await?;
314        }
315
316        self.send_elf_chunks(program_pubkey, authority_pubkey, authority_keypair, elf)
317            .await
318    }
319
320    async fn resize_program_account(
321        &self,
322        program_pubkey: Pubkey,
323        authority_pubkey: Pubkey,
324        program_keypair: Keypair,
325        authority_keypair: Keypair,
326        account_info: &crate::types::AccountInfo,
327        elf_len: usize,
328    ) -> Result<(), ProgramDeployerError> {
329        debug!(program = %program_pubkey, "Truncating program account to match ELF size");
330
331        let minimum_rent =
332            arch_program::rent::minimum_rent(LoaderState::program_data_offset() + elf_len);
333        let missing_lamports = minimum_rent.saturating_sub(account_info.lamports);
334
335        if missing_lamports > 0 {
336            let recent_blockhash = self.client.get_best_finalized_block_hash().await?;
337            let transfer_tx = build_and_sign_transaction(
338                ArchMessage::new(
339                    &[system_instruction::transfer(
340                        &authority_pubkey,
341                        &program_pubkey,
342                        missing_lamports,
343                    )],
344                    Some(authority_pubkey),
345                    recent_blockhash,
346                ),
347                vec![authority_keypair],
348                self.client.config.network,
349            )?;
350
351            let transfer_txid = self.client.send_transaction(transfer_tx).await?;
352            self.client
353                .wait_for_processed_transaction(&transfer_txid)
354                .await?;
355        }
356
357        let recent_blockhash = self.client.get_best_finalized_block_hash().await?;
358        let truncate_tx = build_and_sign_transaction(
359            ArchMessage::new(
360                &[loader_instruction::truncate(
361                    program_pubkey,
362                    authority_pubkey,
363                    elf_len as u32,
364                )],
365                Some(authority_pubkey),
366                recent_blockhash,
367            ),
368            vec![program_keypair, authority_keypair],
369            self.client.config.network,
370        )?;
371
372        let truncate_txid = self.client.send_transaction(truncate_tx).await?;
373        self.client
374            .wait_for_processed_transaction(&truncate_txid)
375            .await?;
376
377        Ok(())
378    }
379
380    async fn send_elf_chunks(
381        &self,
382        program_pubkey: Pubkey,
383        authority_pubkey: Pubkey,
384        authority_keypair: Keypair,
385        elf: &[u8],
386    ) -> Result<(), ProgramDeployerError> {
387        let recent_blockhash = self.client.get_best_finalized_block_hash().await?;
388        let chunk_size = extend_bytes_max_len();
389        let num_chunks = elf.chunks(chunk_size).len();
390
391        debug!(
392            program = %program_pubkey,
393            chunks = num_chunks,
394            blockhash = %recent_blockhash,
395            "Building ELF write transactions"
396        );
397
398        let txs = elf
399            .chunks(chunk_size)
400            .enumerate()
401            .map(|(i, chunk)| {
402                let offset: u32 = (i * chunk_size) as u32;
403                let message = ArchMessage::new(
404                    &[loader_instruction::write(
405                        program_pubkey,
406                        authority_pubkey,
407                        offset,
408                        chunk.to_vec(),
409                    )],
410                    Some(authority_pubkey),
411                    recent_blockhash,
412                );
413
414                let digest_slice = message.hash();
415
416                Ok(RuntimeTransaction {
417                    version: 0,
418                    signatures: vec![Signature(sign_message_bip322(
419                        &authority_keypair,
420                        &digest_slice,
421                        self.client.config.network,
422                    )?)],
423                    message,
424                })
425            })
426            .collect::<Result<Vec<RuntimeTransaction>, ProgramDeployerError>>()?;
427
428        let pb = ProgressBar::new(txs.len() as u64);
429        pb.set_style(
430            ProgressStyle::default_bar()
431                .template(
432                    "{spinner:.green} [{elapsed_precise}] Sending ELF [{bar:40.cyan/blue}] {pos}/{len} ({eta})",
433                )
434                .expect("Failed to set progress bar style")
435                .progress_chars("#>-"),
436        );
437
438        let batches = txs
439            .chunks(MAX_TX_BATCH_SIZE)
440            .map(|chunk| chunk.to_vec())
441            .collect::<Vec<Vec<RuntimeTransaction>>>();
442
443        let mut tx_ids = Vec::new();
444        for batch in batches {
445            let ids = self.client.send_transactions(batch).await?;
446            tx_ids.extend(ids);
447        }
448
449        debug!(
450            program = %program_pubkey,
451            sent = tx_ids.len(),
452            "Waiting for ELF write confirmations"
453        );
454
455        for (i, tx_id) in tx_ids.iter().enumerate() {
456            let processed_tx = self.client.wait_for_processed_transaction(tx_id).await?;
457            if let Status::Failed(reason) = processed_tx.status {
458                let offset = (i * chunk_size) as u32;
459                return Err(ProgramDeployerError::ElfWriteFailed {
460                    txid: *tx_id,
461                    offset,
462                    reason,
463                });
464            }
465            pb.inc(1);
466        }
467
468        pb.finish_with_message("ELF write transactions confirmed");
469        Ok(())
470    }
471}
472
473/// Returns the remaining space in an account's data storage
474pub fn extend_bytes_max_len() -> usize {
475    let message = ArchMessage::new(
476        &[loader_instruction::write(
477            Pubkey::system_program(),
478            Pubkey::system_program(),
479            0,
480            vec![0_u8; 256],
481        )],
482        None,
483        Hash::from([0; 32]),
484    );
485
486    RUNTIME_TX_SIZE_LIMIT
487        - RuntimeTransaction {
488            version: 0,
489            signatures: vec![Signature([0_u8; 64])],
490            message,
491        }
492        .serialize()
493        .len()
494}