use jsonrpsee::core::client::ClientT;
use sha2::{Digest, Sha256};
use thruput_json_rpc_client::native::{
NativeClient,
receipt::{ReceiptPollConfig, ReceiptPollError},
};
use thruput_json_rpc_types::{
responses::{ReceiptExecution, TransactionReceipt},
transaction::TxHash,
};
use thruput_signing::default_wallet::{
BuildDefaultWalletDeployTransaction, DeploymentCode, WalletInitCall,
build_default_wallet_deploy_transaction,
};
use thruput_signing::{PubkeyAccount, Signer, SigningError};
use thruput_xdr::{
contract::Contract,
transaction::Transaction,
types::{ContractAddress, NetworkHash, SHA256Hash},
};
use crate::{SubmitCallError, SubmittedCall, sign_transaction};
#[derive(Debug, Clone)]
pub struct DeployRequest<'a> {
pub account: &'a PubkeyAccount,
pub code: DeploymentCode,
pub amount: u128,
pub initializer: Option<WalletInitCall<'a>>,
pub gas_limit: u64,
pub block_expiry_height: i64,
pub nonce: u64,
}
pub fn contract_hash(contract: &Contract) -> Result<SHA256Hash, SigningError> {
thruput_signing::wallet_contract_hash(contract)
}
pub fn deployment_nonce(nonce: u64) -> SHA256Hash {
SHA256Hash::from(<[u8; 32]>::from(Sha256::digest(nonce.to_le_bytes())))
}
pub fn prepare_deploy(
request: DeployRequest<'_>,
network_hash: NetworkHash,
) -> Result<UnsignedDeploy, SigningError> {
let code_hash = match &request.code {
DeploymentCode::Attached(contract) => contract_hash(contract)?,
DeploymentCode::Registered(hash) => *hash,
};
let salt = deployment_nonce(request.nonce);
let address =
thruput_signing::derive_deploy_address(&request.account.address(), &salt, &code_hash)?;
let transaction =
build_default_wallet_deploy_transaction(BuildDefaultWalletDeployTransaction {
account: request.account,
deployment_nonce: salt,
code: request.code,
amount: request.amount,
initializer: request.initializer,
gas_limit: request.gas_limit,
block_expiry_height: request.block_expiry_height,
nonce: request.nonce,
})?;
Ok(UnsignedDeploy {
transaction,
network_hash,
address,
code_hash,
})
}
#[derive(Debug, Clone)]
pub struct UnsignedDeploy {
transaction: Transaction,
network_hash: NetworkHash,
address: ContractAddress,
code_hash: SHA256Hash,
}
impl UnsignedDeploy {
pub fn transaction(&self) -> &Transaction {
&self.transaction
}
pub fn address(&self) -> ContractAddress {
self.address
}
pub fn code_hash(&self) -> SHA256Hash {
self.code_hash
}
pub fn sign(self, signer: &impl Signer) -> Result<SignedDeploy, SigningError> {
let (tx_hash, tx_hex) = sign_transaction(self.transaction, &self.network_hash, signer)?;
Ok(SignedDeploy {
tx_hash,
tx_hex,
address: self.address,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SignedDeploy {
tx_hash: TxHash,
tx_hex: String,
address: ContractAddress,
}
impl SignedDeploy {
pub fn tx_hex(&self) -> &str {
&self.tx_hex
}
pub fn tx_hash(&self) -> TxHash {
self.tx_hash
}
pub fn address(&self) -> ContractAddress {
self.address
}
pub async fn submit<C: ClientT + Sync>(
&self,
client: &NativeClient<C>,
) -> Result<SubmittedDeploy, SubmitCallError> {
crate::submit_signed_transaction(client, self.tx_hash, &self.tx_hex).await?;
Ok(SubmittedDeploy {
tx_hash: self.tx_hash,
address: self.address,
})
}
}
#[derive(Debug, Clone, Copy)]
pub struct SubmittedDeploy {
pub tx_hash: TxHash,
pub address: ContractAddress,
}
impl SubmittedDeploy {
pub async fn wait<C: ClientT + Sync, F: std::future::Future<Output = ()>>(
&self,
client: &NativeClient<C>,
network_hash: thruput_json_rpc_types::NetworkHash,
config: ReceiptPollConfig,
cancelled: F,
) -> Result<TransactionReceipt, ReceiptPollError> {
SubmittedCall {
tx_hash: self.tx_hash,
}
.wait(client, network_hash, config, cancelled)
.await
}
pub fn verify(&self, receipt: &TransactionReceipt) -> Result<(), DeploymentVerificationError> {
if receipt.transaction_hash != self.tx_hash {
return Err(DeploymentVerificationError::ReceiptHashMismatch {
tx_hash: self.tx_hash,
receipt: receipt.transaction_hash,
});
}
if let ReceiptExecution::Return { return_data } = &receipt.execution
&& return_data.as_ref() != self.address.as_ref()
{
return Err(DeploymentVerificationError::AddressMismatch {
tx_hash: self.tx_hash,
predicted: self.address,
});
}
Ok(())
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum DeploymentVerificationError {
#[error("deployment {tx_hash:?} received a receipt for {receipt:?}")]
ReceiptHashMismatch { tx_hash: TxHash, receipt: TxHash },
#[error("deployment {tx_hash:?} did not return the predicted address {predicted:?}")]
AddressMismatch {
tx_hash: TxHash,
predicted: ContractAddress,
},
}
#[cfg(test)]
mod tests {
use super::*;
use thruput_json_rpc_types::{BlockHeight, hex::HexBytes};
use thruput_signing::LocalSigner;
use thruput_xdr::contract::ContractBlob;
use thruput_xdr::contract::ContractEntrypointID;
use xdrs::BytesM;
fn network() -> NetworkHash {
NetworkHash::from(SHA256Hash::from([8; 32]))
}
fn contract() -> Contract {
Contract::kRv32 {
rv32_contract: ContractBlob {
data: BytesM(vec![1, 2, 3]),
},
}
}
fn request(account: &PubkeyAccount, code: DeploymentCode) -> DeployRequest<'_> {
DeployRequest {
account,
code,
amount: 5,
initializer: Some(WalletInitCall {
entrypoint: ContractEntrypointID::from([4; 8]),
gas_limit: 50,
calldata: &[6],
}),
gas_limit: 100,
block_expiry_height: 1000,
nonce: 7,
}
}
fn receipt(tx_hash: TxHash, return_data: Option<Vec<u8>>) -> TransactionReceipt {
TransactionReceipt {
transaction_hash: tx_hash,
block_height: BlockHeight::from(1),
execution: ReceiptExecution::Return {
return_data: HexBytes(return_data.unwrap_or_default()),
},
events: vec![],
gas_used: 1,
}
}
#[test]
fn prepared_deploy_matches_canonical_builder_and_address() {
let key = LocalSigner::from_secret_key([9; 32]);
let account = PubkeyAccount::new(key.public_key(), 0);
let plan = prepare_deploy(
request(&account, DeploymentCode::Attached(contract())),
network(),
)
.unwrap();
let hash = contract_hash(&contract()).unwrap();
let salt = SHA256Hash::from(<[u8; 32]>::from(Sha256::digest(7u64.to_le_bytes())));
assert_eq!(deployment_nonce(7), salt);
assert_eq!(plan.code_hash(), hash);
assert_eq!(
plan.address(),
thruput_signing::derive_deploy_address(&account.address(), &salt, &hash).unwrap()
);
let expected =
build_default_wallet_deploy_transaction(BuildDefaultWalletDeployTransaction {
account: &account,
deployment_nonce: salt,
code: DeploymentCode::Attached(contract()),
amount: 5,
initializer: request(&account, DeploymentCode::Registered(hash)).initializer,
gas_limit: 100,
block_expiry_height: 1000,
nonce: 7,
})
.unwrap();
assert_eq!(plan.transaction(), &expected);
let registered = prepare_deploy(
request(&account, DeploymentCode::Registered(hash)),
network(),
)
.unwrap();
assert_eq!(registered.address(), plan.address());
assert!(registered.transaction().contracts_to_deploy.0.is_empty());
let signed = plan.sign(&key).unwrap();
assert_eq!(
signed.tx_hash(),
TxHash(thruput_signing::transaction_hash(&expected).unwrap().into())
);
assert_eq!(signed.address(), registered.address());
}
#[test]
fn deploy_signer_mismatch_is_rejected() {
let owner = LocalSigner::from_secret_key([9; 32]);
let account = PubkeyAccount::new(owner.public_key(), 0);
let plan = prepare_deploy(
request(&account, DeploymentCode::Attached(contract())),
network(),
)
.unwrap();
assert!(matches!(
plan.sign(&LocalSigner::from_secret_key([10; 32])),
Err(SigningError::SignerMismatch)
));
}
#[test]
fn verification_requires_matching_hash_and_exact_address() {
let tx_hash = TxHash(SHA256Hash::from([1; 32]).into());
let address = ContractAddress::from([2; 32]);
let submitted = SubmittedDeploy { tx_hash, address };
submitted
.verify(&receipt(tx_hash, Some(vec![2; 32])))
.unwrap();
let other = TxHash(SHA256Hash::from([3; 32]).into());
assert!(matches!(
submitted.verify(&receipt(other, Some(vec![2; 32]))),
Err(DeploymentVerificationError::ReceiptHashMismatch { .. })
));
for returned in [None, Some(vec![3; 32]), Some(vec![2; 31])] {
assert!(matches!(
submitted.verify(&receipt(tx_hash, returned)),
Err(DeploymentVerificationError::AddressMismatch { .. })
));
}
let reverted = TransactionReceipt {
execution: ReceiptExecution::Abort {
abort_data: HexBytes(vec![]),
},
..receipt(tx_hash, None)
};
submitted.verify(&reverted).unwrap();
}
}