Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Change types system to support any utxo-based blockchain by the library
- Loading branch information
Showing
37 changed files
with
5,469 additions
and
319 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,7 @@ | ||
[workspace] | ||
members = [ | ||
"blockchain-source", | ||
"cardano-utils", | ||
"core", | ||
"deps", | ||
"fraos", | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
[package] | ||
name = "cardano-utils" | ||
version = "0.1.0" | ||
edition = "2021" | ||
|
||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html | ||
|
||
[dependencies] | ||
deps = { version = "0.1.0", path = "../deps" } | ||
dcspark-core = { version = "0.1.0", path = "../core" } | ||
|
||
anyhow = { version = "1" } | ||
bech32 = { version = "0.9" } | ||
cardano-multiplatform-lib = { git = "https://github.com/dcSpark/cardano-multiplatform-lib.git", branch = "egostkin/utxo-selection-changes", commit = "a41264b88c9b48ac890c2d046fe916f224fccabb" } | ||
cryptoxide = "0.4.2" | ||
hex = { version = "0.4" } | ||
rand = "0.8.5" | ||
serde = { version = "1.0.144", features = ["derive", "rc" ] } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
use anyhow::{anyhow, Context as _, Result}; | ||
use bech32::ToBase32; | ||
use cryptoxide::hashing::blake2b::Blake2b; | ||
use dcspark_core::{AssetName, PolicyId, TokenId}; | ||
|
||
const HRP: &str = "asset"; | ||
|
||
pub fn fingerprint(policy: &PolicyId, name: &AssetName) -> Result<TokenId> { | ||
let mut buf = vec![0u8; 28 + name.as_ref().len() / 2]; | ||
hex::decode_to_slice(policy.as_ref(), &mut buf[..28]) | ||
.with_context(|| anyhow!("Failed to decode PolicyId: {policy}"))?; | ||
hex::decode_to_slice(name.as_ref(), &mut buf[28..]) | ||
.with_context(|| anyhow!("Failed to decode AssetName: {name}"))?; | ||
|
||
let b2b = Blake2b::<{ 20 * 8 }>::new().update(&buf); | ||
|
||
let mut out = [0; 20]; | ||
b2b.finalize_at(&mut out); | ||
|
||
bech32::encode(HRP, out.to_base32(), bech32::Variant::Bech32) | ||
// this probably can't fail | ||
.context("Couldn't compute bech32 asset fingerprint") | ||
.map(TokenId::new) | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use super::*; | ||
|
||
struct TestVector { | ||
policy_id: PolicyId, | ||
asset_name: AssetName, | ||
asset_fingerprint: TokenId, | ||
} | ||
|
||
const TESTS: &[TestVector] = &[ | ||
TestVector { | ||
policy_id: PolicyId::new_static( | ||
"7eae28af2208be856f7a119668ae52a49b73725e326dc16579dcc373", | ||
), | ||
asset_name: AssetName::new_static(""), | ||
asset_fingerprint: TokenId::new_static("asset1rjklcrnsdzqp65wjgrg55sy9723kw09mlgvlc3"), | ||
}, | ||
TestVector { | ||
policy_id: PolicyId::new_static( | ||
"7eae28af2208be856f7a119668ae52a49b73725e326dc16579dcc37e", | ||
), | ||
asset_name: AssetName::new_static(""), | ||
asset_fingerprint: TokenId::new_static("asset1nl0puwxmhas8fawxp8nx4e2q3wekg969n2auw3"), | ||
}, | ||
TestVector { | ||
policy_id: PolicyId::new_static( | ||
"1e349c9bdea19fd6c147626a5260bc44b71635f398b67c59881df209", | ||
), | ||
asset_name: AssetName::new_static(""), | ||
asset_fingerprint: TokenId::new_static("asset1uyuxku60yqe57nusqzjx38aan3f2wq6s93f6ea"), | ||
}, | ||
TestVector { | ||
policy_id: PolicyId::new_static( | ||
"7eae28af2208be856f7a119668ae52a49b73725e326dc16579dcc373", | ||
), | ||
asset_name: AssetName::new_static("504154415445"), | ||
asset_fingerprint: TokenId::new_static("asset13n25uv0yaf5kus35fm2k86cqy60z58d9xmde92"), | ||
}, | ||
TestVector { | ||
policy_id: PolicyId::new_static( | ||
"1e349c9bdea19fd6c147626a5260bc44b71635f398b67c59881df209", | ||
), | ||
asset_name: AssetName::new_static("504154415445"), | ||
asset_fingerprint: TokenId::new_static("asset1hv4p5tv2a837mzqrst04d0dcptdjmluqvdx9k3"), | ||
}, | ||
TestVector { | ||
policy_id: PolicyId::new_static( | ||
"1e349c9bdea19fd6c147626a5260bc44b71635f398b67c59881df209", | ||
), | ||
asset_name: AssetName::new_static( | ||
"7eae28af2208be856f7a119668ae52a49b73725e326dc16579dcc373", | ||
), | ||
asset_fingerprint: TokenId::new_static("asset1aqrdypg669jgazruv5ah07nuyqe0wxjhe2el6f"), | ||
}, | ||
TestVector { | ||
policy_id: PolicyId::new_static( | ||
"7eae28af2208be856f7a119668ae52a49b73725e326dc16579dcc373", | ||
), | ||
asset_name: AssetName::new_static( | ||
"1e349c9bdea19fd6c147626a5260bc44b71635f398b67c59881df209", | ||
), | ||
asset_fingerprint: TokenId::new_static("asset17jd78wukhtrnmjh3fngzasxm8rck0l2r4hhyyt"), | ||
}, | ||
TestVector { | ||
policy_id: PolicyId::new_static( | ||
"7eae28af2208be856f7a119668ae52a49b73725e326dc16579dcc373", | ||
), | ||
asset_name: AssetName::new_static( | ||
"0000000000000000000000000000000000000000000000000000000000000000", | ||
), | ||
asset_fingerprint: TokenId::new_static("asset1pkpwyknlvul7az0xx8czhl60pyel45rpje4z8w"), | ||
}, | ||
]; | ||
|
||
#[test] | ||
fn test_vectors() { | ||
for (index, test) in TESTS.iter().enumerate() { | ||
let computed = fingerprint(&test.policy_id, &test.asset_name).unwrap(); | ||
|
||
assert_eq!( | ||
computed, test.asset_fingerprint, | ||
"Failed to run the vector test {index}" | ||
) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
use cardano_multiplatform_lib::ledger::common::value::{BigNum, Coin}; | ||
use cardano_multiplatform_lib::{MultiAsset, PolicyID}; | ||
use dcspark_core::tx::TransactionAsset; | ||
use dcspark_core::{AssetName, PolicyId, Regulated, TokenId, Value}; | ||
use deps::bigdecimal::ToPrimitive; | ||
use std::collections::HashMap; | ||
use anyhow::anyhow; | ||
|
||
pub fn value_to_csl_coin(value: &Value<Regulated>) -> anyhow::Result<Coin> { | ||
Ok( | ||
cardano_multiplatform_lib::ledger::common::value::Coin::from( | ||
value | ||
.to_u64() | ||
.ok_or_else(|| anyhow!("Can't convert input balance to u64"))?, | ||
), | ||
) | ||
} | ||
|
||
pub fn csl_coin_to_value(value: &Coin) -> anyhow::Result<Value<Regulated>> { | ||
Ok(Value::new(deps::bigdecimal::BigDecimal::from(u64::from( | ||
*value, | ||
)))) | ||
} | ||
|
||
pub fn multiasset_iter<F>( | ||
value: &cardano_multiplatform_lib::ledger::common::value::Value, | ||
mut f: F, | ||
) -> anyhow::Result<()> | ||
where | ||
F: FnMut( | ||
&PolicyID, | ||
&cardano_multiplatform_lib::AssetName, | ||
&Option<BigNum>, | ||
) -> anyhow::Result<()>, | ||
{ | ||
if let Some(ma) = &value.multiasset() { | ||
let policy_ids = ma.keys(); | ||
for policy_id_index in 0..policy_ids.len() { | ||
let policy_id = policy_ids.get(policy_id_index); | ||
let assets = if let Some(assets) = ma.get(&policy_id) { | ||
assets | ||
} else { | ||
continue; | ||
}; | ||
let asset_names = assets.keys(); | ||
for asset_name_index in 0..asset_names.len() { | ||
let asset_name = asset_names.get(asset_name_index); | ||
let quantity = assets.get(&asset_name); | ||
f(&policy_id, &asset_name, &quantity)?; | ||
} | ||
} | ||
} | ||
Ok(()) | ||
} | ||
|
||
pub fn tokens_to_csl_value( | ||
coin: &Value<Regulated>, | ||
assets: &HashMap<TokenId, TransactionAsset>, | ||
) -> anyhow::Result<cardano_multiplatform_lib::ledger::common::value::Value> { | ||
let coin = value_to_csl_coin(coin)?; | ||
let mut value = cardano_multiplatform_lib::ledger::common::value::Value::new(&coin); | ||
if !assets.is_empty() { | ||
let mut multi_assets = MultiAsset::new(); | ||
for (_, asset) in assets.iter() { | ||
let policy_id = | ||
PolicyID::from_bytes(hex::decode(asset.policy_id.to_string()).map_err(|err| { | ||
anyhow!("Failed to decode the policy id: hex error {err}") | ||
})?) | ||
.map_err(|error| { | ||
anyhow!("Failed to decode the policy id: {error}") | ||
})?; | ||
let asset_name = cardano_multiplatform_lib::AssetName::new( | ||
hex::decode(asset.asset_name.as_ref()).map_err(|err| { | ||
anyhow!("Failed to decode the asset name {err}") | ||
})?, | ||
).map_err(|err| anyhow!("can't decode asset_name {err}"))?; | ||
|
||
let value = | ||
BigNum::from_str(&asset.quantity.truncate().to_string()).map_err(|error| { | ||
anyhow!( | ||
"Value {value} ({fingerprint}) was not within the boundaries: {error}", | ||
value = asset.quantity.truncate(), | ||
fingerprint = asset.fingerprint, | ||
) | ||
})?; | ||
|
||
multi_assets.set_asset(&policy_id, &asset_name, &value); | ||
} | ||
|
||
value.set_multiasset(&multi_assets); | ||
} | ||
|
||
Ok(value) | ||
} | ||
|
||
pub fn csl_value_to_tokens( | ||
value: &cardano_multiplatform_lib::ledger::common::value::Value, | ||
) -> anyhow::Result<(Value<Regulated>, HashMap<TokenId, TransactionAsset>)> { | ||
let coin = csl_coin_to_value(&value.coin())?; | ||
let mut tokens = HashMap::<TokenId, TransactionAsset>::new(); | ||
multiasset_iter(value, |policy_id, asset_name, quantity| { | ||
let policy_id = PolicyId::new(hex::encode(policy_id.to_bytes())); | ||
let asset_name = AssetName::new(hex::encode(asset_name.to_bytes())); | ||
let fingerprint = crate::cip14::fingerprint(&policy_id, &asset_name).map_err(|err| { | ||
anyhow!("Can't create fingerprint {err}") | ||
})?; | ||
let quantity = quantity.ok_or_else(|| anyhow!("not found asset quantity"))?; | ||
let asset = TransactionAsset { | ||
policy_id, | ||
asset_name, | ||
fingerprint: fingerprint.clone(), | ||
quantity: csl_coin_to_value(&quantity)?, | ||
}; | ||
tokens.insert(fingerprint, asset); | ||
Ok(()) | ||
})?; | ||
Ok((coin, tokens)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
pub mod cip14; | ||
pub mod conversion; | ||
pub mod multisig_plan; | ||
pub mod network_id; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
use anyhow::{anyhow, Context as _}; | ||
use cardano_multiplatform_lib::address::{Address, EnterpriseAddress, StakeCredential}; | ||
use cardano_multiplatform_lib::crypto::{Ed25519KeyHash, ScriptHash}; | ||
use cardano_multiplatform_lib::{NativeScript, NativeScripts, ScriptNOfK, ScriptPubkey}; | ||
use deps::serde_json; | ||
use serde::{Deserialize, Deserializer}; | ||
use std::path::Path; | ||
|
||
#[derive(Debug, Clone, Deserialize)] | ||
pub struct MultisigPlan { | ||
pub quorum: u32, | ||
pub keys: Vec<Hash>, | ||
} | ||
|
||
impl MultisigPlan { | ||
pub fn load(path: impl AsRef<Path>) -> anyhow::Result<Self> { | ||
let path = path.as_ref(); | ||
let file = std::fs::File::open(path) | ||
.with_context(|| anyhow!("failed to read multisig plan from {}", path.display()))?; | ||
serde_json::from_reader(file) | ||
.with_context(|| anyhow!("failed to read multisig plan from {}", path.display())) | ||
} | ||
|
||
pub fn hash(&self) -> ScriptHash { | ||
let script = self.to_script().get(0).hash().to_bytes(); | ||
|
||
ScriptHash::from_bytes(script) | ||
.map_err(|error| anyhow!("Invalid hash {}", error)) | ||
.expect("Script should be valid all the time already") | ||
} | ||
|
||
pub fn address(&self, network_id: u8) -> Address { | ||
let native_bytes = self.hash(); | ||
|
||
let address = StakeCredential::from_scripthash(&native_bytes); | ||
EnterpriseAddress::new(network_id, &address).to_address() | ||
} | ||
|
||
pub fn to_script(&self) -> NativeScripts { | ||
let keys = { | ||
let mut scripts = NativeScripts::new(); | ||
|
||
for key in self.keys.iter().map(|k| &k.0) { | ||
scripts.add(&NativeScript::new_script_pubkey(&ScriptPubkey::new(key))); | ||
} | ||
|
||
scripts | ||
}; | ||
|
||
let mut scripts = NativeScripts::new(); | ||
let script = ScriptNOfK::new(self.quorum, &keys); | ||
let script = NativeScript::new_script_n_of_k(&script); | ||
scripts.add(&script); | ||
|
||
scripts | ||
} | ||
} | ||
|
||
#[derive(Debug, Clone, Deserialize)] | ||
pub struct Hash(#[serde(deserialize_with = "deserialize_key_hash")] pub Ed25519KeyHash); | ||
|
||
fn deserialize_key_hash<'de, D>(deserializer: D) -> Result<Ed25519KeyHash, D::Error> | ||
where | ||
D: Deserializer<'de>, | ||
D::Error: serde::de::Error, | ||
{ | ||
use serde::de::Error as _; | ||
|
||
let bytes = String::deserialize(deserializer)?; | ||
let bytes = hex::decode(bytes).map_err(D::Error::custom)?; | ||
|
||
Ed25519KeyHash::from_bytes(bytes).map_err(D::Error::custom) | ||
} |
Oops, something went wrong.