Skip to content

Commit 4f3587b

Browse files
committed
Add Chainflip swap mapping & Solana tx parsing
Add end-to-end support for Chainflip swap result mapping and improve Solana transaction parsing. Key changes: - Implement Transaction::with_swap_state and use it when publishing swap updates in the daemon. - Enhance gem_solana models: record meta.err, add Instruction struct and instructions on TransactionMessage, add get_balance_change utility and unit tests, and introduce SYSTEM_PROGRAMS constant and extra program IDs. - Update transaction_mapper to only accept single-signature transactions, mark failed transactions when meta.err is present, compute transfer values via get_balance_change, and detect smart-contract calls (skipping known system programs). Add test and testdata for a Chainflip vault swap. - Implement Chainflip mapping in swapper: new SwapTxResponse parsing, map_swap_result to produce SwapResult with TransactionSwapMetadata, add test fixtures for various swap scenarios, and wire map_swap_result into the provider. Also expose vault addresses for deposit lookups. - Adjust Mayan proxy mapping logic and tests to treat non-InProgress statuses as final and assert expected metadata. These changes enable accurate extraction of value/contract info from Solana transactions and populate swap metadata from Chainflip responses for downstream processing and tests.
1 parent f77ef89 commit 4f3587b

15 files changed

Lines changed: 540 additions & 32 deletions

File tree

apps/daemon/src/worker/transactions/in_transit_updater.rs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -119,10 +119,7 @@ impl InTransitUpdater {
119119
}
120120
self.database.transactions()?.update_transaction(chain.as_ref(), &row.hash, updates)?;
121121

122-
let mut transaction = row.as_primitive(row.get_addresses());
123-
transaction.state = state.clone().into();
124-
transaction.transaction_type = TransactionType::Swap;
125-
transaction.metadata = metadata;
122+
let transaction = row.as_primitive(row.get_addresses()).with_swap_state(state.clone().into(), metadata.clone());
126123
self.stream_producer
127124
.publish_transactions(TransactionsPayload::new(chain, vec![0], vec![transaction]))
128125
.await?;

crates/gem_solana/src/lib.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,30 @@ pub const SYSTEM_PROGRAM_ID: &str = "11111111111111111111111111111111";
3737
pub const COMPUTE_BUDGET_PROGRAM_ID: &str = "ComputeBudget111111111111111111111111111111";
3838
pub const COMPUTE_UNIT_LIMIT_DISCRIMINANT: u8 = 2;
3939
pub const COMPUTE_UNIT_PRICE_DISCRIMINANT: u8 = 3;
40+
pub const VOTE_PROGRAM_ID: &str = "Vote111111111111111111111111111111111111111";
41+
pub const STAKE_PROGRAM_ID: &str = "Stake11111111111111111111111111111111111111";
42+
pub const SYSVAR_CLOCK_ID: &str = "SysvarC1ock11111111111111111111111111111111";
43+
pub const SYSVAR_RENT_ID: &str = "SysvarRent111111111111111111111111111111111";
44+
pub const SYSVAR_INSTRUCTIONS_ID: &str = "Sysvar1nstructions1111111111111111111111111";
45+
pub const BPF_LOADER_PROGRAM_ID: &str = "BPFLoaderUpgradeab1e11111111111111111111111";
46+
pub const MEMO_PROGRAM_ID: &str = "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr";
47+
pub const JITO_TIP_PROGRAM_ID: &str = "9H6tua7jkLhdm3w8BvgpTn5LZNU7g4ZynDmCiNN3q6Rp";
4048
pub const JUPITER_PROGRAM_ID: &str = "JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4";
49+
pub const SYSTEM_PROGRAMS: &[&str] = &[
50+
SYSTEM_PROGRAM_ID,
51+
COMPUTE_BUDGET_PROGRAM_ID,
52+
TOKEN_PROGRAM,
53+
TOKEN_PROGRAM_2022,
54+
ASSOCIATED_TOKEN_ACCOUNT_PROGRAM,
55+
VOTE_PROGRAM_ID,
56+
STAKE_PROGRAM_ID,
57+
SYSVAR_CLOCK_ID,
58+
SYSVAR_RENT_ID,
59+
SYSVAR_INSTRUCTIONS_ID,
60+
BPF_LOADER_PROGRAM_ID,
61+
MEMO_PROGRAM_ID,
62+
JITO_TIP_PROGRAM_ID,
63+
];
4164
pub const COMMITMENT_CONFIRMED: &str = "confirmed";
4265

4366
use primitives::{AssetId, SolanaTokenProgramId};

crates/gem_solana/src/models/transaction.rs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ pub struct SolanaTransactionError {}
2323
#[derive(Debug, Deserialize, Clone)]
2424
#[serde(rename_all = "camelCase")]
2525
pub struct Meta {
26+
pub err: Option<serde_json::Value>,
2627
pub fee: u64,
2728
pub pre_balances: Vec<u64>,
2829
pub post_balances: Vec<u64>,
@@ -110,10 +111,18 @@ pub struct Signature {
110111
pub signature: String,
111112
}
112113

114+
#[derive(Debug, Deserialize, Serialize, Clone)]
115+
#[serde(rename_all = "camelCase")]
116+
pub struct Instruction {
117+
pub program_id_index: usize,
118+
}
119+
113120
#[derive(Debug, Deserialize, Serialize, Clone)]
114121
#[serde(rename_all = "camelCase")]
115122
pub struct TransactionMessage {
116123
pub account_keys: Vec<String>,
124+
#[serde(default)]
125+
pub instructions: Vec<Instruction>,
117126
}
118127

119128
#[derive(Debug, Deserialize, Clone)]
@@ -128,6 +137,18 @@ impl BlockTransaction {
128137
BigUint::from(self.meta.fee)
129138
}
130139

140+
pub fn get_balance_change(&self, address: &str) -> u64 {
141+
let index = self.transaction.message.account_keys.iter().position(|k| k == address);
142+
match index {
143+
Some(i) => {
144+
let pre = self.meta.pre_balances.get(i).copied().unwrap_or(0);
145+
let post = self.meta.post_balances.get(i).copied().unwrap_or(0);
146+
pre.saturating_sub(post).saturating_sub(self.meta.fee)
147+
}
148+
None => 0,
149+
}
150+
}
151+
131152
pub fn get_balance_changes_by_owner(&self, owner: &str) -> TokenBalanceChange {
132153
// Find all account indices that belong to the owner
133154
let account_indices: Vec<usize> = self
@@ -176,3 +197,52 @@ pub struct SingleTransaction {
176197
pub meta: Meta,
177198
pub transaction: Transaction,
178199
}
200+
201+
#[cfg(test)]
202+
mod tests {
203+
use super::*;
204+
205+
fn block_transaction(fee: u64, keys: Vec<&str>, pre: Vec<u64>, post: Vec<u64>) -> BlockTransaction {
206+
BlockTransaction {
207+
meta: Meta {
208+
err: None,
209+
fee,
210+
pre_balances: pre,
211+
post_balances: post,
212+
pre_token_balances: vec![],
213+
post_token_balances: vec![],
214+
},
215+
transaction: Transaction {
216+
message: TransactionMessage {
217+
account_keys: keys.into_iter().map(String::from).collect(),
218+
instructions: vec![],
219+
},
220+
signatures: vec![],
221+
},
222+
}
223+
}
224+
225+
#[test]
226+
fn test_balance_change() {
227+
let tx = block_transaction(5000, vec!["sender", "recipient"], vec![100_000, 0], vec![85_000, 10_000]);
228+
assert_eq!(tx.get_balance_change("sender"), 10_000);
229+
}
230+
231+
#[test]
232+
fn test_balance_change_no_change() {
233+
let tx = block_transaction(5000, vec!["sender"], vec![100_000], vec![95_000]);
234+
assert_eq!(tx.get_balance_change("sender"), 0);
235+
}
236+
237+
#[test]
238+
fn test_balance_change_received() {
239+
let tx = block_transaction(5000, vec!["sender"], vec![100_000], vec![200_000]);
240+
assert_eq!(tx.get_balance_change("sender"), 0);
241+
}
242+
243+
#[test]
244+
fn test_balance_change_unknown_address() {
245+
let tx = block_transaction(5000, vec!["sender"], vec![100_000], vec![85_000]);
246+
assert_eq!(tx.get_balance_change("unknown"), 0);
247+
}
248+
}

crates/gem_solana/src/provider/transaction_mapper.rs

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use chrono::DateTime;
22
use num_bigint::Sign;
33

44
use crate::{
5-
COMPUTE_BUDGET_PROGRAM_ID, JUPITER_PROGRAM_ID, SYSTEM_PROGRAM_ID, TOKEN_PROGRAM,
5+
COMPUTE_BUDGET_PROGRAM_ID, JUPITER_PROGRAM_ID, SYSTEM_PROGRAMS, SYSTEM_PROGRAM_ID, TOKEN_PROGRAM,
66
models::{BlockTransaction, BlockTransactions, Signature},
77
};
88
use primitives::{AssetId, Chain, SwapProvider, Transaction, TransactionState, TransactionSwapMetadata, TransactionType};
@@ -26,28 +26,27 @@ pub fn map_signatures_transactions(transactions: Vec<BlockTransaction>, signatur
2626
}
2727

2828
pub fn map_transaction(transaction: &BlockTransaction, block_time: i64) -> Option<primitives::Transaction> {
29+
// only accept single signature transactions
30+
if transaction.transaction.signatures.len() != 1 {
31+
return None;
32+
}
33+
2934
let chain = CHAIN;
3035
let account_keys = transaction.transaction.message.account_keys.clone();
31-
let signatures = transaction.transaction.signatures.clone();
3236
let hash = transaction.transaction.signatures.first()?.to_string();
3337
let fee = transaction.meta.fee;
34-
let state = TransactionState::Confirmed;
38+
let state = if transaction.meta.err.is_some() { TransactionState::Failed } else { TransactionState::Confirmed };
3539
let fee_asset_id = chain.as_asset_id();
3640
let created_at = DateTime::from_timestamp(block_time, 0)?;
3741

38-
// only accept single signature transactions
39-
if signatures.len() != 1 {
40-
return None;
41-
}
42-
4342
// system transfer
4443
if (account_keys.len() == 3) && account_keys.last()? == SYSTEM_PROGRAM_ID
4544
|| (account_keys.len() == 4 && account_keys.last()? == SYSTEM_PROGRAM_ID && account_keys.contains(&COMPUTE_BUDGET_PROGRAM_ID.to_string()))
4645
{
4746
let from = account_keys.first()?.clone();
4847
let to = account_keys[1].clone();
4948

50-
let value = transaction.meta.pre_balances[0] - transaction.meta.post_balances[0] - fee;
49+
let value = transaction.get_balance_change(&from);
5150

5251
let transaction = Transaction::new(
5352
hash,
@@ -168,7 +167,32 @@ pub fn map_transaction(transaction: &BlockTransaction, block_time: i64) -> Optio
168167
return Some(transaction);
169168
}
170169

171-
None
170+
// smart contract call
171+
let contract = transaction
172+
.transaction
173+
.message
174+
.instructions
175+
.iter()
176+
.map(|ix| &account_keys[ix.program_id_index])
177+
.find(|key| !SYSTEM_PROGRAMS.contains(&key.as_str()))?;
178+
let sender = account_keys.first()?.clone();
179+
let value = transaction.get_balance_change(&sender);
180+
181+
Some(Transaction::new(
182+
hash,
183+
chain.as_asset_id(),
184+
sender.clone(),
185+
sender,
186+
Some(contract.to_string()),
187+
TransactionType::SmartContractCall,
188+
state,
189+
fee.to_string(),
190+
fee_asset_id,
191+
value.to_string(),
192+
None,
193+
None,
194+
created_at,
195+
))
172196
}
173197

174198
#[cfg(test)]
@@ -323,6 +347,17 @@ mod tests {
323347
assert_eq!(transaction.slot, 361169359);
324348
}
325349

350+
#[test]
351+
fn test_transaction_chainflip_vault_swap() {
352+
let result: JsonRpcResult<BlockTransaction> = serde_json::from_str(include_str!("../../testdata/chainflip_vault_swap.json")).unwrap();
353+
let transaction = map_transaction(&result.result, 1772283531).unwrap();
354+
355+
assert_eq!(transaction.transaction_type, TransactionType::SmartContractCall);
356+
assert_eq!(transaction.from, "CabroWmzUzcqqGvprUoC7RnJznuwX6qf5W1tSSaomri7");
357+
assert_eq!(transaction.contract, Some("J88B7gmadHzTNGiy54c9Ms8BsEXNdB2fntFyhKpk3qoT".to_string()));
358+
assert_eq!(transaction.value, "152686560");
359+
}
360+
326361
#[test]
327362
fn test_transaction_broadcast_error() {
328363
let error_response: JsonRpcErrorResponse = serde_json::from_str(include_str!("../../testdata/transaction_broadcast_swap_error.json")).unwrap();

crates/gem_solana/src/rpc/client.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::COMMITMENT_CONFIRMED;
1+
use crate::{COMMITMENT_CONFIRMED, STAKE_PROGRAM_ID};
22
use crate::models::{
33
EpochInfo, InflationRate, ResultTokenInfo, Signature, TokenAccountInfo, ValueResult, VoteAccounts,
44
balances::SolanaBalance,
@@ -108,9 +108,8 @@ impl<C: Client + Clone> SolanaClient<C> {
108108
}
109109

110110
pub async fn get_staking_balance(&self, address: &str) -> Result<Vec<TokenAccountInfo>, JsonRpcError> {
111-
let stake_program_id = "Stake11111111111111111111111111111111111111";
112111
let params = serde_json::json!([
113-
stake_program_id,
112+
STAKE_PROGRAM_ID,
114113
{
115114
"encoding": "jsonParsed",
116115
"filters": [

0 commit comments

Comments
 (0)