ord 0.27.1

◉ Ordinal wallet and block explorer
Documentation
use super::*;

#[derive(Debug, Parser)]
pub(crate) struct Sats {
  #[arg(
    long,
    conflicts_with = "all",
    help = "Find satoshis listed in first column of tab-separated value file <TSV>."
  )]
  tsv: Option<PathBuf>,
  #[arg(
    long,
    conflicts_with = "tsv",
    help = "Display list of all sat ranges in wallet."
  )]
  all: bool,
}

#[derive(Serialize, Deserialize)]
pub struct OutputTsv {
  pub found: BTreeMap<String, SatPoint>,
  pub lost: BTreeSet<String>,
}

#[derive(Serialize, Deserialize)]
pub struct OutputRare {
  pub sat: Sat,
  pub output: OutPoint,
  pub offset: u64,
  pub rarity: Rarity,
}

#[derive(Serialize, Deserialize, Debug, PartialEq)]
pub struct OutputAll {
  pub output: OutPoint,
  pub ranges: Vec<String>,
}

impl Sats {
  pub(crate) fn run(&self, wallet: Wallet) -> SubcommandResult {
    ensure!(
      wallet.has_sat_index(),
      "sats requires index created with `--index-sats` flag"
    );

    let haystacks = wallet.get_wallet_sat_ranges()?;

    if self.all {
      Ok(Some(Box::new(
        haystacks
          .into_iter()
          .map(|(outpoint, ranges)| OutputAll {
            output: outpoint,
            ranges: ranges
              .into_iter()
              .map(|range| format!("{}-{}", range.0, range.1))
              .collect::<Vec<String>>(),
          })
          .collect::<Vec<OutputAll>>(),
      )))
    } else if let Some(path) = &self.tsv {
      let tsv = fs::read_to_string(path)
        .with_context(|| format!("I/O error reading `{}`", path.display()))?;

      let needles = Self::needles(&tsv)?;

      let found = Self::find(&needles, &haystacks);

      let lost = needles
        .into_iter()
        .filter(|(_sat, value)| !found.contains_key(*value))
        .map(|(_sat, value)| value.into())
        .collect();

      Ok(Some(Box::new(OutputTsv { found, lost })))
    } else {
      let mut output = Vec::new();
      for (outpoint, sat, offset, rarity) in Self::rare_sats(haystacks) {
        output.push(OutputRare {
          sat,
          output: outpoint,
          offset,
          rarity,
        });
      }

      Ok(Some(Box::new(output)))
    }
  }

  fn find(
    needles: &[(Sat, &str)],
    ranges: &[(OutPoint, Vec<(u64, u64)>)],
  ) -> BTreeMap<String, SatPoint> {
    let mut haystacks = Vec::new();

    for (outpoint, ranges) in ranges {
      let mut offset = 0;
      for (start, end) in ranges {
        haystacks.push((start, end, offset, outpoint));
        offset += end - start;
      }
    }

    haystacks.sort_by_key(|(start, _, _, _)| *start);

    let mut i = 0;
    let mut j = 0;
    let mut results = BTreeMap::new();
    while i < needles.len() && j < haystacks.len() {
      let (needle, value) = needles[i];
      let (&start, &end, offset, outpoint) = haystacks[j];

      if needle >= start && needle < end {
        results.insert(
          value.into(),
          SatPoint {
            outpoint: *outpoint,
            offset: offset + needle.0 - start,
          },
        );
      }

      if needle >= end {
        j += 1;
      } else {
        i += 1;
      }
    }

    results
  }

  fn needles(tsv: &str) -> Result<Vec<(Sat, &str)>> {
    let mut needles = tsv
      .lines()
      .enumerate()
      .filter(|(_i, line)| !line.starts_with('#') && !line.is_empty())
      .filter_map(|(i, line)| {
        line.split('\t').next().map(|value| {
          Sat::from_str(value).map(|sat| (sat, value)).map_err(|err| {
            anyhow!(
              "failed to parse sat from string \"{value}\" on line {}: {err}",
              i + 1,
            )
          })
        })
      })
      .collect::<Result<Vec<(Sat, &str)>>>()?;

    needles.sort();

    Ok(needles)
  }

  fn rare_sats(haystacks: Vec<(OutPoint, Vec<(u64, u64)>)>) -> Vec<(OutPoint, Sat, u64, Rarity)> {
    haystacks
      .into_iter()
      .flat_map(|(outpoint, sat_ranges)| {
        let mut offset = 0;
        sat_ranges.into_iter().filter_map(move |(start, end)| {
          let sat = Sat(start);
          let rarity = sat.rarity();
          let start_offset = offset;
          offset += end - start;
          if rarity > Rarity::Common {
            Some((outpoint, sat, start_offset, rarity))
          } else {
            None
          }
        })
      })
      .collect()
  }
}

#[cfg(test)]
mod tests {
  use super::*;

  #[test]
  fn identify_no_rare_sats() {
    assert_eq!(
      Sats::rare_sats(vec![(
        outpoint(1),
        vec![(51 * COIN_VALUE, 100 * COIN_VALUE), (1234, 5678)],
      )]),
      Vec::new()
    )
  }

  #[test]
  fn identify_one_rare_sat() {
    assert_eq!(
      Sats::rare_sats(vec![(
        outpoint(1),
        vec![(10, 80), (50 * COIN_VALUE, 100 * COIN_VALUE)],
      )]),
      vec![(outpoint(1), Sat(50 * COIN_VALUE), 70, Rarity::Uncommon)]
    )
  }

  #[test]
  fn identify_two_rare_sats() {
    assert_eq!(
      Sats::rare_sats(vec![(
        outpoint(1),
        vec![(0, 100), (1050000000000000, 1150000000000000)],
      )]),
      vec![
        (outpoint(1), Sat(0), 0, Rarity::Mythic),
        (outpoint(1), Sat(1050000000000000), 100, Rarity::Epic)
      ]
    )
  }

  #[test]
  fn identify_rare_sats_in_different_outpoints() {
    assert_eq!(
      Sats::rare_sats(vec![
        (outpoint(1), vec![(50 * COIN_VALUE, 55 * COIN_VALUE)]),
        (outpoint(2), vec![(100 * COIN_VALUE, 111 * COIN_VALUE)],),
      ]),
      vec![
        (outpoint(1), Sat(50 * COIN_VALUE), 0, Rarity::Uncommon),
        (outpoint(2), Sat(100 * COIN_VALUE), 0, Rarity::Uncommon)
      ]
    )
  }

  #[track_caller]
  fn case(tsv: &str, haystacks: &[(OutPoint, Vec<(u64, u64)>)], expected: &[(&str, SatPoint)]) {
    assert_eq!(
      Sats::find(&Sats::needles(tsv).unwrap(), haystacks),
      expected
        .iter()
        .map(|(sat, satpoint)| (sat.to_string(), *satpoint))
        .collect()
    );
  }

  #[test]
  fn tsv() {
    case("1\n", &[(outpoint(1), vec![(0, 1)])], &[]);
  }

  #[test]
  fn identify_from_tsv_single() {
    case(
      "0\n",
      &[(outpoint(1), vec![(0, 1)])],
      &[("0", satpoint(1, 0))],
    );
  }

  #[test]
  fn identify_from_tsv_two_in_one_range() {
    case(
      "0\n1\n",
      &[(outpoint(1), vec![(0, 2)])],
      &[("0", satpoint(1, 0)), ("1", satpoint(1, 1))],
    );
  }

  #[test]
  fn identify_from_tsv_out_of_order_tsv() {
    case(
      "1\n0\n",
      &[(outpoint(1), vec![(0, 2)])],
      &[("0", satpoint(1, 0)), ("1", satpoint(1, 1))],
    );
  }

  #[test]
  fn identify_from_tsv_out_of_order_ranges() {
    case(
      "1\n0\n",
      &[(outpoint(1), vec![(1, 2), (0, 1)])],
      &[("0", satpoint(1, 1)), ("1", satpoint(1, 0))],
    );
  }

  #[test]
  fn identify_from_tsv_two_in_two_ranges() {
    case(
      "0\n1\n",
      &[(outpoint(1), vec![(0, 1), (1, 2)])],
      &[("0", satpoint(1, 0)), ("1", satpoint(1, 1))],
    )
  }

  #[test]
  fn identify_from_tsv_two_in_two_outputs() {
    case(
      "0\n1\n",
      &[(outpoint(1), vec![(0, 1)]), (outpoint(2), vec![(1, 2)])],
      &[("0", satpoint(1, 0)), ("1", satpoint(2, 0))],
    );
  }

  #[test]
  fn identify_from_tsv_ignores_extra_columns() {
    case(
      "0\t===\n",
      &[(outpoint(1), vec![(0, 1)])],
      &[("0", satpoint(1, 0))],
    );
  }

  #[test]
  fn identify_from_tsv_ignores_empty_lines() {
    case(
      "0\n\n\n",
      &[(outpoint(1), vec![(0, 1)])],
      &[("0", satpoint(1, 0))],
    );
  }

  #[test]
  fn identify_from_tsv_ignores_comments() {
    case(
      "0\n#===\n",
      &[(outpoint(1), vec![(0, 1)])],
      &[("0", satpoint(1, 0))],
    );
  }

  #[test]
  fn parse_error_reports_line_and_value() {
    assert_eq!(
      Sats::needles("0\n===\n").unwrap_err().to_string(),
      "failed to parse sat from string \"===\" on line 2: failed to parse sat `===`: invalid integer: invalid digit found in string",
    );
  }
}