2024-05-09 11:25:27 +02:00
|
|
|
// 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
|
|
|
|
|
|
2024-09-10 22:37:44 +02:00
|
|
|
use crate::bcmr::parsedbcmr::{FileMeta, ParsedBCMR, Token, Uris, SOURCE_ON_CHAIN};
|
2024-05-09 11:25:27 +02:00
|
|
|
use anyhow::*;
|
|
|
|
|
use bitcoin_hashes::hex::{FromHex, ToHex};
|
|
|
|
|
use bitcoincash::{BlockHash, TokenID, Txid};
|
|
|
|
|
use log::info;
|
|
|
|
|
use riftenlabs_defi::chainutil::OutPointHash;
|
|
|
|
|
use rusqlite::{params, Connection};
|
|
|
|
|
|
|
|
|
|
// Flag BCMR download as give up after this many attempts.
|
|
|
|
|
const MAX_DOWNLOAD_ATTEMPTS: usize = 100;
|
|
|
|
|
|
|
|
|
|
// BCMR auth chain entry, matches auth_chain_entry table
|
2024-09-04 11:11:43 +02:00
|
|
|
#[allow(dead_code)]
|
2024-05-09 11:25:27 +02:00
|
|
|
pub struct AuthChainEntry {
|
|
|
|
|
pub utxo: OutPointHash,
|
|
|
|
|
pub txid: Txid,
|
|
|
|
|
pub token_id: TokenID,
|
|
|
|
|
pub height: usize,
|
|
|
|
|
pub bcmr_data: Option<Vec<u8>>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn prepare_tables(conn: &Connection) {
|
|
|
|
|
conn.execute(
|
|
|
|
|
"CREATE TABLE auth_chain_entry (
|
|
|
|
|
utxo TEXT PRIMARY KEY,
|
|
|
|
|
blockhash TEXT NOT NULL,
|
|
|
|
|
txid TEXT NOT NULL,
|
|
|
|
|
token_id TEXT NOT NULL,
|
|
|
|
|
height INT NOT NULL,
|
|
|
|
|
bcmr_data TEXT
|
|
|
|
|
)",
|
|
|
|
|
[],
|
|
|
|
|
)
|
|
|
|
|
.expect("failed to create auth_chain_entry table");
|
|
|
|
|
|
|
|
|
|
conn.execute(
|
|
|
|
|
"CREATE TABLE bcmr_data (
|
|
|
|
|
utxo TEXT PRIMARY KEY,
|
|
|
|
|
symbol TEXT NOT NULL,
|
|
|
|
|
decimals INT NOT NULL,
|
|
|
|
|
name TEXT NOT NULL,
|
|
|
|
|
description TEXT NOT NULL,
|
|
|
|
|
icon TEXT NOT NULL,
|
|
|
|
|
web TEXT NOT NULL,
|
|
|
|
|
expected_hash TEXT NOT NULL,
|
|
|
|
|
actual_hash TEXT NOT NULL,
|
2024-10-25 14:21:02 +02:00
|
|
|
FOREIGN KEY (utxo) REFERENCES auth_chain_entry(utxo) ON DELETE CASCADE
|
2024-05-09 11:25:27 +02:00
|
|
|
)",
|
|
|
|
|
[],
|
|
|
|
|
)
|
|
|
|
|
.expect("failed to create bcmr_data table");
|
|
|
|
|
|
|
|
|
|
conn.execute(
|
|
|
|
|
"CREATE TABLE bcmr_failure (
|
|
|
|
|
utxo TEXT PRIMARY KEY,
|
|
|
|
|
last_attempt INT NOT NULL,
|
|
|
|
|
attempts INT NOT NULL,
|
|
|
|
|
error_message TEXT,
|
|
|
|
|
give_up BOOLEAN,
|
2024-10-25 14:21:02 +02:00
|
|
|
FOREIGN KEY (utxo) REFERENCES auth_chain_entry(utxo) ON DELETE CASCADE
|
2024-05-09 11:25:27 +02:00
|
|
|
)",
|
|
|
|
|
[],
|
|
|
|
|
)
|
|
|
|
|
.expect("failed to create bcmr_failure table");
|
2024-09-10 22:37:44 +02:00
|
|
|
|
|
|
|
|
conn.execute(
|
|
|
|
|
"
|
|
|
|
|
CREATE TABLE bcmr_well_known (
|
|
|
|
|
source TEXT NOT NULL,
|
|
|
|
|
symbol TEXT NOT NULL,
|
|
|
|
|
token_id TEXT NOT NULL,
|
|
|
|
|
decimals INT NOT NULL,
|
|
|
|
|
name TEXT NOT NULL,
|
|
|
|
|
description TEXT NOT NULL,
|
|
|
|
|
icon TEXT NOT NULL,
|
|
|
|
|
web TEXT NOT NULL,
|
|
|
|
|
PRIMARY KEY (source, token_id)
|
|
|
|
|
)
|
|
|
|
|
",
|
|
|
|
|
[],
|
|
|
|
|
)
|
|
|
|
|
.unwrap();
|
2025-09-29 14:08:22 +02:00
|
|
|
|
|
|
|
|
// Create index for efficient token BCMR lookups
|
|
|
|
|
conn.execute(
|
|
|
|
|
"CREATE INDEX IF NOT EXISTS idx_auth_chain_token_bcmr_height
|
|
|
|
|
ON auth_chain_entry(token_id, height DESC)
|
|
|
|
|
WHERE bcmr_data IS NOT NULL",
|
|
|
|
|
[],
|
|
|
|
|
)
|
|
|
|
|
.expect("failed to create index for token BCMR lookups");
|
2024-09-10 22:37:44 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn delete_entries_for_well_known(tx: &Connection, source: &str) -> Result<()> {
|
|
|
|
|
let mut stmt = tx.prepare("DELETE FROM bcmr_well_known WHERE source = ?")?;
|
|
|
|
|
stmt.execute(params![source])?;
|
|
|
|
|
Ok(())
|
2024-05-09 11:25:27 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn delete_entries_for_block(tx: &Connection, blockhash: &BlockHash) -> Result<bool> {
|
|
|
|
|
let mut stmt = tx.prepare("DELETE FROM auth_chain_entry WHERE blockhash = ?")?;
|
|
|
|
|
let rows_deleted = stmt.execute(params![blockhash.to_hex()])?;
|
|
|
|
|
|
|
|
|
|
Ok(rows_deleted != 0)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Add or update config entry
|
|
|
|
|
pub fn insert_authheader(
|
|
|
|
|
conn: &Connection,
|
|
|
|
|
utxo: &OutPointHash,
|
|
|
|
|
blockhash: &BlockHash,
|
|
|
|
|
txid: &Txid,
|
|
|
|
|
token_id: &TokenID,
|
|
|
|
|
height: usize,
|
|
|
|
|
bcmr_data: Option<Vec<u8>>,
|
|
|
|
|
) -> Result<()> {
|
|
|
|
|
let mut stmt = conn
|
|
|
|
|
.prepare("INSERT OR REPLACE INTO auth_chain_entry (utxo, blockhash, txid, token_id, height, bcmr_data) VALUES (?, ?, ?, ?, ?, ?)")?;
|
|
|
|
|
stmt.execute(params![
|
|
|
|
|
utxo.to_hex(),
|
|
|
|
|
blockhash.to_hex(),
|
|
|
|
|
txid.to_hex(),
|
|
|
|
|
token_id.to_hex(),
|
|
|
|
|
height,
|
|
|
|
|
bcmr_data.map(|bcmr| bcmr.to_hex())
|
|
|
|
|
])?;
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Get config entry
|
|
|
|
|
#[allow(dead_code)]
|
|
|
|
|
pub fn get_authheader(conn: &Connection, utxo: &OutPointHash) -> Result<Option<AuthChainEntry>> {
|
|
|
|
|
let mut stmt = conn
|
|
|
|
|
.prepare("SELECT token_id, txid, height, bcmr_data FROM auth_chain_entry WHERE utxo = ?")?;
|
|
|
|
|
let mut row = stmt.query([utxo.to_hex()])?;
|
|
|
|
|
|
|
|
|
|
let auth_header = row.next()?;
|
|
|
|
|
|
|
|
|
|
if let Some(header) = auth_header {
|
|
|
|
|
let token_hex: String = header.get(0)?;
|
|
|
|
|
let txid_hex: String = header.get(1)?;
|
|
|
|
|
let height = header.get(2)?;
|
|
|
|
|
let bcmr_data_hex: Option<String> = header.get(3)?;
|
|
|
|
|
let bcmr_data = if let Some(data) = bcmr_data_hex {
|
|
|
|
|
Some(hex::decode(data).context("failed to decode bcmr data")?)
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Ok(Some(AuthChainEntry {
|
|
|
|
|
utxo: *utxo,
|
|
|
|
|
token_id: TokenID::from_hex(&token_hex).context("failed to decode token hex")?,
|
|
|
|
|
txid: Txid::from_hex(&txid_hex).context("failed to decode txid")?,
|
|
|
|
|
height,
|
|
|
|
|
bcmr_data,
|
|
|
|
|
}))
|
|
|
|
|
} else {
|
|
|
|
|
Ok(None)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn get_matching_autheaders<'a>(
|
|
|
|
|
conn: &Connection,
|
|
|
|
|
keys: impl Iterator<Item = &'a OutPointHash>,
|
|
|
|
|
) -> Result<Vec<AuthChainEntry>> {
|
|
|
|
|
// To not exceed SQL query length; we chunk the queries into entries of this size.
|
|
|
|
|
let chunk_size = 10000;
|
|
|
|
|
|
|
|
|
|
let keys: Vec<String> = keys.map(|utxo| utxo.to_hex()).collect();
|
|
|
|
|
let mut matches: Vec<AuthChainEntry> = Vec::new();
|
|
|
|
|
|
|
|
|
|
// Process in chunks
|
|
|
|
|
for chunk in keys.chunks(chunk_size) {
|
|
|
|
|
let in_clause = chunk.join("','");
|
|
|
|
|
let sql = format!(
|
2025-07-16 09:31:14 +02:00
|
|
|
"SELECT token_id, txid, height, bcmr_data, utxo FROM auth_chain_entry WHERE utxo IN ('{in_clause}')"
|
2024-05-09 11:25:27 +02:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let mut stmt = conn.prepare(&sql)?;
|
|
|
|
|
|
|
|
|
|
let mut rows = stmt.query([])?;
|
|
|
|
|
|
|
|
|
|
while let Some(header) = rows.next()? {
|
|
|
|
|
let token_hex: String = header.get(0)?;
|
|
|
|
|
let txid_hex: String = header.get(1)?;
|
|
|
|
|
let height = header.get(2)?;
|
|
|
|
|
let bcmr_data_hex: Option<String> = header.get(3)?;
|
|
|
|
|
let bcmr_data = if let Some(data) = bcmr_data_hex {
|
|
|
|
|
Some(hex::decode(&data).context("failed to decode bcmr data")?)
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let utxo_hex: String = header.get(4)?;
|
|
|
|
|
let utxo = OutPointHash::from_hex(&utxo_hex)?;
|
|
|
|
|
|
|
|
|
|
matches.push(AuthChainEntry {
|
|
|
|
|
utxo,
|
|
|
|
|
token_id: TokenID::from_hex(&token_hex).context("failed to decode token hex")?,
|
|
|
|
|
txid: Txid::from_hex(&txid_hex).context("failed to decode txid")?,
|
|
|
|
|
height,
|
|
|
|
|
bcmr_data,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(matches)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn get_entries_missing_bcmr_download(conn: &Connection) -> Result<Vec<AuthChainEntry>> {
|
|
|
|
|
let sql = "WITH MaxHeight AS (
|
|
|
|
|
-- CTE to find the highest height for each token_id where bcmr_data is not null initially
|
|
|
|
|
SELECT
|
|
|
|
|
token_id,
|
|
|
|
|
MAX(height) AS max_height
|
|
|
|
|
FROM
|
|
|
|
|
auth_chain_entry
|
|
|
|
|
WHERE
|
|
|
|
|
bcmr_data IS NOT NULL
|
|
|
|
|
GROUP BY
|
|
|
|
|
token_id
|
|
|
|
|
)
|
|
|
|
|
SELECT
|
|
|
|
|
ace.token_id, ace.txid, ace.height, ace.bcmr_data, ace.utxo
|
|
|
|
|
FROM
|
|
|
|
|
auth_chain_entry AS ace
|
|
|
|
|
-- Join with MaxHeight to get the maximum height entries
|
|
|
|
|
JOIN MaxHeight mh ON ace.token_id = mh.token_id AND ace.height = mh.max_height
|
|
|
|
|
-- Exclude entries that have a corresponding entry in bcmr_data
|
|
|
|
|
LEFT JOIN bcmr_data bd ON ace.utxo = bd.utxo
|
|
|
|
|
-- Exclude entries that have a corresponding failure entry where give_up = true or last attempt was less than 1800 seconds ago
|
|
|
|
|
LEFT JOIN bcmr_failure bf ON ace.utxo = bf.utxo AND (bf.give_up = 1 OR (strftime('%s', 'now') - bf.last_attempt) < 1800)
|
|
|
|
|
WHERE
|
|
|
|
|
-- Ensure no matching entries in bcmr_data and only include bcmr_failure when filtered conditions aren't met
|
|
|
|
|
bd.utxo IS NULL AND
|
|
|
|
|
bf.utxo IS NULL;
|
|
|
|
|
";
|
|
|
|
|
|
|
|
|
|
let mut stmt = conn.prepare(sql)?;
|
|
|
|
|
|
|
|
|
|
let mut rows = stmt.query([])?;
|
|
|
|
|
|
|
|
|
|
let mut matches: Vec<AuthChainEntry> = Vec::new();
|
|
|
|
|
|
|
|
|
|
while let Some(header) = rows.next()? {
|
|
|
|
|
let token_hex: String = header.get(0)?;
|
|
|
|
|
let txid_hex: String = header.get(1)?;
|
|
|
|
|
let height = header.get(2)?;
|
|
|
|
|
let bcmr_data_hex: Option<String> = header.get(3)?;
|
|
|
|
|
let bcmr_data = if let Some(data) = bcmr_data_hex {
|
|
|
|
|
Some(hex::decode(&data).context("failed to decode bcmr data")?)
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let utxo_hex: String = header.get(4)?;
|
|
|
|
|
let utxo = OutPointHash::from_hex(&utxo_hex)?;
|
|
|
|
|
|
|
|
|
|
matches.push(AuthChainEntry {
|
|
|
|
|
utxo,
|
|
|
|
|
token_id: TokenID::from_hex(&token_hex).context("failed to decode token hex")?,
|
|
|
|
|
txid: Txid::from_hex(&txid_hex).context("failed to decode txid")?,
|
|
|
|
|
height,
|
|
|
|
|
bcmr_data,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(matches)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn insert_bcmr_data(
|
|
|
|
|
conn: &rusqlite::Connection,
|
|
|
|
|
utxo: &OutPointHash,
|
|
|
|
|
bcmr: &ParsedBCMR,
|
|
|
|
|
) -> Result<()> {
|
|
|
|
|
let sql = "INSERT OR REPLACE INTO bcmr_data (utxo, symbol, decimals, name, description, icon, web, expected_hash, actual_hash)
|
|
|
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)";
|
|
|
|
|
|
|
|
|
|
info!("Trying to insert {}", utxo.to_hex());
|
|
|
|
|
let empty_string: String = "".to_owned();
|
|
|
|
|
conn.execute(
|
|
|
|
|
sql,
|
|
|
|
|
rusqlite::params![
|
|
|
|
|
&utxo.to_hex(),
|
|
|
|
|
&bcmr.token.symbol,
|
|
|
|
|
bcmr.token.decimals,
|
|
|
|
|
&bcmr.name,
|
|
|
|
|
&bcmr.description,
|
|
|
|
|
&bcmr.uris.icon.as_ref().unwrap_or(&empty_string),
|
|
|
|
|
&bcmr.uris.web.as_ref().unwrap_or(&empty_string),
|
|
|
|
|
&bcmr.filemeta.expected_hash,
|
|
|
|
|
&bcmr.filemeta.actual_hash
|
|
|
|
|
],
|
|
|
|
|
)?;
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-10 22:37:44 +02:00
|
|
|
pub fn insert_well_known_bcmr(
|
|
|
|
|
conn: &rusqlite::Connection,
|
|
|
|
|
source: &str,
|
|
|
|
|
bcmr: &ParsedBCMR,
|
|
|
|
|
) -> Result<()> {
|
|
|
|
|
let sql = "INSERT OR REPLACE INTO bcmr_well_known (source, symbol, token_id, decimals, name, description, icon, web)
|
|
|
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)";
|
|
|
|
|
|
|
|
|
|
let empty_string: String = "".to_owned();
|
|
|
|
|
|
|
|
|
|
conn.execute(
|
|
|
|
|
sql,
|
|
|
|
|
rusqlite::params![
|
|
|
|
|
source,
|
|
|
|
|
bcmr.token.symbol,
|
|
|
|
|
bcmr.token.category,
|
|
|
|
|
bcmr.token.decimals,
|
|
|
|
|
&bcmr.name,
|
|
|
|
|
&bcmr.description,
|
|
|
|
|
&bcmr.uris.icon.as_ref().unwrap_or(&empty_string),
|
|
|
|
|
&bcmr.uris.web.as_ref().unwrap_or(&empty_string)
|
|
|
|
|
],
|
|
|
|
|
)?;
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-09 11:25:27 +02:00
|
|
|
pub fn update_bcmr_failure(
|
|
|
|
|
conn: &Connection,
|
|
|
|
|
utxo: &OutPointHash,
|
|
|
|
|
error_message: &str,
|
|
|
|
|
give_up: bool,
|
|
|
|
|
) -> Result<()> {
|
|
|
|
|
let sql = format!(
|
|
|
|
|
"
|
|
|
|
|
INSERT INTO bcmr_failure (utxo, last_attempt, attempts, error_message, give_up)
|
|
|
|
|
VALUES (?1, strftime('%s', 'now'), 1, ?2, ?3)
|
|
|
|
|
ON CONFLICT(utxo) DO UPDATE SET
|
|
|
|
|
last_attempt = excluded.last_attempt,
|
|
|
|
|
attempts = bcmr_failure.attempts + 1,
|
|
|
|
|
error_message = excluded.error_message,
|
|
|
|
|
give_up = CASE
|
2025-07-16 09:31:14 +02:00
|
|
|
WHEN bcmr_failure.attempts + 1 > {MAX_DOWNLOAD_ATTEMPTS} THEN true
|
2024-05-09 11:25:27 +02:00
|
|
|
ELSE excluded.give_up
|
2025-07-16 09:31:14 +02:00
|
|
|
END"
|
2024-05-09 11:25:27 +02:00
|
|
|
);
|
|
|
|
|
conn.execute(&sql, params![&utxo.to_hex(), error_message, give_up])?;
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn get_token_bcmr(conn: &Connection, token_hex: &str) -> Result<Option<ParsedBCMR>> {
|
|
|
|
|
let sql = "SELECT
|
|
|
|
|
b.symbol, b.decimals, b.name, b.description, b.icon, b.web, b.actual_hash, b.expected_hash
|
|
|
|
|
FROM auth_chain_entry a
|
|
|
|
|
JOIN bcmr_data b ON a.utxo = b.utxo
|
|
|
|
|
WHERE a.token_id = ?
|
|
|
|
|
AND a.bcmr_data IS NOT NULL
|
|
|
|
|
ORDER BY a.height DESC
|
|
|
|
|
LIMIT 1";
|
|
|
|
|
|
|
|
|
|
let mut stmt = conn.prepare(sql)?;
|
|
|
|
|
let mut row = stmt.query([token_hex])?;
|
|
|
|
|
|
|
|
|
|
if let Some(r) = row.next()? {
|
|
|
|
|
let symbol: String = r.get(0)?;
|
|
|
|
|
let decimals: usize = r.get(1)?;
|
|
|
|
|
let name = r.get(2)?;
|
|
|
|
|
let description = r.get(3)?;
|
|
|
|
|
let icon: String = r.get(4)?;
|
|
|
|
|
let web: String = r.get(5)?;
|
|
|
|
|
let actual_hash: String = r.get(6)?;
|
|
|
|
|
let expected_hash: String = r.get(7)?;
|
|
|
|
|
|
|
|
|
|
Ok(Some(ParsedBCMR {
|
|
|
|
|
name,
|
|
|
|
|
description,
|
|
|
|
|
token: Token {
|
|
|
|
|
category: token_hex.to_owned(),
|
|
|
|
|
symbol,
|
|
|
|
|
decimals,
|
|
|
|
|
},
|
|
|
|
|
uris: Uris {
|
|
|
|
|
web: if web.is_empty() { None } else { Some(web) },
|
|
|
|
|
icon: if icon.is_empty() { None } else { Some(icon) },
|
|
|
|
|
},
|
|
|
|
|
filemeta: FileMeta {
|
2024-09-10 22:37:44 +02:00
|
|
|
expected_hash: Some(expected_hash),
|
|
|
|
|
actual_hash: Some(actual_hash),
|
|
|
|
|
source: SOURCE_ON_CHAIN.into(),
|
2024-05-09 11:25:27 +02:00
|
|
|
},
|
|
|
|
|
}))
|
|
|
|
|
} else {
|
|
|
|
|
Ok(None)
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-09-10 22:37:44 +02:00
|
|
|
|
|
|
|
|
pub fn get_well_known_bcmr(conn: &Connection, token_hex: &str) -> Result<Vec<ParsedBCMR>> {
|
|
|
|
|
let sql = "SELECT
|
|
|
|
|
source, symbol, decimals, name, description, icon, web
|
|
|
|
|
FROM bcmr_well_known
|
|
|
|
|
WHERE token_id = ?";
|
|
|
|
|
|
|
|
|
|
let mut stmt = conn.prepare(sql)?;
|
|
|
|
|
let mut row = stmt.query([token_hex])?;
|
|
|
|
|
|
|
|
|
|
let mut entries: Vec<ParsedBCMR> = Vec::default();
|
|
|
|
|
|
|
|
|
|
while let Some(r) = row.next()? {
|
|
|
|
|
let source: String = r.get(0)?;
|
|
|
|
|
let symbol: String = r.get(1)?;
|
|
|
|
|
let decimals: usize = r.get(2)?;
|
|
|
|
|
let name = r.get(3)?;
|
|
|
|
|
let description = r.get(4)?;
|
|
|
|
|
let icon: String = r.get(5)?;
|
|
|
|
|
let web: String = r.get(6)?;
|
|
|
|
|
|
|
|
|
|
entries.push(ParsedBCMR {
|
|
|
|
|
name,
|
|
|
|
|
description,
|
|
|
|
|
token: Token {
|
|
|
|
|
category: token_hex.to_owned(),
|
|
|
|
|
symbol,
|
|
|
|
|
decimals,
|
|
|
|
|
},
|
|
|
|
|
uris: Uris {
|
|
|
|
|
web: if web.is_empty() { None } else { Some(web) },
|
|
|
|
|
icon: if icon.is_empty() { None } else { Some(icon) },
|
|
|
|
|
},
|
|
|
|
|
filemeta: FileMeta {
|
|
|
|
|
expected_hash: None,
|
|
|
|
|
actual_hash: None,
|
|
|
|
|
source,
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(entries)
|
|
|
|
|
}
|