pub mod codec;
mod deploy;
mod native;
pub use native::{
NativeRpcError, SubmitCallError, SubmittedCall, check_endpoint, first_submission_not_admitted,
resolve_block_expiry_height, submit_signed_transaction, validate_network,
};
pub use thruput_json_rpc_client::native::NativeClient;
pub use thruput_json_rpc_client::native::receipt::{ReceiptPollConfig, ReceiptPollError};
pub use deploy::{
DeployRequest, DeploymentVerificationError, SignedDeploy, SubmittedDeploy, UnsignedDeploy,
contract_hash, deployment_nonce, prepare_deploy,
};
pub use jsonrpsee;
pub use thruput_contract_spec as spec;
pub use thruput_json_rpc_client as rpc;
pub use thruput_json_rpc_types as types;
pub use thruput_signing as signing;
pub use thruput_signing::strkey;
pub use thruput_xdr as xdr;
use thruput_json_rpc_types::transaction::TxHash;
use thruput_signing::{SignTransactionParams, Signer, SigningError};
use thruput_xdr::{transaction::Transaction, types::NetworkHash};
#[derive(Debug, Clone)]
pub struct UnsignedCall {
transaction: Transaction,
network_hash: NetworkHash,
native_return: bool,
}
#[derive(Debug, Clone, Copy)]
pub enum Wallet<'a> {
Native(&'a signing::PubkeyAccount),
Legacy {
address: xdr::types::ContractAddress,
entrypoint: xdr::contract::ContractEntrypointID,
},
}
#[derive(Debug, Clone, Copy)]
pub struct CallRequest<'a> {
pub wallet: Wallet<'a>,
pub target_address: xdr::types::ContractAddress,
pub target_entrypoint: xdr::contract::ContractEntrypointID,
pub target_calldata: &'a [u8],
pub gas_limit: u64,
pub target_gas_limit: u64,
pub block_expiry_height: i64,
pub nonce: u64,
}
pub fn prepare_call(
request: CallRequest<'_>,
network_hash: NetworkHash,
) -> Result<UnsignedCall, SigningError> {
let transaction = match request.wallet {
Wallet::Native(account) => signing::default_wallet::build_default_wallet_call_transaction(
signing::default_wallet::BuildDefaultWalletCallTransaction {
account,
target_address: request.target_address,
target_entrypoint: request.target_entrypoint,
target_calldata: request.target_calldata,
target_gas_limit: request.target_gas_limit,
gas_limit: request.gas_limit,
block_expiry_height: request.block_expiry_height,
nonce: request.nonce,
},
)?,
Wallet::Legacy {
address,
entrypoint,
} => signing::build_dev_wallet_do_action_transaction(
signing::BuildDevWalletDoActionTransaction {
wallet_address: address,
wallet_entrypoint: entrypoint,
target_address: request.target_address,
target_entrypoint: request.target_entrypoint,
target_calldata: request.target_calldata,
target_gas_limit: request.target_gas_limit,
gas_limit: request.gas_limit,
block_expiry_height: request.block_expiry_height,
nonce: request.nonce,
},
)?,
};
Ok(UnsignedCall {
transaction,
network_hash,
native_return: matches!(request.wallet, Wallet::Native(_)),
})
}
impl UnsignedCall {
pub fn transaction(&self) -> &Transaction {
&self.transaction
}
pub fn sign(self, signer: &impl Signer) -> Result<SignedCall, SigningError> {
let (tx_hash, tx_hex) = sign_transaction(self.transaction, &self.network_hash, signer)?;
Ok(SignedCall {
tx_hash,
tx_hex,
native_return: self.native_return,
})
}
}
fn sign_transaction(
transaction: Transaction,
network_hash: &NetworkHash,
signer: &impl Signer,
) -> Result<(TxHash, String), SigningError> {
let tx_hash = TxHash(thruput_signing::transaction_hash(&transaction)?.into());
let tx_hex = thruput_signing::sign_and_encode_transaction_hex(
transaction,
signer,
SignTransactionParams::default(),
network_hash,
)?;
Ok((tx_hash, tx_hex))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SignedCall {
tx_hash: TxHash,
tx_hex: String,
native_return: bool,
}
impl SignedCall {
pub fn is_native(&self) -> bool {
self.native_return
}
pub fn decode_return(&self, data: &[u8]) -> Result<Vec<u8>, SigningError> {
if self.native_return {
signing::default_wallet::decode_default_wallet_call_return(data)
} else {
Ok(data.to_vec())
}
}
pub fn tx_hex(&self) -> &str {
&self.tx_hex
}
pub fn tx_hash(&self) -> TxHash {
self.tx_hash
}
}
#[cfg(test)]
mod tests {
use super::*;
use thruput_signing::LocalSigner;
use thruput_xdr::{
contract::ContractEntrypointID,
types::{ContractAddress, SHA256Hash},
};
fn prepared() -> UnsignedCall {
prepared_with_wallet(Wallet::Legacy {
address: ContractAddress::from([1; 32]),
entrypoint: ContractEntrypointID::from([2; 8]),
})
}
fn prepared_with_wallet(wallet: Wallet<'_>) -> UnsignedCall {
prepare_call(
CallRequest {
wallet,
target_address: ContractAddress::from([3; 32]),
target_entrypoint: ContractEntrypointID::from([4; 8]),
target_calldata: &[5, 6],
gas_limit: 100,
target_gas_limit: 90,
block_expiry_height: 1000,
nonce: 7,
},
NetworkHash::from(SHA256Hash::from([8; 32])),
)
.unwrap()
}
#[test]
fn native_signer_mismatch_is_rejected_before_signing() {
struct WrongSigner;
impl Signer for WrongSigner {
fn public_key(&self) -> xdr::types::PublicKey {
LocalSigner::from_secret_key([10; 32]).public_key()
}
fn sign(&self, _: &[u8]) -> Result<xdr::types::Signature, SigningError> {
panic!("mismatching signer must not be invoked")
}
}
let owner = LocalSigner::from_secret_key([9; 32]);
for nonce in [0, 17] {
let account = signing::PubkeyAccount::new(owner.public_key(), nonce);
let plan = prepared_with_wallet(Wallet::Native(&account));
assert!(matches!(
plan.sign(&WrongSigner),
Err(SigningError::SignerMismatch)
));
}
}
#[test]
fn matching_native_signer_preserves_canonical_bytes() {
let owner = LocalSigner::from_secret_key([9; 32]);
let account = signing::PubkeyAccount::new(owner.public_key(), 17);
let plan = prepared_with_wallet(Wallet::Native(&account));
let expected = signing::sign_and_encode_transaction_hex(
plan.transaction().clone(),
&owner,
SignTransactionParams::default(),
&NetworkHash::from(SHA256Hash::from([8; 32])),
)
.unwrap();
assert_eq!(plan.sign(&owner).unwrap().tx_hex(), expected);
}
#[test]
fn signed_bytes_match_canonical_signing_and_remain_network_bound() {
let key = LocalSigner::from_secret_key([9; 32]);
let plan = prepared();
let expected = thruput_signing::sign_and_encode_transaction_hex(
plan.transaction().clone(),
&key,
SignTransactionParams::default(),
&NetworkHash::from(SHA256Hash::from([8; 32])),
)
.unwrap();
let signed = plan.clone().sign(&key).unwrap();
assert_eq!(signed.tx_hex(), expected);
assert_eq!(
signed.tx_hash(),
TxHash(
thruput_signing::transaction_hash(plan.transaction())
.unwrap()
.into()
)
);
let mut elsewhere = plan;
elsewhere.network_hash = NetworkHash::from(SHA256Hash::from([10; 32]));
let elsewhere = elsewhere.sign(&key).unwrap();
assert_eq!(elsewhere.tx_hash(), signed.tx_hash());
assert_ne!(elsewhere.tx_hex(), signed.tx_hex());
}
#[test]
fn wallet_dispatch_preserves_legacy_bytes_and_unwraps_native_returns() {
let key = LocalSigner::from_secret_key([9; 32]);
let account = signing::PubkeyAccount::from_public_key_bytes(key.verifying_key_bytes(), 17);
let request = CallRequest {
wallet: Wallet::Legacy {
address: ContractAddress::from([1; 32]),
entrypoint: ContractEntrypointID::from([2; 8]),
},
target_address: ContractAddress::from([3; 32]),
target_entrypoint: ContractEntrypointID::from([4; 8]),
target_calldata: &[5, 6],
gas_limit: 100,
target_gas_limit: 90,
block_expiry_height: 1000,
nonce: 7,
};
let network = NetworkHash::from(SHA256Hash::from([8; 32]));
let legacy = prepare_call(request, network.clone()).unwrap();
assert_eq!(legacy.transaction(), prepared().transaction());
let legacy = legacy.sign(&key).unwrap();
assert!(!legacy.is_native());
assert_eq!(legacy.decode_return(&[42]).unwrap(), [42]);
let native = prepare_call(
CallRequest {
wallet: Wallet::Native(&account),
..request
},
network,
)
.unwrap();
assert_eq!(
native.transaction().invocation.invoked_address,
account.address()
);
let signed = native.sign(&key).unwrap();
assert!(signed.is_native());
let bytes = xdrs::cxs::encode(&vec![vec![42u8]]).unwrap();
assert_eq!(signed.decode_return(&bytes).unwrap(), [42]);
for returns in [vec![], vec![vec![1u8], vec![2u8]]] {
assert!(
signed
.decode_return(&xdrs::cxs::encode(&returns).unwrap())
.is_err()
);
}
assert!(signed.decode_return(&[255]).is_err());
}
}