1use jsonrpsee::core::client::ClientT;
8use sha2::{Digest, Sha256};
9use thruput_json_rpc_client::native::{
10 NativeClient,
11 receipt::{ReceiptPollConfig, ReceiptPollError},
12};
13use thruput_json_rpc_types::{
14 responses::{ReceiptExecution, TransactionReceipt},
15 transaction::TxHash,
16};
17use thruput_signing::default_wallet::{
18 BuildDefaultWalletDeployTransaction, DeploymentCode, WalletInitCall,
19 build_default_wallet_deploy_transaction,
20};
21use thruput_signing::{PubkeyAccount, Signer, SigningError};
22use thruput_xdr::{
23 contract::Contract,
24 transaction::Transaction,
25 types::{ContractAddress, NetworkHash, SHA256Hash},
26};
27
28use crate::{SubmitCallError, SubmittedCall, sign_transaction};
29
30#[derive(Debug, Clone)]
33pub struct DeployRequest<'a> {
34 pub account: &'a PubkeyAccount,
36 pub code: DeploymentCode,
38 pub amount: u128,
40 pub initializer: Option<WalletInitCall<'a>>,
42 pub gas_limit: u64,
44 pub block_expiry_height: i64,
46 pub nonce: u64,
48}
49
50pub fn contract_hash(contract: &Contract) -> Result<SHA256Hash, SigningError> {
52 thruput_signing::wallet_contract_hash(contract)
53}
54
55pub fn deployment_nonce(nonce: u64) -> SHA256Hash {
57 SHA256Hash::from(<[u8; 32]>::from(Sha256::digest(nonce.to_le_bytes())))
58}
59
60pub fn prepare_deploy(
62 request: DeployRequest<'_>,
63 network_hash: NetworkHash,
64) -> Result<UnsignedDeploy, SigningError> {
65 let code_hash = match &request.code {
66 DeploymentCode::Attached(contract) => contract_hash(contract)?,
67 DeploymentCode::Registered(hash) => *hash,
68 };
69 let salt = deployment_nonce(request.nonce);
70 let address =
71 thruput_signing::derive_deploy_address(&request.account.address(), &salt, &code_hash)?;
72 let transaction =
73 build_default_wallet_deploy_transaction(BuildDefaultWalletDeployTransaction {
74 account: request.account,
75 deployment_nonce: salt,
76 code: request.code,
77 amount: request.amount,
78 initializer: request.initializer,
79 gas_limit: request.gas_limit,
80 block_expiry_height: request.block_expiry_height,
81 nonce: request.nonce,
82 })?;
83 Ok(UnsignedDeploy {
84 transaction,
85 network_hash,
86 address,
87 code_hash,
88 })
89}
90
91#[derive(Debug, Clone)]
93pub struct UnsignedDeploy {
94 transaction: Transaction,
95 network_hash: NetworkHash,
96 address: ContractAddress,
97 code_hash: SHA256Hash,
98}
99
100impl UnsignedDeploy {
101 pub fn transaction(&self) -> &Transaction {
103 &self.transaction
104 }
105
106 pub fn address(&self) -> ContractAddress {
108 self.address
109 }
110
111 pub fn code_hash(&self) -> SHA256Hash {
113 self.code_hash
114 }
115
116 pub fn sign(self, signer: &impl Signer) -> Result<SignedDeploy, SigningError> {
119 let (tx_hash, tx_hex) = sign_transaction(self.transaction, &self.network_hash, signer)?;
120 Ok(SignedDeploy {
121 tx_hash,
122 tx_hex,
123 address: self.address,
124 })
125 }
126}
127
128#[derive(Debug, Clone, PartialEq, Eq)]
131pub struct SignedDeploy {
132 tx_hash: TxHash,
133 tx_hex: String,
134 address: ContractAddress,
135}
136
137impl SignedDeploy {
138 pub fn tx_hex(&self) -> &str {
140 &self.tx_hex
141 }
142
143 pub fn tx_hash(&self) -> TxHash {
145 self.tx_hash
146 }
147
148 pub fn address(&self) -> ContractAddress {
150 self.address
151 }
152
153 pub async fn submit<C: ClientT + Sync>(
155 &self,
156 client: &NativeClient<C>,
157 ) -> Result<SubmittedDeploy, SubmitCallError> {
158 crate::submit_signed_transaction(client, self.tx_hash, &self.tx_hex).await?;
159 Ok(SubmittedDeploy {
160 tx_hash: self.tx_hash,
161 address: self.address,
162 })
163 }
164}
165
166#[derive(Debug, Clone, Copy)]
169pub struct SubmittedDeploy {
170 pub tx_hash: TxHash,
171 pub address: ContractAddress,
172}
173
174impl SubmittedDeploy {
175 pub async fn wait<C: ClientT + Sync, F: std::future::Future<Output = ()>>(
178 &self,
179 client: &NativeClient<C>,
180 network_hash: thruput_json_rpc_types::NetworkHash,
181 config: ReceiptPollConfig,
182 cancelled: F,
183 ) -> Result<TransactionReceipt, ReceiptPollError> {
184 SubmittedCall {
185 tx_hash: self.tx_hash,
186 }
187 .wait(client, network_hash, config, cancelled)
188 .await
189 }
190
191 pub fn verify(&self, receipt: &TransactionReceipt) -> Result<(), DeploymentVerificationError> {
195 if receipt.transaction_hash != self.tx_hash {
196 return Err(DeploymentVerificationError::ReceiptHashMismatch {
197 tx_hash: self.tx_hash,
198 receipt: receipt.transaction_hash,
199 });
200 }
201 if let ReceiptExecution::Return { return_data } = &receipt.execution
202 && return_data.as_ref() != self.address.as_ref()
203 {
204 return Err(DeploymentVerificationError::AddressMismatch {
205 tx_hash: self.tx_hash,
206 predicted: self.address,
207 });
208 }
209 Ok(())
210 }
211}
212
213#[derive(Debug, thiserror::Error)]
215#[non_exhaustive]
216pub enum DeploymentVerificationError {
217 #[error("deployment {tx_hash:?} received a receipt for {receipt:?}")]
219 ReceiptHashMismatch { tx_hash: TxHash, receipt: TxHash },
220 #[error("deployment {tx_hash:?} did not return the predicted address {predicted:?}")]
222 AddressMismatch {
223 tx_hash: TxHash,
224 predicted: ContractAddress,
225 },
226}
227
228#[cfg(test)]
229mod tests {
230 use super::*;
231 use thruput_json_rpc_types::{BlockHeight, hex::HexBytes};
232 use thruput_signing::LocalSigner;
233 use thruput_xdr::contract::ContractBlob;
234 use thruput_xdr::contract::ContractEntrypointID;
235 use xdrs::BytesM;
236
237 fn network() -> NetworkHash {
238 NetworkHash::from(SHA256Hash::from([8; 32]))
239 }
240
241 fn contract() -> Contract {
242 Contract::kRv32 {
243 rv32_contract: ContractBlob {
244 data: BytesM(vec![1, 2, 3]),
245 },
246 }
247 }
248
249 fn request(account: &PubkeyAccount, code: DeploymentCode) -> DeployRequest<'_> {
250 DeployRequest {
251 account,
252 code,
253 amount: 5,
254 initializer: Some(WalletInitCall {
255 entrypoint: ContractEntrypointID::from([4; 8]),
256 gas_limit: 50,
257 calldata: &[6],
258 }),
259 gas_limit: 100,
260 block_expiry_height: 1000,
261 nonce: 7,
262 }
263 }
264
265 fn receipt(tx_hash: TxHash, return_data: Option<Vec<u8>>) -> TransactionReceipt {
266 TransactionReceipt {
267 transaction_hash: tx_hash,
268 block_height: BlockHeight::from(1),
269 execution: ReceiptExecution::Return {
270 return_data: HexBytes(return_data.unwrap_or_default()),
271 },
272 events: vec![],
273 gas_used: 1,
274 }
275 }
276
277 #[test]
278 fn prepared_deploy_matches_canonical_builder_and_address() {
279 let key = LocalSigner::from_secret_key([9; 32]);
280 let account = PubkeyAccount::new(key.public_key(), 0);
281 let plan = prepare_deploy(
282 request(&account, DeploymentCode::Attached(contract())),
283 network(),
284 )
285 .unwrap();
286
287 let hash = contract_hash(&contract()).unwrap();
288 let salt = SHA256Hash::from(<[u8; 32]>::from(Sha256::digest(7u64.to_le_bytes())));
289 assert_eq!(deployment_nonce(7), salt);
290 assert_eq!(plan.code_hash(), hash);
291 assert_eq!(
292 plan.address(),
293 thruput_signing::derive_deploy_address(&account.address(), &salt, &hash).unwrap()
294 );
295 let expected =
296 build_default_wallet_deploy_transaction(BuildDefaultWalletDeployTransaction {
297 account: &account,
298 deployment_nonce: salt,
299 code: DeploymentCode::Attached(contract()),
300 amount: 5,
301 initializer: request(&account, DeploymentCode::Registered(hash)).initializer,
302 gas_limit: 100,
303 block_expiry_height: 1000,
304 nonce: 7,
305 })
306 .unwrap();
307 assert_eq!(plan.transaction(), &expected);
308
309 let registered = prepare_deploy(
311 request(&account, DeploymentCode::Registered(hash)),
312 network(),
313 )
314 .unwrap();
315 assert_eq!(registered.address(), plan.address());
316 assert!(registered.transaction().contracts_to_deploy.0.is_empty());
317
318 let signed = plan.sign(&key).unwrap();
319 assert_eq!(
320 signed.tx_hash(),
321 TxHash(thruput_signing::transaction_hash(&expected).unwrap().into())
322 );
323 assert_eq!(signed.address(), registered.address());
324 }
325
326 #[test]
327 fn deploy_signer_mismatch_is_rejected() {
328 let owner = LocalSigner::from_secret_key([9; 32]);
329 let account = PubkeyAccount::new(owner.public_key(), 0);
330 let plan = prepare_deploy(
331 request(&account, DeploymentCode::Attached(contract())),
332 network(),
333 )
334 .unwrap();
335 assert!(matches!(
336 plan.sign(&LocalSigner::from_secret_key([10; 32])),
337 Err(SigningError::SignerMismatch)
338 ));
339 }
340
341 #[test]
342 fn verification_requires_matching_hash_and_exact_address() {
343 let tx_hash = TxHash(SHA256Hash::from([1; 32]).into());
344 let address = ContractAddress::from([2; 32]);
345 let submitted = SubmittedDeploy { tx_hash, address };
346
347 submitted
348 .verify(&receipt(tx_hash, Some(vec![2; 32])))
349 .unwrap();
350 let other = TxHash(SHA256Hash::from([3; 32]).into());
351 assert!(matches!(
352 submitted.verify(&receipt(other, Some(vec![2; 32]))),
353 Err(DeploymentVerificationError::ReceiptHashMismatch { .. })
354 ));
355 for returned in [None, Some(vec![3; 32]), Some(vec![2; 31])] {
356 assert!(matches!(
357 submitted.verify(&receipt(tx_hash, returned)),
358 Err(DeploymentVerificationError::AddressMismatch { .. })
359 ));
360 }
361 let reverted = TransactionReceipt {
362 execution: ReceiptExecution::Abort {
363 abort_data: HexBytes(vec![]),
364 },
365 ..receipt(tx_hash, None)
366 };
367 submitted.verify(&reverted).unwrap();
368 }
369}