381 lines
13 KiB
Rust
381 lines
13 KiB
Rust
use {
|
|
chrono::prelude::*,
|
|
pickledb::{error::Error, PickleDb, PickleDbDumpPolicy},
|
|
serde::{Deserialize, Serialize},
|
|
solana_sdk::{clock::Slot, pubkey::Pubkey, signature::Signature, transaction::Transaction},
|
|
solana_transaction_status::TransactionStatus,
|
|
std::{cmp::Ordering, fs, io, path::Path},
|
|
};
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
|
pub struct TransactionInfo {
|
|
pub recipient: Pubkey,
|
|
pub amount: u64,
|
|
pub new_stake_account_address: Option<Pubkey>,
|
|
pub finalized_date: Option<DateTime<Utc>>,
|
|
pub transaction: Transaction,
|
|
pub last_valid_block_height: Slot,
|
|
pub lockup_date: Option<DateTime<Utc>>,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Default, PartialEq, Eq)]
|
|
struct SignedTransactionInfo {
|
|
recipient: String,
|
|
amount: u64,
|
|
#[serde(skip_serializing_if = "String::is_empty", default)]
|
|
new_stake_account_address: String,
|
|
finalized_date: Option<DateTime<Utc>>,
|
|
signature: String,
|
|
}
|
|
|
|
impl Default for TransactionInfo {
|
|
fn default() -> Self {
|
|
let transaction = Transaction {
|
|
signatures: vec![Signature::default()],
|
|
..Transaction::default()
|
|
};
|
|
Self {
|
|
recipient: Pubkey::default(),
|
|
amount: 0,
|
|
new_stake_account_address: None,
|
|
finalized_date: None,
|
|
transaction,
|
|
last_valid_block_height: 0,
|
|
lockup_date: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn open_db(path: &str, dry_run: bool) -> Result<PickleDb, Error> {
|
|
let policy = if dry_run {
|
|
PickleDbDumpPolicy::NeverDump
|
|
} else {
|
|
PickleDbDumpPolicy::DumpUponRequest
|
|
};
|
|
let path = Path::new(path);
|
|
let db = if path.exists() {
|
|
PickleDb::load_yaml(path, policy)?
|
|
} else {
|
|
if let Some(parent) = path.parent() {
|
|
fs::create_dir_all(parent).unwrap();
|
|
}
|
|
PickleDb::new_yaml(path, policy)
|
|
};
|
|
Ok(db)
|
|
}
|
|
|
|
pub fn compare_transaction_infos(a: &TransactionInfo, b: &TransactionInfo) -> Ordering {
|
|
let ordering = match (a.finalized_date, b.finalized_date) {
|
|
(Some(a), Some(b)) => a.cmp(&b),
|
|
(None, Some(_)) => Ordering::Greater,
|
|
(Some(_), None) => Ordering::Less, // Future finalized date will be greater
|
|
_ => Ordering::Equal,
|
|
};
|
|
if ordering == Ordering::Equal {
|
|
return a.recipient.to_string().cmp(&b.recipient.to_string());
|
|
}
|
|
ordering
|
|
}
|
|
|
|
pub fn write_transaction_log<P: AsRef<Path>>(db: &PickleDb, path: &P) -> Result<(), io::Error> {
|
|
let mut wtr = csv::WriterBuilder::new().from_path(path).unwrap();
|
|
let mut transaction_infos = read_transaction_infos(db);
|
|
transaction_infos.sort_by(compare_transaction_infos);
|
|
for info in transaction_infos {
|
|
let signed_info = SignedTransactionInfo {
|
|
recipient: info.recipient.to_string(),
|
|
amount: info.amount,
|
|
new_stake_account_address: info
|
|
.new_stake_account_address
|
|
.map(|x| x.to_string())
|
|
.unwrap_or_else(|| "".to_string()),
|
|
finalized_date: info.finalized_date,
|
|
signature: info.transaction.signatures[0].to_string(),
|
|
};
|
|
wtr.serialize(&signed_info)?;
|
|
}
|
|
wtr.flush()
|
|
}
|
|
|
|
pub fn read_transaction_infos(db: &PickleDb) -> Vec<TransactionInfo> {
|
|
db.iter()
|
|
.map(|kv| kv.get_value::<TransactionInfo>().unwrap())
|
|
.collect()
|
|
}
|
|
|
|
pub fn set_transaction_info(
|
|
db: &mut PickleDb,
|
|
recipient: &Pubkey,
|
|
amount: u64,
|
|
transaction: &Transaction,
|
|
new_stake_account_address: Option<&Pubkey>,
|
|
finalized: bool,
|
|
last_valid_block_height: u64,
|
|
lockup_date: Option<DateTime<Utc>>,
|
|
) -> Result<(), Error> {
|
|
let finalized_date = if finalized { Some(Utc::now()) } else { None };
|
|
let transaction_info = TransactionInfo {
|
|
recipient: *recipient,
|
|
amount,
|
|
new_stake_account_address: new_stake_account_address.cloned(),
|
|
finalized_date,
|
|
transaction: transaction.clone(),
|
|
last_valid_block_height,
|
|
lockup_date,
|
|
};
|
|
let signature = transaction.signatures[0];
|
|
db.set(&signature.to_string(), &transaction_info)?;
|
|
Ok(())
|
|
}
|
|
|
|
// Set the finalized bit in the database if the transaction is rooted.
|
|
// Remove the TransactionInfo from the database if the transaction failed.
|
|
// Return the number of confirmations on the transaction or None if either
|
|
// finalized or discarded.
|
|
pub fn update_finalized_transaction(
|
|
db: &mut PickleDb,
|
|
signature: &Signature,
|
|
opt_transaction_status: Option<TransactionStatus>,
|
|
last_valid_block_height: u64,
|
|
finalized_block_height: u64,
|
|
) -> Result<Option<usize>, Error> {
|
|
if opt_transaction_status.is_none() {
|
|
if finalized_block_height > last_valid_block_height {
|
|
eprintln!(
|
|
"Signature not found {signature} and blockhash expired. Transaction either dropped or the validator purged the transaction status."
|
|
);
|
|
eprintln!();
|
|
|
|
// Don't discard the transaction, because we are not certain the
|
|
// blockhash is expired. Instead, return None to signal that
|
|
// we don't need to wait for confirmations.
|
|
return Ok(None);
|
|
}
|
|
|
|
// Return zero to signal the transaction may still be in flight.
|
|
return Ok(Some(0));
|
|
}
|
|
let transaction_status = opt_transaction_status.unwrap();
|
|
|
|
if let Some(confirmations) = transaction_status.confirmations {
|
|
// The transaction was found but is not yet finalized.
|
|
return Ok(Some(confirmations));
|
|
}
|
|
|
|
if let Some(e) = &transaction_status.err {
|
|
// The transaction was finalized, but execution failed. Drop it.
|
|
eprintln!("Error in transaction with signature {signature}: {e}");
|
|
eprintln!("Discarding transaction record");
|
|
eprintln!();
|
|
db.rem(&signature.to_string())?;
|
|
return Ok(None);
|
|
}
|
|
|
|
// Transaction is rooted. Set the finalized date in the database.
|
|
let mut transaction_info = db.get::<TransactionInfo>(&signature.to_string()).unwrap();
|
|
transaction_info.finalized_date = Some(Utc::now());
|
|
db.set(&signature.to_string(), &transaction_info)?;
|
|
Ok(None)
|
|
}
|
|
|
|
use csv::{ReaderBuilder, Trim};
|
|
pub(crate) fn check_output_file(path: &str, db: &PickleDb) {
|
|
let mut rdr = ReaderBuilder::new()
|
|
.trim(Trim::All)
|
|
.from_path(path)
|
|
.unwrap();
|
|
let logged_infos: Vec<SignedTransactionInfo> =
|
|
rdr.deserialize().map(|entry| entry.unwrap()).collect();
|
|
|
|
let mut transaction_infos = read_transaction_infos(db);
|
|
transaction_infos.sort_by(compare_transaction_infos);
|
|
let transaction_infos: Vec<SignedTransactionInfo> = transaction_infos
|
|
.iter()
|
|
.map(|info| SignedTransactionInfo {
|
|
recipient: info.recipient.to_string(),
|
|
amount: info.amount,
|
|
new_stake_account_address: info
|
|
.new_stake_account_address
|
|
.map(|x| x.to_string())
|
|
.unwrap_or_default(),
|
|
finalized_date: info.finalized_date,
|
|
signature: info.transaction.signatures[0].to_string(),
|
|
})
|
|
.collect();
|
|
assert_eq!(logged_infos, transaction_infos);
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use {
|
|
super::*,
|
|
assert_matches::assert_matches,
|
|
csv::{ReaderBuilder, Trim},
|
|
solana_sdk::transaction::TransactionError,
|
|
solana_transaction_status::TransactionConfirmationStatus,
|
|
tempfile::NamedTempFile,
|
|
};
|
|
|
|
#[test]
|
|
fn test_sort_transaction_infos_finalized_first() {
|
|
let info0 = TransactionInfo {
|
|
finalized_date: Some(Utc.with_ymd_and_hms(2014, 7, 8, 9, 10, 11).unwrap()),
|
|
..TransactionInfo::default()
|
|
};
|
|
let info1 = TransactionInfo {
|
|
finalized_date: Some(Utc.with_ymd_and_hms(2014, 7, 8, 9, 10, 42).unwrap()),
|
|
..TransactionInfo::default()
|
|
};
|
|
let info2 = TransactionInfo::default();
|
|
let info3 = TransactionInfo {
|
|
recipient: solana_sdk::pubkey::new_rand(),
|
|
..TransactionInfo::default()
|
|
};
|
|
|
|
// Sorted first by date
|
|
assert_eq!(compare_transaction_infos(&info0, &info1), Ordering::Less);
|
|
|
|
// Finalized transactions should be before unfinalized ones
|
|
assert_eq!(compare_transaction_infos(&info1, &info2), Ordering::Less);
|
|
|
|
// Then sorted by recipient
|
|
assert_eq!(compare_transaction_infos(&info2, &info3), Ordering::Less);
|
|
}
|
|
|
|
#[test]
|
|
fn test_write_transaction_log() {
|
|
let mut db =
|
|
PickleDb::new_yaml(NamedTempFile::new().unwrap(), PickleDbDumpPolicy::NeverDump);
|
|
let signature = Signature::default();
|
|
let transaction_info = TransactionInfo::default();
|
|
db.set(&signature.to_string(), &transaction_info).unwrap();
|
|
|
|
let csv_file = NamedTempFile::new().unwrap();
|
|
write_transaction_log(&db, &csv_file).unwrap();
|
|
|
|
let mut rdr = ReaderBuilder::new().trim(Trim::All).from_reader(csv_file);
|
|
let signed_infos: Vec<SignedTransactionInfo> =
|
|
rdr.deserialize().map(|entry| entry.unwrap()).collect();
|
|
|
|
let signed_info = SignedTransactionInfo {
|
|
recipient: Pubkey::default().to_string(),
|
|
signature: Signature::default().to_string(),
|
|
..SignedTransactionInfo::default()
|
|
};
|
|
assert_eq!(signed_infos, vec![signed_info]);
|
|
}
|
|
|
|
#[test]
|
|
fn test_update_finalized_transaction_not_landed() {
|
|
// Keep waiting for a transaction that hasn't landed yet.
|
|
let mut db =
|
|
PickleDb::new_yaml(NamedTempFile::new().unwrap(), PickleDbDumpPolicy::NeverDump);
|
|
let signature = Signature::default();
|
|
let transaction_info = TransactionInfo::default();
|
|
db.set(&signature.to_string(), &transaction_info).unwrap();
|
|
assert_matches!(
|
|
update_finalized_transaction(&mut db, &signature, None, 0, 0),
|
|
Ok(Some(0))
|
|
);
|
|
|
|
// Unchanged
|
|
assert_eq!(
|
|
db.get::<TransactionInfo>(&signature.to_string()).unwrap(),
|
|
transaction_info
|
|
);
|
|
|
|
// Same as before, but now with an expired blockhash
|
|
assert_eq!(
|
|
update_finalized_transaction(&mut db, &signature, None, 0, 1).unwrap(),
|
|
None
|
|
);
|
|
|
|
// Ensure TransactionInfo has not been purged.
|
|
assert_eq!(
|
|
db.get::<TransactionInfo>(&signature.to_string()).unwrap(),
|
|
transaction_info
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_update_finalized_transaction_confirming() {
|
|
// Keep waiting for a transaction that is still being confirmed.
|
|
let mut db =
|
|
PickleDb::new_yaml(NamedTempFile::new().unwrap(), PickleDbDumpPolicy::NeverDump);
|
|
let signature = Signature::default();
|
|
let transaction_info = TransactionInfo::default();
|
|
db.set(&signature.to_string(), &transaction_info).unwrap();
|
|
let transaction_status = TransactionStatus {
|
|
slot: 0,
|
|
confirmations: Some(1),
|
|
err: None,
|
|
status: Ok(()),
|
|
confirmation_status: Some(TransactionConfirmationStatus::Confirmed),
|
|
};
|
|
assert_eq!(
|
|
update_finalized_transaction(&mut db, &signature, Some(transaction_status), 0, 0)
|
|
.unwrap(),
|
|
Some(1)
|
|
);
|
|
|
|
// Unchanged
|
|
assert_eq!(
|
|
db.get::<TransactionInfo>(&signature.to_string()).unwrap(),
|
|
transaction_info
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_update_finalized_transaction_failed() {
|
|
// Don't wait if the transaction failed to execute.
|
|
let mut db =
|
|
PickleDb::new_yaml(NamedTempFile::new().unwrap(), PickleDbDumpPolicy::NeverDump);
|
|
let signature = Signature::default();
|
|
let transaction_info = TransactionInfo::default();
|
|
db.set(&signature.to_string(), &transaction_info).unwrap();
|
|
let transaction_status = TransactionStatus {
|
|
slot: 0,
|
|
confirmations: None,
|
|
err: Some(TransactionError::AccountNotFound),
|
|
status: Ok(()),
|
|
confirmation_status: Some(TransactionConfirmationStatus::Finalized),
|
|
};
|
|
assert_eq!(
|
|
update_finalized_transaction(&mut db, &signature, Some(transaction_status), 0, 0)
|
|
.unwrap(),
|
|
None
|
|
);
|
|
|
|
// Ensure TransactionInfo has been purged.
|
|
assert_eq!(db.get::<TransactionInfo>(&signature.to_string()), None);
|
|
}
|
|
|
|
#[test]
|
|
fn test_update_finalized_transaction_finalized() {
|
|
// Don't wait once the transaction has been finalized.
|
|
let mut db =
|
|
PickleDb::new_yaml(NamedTempFile::new().unwrap(), PickleDbDumpPolicy::NeverDump);
|
|
let signature = Signature::default();
|
|
let transaction_info = TransactionInfo::default();
|
|
db.set(&signature.to_string(), &transaction_info).unwrap();
|
|
let transaction_status = TransactionStatus {
|
|
slot: 0,
|
|
confirmations: None,
|
|
err: None,
|
|
status: Ok(()),
|
|
confirmation_status: Some(TransactionConfirmationStatus::Finalized),
|
|
};
|
|
assert_eq!(
|
|
update_finalized_transaction(&mut db, &signature, Some(transaction_status), 0, 0)
|
|
.unwrap(),
|
|
None
|
|
);
|
|
|
|
assert!(db
|
|
.get::<TransactionInfo>(&signature.to_string())
|
|
.unwrap()
|
|
.finalized_date
|
|
.is_some());
|
|
}
|
|
}
|