1use std::fmt;
36
37use anyhow::{Context, Result, anyhow, bail};
38use bech32::{FromBase32, ToBase32, Variant};
39use pallas_addresses::{Address, StakePayload};
40use pallas_crypto::hash::Hash;
41use pallas_primitives::conway::{
42 self, Anchor, Certificate, DRep, GovActionId, Vote, Voter, VotingProcedure,
43};
44use pallas_primitives::{Fragment, NonEmptyKeyValuePairs, NonEmptySet, Nullable, StakeCredential};
45use pallas_txbuilder::BuiltTransaction;
46use seedelf_koios::koios::ProtocolParameters;
47
48use crate::build::tx_id;
49
50pub const ALWAYS_ABSTAIN: &str = "drep_always_abstain";
53pub const ALWAYS_NO_CONFIDENCE: &str = "drep_always_no_confidence";
54
55const DREP_KEY_HEADER: u8 = 0x22;
57const DREP_SCRIPT_HEADER: u8 = 0x23;
58
59#[derive(Debug, Clone, PartialEq, Eq)]
61pub struct StakeKey {
62 credential: StakeCredential,
63 reward_account: Vec<u8>,
65}
66
67impl StakeKey {
68 pub fn new(stake_address: &Address) -> Result<Self> {
71 let Address::Stake(stake) = stake_address else {
72 bail!("A stake key is named by its reward (stake) address");
73 };
74 let StakePayload::Stake(hash) = stake.payload() else {
75 bail!("That reward address belongs to a script, not an account's stake key");
76 };
77 Ok(StakeKey {
78 credential: StakeCredential::AddrKeyhash(*hash),
79 reward_account: stake_address.to_vec(),
80 })
81 }
82}
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq)]
87pub struct DrepKey {
88 hash: Hash<28>,
89}
90
91impl DrepKey {
92 pub fn new(hash: Hash<28>) -> Self {
94 DrepKey { hash }
95 }
96
97 pub fn hash(&self) -> Hash<28> {
98 self.hash
99 }
100
101 fn credential(&self) -> StakeCredential {
103 StakeCredential::AddrKeyhash(self.hash)
104 }
105
106 pub fn drep(&self) -> DRep {
108 DRep::Key(self.hash)
109 }
110
111 pub fn id(&self) -> String {
113 drep_id(&self.drep())
114 }
115}
116
117#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
120pub struct DrepState {
121 pub registered: bool,
122 pub deposit: u64,
124 pub own_vote: bool,
126}
127
128#[derive(Debug, Clone, PartialEq)]
130pub enum DrepAction {
131 Register {
134 anchor: Option<Anchor>,
135 delegate: bool,
136 },
137 Update { anchor: Option<Anchor> },
140 Retire,
142 Vote(Vec<(GovActionId, Vote)>),
144}
145
146#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
148pub struct StakeState {
149 pub registered: bool,
150 pub deposit: u64,
152 pub rewards: u64,
154 pub votes: bool,
156}
157
158#[derive(Debug, Clone, PartialEq, Eq)]
160pub enum StakeAction {
161 Delegate(Hash<28>),
163 Vote(DRep),
165 Withdraw,
167 Stop,
170}
171
172#[derive(Debug, Clone, Copy)]
174pub struct RewardsLocked;
175
176impl fmt::Display for RewardsLocked {
177 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
178 f.write_str(
179 "Your rewards can't be withdrawn until you delegate your voting power: to a DRep, or always abstain",
180 )
181 }
182}
183
184impl std::error::Error for RewardsLocked {}
185
186#[derive(Debug, Clone, Default, PartialEq)]
190pub struct Staking {
191 pub certificates: Vec<Certificate>,
192 pub withdrawal: Option<(Vec<u8>, u64)>,
194 pub votes: Option<(Hash<28>, Vec<(GovActionId, Vote)>)>,
197}
198
199impl Staking {
200 pub fn none() -> Self {
202 Self::default()
203 }
204
205 pub fn withdraw(key: &StakeKey, state: &StakeState) -> Result<Self> {
208 if !state.registered || state.rewards == 0 {
209 return Ok(Self::none());
210 }
211 if !state.votes {
212 bail!(RewardsLocked);
213 }
214 Ok(Staking {
215 withdrawal: Some((key.reward_account.clone(), state.rewards)),
216 ..Self::none()
217 })
218 }
219
220 pub fn of(
224 key: &StakeKey,
225 action: &StakeAction,
226 state: &StakeState,
227 key_deposit: u64,
228 ) -> Result<Self> {
229 let cred = key.credential.clone();
230 let certificate = |c: Certificate| Staking {
231 certificates: vec![c],
232 ..Self::none()
233 };
234 Ok(match action {
235 StakeAction::Delegate(pool) if state.registered => {
236 certificate(Certificate::StakeDelegation(cred, *pool))
237 }
238 StakeAction::Delegate(pool) => {
239 certificate(Certificate::StakeRegDeleg(cred, *pool, key_deposit))
240 }
241 StakeAction::Vote(drep) if state.registered => {
242 certificate(Certificate::VoteDeleg(cred, drep.clone()))
243 }
244 StakeAction::Vote(drep) => {
245 certificate(Certificate::VoteRegDeleg(cred, drep.clone(), key_deposit))
246 }
247 StakeAction::Withdraw => {
248 if !state.registered || state.rewards == 0 {
249 bail!("There are no rewards to withdraw");
250 }
251 Self::withdraw(key, state)?
252 }
253 StakeAction::Stop => {
254 if !state.registered {
255 bail!("This account isn't staking");
256 }
257 let mut stop = Self::withdraw(key, state)?;
258 stop.certificates
259 .push(Certificate::UnReg(cred, state.deposit));
260 stop
261 }
262 })
263 }
264
265 pub fn drep(
270 stake: &StakeKey,
271 stake_state: &StakeState,
272 drep: &DrepKey,
273 drep_state: &DrepState,
274 action: &DrepAction,
275 params: &ProtocolParameters,
276 ) -> Result<Self> {
277 let cred = drep.credential();
278 if !drep_state.registered && !matches!(action, DrepAction::Register { .. }) {
279 bail!("This account isn't a DRep: register it first");
280 }
281 let mut certificates = Vec::new();
282 let mut votes = None;
283 match action {
284 DrepAction::Register { anchor, delegate } => {
285 if drep_state.registered {
286 bail!("This account is a DRep already");
287 }
288 let deposit = params.drep_deposit.ok_or_else(|| {
289 anyhow!(
290 "Koios didn't say what registering a DRep locks up. Nothing was built; try again later"
291 )
292 })?;
293 certificates.push(Certificate::RegDRepCert(cred, deposit, nullable(anchor)));
294 if *delegate && !drep_state.own_vote {
295 certificates.push(vote_delegation(stake, stake_state, drep.drep(), params));
296 }
297 }
298 DrepAction::Update { anchor } => {
299 certificates.push(Certificate::UpdateDRepCert(cred, nullable(anchor)));
300 }
301 DrepAction::Retire => {
302 if drep_state.deposit == 0 {
305 bail!(
306 "Koios didn't say what deposit this DRep paid, so its retirement can't be built. Nothing was built; try again later"
307 );
308 }
309 certificates.push(Certificate::UnRegDRepCert(cred, drep_state.deposit));
310 if drep_state.own_vote && stake_state.registered {
311 certificates.push(Certificate::VoteDeleg(
312 stake.credential.clone(),
313 DRep::Abstain,
314 ));
315 }
316 }
317 DrepAction::Vote(ballots) => {
318 if ballots.is_empty() {
319 bail!("Choose a vote on at least one governance action");
320 }
321 let mut sorted = ballots.clone();
322 sorted.sort_by(|(a, _), (b, _)| {
323 (a.transaction_id, a.action_index).cmp(&(b.transaction_id, b.action_index))
324 });
325 if sorted.windows(2).any(|w| w[0].0 == w[1].0) {
326 bail!("A governance action can have only one vote in a transaction");
327 }
328 votes = Some((drep.hash(), sorted));
329 }
330 }
331 Ok(Staking {
332 certificates,
333 withdrawal: None,
334 votes,
335 })
336 }
337
338 pub fn is_empty(&self) -> bool {
339 self.certificates.is_empty() && self.withdrawal.is_none() && self.votes.is_none()
340 }
341
342 pub fn stake_signs(&self) -> bool {
344 self.withdrawal.is_some() || self.certificates.iter().any(|c| !is_drep_certificate(c))
345 }
346
347 pub fn drep_signs(&self) -> bool {
349 self.votes.is_some() || self.certificates.iter().any(is_drep_certificate)
350 }
351
352 pub fn signers(&self) -> usize {
355 usize::from(self.stake_signs()) + usize::from(self.drep_signs())
356 }
357
358 pub fn deposit(&self) -> u64 {
360 self.certificates
361 .iter()
362 .map(|c| match c {
363 Certificate::Reg(_, d)
364 | Certificate::StakeRegDeleg(_, _, d)
365 | Certificate::VoteRegDeleg(_, _, d)
366 | Certificate::StakeVoteRegDeleg(_, _, _, d)
367 | Certificate::RegDRepCert(_, d, _) => *d,
368 _ => 0,
369 })
370 .sum()
371 }
372
373 pub fn drep_deposit(&self) -> u64 {
376 self.certificates
377 .iter()
378 .map(|c| match c {
379 Certificate::RegDRepCert(_, d, _) => *d,
380 _ => 0,
381 })
382 .sum()
383 }
384
385 pub fn refund(&self) -> u64 {
387 self.certificates
388 .iter()
389 .map(|c| match c {
390 Certificate::UnReg(_, d) | Certificate::UnRegDRepCert(_, d) => *d,
391 _ => 0,
392 })
393 .sum()
394 }
395
396 pub fn withdrawn(&self) -> u64 {
398 self.withdrawal.as_ref().map_or(0, |(_, l)| *l)
399 }
400
401 pub fn net(&self, inputs: u64) -> Option<u64> {
404 inputs
405 .checked_add(self.withdrawn())?
406 .checked_add(self.refund())?
407 .checked_sub(self.deposit())
408 }
409
410 pub fn patch(&self, mut built: BuiltTransaction) -> Result<BuiltTransaction> {
414 if self.is_empty() {
415 return Ok(built);
416 }
417 if built.signatures.is_some() {
418 bail!("A transaction is patched before it's signed, not after");
419 }
420 let mut tx = conway::Tx::decode_fragment(&built.tx_bytes.0)
421 .map_err(|e| anyhow!("The built transaction isn't a Conway transaction: {e}"))?;
422 let body = &mut tx.transaction_body;
423 if body.certificates.is_some()
424 || body.withdrawals.is_some()
425 || body.voting_procedures.is_some()
426 {
427 bail!("The transaction already has certificates, withdrawals or votes");
428 }
429 body.certificates = NonEmptySet::from_vec(self.certificates.clone());
430 body.withdrawals = self.withdrawal.as_ref().map(|(account, lovelace)| {
431 NonEmptyKeyValuePairs::Def(vec![(account.clone().into(), *lovelace)])
432 });
433 body.voting_procedures = self.votes.as_ref().map(|(drep, ballots)| {
434 let procedures = ballots
435 .iter()
436 .map(|(action, vote)| {
437 (
438 action.clone(),
439 VotingProcedure {
440 vote: vote.clone(),
441 anchor: Nullable::Null,
442 },
443 )
444 })
445 .collect();
446 NonEmptyKeyValuePairs::Def(vec![(
447 Voter::DRepKey(*drep),
448 NonEmptyKeyValuePairs::Def(procedures),
449 )])
450 });
451 let bytes = tx
452 .encode_fragment()
453 .map_err(|e| anyhow!("Failed to encode the transaction: {e}"))?;
454 built.tx_hash.0 = *tx_id(&bytes)?;
456 built.tx_bytes.0 = bytes;
457 Ok(built)
458 }
459}
460
461fn is_drep_certificate(certificate: &Certificate) -> bool {
464 matches!(
465 certificate,
466 Certificate::RegDRepCert(..)
467 | Certificate::UnRegDRepCert(..)
468 | Certificate::UpdateDRepCert(..)
469 )
470}
471
472fn vote_delegation(
475 stake: &StakeKey,
476 state: &StakeState,
477 drep: DRep,
478 params: &ProtocolParameters,
479) -> Certificate {
480 let cred = stake.credential.clone();
481 if state.registered {
482 Certificate::VoteDeleg(cred, drep)
483 } else {
484 Certificate::VoteRegDeleg(cred, drep, params.key_deposit)
485 }
486}
487
488fn nullable(anchor: &Option<Anchor>) -> Nullable<Anchor> {
489 match anchor {
490 Some(a) => Nullable::Some(a.clone()),
491 None => Nullable::Null,
492 }
493}
494
495pub const MAX_ANCHOR_URL: usize = 128;
497
498pub fn anchor(url: &str, hash: &str) -> Result<Anchor> {
502 let url = url.trim();
503 if url.is_empty() {
504 bail!("Give the address where the file is published");
505 }
506 if url.len() > MAX_ANCHOR_URL {
507 bail!(
508 "That address is {} bytes, over the network's limit of {MAX_ANCHOR_URL}: use a shorter one",
509 url.len()
510 );
511 }
512 if url.chars().any(|c| c.is_whitespace() || c.is_control()) {
513 bail!("An address can't hold spaces or control characters");
514 }
515 let bytes: [u8; 32] = hex::decode(hash.trim())
516 .ok()
517 .and_then(|b| b.try_into().ok())
518 .ok_or_else(|| anyhow!("A file's hash is 64 hex characters"))?;
519 Ok(Anchor {
520 url: url.to_string(),
521 content_hash: Hash::new(bytes),
522 })
523}
524
525pub fn gov_action_id(tx_hash: &str, index: u32) -> Result<GovActionId> {
528 let bytes: [u8; 32] = hex::decode(tx_hash.trim())
529 .ok()
530 .and_then(|b| b.try_into().ok())
531 .ok_or_else(|| anyhow!("A governance action's transaction is 64 hex characters"))?;
532 Ok(GovActionId {
533 transaction_id: Hash::new(bytes),
534 action_index: index,
535 })
536}
537
538pub fn parse_pool_id(id: &str) -> Result<Hash<28>> {
540 let id = id.trim();
541 let bytes = if id.len() == 56 && id.bytes().all(|b| b.is_ascii_hexdigit()) {
542 hex::decode(id).context("A pool ID in hex is 28 bytes")?
543 } else {
544 let (hrp, data) = bech32_of(id).ok_or_else(|| anyhow!("That isn't a stake pool ID"))?;
545 if hrp != "pool" {
546 bail!("That isn't a stake pool ID: pool IDs start pool1");
547 }
548 data
549 };
550 let hash: [u8; 28] = bytes
551 .try_into()
552 .map_err(|_| anyhow!("That isn't a stake pool ID: it's the wrong length"))?;
553 Ok(Hash::new(hash))
554}
555
556pub fn pool_id(hash: &Hash<28>) -> String {
558 bech32::encode("pool", hash.as_ref().to_base32(), Variant::Bech32)
559 .expect("a pool ID always encodes")
560}
561
562pub fn parse_drep(id: &str) -> Result<DRep> {
566 let id = id.trim();
567 match id {
568 ALWAYS_ABSTAIN => return Ok(DRep::Abstain),
569 ALWAYS_NO_CONFIDENCE => return Ok(DRep::NoConfidence),
570 _ => {}
571 }
572 let (hrp, data) = bech32_of(id).ok_or_else(|| anyhow!("That isn't a DRep ID"))?;
573 let hash = |bytes: &[u8]| -> Result<Hash<28>> {
574 let hash: [u8; 28] = bytes
575 .try_into()
576 .map_err(|_| anyhow!("That isn't a DRep ID: it's the wrong length"))?;
577 Ok(Hash::new(hash))
578 };
579 match (hrp.as_str(), data.len()) {
580 ("drep", 29) => match data[0] {
581 DREP_KEY_HEADER => Ok(DRep::Key(hash(&data[1..])?)),
582 DREP_SCRIPT_HEADER => Ok(DRep::Script(hash(&data[1..])?)),
583 _ => bail!("That isn't a DRep ID: its header names something else"),
584 },
585 ("drep", 28) => Ok(DRep::Key(hash(&data)?)),
586 ("drep_script", 28) => Ok(DRep::Script(hash(&data)?)),
587 ("drep" | "drep_script", _) => bail!("That isn't a DRep ID: it's the wrong length"),
588 _ => bail!("That isn't a DRep ID: DRep IDs start drep1"),
589 }
590}
591
592pub fn drep_id(drep: &DRep) -> String {
595 let (header, hash) = match drep {
596 DRep::Abstain => return ALWAYS_ABSTAIN.to_string(),
597 DRep::NoConfidence => return ALWAYS_NO_CONFIDENCE.to_string(),
598 DRep::Key(h) => (DREP_KEY_HEADER, h),
599 DRep::Script(h) => (DREP_SCRIPT_HEADER, h),
600 };
601 let bytes: Vec<u8> = std::iter::once(header)
602 .chain(hash.as_ref().iter().copied())
603 .collect();
604 bech32::encode("drep", bytes.to_base32(), Variant::Bech32).expect("a DRep ID always encodes")
605}
606
607fn bech32_of(text: &str) -> Option<(String, Vec<u8>)> {
609 let (hrp, data, variant) = bech32::decode(text).ok()?;
610 if variant != Variant::Bech32 {
611 return None;
612 }
613 Some((hrp, Vec::<u8>::from_base32(&data).ok()?))
614}