use std::collections::BTreeSet;
use blstrs::Scalar;
use pallas_addresses::Address;
use pallas_primitives::{Fragment, conway};
use pallas_txbuilder::BuiltTransaction;
use seedelf_core::address::wallet_contract;
use seedelf_core::build::{
self, AccountAmount, AccountPay, AccountPayment, Budgets, Chain, Payee, fake_signer,
linear_fee, tx_id,
};
use seedelf_core::constants::get_config;
use seedelf_core::eval::slot_at;
use seedelf_core::note::Note;
use seedelf_core::staking::{StakeAction, StakeKey, StakeState, Staking, parse_pool_id};
use seedelf_crypto::cardano::{CardanoAccount, Role};
use seedelf_crypto::register::Register;
use seedelf_koios::koios::{Asset, ProtocolParameters, UtxoResponse};
const PHRASE: &str =
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
const POLICY: &str = "c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0";
const LOGIC: &str = "pool1rccstu3l9ty3k0a5cd06fl3szsss9r34dcg5j38fqgq9kvng0tg";
const NOW_MS: u64 = 1_790_510_400_000;
const VALID_FOR_MS: u64 = 2 * 60 * 60_000;
const SEND_BEFORE: &str = "5462f10c2d2d6e9c88fa1c64cb9bd22844e92d5414c1190c9aa1c5c3d1f6de38";
const STAKE_BEFORE: &str = "f66ef4c1aa5b06412dd33fdd4069e8e217fc1e455b2ce6216e6b3b2b763e5ac6";
const MINT_DRAFT_BEFORE: &str = "7e9daac5c90a60c0f3d29f186e6f945b3eb267c14a49842da0b35b166e246dd1";
const MINT_BEFORE: &str = "b640c0d5e0f37be195163b7f6b814009bd9a42b115e5002c78fe0434bdf9b017";
fn params() -> ProtocolParameters {
let rows: serde_json::Value =
serde_json::from_str(include_str!("fixtures/epoch_params.json")).unwrap();
ProtocolParameters::from_koios(&rows[0]).unwrap()
}
fn account() -> CardanoAccount {
CardanoAccount::from_phrase(PHRASE, 0).unwrap()
}
fn change() -> Address {
account().base_address(true, Role::Receive, 0).unwrap()
}
fn stake_key() -> StakeKey {
StakeKey::new(&account().stake_address(true).unwrap()).unwrap()
}
fn utxo(n: u8, index: u32, lovelace: u64, tokens: &[(&str, u64)]) -> UtxoResponse {
let account = account();
UtxoResponse {
tx_hash: hex::encode([n; 32]),
tx_index: 0,
address: account
.base_address(true, Role::Receive, index)
.unwrap()
.to_bech32()
.unwrap(),
value: lovelace.to_string(),
payment_cred: hex::encode(account.key_hash(Role::Receive, index).unwrap()),
asset_list: Some(
tokens
.iter()
.map(|(name, quantity)| Asset {
decimals: 0,
quantity: quantity.to_string(),
policy_id: POLICY.to_string(),
asset_name: hex::encode(name),
fingerprint: String::new(),
})
.collect(),
),
..Default::default()
}
}
fn available() -> Vec<UtxoResponse> {
vec![
utxo(1, 0, 20_000_000, &[]),
utxo(2, 1, 5_000_000, &[("tok", 9)]),
]
}
fn body(tx: &BuiltTransaction) -> conway::TransactionBody {
conway::Tx::decode_fragment(&tx.tx_bytes.0)
.unwrap()
.transaction_body
}
fn keys<'a>(utxos: impl IntoIterator<Item = &'a UtxoResponse>) -> usize {
utxos
.into_iter()
.map(|u| u.payment_cred.as_str())
.collect::<BTreeSet<_>>()
.len()
}
fn signed_size(tx: &BuiltTransaction, signers: usize) -> u64 {
let mut signed = tx.clone();
for _ in 0..signers {
signed = signed.sign(fake_signer()).unwrap();
}
signed.tx_bytes.0.len() as u64
}
fn send(invalid_hereafter: Option<u64>) -> AccountPayment {
let to = CardanoAccount::from_phrase(PHRASE, 1)
.unwrap()
.base_address(true, Role::Receive, 0)
.unwrap();
let rewards = Staking::withdraw(
&stake_key(),
&StakeState {
registered: true,
deposit: 2_000_000,
rewards: 1_234_567,
votes: true,
},
)
.unwrap();
let note = Note::new("thanks for lunch").unwrap();
build::account_send_many(
¶ms(),
&available(),
&[AccountPay::new(
Payee::Address(&to),
AccountAmount::Lovelace(3_000_000),
&[(POLICY.to_string(), hex::encode("tok"), 2)],
)],
true,
&change(),
&rewards,
note.as_ref(),
invalid_hereafter,
)
.unwrap()
}
fn stake(invalid_hereafter: Option<u64>) -> AccountPayment {
let params = params();
let delegate = Staking::of(
&stake_key(),
&StakeAction::Delegate(parse_pool_id(LOGIC).unwrap()),
&StakeState::default(),
params.key_deposit,
)
.unwrap();
build::account_staking(
¶ms,
&available(),
&delegate,
&change(),
invalid_hereafter,
)
.unwrap()
}
fn move_in(invalid_hereafter: Option<u64>) -> AccountPayment {
let config = get_config(1, true).unwrap();
build::move_in(
¶ms(),
&available(),
AccountAmount::Lovelace(10_000_000),
&[],
&Register::create(Scalar::from(7u64)).unwrap(),
&wallet_contract(true, config.contract.wallet_contract_hash),
&change(),
&Staking::none(),
invalid_hereafter,
)
.unwrap()
}
fn mint(invalid_hereafter: Option<u64>) -> build::AccountMint {
let chain = Chain {
params: params(),
network_flag: true,
config: get_config(1, true).unwrap(),
};
build::account_mint(
&chain,
&available(),
None,
"validity",
&Register::create(Scalar::from(7u64)).unwrap(),
&change(),
&Staking::none(),
invalid_hereafter,
)
.unwrap()
}
fn recorded() -> Budgets {
let path = format!(
"{}/tests/fixtures/ogmios/account_mint.json",
env!("CARGO_MANIFEST_DIR")
);
Budgets::from_ogmios(&serde_json::from_str(&std::fs::read_to_string(path).unwrap()).unwrap())
.unwrap()
}
fn two_hours_on() -> u64 {
slot_at(true, NOW_MS + VALID_FOR_MS)
}
#[test]
fn a_slot_is_counted_from_each_network_s_first_shelley_slot() {
assert_eq!(slot_at(false, 1_725_227_091_000), 133_660_800);
assert_eq!(slot_at(false, 1_725_227_091_999), 133_660_800);
assert_eq!(slot_at(false, 1_725_227_092_000), 133_660_801);
assert_eq!(slot_at(true, 1_655_769_600_000), 86_400);
assert_eq!(
slot_at(true, 1_655_769_600_000 + VALID_FOR_MS),
86_400 + 7_200
);
assert_eq!(slot_at(true, 0), 86_400);
assert_eq!(slot_at(false, 0), 4_492_800);
assert_eq!(two_hours_on(), 134_834_400);
}
#[test]
fn account_payments_stop_being_valid_at_the_slot_and_pay_for_its_bytes() {
let slot = two_hours_on();
let a = params().min_fee_a;
for (what, without, with, stake_key) in [
("move-in", move_in(None), move_in(Some(slot)), 0),
("send", send(None), send(Some(slot)), 1),
("staking", stake(None), stake(Some(slot)), 1),
] {
let staged = body(&with.tx);
assert_eq!(staged.ttl, Some(slot), "{what}");
assert_eq!(staged.validity_interval_start, None, "{what}");
assert_eq!(body(&without.tx).ttl, None, "{what}");
assert_eq!(*tx_id(&with.tx.tx_bytes.0).unwrap(), with.tx.tx_hash.0);
let signers = keys(&with.inputs) + stake_key;
let size = signed_size(&with.tx, signers);
assert_eq!(size, signed_size(&without.tx, signers) + 6, "{what}");
assert_eq!(with.fee, without.fee + 6 * a, "{what}");
assert!(with.fee >= linear_fee(¶ms(), size), "{what}");
assert_eq!(
with.change_lovelace + 6 * a,
without.change_lovelace,
"{what}: the change pays it"
);
assert_eq!(with.inputs.len(), without.inputs.len(), "{what}");
}
}
#[test]
fn an_account_mint_is_measured_and_finished_with_the_slot() {
let slot = two_hours_on();
let (without, with) = (mint(None), mint(Some(slot)));
assert_eq!(body(&with.draft().unwrap()).ttl, Some(slot));
assert_eq!(body(&without.draft().unwrap()).ttl, None);
let signers = keys(with.inputs().iter().chain([with.collateral()]));
let (without, with) = (
without.finalize(&recorded()).unwrap(),
with.finalize(&recorded()).unwrap(),
);
assert_eq!(body(&with.tx).ttl, Some(slot));
assert_eq!(body(&without.tx).ttl, None);
let size = signed_size(&with.tx, signers);
assert_eq!(size, signed_size(&without.tx, signers) + 6);
assert!(with.fee.size >= linear_fee(¶ms(), size));
assert_eq!(with.fee.compute, without.fee.compute);
assert!(with.fee.total > without.fee.total);
assert_eq!(
with.change_lovelace + with.fee.total,
without.change_lovelace + without.fee.total
);
}
#[test]
fn without_a_slot_the_transactions_are_byte_for_byte_as_before() {
assert_eq!(hex::encode(send(None).tx.tx_hash.0), SEND_BEFORE);
assert_eq!(hex::encode(stake(None).tx.tx_hash.0), STAKE_BEFORE);
let mint = mint(None);
assert_eq!(
hex::encode(mint.draft().unwrap().tx_hash.0),
MINT_DRAFT_BEFORE
);
assert_eq!(
hex::encode(mint.finalize(&recorded()).unwrap().tx.tx_hash.0),
MINT_BEFORE
);
}