Skip to content
This repository was archived by the owner on Jun 1, 2026. It is now read-only.

Commit f55b25b

Browse files
committed
Map MoveCall package IDs for transactions
Add tracking and mapping of MoveCall package IDs to improve identification of smart-contract targets for programmable transactions. Introduces move_call_packages on Digest, decodes MoveCall commands and Transaction.kind in proto handlers, and extracts package IDs in the RPC mapper. Transaction mapping now prefers a primary non-framework contract from move_call_packages (falling back to event packages) and includes tests plus testdata for a Mayan MCTP flow. Also updates RPC client read mask and adds SUI_MCTP_PACKAGE_ID to Mayan constants.
1 parent 3e940ab commit f55b25b

11 files changed

Lines changed: 177 additions & 9 deletions

File tree

crates/gem_sui/src/models/transaction.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ pub struct Checkpoint {
8787
pub struct Digest {
8888
pub digest: String,
8989
pub effects: Effect,
90+
#[serde(default)]
91+
pub move_call_packages: Vec<String>,
9092
#[serde(rename = "balanceChanges")]
9193
pub balance_changes: Option<Vec<BalanceChange>>,
9294
pub events: Vec<Event>,

crates/gem_sui/src/provider/transaction_state_mapper.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ mod tests {
3333
owner: Owner::String("0x123".to_string()),
3434
},
3535
},
36+
move_call_packages: Vec::new(),
3637
balance_changes: None,
3738
events: vec![],
3839
timestamp_ms: 1234567890,

crates/gem_sui/src/provider/transactions_mapper.rs

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use crate::models::{BalanceChange, Digest, Event, EventStake, EventUnstake, GasUsed, TransactionBlocks};
2-
use crate::{SUI_COIN_TYPE, SUI_STAKE_EVENT, SUI_UNSTAKE_EVENT, full_coin_type};
2+
use crate::{SUI_COIN_TYPE, SUI_STAKE_EVENT, SUI_UNSTAKE_EVENT, full_coin_type, sui_framework_package_address};
33
use chain_primitives::{BalanceDiff, SwapMapper};
44
use chrono::{TimeZone, Utc};
55
use num_bigint::{BigUint, Sign};
@@ -33,7 +33,7 @@ pub fn map_transaction(transaction: Digest) -> Option<Transaction> {
3333
};
3434
let owner = effects.gas_object.owner.get_address_owner();
3535

36-
let (asset_id, from, to, transaction_type, value, metadata) = map_transaction_type(&transaction.events, &balance_changes, &owner, &fee)?;
36+
let (asset_id, from, to, transaction_type, value, metadata) = map_transaction_type(&transaction.events, &transaction.move_call_packages, &balance_changes, &owner, &fee)?;
3737

3838
Some(Transaction::new(
3939
hash,
@@ -54,6 +54,7 @@ pub fn map_transaction(transaction: Digest) -> Option<Transaction> {
5454

5555
fn map_transaction_type(
5656
events: &[Event],
57+
move_call_packages: &[String],
5758
balance_changes: &[BalanceChange],
5859
owner: &Option<String>,
5960
fee: &BigUint,
@@ -135,10 +136,11 @@ fn map_transaction_type(
135136
let method_name = events.first()?.event_type.rsplit("::").nth(1)?.to_string();
136137
let metadata = TransactionSmartContractMetadata { method_name };
137138
let owner = owner.clone()?;
139+
let contract = primary_contract(move_call_packages.iter().map(String::as_str)).or_else(|| primary_contract(events.iter().map(|event| event.package_id.as_str())));
138140
return Some((
139141
chain.as_asset_id(),
140142
owner.clone(),
141-
owner,
143+
contract.unwrap_or(owner),
142144
TransactionType::SmartContractCall,
143145
"0".to_string(),
144146
serde_json::to_value(metadata).ok(),
@@ -148,6 +150,26 @@ fn map_transaction_type(
148150
None
149151
}
150152

153+
fn primary_contract<'a>(contracts: impl IntoIterator<Item = &'a str>) -> Option<String> {
154+
let mut first = None;
155+
for contract in contracts {
156+
if contract.is_empty() {
157+
continue;
158+
}
159+
if first.is_none() {
160+
first = Some(contract);
161+
}
162+
if !is_sui_framework_package(contract) {
163+
return Some(contract.to_string());
164+
}
165+
}
166+
first.map(ToString::to_string)
167+
}
168+
169+
fn is_sui_framework_package(package: &str) -> bool {
170+
package.parse::<sui_types::Address>().is_ok_and(|address| address == sui_framework_package_address())
171+
}
172+
151173
fn map_transfer_balance_changes<'a>(balance_changes: &'a [BalanceChange], fee: &BigUint) -> Option<(&'a BalanceChange, &'a BalanceChange)> {
152174
let to_change = single(balance_changes.iter().filter(|change| change.amount.sign() == Sign::Plus))?;
153175
let from_change = single(outgoing_changes(balance_changes, &to_change.coin_type)).or_else(|| select_native_transfer_source(balance_changes, to_change, fee))?;
@@ -273,6 +295,7 @@ mod tests {
273295
status: Status { status: "success".to_string() },
274296
gas_object: GasObject { owner: owner(OWNER_ADDRESS) },
275297
},
298+
move_call_packages: Vec::new(),
276299
balance_changes: Some(balance_changes),
277300
events,
278301
timestamp_ms: 1778964551487,
@@ -298,6 +321,18 @@ mod tests {
298321
assert_eq!(metadata.method_name, "timevy_tipping");
299322
}
300323

324+
#[test]
325+
fn test_map_mayan_mctp_smart_contract_call() {
326+
let digest: Digest = serde_json::from_str(include_str!("../../testdata/mayan_mctp_sui_usdc_to_arbitrum_usdc.json")).unwrap();
327+
let expected_contract = primary_contract(digest.move_call_packages.iter().map(String::as_str)).unwrap();
328+
let transaction = map_transaction(digest).unwrap();
329+
330+
assert_eq!(transaction.hash, "AqXACRuimqMVf4wiVjR3Ch5PBunhQAJY3ZfAMF3MXUsW");
331+
assert_eq!(transaction.transaction_type, TransactionType::SmartContractCall);
332+
assert_eq!(transaction.from, "0x1b4cd8b734f2465614678ca0450ce9c4f2ff4835c6a7545522892a1a8fb67991");
333+
assert_eq!(transaction.to, expected_contract);
334+
}
335+
301336
#[test]
302337
fn test_map_transaction_by_hash() {
303338
let digest: Digest = serde_json::from_str(include_str!("../../testdata/transfer_sui.json")).unwrap();

crates/gem_sui/src/rpc/client.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ use crate::{SUI_COIN_TYPE, SUI_COIN_TYPE_FULL};
2323

2424
const TRANSACTION_READ_MASK: &[&str] = &[
2525
"digest",
26+
"transaction.kind",
2627
"effects.gas_used",
2728
"effects.status",
2829
"effects.gas_object",

crates/gem_sui/src/rpc/mapper.rs

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use std::{error::Error, str::FromStr};
22

33
use num_bigint::{BigInt, BigUint};
44

5-
use super::proto::{self, OwnerKind, Timestamp};
5+
use super::proto::{self, Command, OwnerKind, Timestamp};
66
use crate::models::transaction::SuiStatus;
77
use crate::models::{
88
BalanceChange, Checkpoint, Digest, Effect, Event, GasObject, GasUsed, InspectCommandResult, InspectEffects, InspectGasUsed, InspectResult, Owner, OwnerObject, Status,
@@ -37,12 +37,36 @@ pub(super) fn map_executed_transaction(transaction: proto::ExecutedTransaction)
3737
Ok(Digest {
3838
digest: transaction.digest.ok_or("missing Sui transaction digest")?,
3939
effects: map_effect(transaction.effects.as_ref()),
40+
move_call_packages: map_move_call_packages(transaction.transaction.as_ref()),
4041
balance_changes: Some(transaction.balance_changes.into_iter().map(map_balance_change).collect::<Result<Vec<_>, _>>()?),
4142
events: transaction.events.map(map_events).unwrap_or_default(),
4243
timestamp_ms: transaction.timestamp.as_ref().map(timestamp_millis).unwrap_or_default() as u64,
4344
})
4445
}
4546

47+
fn map_move_call_packages(transaction: Option<&proto::Transaction>) -> Vec<String> {
48+
transaction
49+
.and_then(|transaction| transaction.kind.as_ref())
50+
.and_then(|kind| kind.programmable_transaction.as_ref())
51+
.map(|transaction| {
52+
transaction
53+
.commands
54+
.iter()
55+
.filter_map(|command| match command {
56+
Command::MoveCall(call) => call.package.clone(),
57+
Command::TransferObjects(_)
58+
| Command::SplitCoins(_)
59+
| Command::MergeCoins(_)
60+
| Command::Publish(_)
61+
| Command::MakeMoveVector(_)
62+
| Command::Upgrade(_)
63+
| Command::Unknown => None,
64+
})
65+
.collect()
66+
})
67+
.unwrap_or_default()
68+
}
69+
4670
fn map_effect(effects: Option<&proto::TransactionEffects>) -> Effect {
4771
let gas_object_owner = effects
4872
.and_then(|effects| effects.gas_object.as_ref())

crates/gem_sui/src/rpc/proto/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ pub(crate) use objects::{
2525
pub(crate) use service::{Epoch, GetEpochRequest, GetEpochResponse, GetServiceInfoRequest, GetServiceInfoResponse};
2626
pub(crate) use status::Status;
2727
pub(crate) use timestamp::Timestamp;
28-
pub(crate) use transaction_data::{Argument, Input, MoveCall, ProgrammableTransaction, Transaction, TransactionKind, UserSignature};
28+
pub(crate) use transaction_data::{Argument, Command, Input, MoveCall, ProgrammableTransaction, Transaction, TransactionKind, UserSignature};
2929
pub(crate) use transactions::{
3030
BalanceChange, BatchGetTransactionsRequest, BatchGetTransactionsResponse, ExecuteTransactionRequest, ExecuteTransactionResponse, ExecutedTransaction, GasCostSummary,
3131
GetTransactionRequest, GetTransactionResponse, GetTransactionResult, SimulateTransactionRequest, SimulateTransactionResponse, TransactionChecks, TransactionEffects,

crates/gem_sui/src/rpc/proto/transaction_data/command.rs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use gem_encoding::protobuf::{encode_message_field, proto_encode};
1+
use gem_encoding::protobuf::{encode_message_field, proto_decode, proto_encode};
22
use sui_types as sdk;
33

44
use super::Argument;
@@ -7,7 +7,7 @@ use crate::rpc::proto::MessageResult;
77
// Field numbers mirror sui-rpc v0.3.1 transaction command schemas:
88
// https://docs.rs/crate/sui-rpc/0.3.1/source/vendored/proto/sui/rpc/v2/transaction.proto
99

10-
#[derive(Clone, Debug)]
10+
#[derive(Clone, Debug, Default)]
1111
pub enum Command {
1212
MoveCall(MoveCall),
1313
TransferObjects(TransferObjects),
@@ -16,6 +16,8 @@ pub enum Command {
1616
Publish(Publish),
1717
MakeMoveVector(MakeMoveVector),
1818
Upgrade(Upgrade),
19+
#[default]
20+
Unknown,
1921
}
2022

2123
impl Command {
@@ -48,9 +50,14 @@ proto_encode!(Command as value {
4850
Command::Publish(value) => encode_message_field(5, &value.encode()),
4951
Command::MakeMoveVector(value) => encode_message_field(6, &value.encode()),
5052
Command::Upgrade(value) => encode_message_field(7, &value.encode()),
53+
Command::Unknown => Vec::new(),
5154
},
5255
});
5356

57+
proto_decode!(Command {
58+
1 => |value, field| *value = Self::MoveCall(field.message()?),
59+
});
60+
5461
#[derive(Clone, Debug, Default)]
5562
pub struct MoveCall {
5663
pub package: Option<String>,
@@ -90,6 +97,10 @@ proto_encode!(MoveCall {
9097
5 => arguments: repeated_message,
9198
});
9299

100+
proto_decode!(MoveCall {
101+
1 => package: optional_string,
102+
});
103+
93104
#[derive(Clone, Debug, Default)]
94105
pub struct TransferObjects {
95106
pub objects: Vec<Argument>,

crates/gem_sui/src/rpc/proto/transaction_data/transaction.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use gem_encoding::protobuf::proto_encode;
1+
use gem_encoding::protobuf::{proto_decode, proto_encode};
22
use sui_types as sdk;
33

44
use super::{Command, Input};
@@ -40,6 +40,10 @@ proto_encode!(Transaction {
4040
5 => sender: optional_string,
4141
});
4242

43+
proto_decode!(Transaction {
44+
4 => kind: optional_message,
45+
});
46+
4347
#[derive(Clone, Debug, Default)]
4448
pub struct TransactionKind {
4549
pub kind: Option<i32>,
@@ -74,6 +78,10 @@ proto_encode!(TransactionKind {
7478
2 => programmable_transaction: optional_message,
7579
});
7680

81+
proto_decode!(TransactionKind {
82+
2 => programmable_transaction: optional_message,
83+
});
84+
7785
#[derive(Clone, Debug, Default)]
7886
pub struct ProgrammableTransaction {
7987
pub inputs: Vec<Input>,
@@ -93,3 +101,7 @@ proto_encode!(ProgrammableTransaction {
93101
1 => inputs: repeated_message,
94102
2 => commands: repeated_message,
95103
});
104+
105+
proto_decode!(ProgrammableTransaction {
106+
2 => commands: repeated_message,
107+
});

crates/gem_sui/src/rpc/proto/transactions.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ proto_decode!(CommandOutput {
154154
#[derive(Clone, Debug, Default)]
155155
pub struct ExecutedTransaction {
156156
pub digest: Option<String>,
157+
pub transaction: Option<Transaction>,
157158
pub effects: Option<TransactionEffects>,
158159
pub events: Option<TransactionEvents>,
159160
pub timestamp: Option<Timestamp>,
@@ -162,6 +163,7 @@ pub struct ExecutedTransaction {
162163

163164
proto_decode!(ExecutedTransaction {
164165
1 => digest: optional_string,
166+
2 => transaction: optional_message,
165167
4 => effects: optional_message,
166168
5 => events: optional_message,
167169
7 => timestamp: optional_message,
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
{
2+
"digest": "AqXACRuimqMVf4wiVjR3Ch5PBunhQAJY3ZfAMF3MXUsW",
3+
"move_call_packages": [
4+
"0x0000000000000000000000000000000000000000000000000000000000000002",
5+
"0xb5bd3599ec7f4ae86afd84398f6f2d862deecce965e8ace2d8d8c8108d5076df",
6+
"0xb5bd3599ec7f4ae86afd84398f6f2d862deecce965e8ace2d8d8c8108d5076df",
7+
"0x2aa6c5d56376c371f88a6cc42e852824994993cb9bab8d3e6450cbe3cb32b94e",
8+
"0xb5bd3599ec7f4ae86afd84398f6f2d862deecce965e8ace2d8d8c8108d5076df",
9+
"0x0000000000000000000000000000000000000000000000000000000000000002",
10+
"0x5306f64e312b581766351c07af79c72fcb1cd25147157fdc2f8ad76de9a3fb6a",
11+
"0xb5bd3599ec7f4ae86afd84398f6f2d862deecce965e8ace2d8d8c8108d5076df",
12+
"0x05680e9030c147b413a489f7891273acc221d49bd061c433e5771bc170fc37ac"
13+
],
14+
"effects": {
15+
"status": {
16+
"status": "success"
17+
},
18+
"gasUsed": {
19+
"computationCost": "164000",
20+
"storageCost": "20725200",
21+
"storageRebate": "24761484",
22+
"nonRefundableStorageFee": "250116"
23+
},
24+
"gasObject": {
25+
"owner": {
26+
"AddressOwner": "0x1b4cd8b734f2465614678ca0450ce9c4f2ff4835c6a7545522892a1a8fb67991"
27+
}
28+
}
29+
},
30+
"events": [
31+
{
32+
"type": "0xecf47609d7da919ea98e7fd04f6e0648a0a79b337aaad373fa37aac8febf19c8::treasury::Burn<0xdba34672e30cb065b1f93e3ab55318768fd6fef66c15942c9f7cb846e2f900e7::usdc::USDC>",
33+
"parsedJson": {
34+
"amount": "2605390809"
35+
},
36+
"packageId": "0x2aa6c5d56376c371f88a6cc42e852824994993cb9bab8d3e6450cbe3cb32b94e"
37+
},
38+
{
39+
"type": "0xb5bd3599ec7f4ae86afd84398f6f2d862deecce965e8ace2d8d8c8108d5076df::bridge_with_fee::BridgeSubmittedWithFee",
40+
"parsedJson": {
41+
"amount_bridged": "2605390809",
42+
"dest_domain": 3
43+
},
44+
"packageId": "0xb5bd3599ec7f4ae86afd84398f6f2d862deecce965e8ace2d8d8c8108d5076df"
45+
},
46+
{
47+
"type": "0xb5bd3599ec7f4ae86afd84398f6f2d862deecce965e8ace2d8d8c8108d5076df::init_order::InitMctpLogged",
48+
"parsedJson": {
49+
"amount_in_initial": "2605390809",
50+
"coin_type": "dba34672e30cb065b1f93e3ab55318768fd6fef66c15942c9f7cb846e2f900e7::usdc::USDC"
51+
},
52+
"packageId": "0xb5bd3599ec7f4ae86afd84398f6f2d862deecce965e8ace2d8d8c8108d5076df"
53+
},
54+
{
55+
"type": "0x05680e9030c147b413a489f7891273acc221d49bd061c433e5771bc170fc37ac::referrer_logger::ReferrerSet",
56+
"parsedJson": {
57+
"fee_rate_ref": 50
58+
},
59+
"packageId": "0x05680e9030c147b413a489f7891273acc221d49bd061c433e5771bc170fc37ac"
60+
}
61+
],
62+
"balanceChanges": [
63+
{
64+
"owner": {
65+
"AddressOwner": "0x1b4cd8b734f2465614678ca0450ce9c4f2ff4835c6a7545522892a1a8fb67991"
66+
},
67+
"coinType": "0x2::sui::SUI",
68+
"amount": "3872284"
69+
},
70+
{
71+
"owner": {
72+
"AddressOwner": "0x1b4cd8b734f2465614678ca0450ce9c4f2ff4835c6a7545522892a1a8fb67991"
73+
},
74+
"coinType": "0xdba34672e30cb065b1f93e3ab55318768fd6fef66c15942c9f7cb846e2f900e7::usdc::USDC",
75+
"amount": "-2605390809"
76+
}
77+
],
78+
"timestampMs": "1780011954804"
79+
}

0 commit comments

Comments
 (0)