1use aptos_sdk::account::Account;
9use aptos_sdk::crypto::{AnyPublicKey, AnySignature, MultiKeyPublicKey, MultiKeySignature};
10use aptos_sdk::transaction::authenticator::AccountAuthenticator;
11use aptos_sdk::transaction::payload::TransactionPayload;
12use aptos_sdk::transaction::{
13 EntryFunction, FeePayerRawTransaction, RawTransaction, SignedTransaction,
14 TransactionAuthenticator,
15};
16use aptos_sdk::types::{AccountAddress, TypeTag};
17use aptos_sdk::{AptosConfig, aptos_bcs};
18use base64::Engine;
19use serde::{Deserialize, Serialize};
20
21#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
23#[serde(rename_all = "camelCase")]
24pub struct DecodedAptosPayload {
25 pub transaction: Vec<u8>,
27 pub sender_authenticator: Vec<u8>,
29}
30
31#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
36pub struct SimpleTransaction {
37 pub raw_transaction: RawTransaction,
39 pub fee_payer_address: Option<AccountAddress>,
41}
42
43impl SimpleTransaction {
44 pub fn signing_message(&self) -> Result<Vec<u8>, AptosCodecError> {
51 self.fee_payer_address.map_or_else(
52 || {
53 self.raw_transaction
54 .signing_message()
55 .map_err(|e| AptosCodecError::from_sdk(&e))
56 },
57 |fee_payer| {
58 FeePayerRawTransaction::new_simple(self.raw_transaction.clone(), fee_payer)
59 .signing_message()
60 .map_err(|e| AptosCodecError::from_sdk(&e))
61 },
62 )
63 }
64}
65
66#[derive(Debug, thiserror::Error)]
68pub enum AptosCodecError {
69 #[error("invalid aptos payload encoding: {0}")]
71 Encoding(String),
72 #[error("invalid aptos payload bcs: {0}")]
74 Bcs(String),
75}
76
77impl AptosCodecError {
78 fn from_sdk(err: &aptos_sdk::AptosError) -> Self {
79 Self::Bcs(err.to_string())
80 }
81}
82
83pub fn encode_aptos_payload(
89 transaction: &SimpleTransaction,
90 sender_authenticator: &AccountAuthenticator,
91) -> Result<String, AptosCodecError> {
92 let transaction_bytes =
93 aptos_bcs::to_bytes(transaction).map_err(|e| AptosCodecError::Bcs(e.to_string()))?;
94 let authenticator_bytes = aptos_bcs::to_bytes(sender_authenticator)
95 .map_err(|e| AptosCodecError::Bcs(e.to_string()))?;
96 let decoded = DecodedAptosPayload {
97 transaction: transaction_bytes,
98 sender_authenticator: authenticator_bytes,
99 };
100 let json =
101 serde_json::to_vec(&decoded).map_err(|e| AptosCodecError::Encoding(e.to_string()))?;
102 Ok(base64::engine::general_purpose::STANDARD.encode(json))
103}
104
105pub fn decode_aptos_payload(
111 transaction_base64: &str,
112) -> Result<DecodedAptosPayload, AptosCodecError> {
113 let json = base64::engine::general_purpose::STANDARD
114 .decode(transaction_base64.trim())
115 .map_err(|e| AptosCodecError::Encoding(e.to_string()))?;
116 serde_json::from_slice(&json).map_err(|e| AptosCodecError::Encoding(e.to_string()))
117}
118
119#[derive(Clone, Debug)]
121pub struct DecodedAptosPayment {
122 pub transaction: SimpleTransaction,
124 pub sender_authenticator: AccountAuthenticator,
126}
127
128impl DecodedAptosPayment {
129 pub fn from_base64(transaction_base64: &str) -> Result<Self, AptosCodecError> {
135 let decoded = decode_aptos_payload(transaction_base64)?;
136 let transaction = aptos_bcs::from_bytes::<SimpleTransaction>(&decoded.transaction)
137 .map_err(|e| AptosCodecError::Bcs(e.to_string()))?;
138 let sender_authenticator =
139 deserialize_account_authenticator(&decoded.sender_authenticator)?;
140 Ok(Self {
141 transaction,
142 sender_authenticator,
143 })
144 }
145
146 #[must_use]
148 pub fn sender_long(&self) -> String {
149 self.transaction.raw_transaction.sender.to_long_string()
150 }
151
152 #[must_use]
154 pub const fn entry_function(&self) -> Option<&EntryFunction> {
155 match &self.transaction.raw_transaction.payload {
156 TransactionPayload::EntryFunction(entry) => Some(entry),
157 _ => None,
158 }
159 }
160}
161
162pub fn account_authenticator_for(
168 account: &impl Account,
169 message: &[u8],
170) -> Result<AccountAuthenticator, AptosCodecError> {
171 let signature = account
172 .sign(message)
173 .map_err(|e| AptosCodecError::from_sdk(&e))?;
174 let public_key = account.public_key_bytes();
175 if public_key.len() != 32 || signature.len() != 64 {
176 return Err(AptosCodecError::Bcs(
177 "ed25519 authenticator requires 32-byte public key and 64-byte signature".to_owned(),
178 ));
179 }
180 Ok(AccountAuthenticator::ed25519(public_key, signature))
181}
182
183#[must_use]
185pub fn transaction_authenticator_from_sender(
186 sender: AccountAuthenticator,
187 fee_payer: Option<AccountAddress>,
188) -> TransactionAuthenticator {
189 match fee_payer {
190 Some(fee_payer_address) => TransactionAuthenticator::fee_payer(
191 sender,
192 Vec::new(),
193 Vec::new(),
194 fee_payer_address,
195 AccountAuthenticator::no_account_authenticator(),
196 ),
197 None => match sender {
198 AccountAuthenticator::Ed25519 {
199 public_key,
200 signature,
201 } => TransactionAuthenticator::Ed25519 {
202 public_key,
203 signature,
204 },
205 AccountAuthenticator::MultiEd25519 {
206 public_key,
207 signature,
208 } => TransactionAuthenticator::MultiEd25519 {
209 public_key,
210 signature,
211 },
212 other => TransactionAuthenticator::single_sender(other),
213 },
214 }
215}
216
217#[must_use]
219pub fn signed_from_decoded(decoded: &DecodedAptosPayment) -> SignedTransaction {
220 SignedTransaction::new(
221 decoded.transaction.raw_transaction.clone(),
222 transaction_authenticator_from_sender(
223 decoded.sender_authenticator.clone(),
224 decoded.transaction.fee_payer_address,
225 ),
226 )
227}
228
229pub fn aptos_config_for(
235 chain_id: u8,
236 rpc_url: Option<&str>,
237) -> Result<AptosConfig, AptosCodecError> {
238 if let Some(url) = rpc_url {
239 return AptosConfig::custom(url).map_err(|e| AptosCodecError::Encoding(e.to_string()));
240 }
241 match chain_id {
242 1 => Ok(AptosConfig::mainnet()),
243 2 => Ok(AptosConfig::testnet()),
244 _ => Err(AptosCodecError::Encoding(format!(
245 "unsupported aptos chain id {chain_id}"
246 ))),
247 }
248}
249
250#[must_use]
252pub fn is_metadata_type_tag(tag: &TypeTag) -> bool {
253 match tag {
254 TypeTag::Struct(st) => {
255 st.address == AccountAddress::ONE
256 && st.module.as_str() == "fungible_asset"
257 && st.name.as_str() == "Metadata"
258 && st.type_args.is_empty()
259 }
260 _ => false,
261 }
262}
263
264#[must_use]
266pub fn is_supported_transfer(entry: &EntryFunction) -> bool {
267 if entry.module.address != AccountAddress::ONE || entry.function != "transfer" {
268 return false;
269 }
270 let module = entry.module.name.as_str();
271 module == "primary_fungible_store" || module == "fungible_asset"
272}
273
274fn deserialize_account_authenticator(
275 bytes: &[u8],
276) -> Result<AccountAuthenticator, AptosCodecError> {
277 if let Ok(auth) = aptos_bcs::from_bytes::<AccountAuthenticator>(bytes) {
278 return Ok(auth);
279 }
280 let tag = bytes
282 .first()
283 .copied()
284 .ok_or_else(|| AptosCodecError::Bcs("empty account authenticator".to_owned()))?;
285 match tag {
286 2 => {
287 let rest = bytes.get(1..).ok_or_else(|| {
288 AptosCodecError::Bcs("truncated SingleKey authenticator".to_owned())
289 })?;
290 let (pk, pk_len) = take_any_public_key(rest)?;
291 let sig_bytes = rest
292 .get(pk_len..)
293 .ok_or_else(|| AptosCodecError::Bcs("truncated SingleKey signature".to_owned()))?;
294 let sig = AnySignature::from_bcs_bytes(sig_bytes)
295 .map_err(|e| AptosCodecError::from_sdk(&e))?;
296 Ok(AccountAuthenticator::single_key(
297 pk.to_bcs_bytes(),
298 sig.to_bcs_bytes(),
299 ))
300 }
301 3 => {
302 let rest = bytes.get(1..).ok_or_else(|| {
303 AptosCodecError::Bcs("truncated MultiKey authenticator".to_owned())
304 })?;
305 let (pk, pk_len) = take_multi_key_public(rest)?;
306 let sig_bytes = rest
307 .get(pk_len..)
308 .ok_or_else(|| AptosCodecError::Bcs("truncated MultiKey signature".to_owned()))?;
309 let sig = MultiKeySignature::from_bytes(sig_bytes)
310 .map_err(|e| AptosCodecError::from_sdk(&e))?;
311 Ok(AccountAuthenticator::multi_key(
312 pk.to_bytes(),
313 sig.to_bytes(),
314 ))
315 }
316 _ => Err(AptosCodecError::Bcs(format!(
317 "unsupported account authenticator variant {tag}"
318 ))),
319 }
320}
321
322fn take_any_public_key(bytes: &[u8]) -> Result<(AnyPublicKey, usize), AptosCodecError> {
324 let len = any_public_key_declared_len(bytes)?;
325 let prefix = bytes
326 .get(..len)
327 .ok_or_else(|| AptosCodecError::Bcs("truncated AnyPublicKey".to_owned()))?;
328 let pk = AnyPublicKey::from_bcs_bytes(prefix).map_err(|e| AptosCodecError::from_sdk(&e))?;
329 Ok((pk, len))
330}
331
332fn any_public_key_declared_len(bytes: &[u8]) -> Result<usize, AptosCodecError> {
333 let rest = bytes
334 .get(1..)
335 .ok_or_else(|| AptosCodecError::Bcs("truncated AnyPublicKey".to_owned()))?;
336 let (payload_len, prefix_len) = uleb128_len(rest)?;
337 1usize
338 .checked_add(prefix_len)
339 .and_then(|n| n.checked_add(payload_len))
340 .ok_or_else(|| AptosCodecError::Bcs("AnyPublicKey length overflow".to_owned()))
341}
342
343fn take_multi_key_public(bytes: &[u8]) -> Result<(MultiKeyPublicKey, usize), AptosCodecError> {
345 let num_keys = usize::from(
346 *bytes
347 .first()
348 .ok_or_else(|| AptosCodecError::Bcs("truncated MultiKeyPublicKey".to_owned()))?,
349 );
350 if num_keys == 0 || num_keys > 32 {
351 return Err(AptosCodecError::Bcs(format!(
352 "invalid MultiKeyPublicKey key count {num_keys}"
353 )));
354 }
355 let mut offset = 1usize;
356 for _ in 0..num_keys {
357 let used =
358 any_public_key_declared_len(bytes.get(offset..).ok_or_else(|| {
359 AptosCodecError::Bcs("truncated MultiKeyPublicKey key".to_owned())
360 })?)?;
361 offset = offset
362 .checked_add(used)
363 .ok_or_else(|| AptosCodecError::Bcs("MultiKeyPublicKey offset overflow".to_owned()))?;
364 }
365 offset = offset
366 .checked_add(1)
367 .ok_or_else(|| AptosCodecError::Bcs("truncated MultiKeyPublicKey threshold".to_owned()))?;
368 let prefix = bytes
369 .get(..offset)
370 .ok_or_else(|| AptosCodecError::Bcs("truncated MultiKeyPublicKey".to_owned()))?;
371 let pk = MultiKeyPublicKey::from_bytes(prefix).map_err(|e| AptosCodecError::from_sdk(&e))?;
372 Ok((pk, offset))
373}
374
375fn uleb128_len(bytes: &[u8]) -> Result<(usize, usize), AptosCodecError> {
376 let mut value = 0usize;
377 let mut shift = 0;
378 for (i, &byte) in bytes.iter().enumerate() {
379 if i >= 5 {
380 return Err(AptosCodecError::Bcs("ULEB128 too long".to_owned()));
381 }
382 value |= usize::from(byte & 0x7f) << shift;
383 if byte & 0x80 == 0 {
384 return Ok((value, i + 1));
385 }
386 shift += 7;
387 }
388 Err(AptosCodecError::Bcs("truncated ULEB128".to_owned()))
389}
390
391#[cfg(test)]
392#[allow(clippy::unwrap_used, reason = "test assertions")]
393mod tests {
394 use aptos_sdk::account::Ed25519Account;
395 use aptos_sdk::transaction::InputEntryFunctionData;
396 use aptos_sdk::transaction::builder::TransactionBuilder;
397 use aptos_sdk::types::ChainId;
398
399 use super::*;
400 use crate::DEFAULT_CLIENT_MAX_GAS;
401 use crate::chain::USDC_TESTNET_FA;
402
403 fn sample_payment() -> (SimpleTransaction, AccountAuthenticator) {
404 let sender = Ed25519Account::generate();
405 let fee_payer = Ed25519Account::generate();
406 let pay_to = AccountAddress::ONE;
407 let asset = AccountAddress::from_hex(USDC_TESTNET_FA).unwrap();
408 let payload = InputEntryFunctionData::transfer_fungible_asset(asset, pay_to, 1000).unwrap();
409 let raw = TransactionBuilder::new()
410 .sender(sender.address())
411 .sequence_number(0)
412 .payload(payload)
413 .max_gas_amount(DEFAULT_CLIENT_MAX_GAS)
414 .gas_unit_price(100)
415 .expiration_timestamp_secs(1_900_000_000)
416 .chain_id(ChainId::testnet())
417 .build()
418 .unwrap();
419 let tx = SimpleTransaction {
420 raw_transaction: raw,
421 fee_payer_address: Some(fee_payer.address()),
422 };
423 let message = tx.signing_message().unwrap();
424 let auth = account_authenticator_for(&sender, &message).unwrap();
425 (tx, auth)
426 }
427
428 #[test]
429 fn encode_decode_roundtrip_matches_ts_json_shape() {
430 let (tx, auth) = sample_payment();
431 let encoded = encode_aptos_payload(&tx, &auth).unwrap();
432 let decoded_json = decode_aptos_payload(&encoded).unwrap();
433 assert!(!decoded_json.transaction.is_empty());
434 assert!(!decoded_json.sender_authenticator.is_empty());
435
436 let json = base64::engine::general_purpose::STANDARD
437 .decode(&encoded)
438 .unwrap();
439 let value: serde_json::Value = serde_json::from_slice(&json).unwrap();
440 assert!(
441 value
442 .get("transaction")
443 .and_then(serde_json::Value::as_array)
444 .is_some()
445 );
446 assert!(
447 value
448 .get("senderAuthenticator")
449 .and_then(serde_json::Value::as_array)
450 .is_some()
451 );
452
453 let payment = DecodedAptosPayment::from_base64(&encoded).unwrap();
454 assert_eq!(payment.transaction, tx);
455 assert_eq!(
456 payment.transaction.raw_transaction.chain_id,
457 ChainId::testnet()
458 );
459 let entry = payment.entry_function().unwrap();
460 assert!(is_supported_transfer(entry));
461 assert_eq!(entry.args.len(), 3);
462 let asset: AccountAddress = aptos_bcs::from_bytes(entry.args.first().unwrap()).unwrap();
463 assert_eq!(asset, AccountAddress::from_hex(USDC_TESTNET_FA).unwrap());
464 payment
465 .sender_authenticator
466 .verify(&tx.signing_message().unwrap())
467 .unwrap();
468 }
469
470 fn ts_fixture(name: &str) -> (String, serde_json::Value) {
471 let root: serde_json::Value = serde_json::from_str(include_str!(
472 "../exact/fixtures/ts_encode_aptos_payload.json"
473 ))
474 .unwrap();
475 let entry = root.get(name).unwrap();
476 (
477 entry
478 .get("encoded")
479 .and_then(serde_json::Value::as_str)
480 .unwrap()
481 .to_owned(),
482 entry.clone(),
483 )
484 }
485
486 fn assert_ts_payment(name: &str, expected_variant: fn(&AccountAuthenticator) -> bool) {
487 let (encoded, meta) = ts_fixture(name);
488 let payment = DecodedAptosPayment::from_base64(&encoded).unwrap();
489 assert!(
490 expected_variant(&payment.sender_authenticator),
491 "{name} authenticator variant"
492 );
493 assert_eq!(
494 payment.sender_long(),
495 meta.get("sender")
496 .and_then(serde_json::Value::as_str)
497 .unwrap()
498 );
499 assert_eq!(
500 payment
501 .transaction
502 .fee_payer_address
503 .unwrap()
504 .to_long_string(),
505 meta.get("feePayer")
506 .and_then(serde_json::Value::as_str)
507 .unwrap()
508 );
509 let entry = payment.entry_function().unwrap();
510 assert!(is_supported_transfer(entry));
511 let asset: AccountAddress = aptos_bcs::from_bytes(entry.args.first().unwrap()).unwrap();
512 assert_eq!(
513 asset.to_long_string(),
514 meta.get("asset")
515 .and_then(serde_json::Value::as_str)
516 .unwrap()
517 );
518 let pay_to: AccountAddress = aptos_bcs::from_bytes(entry.args.get(1).unwrap()).unwrap();
519 assert_eq!(
520 pay_to.to_long_string(),
521 meta.get("payTo")
522 .and_then(serde_json::Value::as_str)
523 .unwrap()
524 );
525 let amount: u64 = aptos_bcs::from_bytes(entry.args.get(2).unwrap()).unwrap();
526 assert_eq!(
527 amount.to_string(),
528 meta.get("amount")
529 .and_then(serde_json::Value::as_str)
530 .unwrap()
531 );
532 payment
533 .sender_authenticator
534 .verify(&payment.transaction.signing_message().unwrap())
535 .unwrap();
536 }
537
538 #[test]
539 fn decodes_ts_encode_aptos_payload_ed25519() {
540 assert_ts_payment("ed25519", |a| {
541 matches!(a, AccountAuthenticator::Ed25519 { .. })
542 });
543 }
544
545 #[test]
546 fn decodes_ts_encode_aptos_payload_single_key_ed25519() {
547 assert_ts_payment("singleKeyEd25519", |a| {
548 matches!(a, AccountAuthenticator::SingleKey { .. })
549 });
550 }
551
552 #[test]
553 fn decodes_ts_encode_aptos_payload_single_key_secp256k1() {
554 assert_ts_payment("singleKeySecp256k1", |a| {
555 matches!(a, AccountAuthenticator::SingleKey { .. })
556 });
557 }
558
559 #[test]
560 fn decodes_ts_encode_aptos_payload_multi_key() {
561 assert_ts_payment("multiKey", |a| {
562 matches!(a, AccountAuthenticator::MultiKey { .. })
563 });
564 }
565}