1pub mod codec;
8mod deploy;
9mod native;
10pub use native::{
11 NativeRpcError, SubmitCallError, SubmittedCall, check_endpoint, first_submission_not_admitted,
12 resolve_block_expiry_height, submit_signed_transaction, validate_network,
13};
14pub use thruput_json_rpc_client::native::NativeClient;
15pub use thruput_json_rpc_client::native::receipt::{ReceiptPollConfig, ReceiptPollError};
16
17pub use deploy::{
18 DeployRequest, DeploymentVerificationError, SignedDeploy, SubmittedDeploy, UnsignedDeploy,
19 contract_hash, deployment_nonce, prepare_deploy,
20};
21pub use jsonrpsee;
22pub use thruput_contract_spec as spec;
23pub use thruput_json_rpc_client as rpc;
24pub use thruput_json_rpc_types as types;
25pub use thruput_signing as signing;
26pub use thruput_signing::strkey;
28pub use thruput_xdr as xdr;
29
30use thruput_json_rpc_types::transaction::TxHash;
31use thruput_signing::{SignTransactionParams, Signer, SigningError};
32use thruput_xdr::{transaction::Transaction, types::NetworkHash};
33
34#[derive(Debug, Clone)]
36pub struct UnsignedCall {
37 transaction: Transaction,
38 network_hash: NetworkHash,
39 native_return: bool,
40}
41
42#[derive(Debug, Clone, Copy)]
45pub enum Wallet<'a> {
46 Native(&'a signing::PubkeyAccount),
48 Legacy {
50 address: xdr::types::ContractAddress,
52 entrypoint: xdr::contract::ContractEntrypointID,
54 },
55}
56
57#[derive(Debug, Clone, Copy)]
59pub struct CallRequest<'a> {
60 pub wallet: Wallet<'a>,
62 pub target_address: xdr::types::ContractAddress,
64 pub target_entrypoint: xdr::contract::ContractEntrypointID,
66 pub target_calldata: &'a [u8],
68 pub gas_limit: u64,
70 pub target_gas_limit: u64,
72 pub block_expiry_height: i64,
74 pub nonce: u64,
76}
77
78pub fn prepare_call(
80 request: CallRequest<'_>,
81 network_hash: NetworkHash,
82) -> Result<UnsignedCall, SigningError> {
83 let transaction = match request.wallet {
84 Wallet::Native(account) => signing::default_wallet::build_default_wallet_call_transaction(
85 signing::default_wallet::BuildDefaultWalletCallTransaction {
86 account,
87 target_address: request.target_address,
88 target_entrypoint: request.target_entrypoint,
89 target_calldata: request.target_calldata,
90 target_gas_limit: request.target_gas_limit,
91 gas_limit: request.gas_limit,
92 block_expiry_height: request.block_expiry_height,
93 nonce: request.nonce,
94 },
95 )?,
96 Wallet::Legacy {
97 address,
98 entrypoint,
99 } => signing::build_dev_wallet_do_action_transaction(
100 signing::BuildDevWalletDoActionTransaction {
101 wallet_address: address,
102 wallet_entrypoint: entrypoint,
103 target_address: request.target_address,
104 target_entrypoint: request.target_entrypoint,
105 target_calldata: request.target_calldata,
106 target_gas_limit: request.target_gas_limit,
107 gas_limit: request.gas_limit,
108 block_expiry_height: request.block_expiry_height,
109 nonce: request.nonce,
110 },
111 )?,
112 };
113 Ok(UnsignedCall {
114 transaction,
115 network_hash,
116 native_return: matches!(request.wallet, Wallet::Native(_)),
117 })
118}
119
120impl UnsignedCall {
121 pub fn transaction(&self) -> &Transaction {
123 &self.transaction
124 }
125
126 pub fn sign(self, signer: &impl Signer) -> Result<SignedCall, SigningError> {
129 let (tx_hash, tx_hex) = sign_transaction(self.transaction, &self.network_hash, signer)?;
130 Ok(SignedCall {
131 tx_hash,
132 tx_hex,
133 native_return: self.native_return,
134 })
135 }
136}
137
138fn sign_transaction(
141 transaction: Transaction,
142 network_hash: &NetworkHash,
143 signer: &impl Signer,
144) -> Result<(TxHash, String), SigningError> {
145 let tx_hash = TxHash(thruput_signing::transaction_hash(&transaction)?.into());
146 let tx_hex = thruput_signing::sign_and_encode_transaction_hex(
147 transaction,
148 signer,
149 SignTransactionParams::default(),
150 network_hash,
151 )?;
152 Ok((tx_hash, tx_hex))
153}
154
155#[derive(Debug, Clone, PartialEq, Eq)]
157pub struct SignedCall {
158 tx_hash: TxHash,
159 tx_hex: String,
160 native_return: bool,
161}
162
163impl SignedCall {
164 pub fn is_native(&self) -> bool {
166 self.native_return
167 }
168
169 pub fn decode_return(&self, data: &[u8]) -> Result<Vec<u8>, SigningError> {
172 if self.native_return {
173 signing::default_wallet::decode_default_wallet_call_return(data)
174 } else {
175 Ok(data.to_vec())
176 }
177 }
178 pub fn tx_hex(&self) -> &str {
180 &self.tx_hex
181 }
182 pub fn tx_hash(&self) -> TxHash {
184 self.tx_hash
185 }
186}
187
188#[cfg(test)]
189mod tests {
190 use super::*;
191 use thruput_signing::LocalSigner;
192 use thruput_xdr::{
193 contract::ContractEntrypointID,
194 types::{ContractAddress, SHA256Hash},
195 };
196
197 fn prepared() -> UnsignedCall {
198 prepared_with_wallet(Wallet::Legacy {
199 address: ContractAddress::from([1; 32]),
200 entrypoint: ContractEntrypointID::from([2; 8]),
201 })
202 }
203
204 fn prepared_with_wallet(wallet: Wallet<'_>) -> UnsignedCall {
205 prepare_call(
206 CallRequest {
207 wallet,
208 target_address: ContractAddress::from([3; 32]),
209 target_entrypoint: ContractEntrypointID::from([4; 8]),
210 target_calldata: &[5, 6],
211 gas_limit: 100,
212 target_gas_limit: 90,
213 block_expiry_height: 1000,
214 nonce: 7,
215 },
216 NetworkHash::from(SHA256Hash::from([8; 32])),
217 )
218 .unwrap()
219 }
220
221 #[test]
222 fn native_signer_mismatch_is_rejected_before_signing() {
223 struct WrongSigner;
224 impl Signer for WrongSigner {
225 fn public_key(&self) -> xdr::types::PublicKey {
226 LocalSigner::from_secret_key([10; 32]).public_key()
227 }
228
229 fn sign(&self, _: &[u8]) -> Result<xdr::types::Signature, SigningError> {
230 panic!("mismatching signer must not be invoked")
231 }
232 }
233
234 let owner = LocalSigner::from_secret_key([9; 32]);
235 for nonce in [0, 17] {
236 let account = signing::PubkeyAccount::new(owner.public_key(), nonce);
237 let plan = prepared_with_wallet(Wallet::Native(&account));
238 assert!(matches!(
239 plan.sign(&WrongSigner),
240 Err(SigningError::SignerMismatch)
241 ));
242 }
243 }
244
245 #[test]
246 fn matching_native_signer_preserves_canonical_bytes() {
247 let owner = LocalSigner::from_secret_key([9; 32]);
248 let account = signing::PubkeyAccount::new(owner.public_key(), 17);
249 let plan = prepared_with_wallet(Wallet::Native(&account));
250 let expected = signing::sign_and_encode_transaction_hex(
251 plan.transaction().clone(),
252 &owner,
253 SignTransactionParams::default(),
254 &NetworkHash::from(SHA256Hash::from([8; 32])),
255 )
256 .unwrap();
257 assert_eq!(plan.sign(&owner).unwrap().tx_hex(), expected);
258 }
259
260 #[test]
261 fn signed_bytes_match_canonical_signing_and_remain_network_bound() {
262 let key = LocalSigner::from_secret_key([9; 32]);
263 let plan = prepared();
264 let expected = thruput_signing::sign_and_encode_transaction_hex(
265 plan.transaction().clone(),
266 &key,
267 SignTransactionParams::default(),
268 &NetworkHash::from(SHA256Hash::from([8; 32])),
269 )
270 .unwrap();
271 let signed = plan.clone().sign(&key).unwrap();
272 assert_eq!(signed.tx_hex(), expected);
273 assert_eq!(
274 signed.tx_hash(),
275 TxHash(
276 thruput_signing::transaction_hash(plan.transaction())
277 .unwrap()
278 .into()
279 )
280 );
281 let mut elsewhere = plan;
282 elsewhere.network_hash = NetworkHash::from(SHA256Hash::from([10; 32]));
283 let elsewhere = elsewhere.sign(&key).unwrap();
284 assert_eq!(elsewhere.tx_hash(), signed.tx_hash());
285 assert_ne!(elsewhere.tx_hex(), signed.tx_hex());
286 }
287
288 #[test]
289 fn wallet_dispatch_preserves_legacy_bytes_and_unwraps_native_returns() {
290 let key = LocalSigner::from_secret_key([9; 32]);
291 let account = signing::PubkeyAccount::from_public_key_bytes(key.verifying_key_bytes(), 17);
292 let request = CallRequest {
293 wallet: Wallet::Legacy {
294 address: ContractAddress::from([1; 32]),
295 entrypoint: ContractEntrypointID::from([2; 8]),
296 },
297 target_address: ContractAddress::from([3; 32]),
298 target_entrypoint: ContractEntrypointID::from([4; 8]),
299 target_calldata: &[5, 6],
300 gas_limit: 100,
301 target_gas_limit: 90,
302 block_expiry_height: 1000,
303 nonce: 7,
304 };
305 let network = NetworkHash::from(SHA256Hash::from([8; 32]));
306 let legacy = prepare_call(request, network.clone()).unwrap();
307 assert_eq!(legacy.transaction(), prepared().transaction());
308 let legacy = legacy.sign(&key).unwrap();
309 assert!(!legacy.is_native());
310 assert_eq!(legacy.decode_return(&[42]).unwrap(), [42]);
311 let native = prepare_call(
312 CallRequest {
313 wallet: Wallet::Native(&account),
314 ..request
315 },
316 network,
317 )
318 .unwrap();
319 assert_eq!(
320 native.transaction().invocation.invoked_address,
321 account.address()
322 );
323 let signed = native.sign(&key).unwrap();
324 assert!(signed.is_native());
325 let bytes = xdrs::cxs::encode(&vec![vec![42u8]]).unwrap();
326 assert_eq!(signed.decode_return(&bytes).unwrap(), [42]);
327 for returns in [vec![], vec![vec![1u8], vec![2u8]]] {
328 assert!(
329 signed
330 .decode_return(&xdrs::cxs::encode(&returns).unwrap())
331 .is_err()
332 );
333 }
334 assert!(signed.decode_return(&[255]).is_err());
335 }
336}