Merge branch 'crc20-index' into 'master'

Add CRC20 index

See merge request riftenlabs/riftenlabs-indexer!16
This commit is contained in:
Dagur Valberg Johannsson 2024-10-21 11:39:59 +00:00
commit d2b169d03a
9 changed files with 443 additions and 56 deletions

View file

@ -19,7 +19,10 @@ use riftenlabs_defi::chainutil::{compute_outpoint_hash, read_push_from_script, O
use rusqlite::Connection;
use serde::{Deserialize, Serialize, Serializer};
use crate::db::bcmr::{get_matching_autheaders, insert_authheader, AuthChainEntry};
use crate::{
db::bcmr::{get_matching_autheaders, insert_authheader, AuthChainEntry},
utiltoken::is_genesis_tx,
};
pub mod bcmrdownloader;
pub mod parsedbcmr;
@ -92,27 +95,6 @@ pub fn parse_bcmr(tx: &Transaction) -> Option<BCMR> {
parse_bcmr_from_opreturn(&bcmr_op_return.script_pubkey)
}
fn is_genesis_tx(tx: &Transaction) -> Option<TokenID> {
let potential_token_ids: HashSet<[u8; 32]> = tx
.input
.iter()
.filter(|i| i.previous_output.vout == 0)
.map(|i| i.previous_output.txid.into_inner())
.collect();
for o in &tx.output {
match &o.token {
Some(token) => {
if potential_token_ids.contains(token.id.as_inner()) {
return Some(token.id);
}
}
None => continue,
}
}
None
}
pub fn ttor_sorted(txs: Vec<Transaction>) -> Vec<Transaction> {
let txs = {
let mut queue: VecDeque<Transaction> = txs.into_iter().collect();
@ -325,16 +307,4 @@ mod tests {
// The hash value in this BCMR is 64 bytes. Not a valid sha256 hash.
assert!(result.is_none());
}
#[test]
fn test_is_genesis_tx() {
let tx_hex = "01000000032b70d6279de034fea76d4dd329c44a3e162b17a0cebc32eeeebbd14f5e8a97250100000064411b42a9dcb22c59ff0e800b3c63d6b8ac045bd18eb1effc8d2f23ec49ed0abdc316215a469a206d45153eac999d14eb8abc3d33b91c5ac4b61e7868d162d5d5184121021735db91ac477c1007da74c8c117c383eebc664d21f12871ca6e69401f8072f8feffffff2b70d6279de034fea76d4dd329c44a3e162b17a0cebc32eeeebbd14f5e8a972502000000644193e4d5bb533279fbcbebe15ba62f226e53e5c64a1624c755b255b13f0b1011e382182896026575370b5e30d03390d31faabf854a9f41c215b8a5bb57bae221df4121021735db91ac477c1007da74c8c117c383eebc664d21f12871ca6e69401f8072f8feffffff8d80b5567b6a3cac6e19a6934fd7544a02616fd6868a304c93bff6aed3eb12280100000064418b576b5defc8942182d01654cc2a0706b3e86e9031f7a13a694ffd830944ca00ecf5ad3271a242993580cbe78c13a41e35b06dfd68e72c4242dd7ff9e7e039a6412103b666cf8bc6ef2e3c3f100fc883013040f01f7085d75e5ab0811ae00b6a0a6d24feffffff03200300000000000040ef8d80b5567b6a3cac6e19a6934fd7544a02616fd6868a304c93bff6aed3eb1228600401db010076a914bfe9ed4ea9c7830c2ee96c17a3bd5ff563c12f2b88ac200300000000000040ef8d80b5567b6a3cac6e19a6934fd7544a02616fd6868a304c93bff6aed3eb1228600402db010076a914bfe9ed4ea9c7830c2ee96c17a3bd5ff563c12f2b88ac9f117901000000001976a91402a67e8d884367e7a22d6cda96a6df21e56a2f3288ac851b0c00";
let not_genesis_tx: Transaction = deserialize(&hex::decode(&tx_hex).unwrap()).unwrap();
let tx_hex = "0200000001925bd92e424c0f0bc290a794f491abf19e61b6dcdec7e70747fcb54682fc9bb700000000644181707613f45069c8df981b2ef0cbd05158d5623602fac3ae2e13f69d05fa356c278f2f3a4e34f33a79e0f8375abd35051150ec024b3625807cea185528a5de9e412103b4680dffa1e34b3bfdb024fd0a8498eb24f59ba5ead34acfb74f9d8549db6f3a0000000003e8030000000000003eef925bd92e424c0f0bc290a794f491abf19e61b6dcdec7e70747fcb54682fc9bb710fdf40176a914bd4c90f2c64743fc0d3ea6a14973a5d628260b7388ac0000000000000000456a0442434d5220c705cc90a56ac7ef9a15ef90ebbc8ba7e60e4c622e5464d52d8baf7887949fcc1d736f636b2e6361756c64726f6e2e71756573742f62636d722e6a736f6ed9210000000000001976a914bd4c90f2c64743fc0d3ea6a14973a5d628260b7388ac00000000";
let genesis_tx: Transaction = deserialize(&hex::decode(&tx_hex).unwrap()).unwrap();
assert!(!is_genesis_tx(&not_genesis_tx).is_some());
assert!(is_genesis_tx(&genesis_tx).is_some());
}
}

173
src/crc20/crc20fetcher.rs Normal file
View file

@ -0,0 +1,173 @@
// Copyright (C) 2024 Riften Labs AS
//
// This software is licensed under the GNU Affero General Public License (AGPL), version 3.0 or later.
// A copy of the license can be found in the LICENSE file or at https://www.gnu.org/licenses/agpl-3.0.html
use std::{
sync::{atomic::AtomicBool, Arc, Mutex},
thread::{self, JoinHandle},
time::Duration,
};
use crate::db::crc20::{
bump_failed_attempts, get_not_indexed_tokens, update_to_crc20, update_to_not_crc20,
};
use crate::db::DBPool;
use anyhow::*;
use electrum_client_netagnostic::{Client, ElectrumApi, Param};
use log::{info, warn};
use rand::thread_rng;
use serde_json::Value;
use rand::seq::SliceRandom;
use std::result::Result::Ok;
pub struct CRC20Fetcher {
keep_running: Arc<AtomicBool>,
update_thread: Option<JoinHandle<()>>,
}
const LOOP_SLEEP_TIME: Duration = Duration::from_secs(10);
fn parse_crc20_info(json: &Value) -> Option<(String, String, i32)> {
let crc20 = json.get("crc20");
if crc20.is_none() || crc20 == Some(&Value::Null) {
return None;
}
let symbol = crc20
.and_then(|c| c.get("symbol"))
.and_then(|s| s.as_str())
.map(|s| s.to_string());
let name = crc20
.and_then(|c| c.get("name"))
.and_then(|n| n.as_str())
.map(|n| n.to_string());
let decimals = crc20
.and_then(|c| c.get("decimals"))
.and_then(|d| d.as_i64())
.map(|d| d as i32);
Some((
symbol.unwrap_or_default(),
name.unwrap_or_default(),
decimals.unwrap_or_default(),
))
}
impl CRC20Fetcher {
pub fn new() -> Self {
Self {
keep_running: Arc::new(AtomicBool::new(true)),
update_thread: None,
}
}
pub fn start(&mut self, db: DBPool, electrum: Arc<Mutex<Client>>) -> Result<()> {
let keep_running_cpy = self.keep_running.clone();
self.update_thread = Some(
thread::Builder::new()
.name("crc20_fetcher".to_string())
.spawn(move || loop {
if !keep_running_cpy.load(std::sync::atomic::Ordering::Relaxed) {
info!("Exiting crc20 fetcher thread");
return;
}
let mut queue = {
let db = match db.get() {
Ok(db) => db,
Err(e) => {
warn!("Failed to get crc20 db connection {}", e);
thread::sleep(LOOP_SLEEP_TIME);
continue;
}
};
match get_not_indexed_tokens(&db) {
Ok(tokens) => tokens,
Err(e) => {
warn!("Failed to get unindexed crc20 tokens {}", e);
thread::sleep(LOOP_SLEEP_TIME);
continue;
}
}
};
// in case electrum has issues with a token; with rng we'll eventually get all others
let mut rng = thread_rng();
queue.shuffle(&mut rng);
if queue.is_empty() {
thread::sleep(LOOP_SLEEP_TIME);
continue;
}
info!("crc20: {} tokens need fetching", queue.len());
for token in queue {
let genesis_info = match electrum
.lock()
.unwrap()
.raw_call("token.genesis.info", vec![Param::String(token.clone())])
{
Ok(i) => Some(i),
Err(e) => {
info!("Failed to fetch crc20 info for {}: {}", token, e);
None
}
};
let db = match db.get() {
Ok(db) => db,
Err(e) => {
warn!("Failed to get crc20 db connection {}", e);
break;
}
};
if genesis_info.is_none() {
if let Err(e) = bump_failed_attempts(&db, &token) {
warn!("Failed to bump failed attempts for {}: {}", token, e);
}
continue;
}
let res = if let Some((symbol, name, decimals)) =
parse_crc20_info(&genesis_info.unwrap())
{
update_to_crc20(&db, &token, &symbol, &name, decimals)
} else {
update_to_not_crc20(&db, &token)
};
if let Err(e) = res {
warn!("Failed to update crc20 for token {}: {}", token, e);
}
}
thread::sleep(LOOP_SLEEP_TIME)
})
.expect("failed to start bcmr download thread"),
);
Ok(())
}
}
impl Drop for CRC20Fetcher {
fn drop(&mut self) {
self.keep_running
.store(false, std::sync::atomic::Ordering::SeqCst);
if let Some(thread) = self.update_thread.take() {
// Wake the thread in case it is sleeping
thread.thread().unpark();
match thread.join() {
Ok(_) => info!("crc20 fetcher thread done"),
Err(e) => warn!("Failed to join crc20 fetcher thread: {:?}", e),
}
}
}
}

23
src/crc20/mod.rs Normal file
View file

@ -0,0 +1,23 @@
// Copyright (C) 2024 Riften Labs AS
//
// This software is licensed under the GNU Affero General Public License (AGPL), version 3.0 or later.
// A copy of the license can be found in the LICENSE file or at https://www.gnu.org/licenses/agpl-3.0.html
use anyhow::Result;
use bitcoincash::{TokenID, Transaction};
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
use rusqlite::Connection;
pub mod crc20fetcher;
use crate::{db::crc20::insert_crc20_candidate, utiltoken::is_genesis_tx};
pub fn index_crc20(conn: &Connection, txs: &Vec<Transaction>) -> Result<()> {
let token_genesis: Vec<TokenID> = txs.par_iter().filter_map(is_genesis_tx).collect();
for token_id in token_genesis {
insert_crc20_candidate(conn, &token_id)?
}
Ok(())
}

137
src/db/crc20/mod.rs Normal file
View file

@ -0,0 +1,137 @@
// Copyright (C) 2024 Riften Labs AS
//
// This software is licensed under the GNU Affero General Public License (AGPL), version 3.0 or later.
// A copy of the license can be found in the LICENSE file or at https://www.gnu.org/licenses/agpl-3.0.html
use anyhow::Result;
use bitcoin_hashes::hex::ToHex;
use bitcoincash::TokenID;
use rusqlite::{params, Connection};
const STATE_NOT_INDEXED: i32 = -1;
const STATE_NOT_CRC20: i32 = 0;
const STATE_IS_CRC20: i32 = 1;
const MAX_FAILED_ATTEMPTS: i32 = 20;
pub fn prepare_tables(conn: &Connection) {
conn.execute(
"CREATE TABLE crc20 (
token_id TEXT PRIMARY KEY,
symbol TEXT NOT NULL,
name TEXT NOT NULL,
decimals INT NOT NULL
)",
[],
)
.expect("failed to create crc20 table");
conn.execute(
"CREATE TABLE crc20_candidates (
token_id TEXT PRIMARY KEY,
is_crc20 INT NOT NULL,
failed_attempts INT NOT NULL
)",
[],
)
.expect("failed to create crc20 table");
}
pub fn insert_crc20_candidate(conn: &Connection, token_id: &TokenID) -> Result<()> {
conn.execute(
"INSERT OR IGNORE INTO crc20_candidates (token_id, is_crc20, failed_attempts)
VALUES (?1, ?2, ?3)",
params![token_id.to_hex(), STATE_NOT_INDEXED, 0],
)
.map_err(|e| {
anyhow::anyhow!(
"failed to insert crc20 candidate: token_id = {}. Original error: {:?}",
token_id.to_hex(),
e
)
})?;
Ok(())
}
pub fn get_not_indexed_tokens(conn: &Connection) -> Result<Vec<String>> {
let mut stmt = conn.prepare(
"SELECT token_id FROM crc20_candidates WHERE is_crc20 = ?1 AND failed_attempts <= ?2",
)?;
let token_ids = stmt
.query_map(params![STATE_NOT_INDEXED, MAX_FAILED_ATTEMPTS], |row| {
row.get(0)
})?
.collect::<Result<Vec<String>, _>>()?;
Ok(token_ids)
}
pub fn update_to_not_crc20(conn: &Connection, token: &str) -> Result<()> {
conn.execute(
"UPDATE crc20_candidates SET is_crc20 = ?1 WHERE token_id = ?2",
params![STATE_NOT_CRC20, token],
)
.map_err(|e| {
anyhow::anyhow!(
"failed to update token_id = {} to STATE_NOT_CRC20. Original error: {:?}",
token,
e
)
})?;
Ok(())
}
pub fn update_to_crc20(
conn: &Connection,
token_id: &str,
symbol: &str,
name: &str,
decimals: i32,
) -> Result<()> {
// Update is_crc20 in crc20_candidates table
conn.execute(
"UPDATE crc20_candidates SET is_crc20 = ?1 WHERE token_id = ?2",
params![STATE_IS_CRC20, token_id],
)
.map_err(|e| {
anyhow::anyhow!(
"failed to update token_id = {} to STATE_IS_CRC20 in crc20_candidates. Original error: {:?}",
token_id,
e
)
})?;
// Insert or replace the token in the crc20 table
conn.execute(
"INSERT OR REPLACE INTO crc20 (token_id, symbol, name, decimals)
VALUES (?1, ?2, ?3, ?4)",
params![token_id, symbol, name, decimals],
)
.map_err(|e| {
anyhow::anyhow!(
"failed to insert or replace token_id = {} in crc20. Original error: {:?}",
token_id,
e
)
})?;
Ok(())
}
pub fn bump_failed_attempts(conn: &Connection, token_id: &str) -> Result<()> {
conn.execute(
"UPDATE crc20_candidates SET failed_attempts = failed_attempts + 1 WHERE token_id = ?1",
params![token_id],
)
.map_err(|e| {
anyhow::anyhow!(
"Failed to bump failed_attempts for token_id = {}. Original error: {:?}",
token_id,
e
)
})?;
Ok(())
}

View file

@ -7,6 +7,7 @@ use std::sync::Arc;
pub mod bcmr;
pub mod cauldron;
pub mod crc20;
pub type DBPool = Arc<r2d2::Pool<r2d2_sqlite::SqliteConnectionManager>>;
@ -20,4 +21,9 @@ pub struct DB {
pub bcmr_r: DBPool,
// readwrite
pub bcmr_w: DBPool,
// rewadwrite
pub crc20_w: DBPool,
// readonly
#[allow(dead_code)]
pub crc20_r: DBPool,
}

View file

@ -11,7 +11,7 @@ use std::{
use bitcoin_hashes::hex::{FromHex, ToHex};
use bitcoincash::{consensus::deserialize, Block, BlockHash, Transaction, Txid};
use electrum_client_netagnostic::{Client, Param, ElectrumApi};
use electrum_client_netagnostic::{Client, ElectrumApi, Param};
use log::{debug, info, warn};
use rayon::prelude::*;
use riftenlabs_defi::cauldron::{parse_cauldron, ParsedContract};
@ -19,6 +19,7 @@ use riftenlabs_defi::cauldron::{parse_cauldron, ParsedContract};
use crate::{
bcmr::index_bcmr,
chain::{get_new_headers, Chain, StoreBlockUndoer},
crc20::index_crc20,
db::{
self,
cauldron::{
@ -245,6 +246,14 @@ pub fn index_blocks(
.context("update pool history")?;
config_set(&db_tx, KEY_LAST_INDEXED, &blockhash.to_hex());
{
// crc20
let mut conn = db.crc20_w.get().context("failed to get crc20 db")?;
let tx = conn.transaction()?;
index_crc20(&tx, &block.txdata)?;
tx.commit()?;
}
let autheader_updates = if bcmr_enabled {
let mut conn = db.bcmr_w.get().context("failed to get bcmr db")?;
let bcmr_db_tx = conn.transaction()?;

View file

@ -6,6 +6,7 @@
use anyhow::Result;
use bcmr::wellknowndowloader::WellKnownDownloader;
use bitcoincash::{consensus::deserialize, Block, BlockHash};
use crc20::crc20fetcher::CRC20Fetcher;
use db::{DBPool, DB};
use electrum::electrum_get_tip;
use electrum_client_netagnostic::{Client, ElectrumApi, Param};
@ -45,11 +46,13 @@ const KEY_LAST_INDEXED: &str = "last_indexed";
mod bcmr;
mod cashaddr;
mod chain;
mod crc20;
mod db;
mod electrum;
mod index;
mod rpc;
mod timeutil;
mod utiltoken;
fn set_panic_hook() {
panic::set_hook(Box::new(|panic_info| {
@ -64,6 +67,8 @@ fn set_panic_hook() {
}
} else if let Some(message) = panic_info.payload().downcast_ref::<&str>() {
error!("Panic occurred: {}", message);
} else if let Some(message) = panic_info.payload().downcast_ref::<String>() {
error!("Panic occurred: {}", message);
} else {
error!("Panic info: {:?}", panic_info);
}
@ -74,7 +79,7 @@ fn set_panic_hook() {
}));
}
fn start_program() -> Result<(DB, BCMRDownloader, WellKnownDownloader)> {
fn start_program() -> Result<(DB, BCMRDownloader, WellKnownDownloader, CRC20Fetcher)> {
let create_db_pool = |db_path| -> (bool, DBPool, DBPool) {
let db_exists = Path::new(db_path).exists();
@ -116,6 +121,15 @@ fn start_program() -> Result<(DB, BCMRDownloader, WellKnownDownloader)> {
);
}
let (db_exists, crc20_db_write, crc20_db_read) = create_db_pool("crc20.db");
if !db_exists {
db::crc20::prepare_tables(
&crc20_db_write
.get()
.expect("failed to create sqlite crc20 connection"),
);
}
let client = Arc::new(Mutex::new(
Client::new("tcp://rostrum.cauldron.quest:50001").unwrap(),
));
@ -139,14 +153,17 @@ fn start_program() -> Result<(DB, BCMRDownloader, WellKnownDownloader)> {
cauldron_r: cauldron_db_read,
bcmr_w: bcmr_db_write,
bcmr_r: bcmr_db_read,
crc20_w: crc20_db_write,
crc20_r: crc20_db_read,
};
let db_cpy = db.clone();
// let client_cpy = client.clone();
let mut crc20fetcher = CRC20Fetcher::new();
crc20fetcher.start(db.crc20_w.clone(), client.clone())?;
thread::spawn(move || {
let db = db_cpy;
//let client = client_cpy;
let mut tip: BlockHash = loop {
break match index_blocks(chain.clone(), db.clone(), client.clone(), true) {
@ -192,7 +209,7 @@ fn start_program() -> Result<(DB, BCMRDownloader, WellKnownDownloader)> {
let mut wellknowndownloader = WellKnownDownloader::new(db.bcmr_w.clone());
wellknowndownloader.start()?;
Ok((db, bcmrdownloader, wellknowndownloader))
Ok((db, bcmrdownloader, wellknowndownloader, crc20fetcher))
}
#[launch]
@ -204,7 +221,7 @@ fn launch() -> _ {
set_panic_hook();
let (dbpool, bcmrdownloader, wellknowndownloader) = match start_program() {
let (dbpool, bcmrdownloader, wellknowndownloader, crc20fetcher) = match start_program() {
Ok(db) => db,
Err(e) => {
let backtrace = Backtrace::capture();
@ -239,6 +256,7 @@ fn launch() -> _ {
// give rocket ownership of downloader to ensure thread isn't dropped
.manage(bcmrdownloader)
.manage(wellknowndownloader)
.manage(crc20fetcher)
.mount(
"/cauldron/",
routes![

View file

@ -191,33 +191,33 @@ fn price_at_or_before(
) -> Result<(i64, f64)> {
let sql = "
WITH max_timestamps AS (
SELECT
SELECT
phe.pool,
MAX(COALESCE(phe.first_seen_timestamp, phe.mtp_timestamp)) AS max_effective_timestamp
FROM
FROM
pool_history_entry phe
JOIN
JOIN
utxo_funding uf ON phe.utxo = uf.new_utxo_hash
WHERE
WHERE
COALESCE(phe.first_seen_timestamp, phe.mtp_timestamp) <= ?
AND uf.token_id = ?
GROUP BY
GROUP BY
phe.pool
)
SELECT
uf.token_amount,
uf.sats,
COALESCE(phe.first_seen_timestamp, phe.mtp_timestamp) AS effective_timestamp
FROM
FROM
pool_history_entry phe
JOIN
JOIN
utxo_funding uf ON phe.utxo = uf.new_utxo_hash
JOIN
JOIN
pool p ON p.creation_utxo = phe.pool
JOIN
JOIN
max_timestamps mt ON phe.pool = mt.pool
AND COALESCE(phe.first_seen_timestamp, phe.mtp_timestamp) = mt.max_effective_timestamp
WHERE
WHERE
p.withdrawn_in_utxo IS NULL
";
@ -503,6 +503,8 @@ mod tests {
cauldron_r: Arc::new(pool.clone()),
bcmr_w: Arc::new(pool.clone()),
bcmr_r: Arc::new(pool.clone()),
crc20_w: Arc::new(pool.clone()),
crc20_r: Arc::new(pool.clone()),
}
}
#[test]
@ -760,8 +762,8 @@ mod tests {
.execute(
"INSERT INTO utxo_funding (new_utxo_hash, txid, sats, token_amount, token_id) VALUES (?, ?, ?, ?, ?)",
params![
"mock_utxo_hash_high_tokens",
"test_txid_high_tokens",
"mock_utxo_hash_high_tokens",
"test_txid_high_tokens",
1_i64, // Low sats
9999999999999999_i64, // Very high token amount
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
@ -773,10 +775,10 @@ mod tests {
.execute(
"INSERT INTO pool_history_entry (pool, utxo, txid, tx_pos, mtp_timestamp, first_seen_timestamp) VALUES (?, ?, ?, ?, ?, ?)",
params![
"pool_high_tokens",
"mock_utxo_hash_high_tokens",
"test_txid_high_tokens",
"tx_pos_high_tokens",
"pool_high_tokens",
"mock_utxo_hash_high_tokens",
"test_txid_high_tokens",
"tx_pos_high_tokens",
1727963300,
1727963300
],

49
src/utiltoken.rs Normal file
View file

@ -0,0 +1,49 @@
// Copyright (C) 2024 Riften Labs AS
//
// This software is licensed under the GNU Affero General Public License (AGPL), version 3.0 or later.
// A copy of the license can be found in the LICENSE file or at https://www.gnu.org/licenses/agpl-3.0.html
use std::collections::HashSet;
use bitcoin_hashes::Hash;
use bitcoincash::{TokenID, Transaction};
pub fn is_genesis_tx(tx: &Transaction) -> Option<TokenID> {
let potential_token_ids: HashSet<[u8; 32]> = tx
.input
.iter()
.filter(|i| i.previous_output.vout == 0)
.map(|i| i.previous_output.txid.into_inner())
.collect();
for o in &tx.output {
match &o.token {
Some(token) => {
if potential_token_ids.contains(token.id.as_inner()) {
return Some(token.id);
}
}
None => continue,
}
}
None
}
#[cfg(test)]
mod tests {
use bitcoincash::consensus::deserialize;
use super::*;
#[test]
fn test_is_genesis_tx() {
let tx_hex = "01000000032b70d6279de034fea76d4dd329c44a3e162b17a0cebc32eeeebbd14f5e8a97250100000064411b42a9dcb22c59ff0e800b3c63d6b8ac045bd18eb1effc8d2f23ec49ed0abdc316215a469a206d45153eac999d14eb8abc3d33b91c5ac4b61e7868d162d5d5184121021735db91ac477c1007da74c8c117c383eebc664d21f12871ca6e69401f8072f8feffffff2b70d6279de034fea76d4dd329c44a3e162b17a0cebc32eeeebbd14f5e8a972502000000644193e4d5bb533279fbcbebe15ba62f226e53e5c64a1624c755b255b13f0b1011e382182896026575370b5e30d03390d31faabf854a9f41c215b8a5bb57bae221df4121021735db91ac477c1007da74c8c117c383eebc664d21f12871ca6e69401f8072f8feffffff8d80b5567b6a3cac6e19a6934fd7544a02616fd6868a304c93bff6aed3eb12280100000064418b576b5defc8942182d01654cc2a0706b3e86e9031f7a13a694ffd830944ca00ecf5ad3271a242993580cbe78c13a41e35b06dfd68e72c4242dd7ff9e7e039a6412103b666cf8bc6ef2e3c3f100fc883013040f01f7085d75e5ab0811ae00b6a0a6d24feffffff03200300000000000040ef8d80b5567b6a3cac6e19a6934fd7544a02616fd6868a304c93bff6aed3eb1228600401db010076a914bfe9ed4ea9c7830c2ee96c17a3bd5ff563c12f2b88ac200300000000000040ef8d80b5567b6a3cac6e19a6934fd7544a02616fd6868a304c93bff6aed3eb1228600402db010076a914bfe9ed4ea9c7830c2ee96c17a3bd5ff563c12f2b88ac9f117901000000001976a91402a67e8d884367e7a22d6cda96a6df21e56a2f3288ac851b0c00";
let not_genesis_tx: Transaction = deserialize(&hex::decode(&tx_hex).unwrap()).unwrap();
let tx_hex = "0200000001925bd92e424c0f0bc290a794f491abf19e61b6dcdec7e70747fcb54682fc9bb700000000644181707613f45069c8df981b2ef0cbd05158d5623602fac3ae2e13f69d05fa356c278f2f3a4e34f33a79e0f8375abd35051150ec024b3625807cea185528a5de9e412103b4680dffa1e34b3bfdb024fd0a8498eb24f59ba5ead34acfb74f9d8549db6f3a0000000003e8030000000000003eef925bd92e424c0f0bc290a794f491abf19e61b6dcdec7e70747fcb54682fc9bb710fdf40176a914bd4c90f2c64743fc0d3ea6a14973a5d628260b7388ac0000000000000000456a0442434d5220c705cc90a56ac7ef9a15ef90ebbc8ba7e60e4c622e5464d52d8baf7887949fcc1d736f636b2e6361756c64726f6e2e71756573742f62636d722e6a736f6ed9210000000000001976a914bd4c90f2c64743fc0d3ea6a14973a5d628260b7388ac00000000";
let genesis_tx: Transaction = deserialize(&hex::decode(&tx_hex).unwrap()).unwrap();
assert!(!is_genesis_tx(&not_genesis_tx).is_some());
assert!(is_genesis_tx(&genesis_tx).is_some());
}
}