Skip to content

Commit

Permalink
Send runes with ord wallet send (ordinals#2858)
Browse files Browse the repository at this point in the history
  • Loading branch information
casey authored Dec 15, 2023
1 parent bf37836 commit c6a87ab
Show file tree
Hide file tree
Showing 23 changed files with 1,003 additions and 149 deletions.
3 changes: 0 additions & 3 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,6 @@ fmt:
clippy:
cargo clippy --all --all-targets -- -D warnings

lclippy:
cargo lclippy --all --all-targets -- -D warnings

deploy branch remote chain domain:
ssh root@{{domain}} "mkdir -p deploy \
&& apt-get update --yes \
Expand Down
150 changes: 125 additions & 25 deletions src/decimal.rs
Original file line number Diff line number Diff line change
@@ -1,23 +1,68 @@
use super::*;

#[derive(PartialEq, Debug)]
#[derive(Debug, PartialEq, Copy, Clone)]
pub(crate) struct Decimal {
height: Height,
offset: u64,
value: u128,
scale: u8,
}

impl From<Sat> for Decimal {
fn from(sat: Sat) -> Self {
Self {
height: sat.height(),
offset: sat.third(),
impl Decimal {
pub(crate) fn to_amount(self, divisibility: u8) -> Result<u128> {
match divisibility.checked_sub(self.scale) {
Some(difference) => Ok(
self
.value
.checked_mul(
10u128
.checked_pow(u32::from(difference))
.context("divisibility out of range")?,
)
.context("amount out of range")?,
),
None => bail!("excessive precision"),
}
}
}

impl Display for Decimal {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "{}.{}", self.height, self.offset)
impl FromStr for Decimal {
type Err = Error;

fn from_str(s: &str) -> Result<Self, Self::Err> {
if let Some((integer, decimal)) = s.split_once('.') {
if integer.is_empty() && decimal.is_empty() {
bail!("empty decimal");
}

let integer = if integer.is_empty() {
0
} else {
integer.parse::<u128>()?
};

let decimal = if decimal.is_empty() {
0
} else {
decimal.parse::<u128>()?
};

let scale = s
.trim_end_matches('0')
.chars()
.skip_while(|c| *c != '.')
.skip(1)
.count()
.try_into()?;

Ok(Self {
value: integer * 10u128.pow(u32::from(scale)) + decimal,
scale,
})
} else {
Ok(Self {
value: s.parse::<u128>()?,
scale: 0,
})
}
}
}

Expand All @@ -26,27 +71,82 @@ mod tests {
use super::*;

#[test]
fn decimal() {
fn from_str() {
#[track_caller]
fn case(s: &str, value: u128, scale: u8) {
assert_eq!(s.parse::<Decimal>().unwrap(), Decimal { value, scale });
}

assert_eq!(
Sat(0).decimal(),
Decimal {
height: Height(0),
offset: 0
}
".".parse::<Decimal>().unwrap_err().to_string(),
"empty decimal",
);

assert_eq!(
Sat(1).decimal(),
Decimal {
height: Height(0),
offset: 1
}
"a.b".parse::<Decimal>().unwrap_err().to_string(),
"invalid digit found in string",
);

assert_eq!(
" 0.1 ".parse::<Decimal>().unwrap_err().to_string(),
"invalid digit found in string",
);

case("0", 0, 0);
case("0.00000", 0, 0);
case("1.0", 1, 0);
case("1.1", 11, 1);
case("1.11", 111, 2);
case("1.", 1, 0);
case(".1", 1, 1);
}

#[test]
fn to_amount() {
#[track_caller]
fn case(s: &str, divisibility: u8, amount: u128) {
assert_eq!(
s.parse::<Decimal>()
.unwrap()
.to_amount(divisibility)
.unwrap(),
amount,
);
}

assert_eq!(
Decimal { value: 0, scale: 0 }
.to_amount(255)
.unwrap_err()
.to_string(),
"divisibility out of range"
);

assert_eq!(
Sat(2099999997689999).decimal(),
Decimal {
height: Height(6929999),
offset: 0
value: u128::MAX,
scale: 0,
}
.to_amount(1)
.unwrap_err()
.to_string(),
"amount out of range",
);

assert_eq!(
Decimal { value: 1, scale: 1 }
.to_amount(0)
.unwrap_err()
.to_string(),
"excessive precision",
);

case("1", 0, 1);
case("1.0", 0, 1);
case("1.0", 1, 10);
case("1.2", 1, 12);
case("1.2", 2, 120);
case("123.456", 3, 123456);
case("123.456", 6, 123456000);
}
}
52 changes: 52 additions & 0 deletions src/decimal_sat.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
use super::*;

#[derive(PartialEq, Debug)]
pub(crate) struct DecimalSat {
height: Height,
offset: u64,
}

impl From<Sat> for DecimalSat {
fn from(sat: Sat) -> Self {
Self {
height: sat.height(),
offset: sat.third(),
}
}
}

impl Display for DecimalSat {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "{}.{}", self.height, self.offset)
}
}

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

#[test]
fn decimal() {
assert_eq!(
Sat(0).decimal(),
DecimalSat {
height: Height(0),
offset: 0
}
);
assert_eq!(
Sat(1).decimal(),
DecimalSat {
height: Height(0),
offset: 1
}
);
assert_eq!(
Sat(2099999997689999).decimal(),
DecimalSat {
height: Height(6929999),
offset: 0
}
);
}
}
2 changes: 1 addition & 1 deletion src/envelope.rs
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ impl RawEnvelope {

#[cfg(test)]
mod tests {
use {super::*, bitcoin::absolute::LockTime};
use super::*;

fn parse(witnesses: &[Witness]) -> Vec<ParsedEnvelope> {
ParsedEnvelope::from_transaction(&Transaction {
Expand Down
26 changes: 26 additions & 0 deletions src/index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -888,6 +888,32 @@ impl Index {
Ok(entries)
}

pub(crate) fn get_rune_balance(&self, outpoint: OutPoint, id: RuneId) -> Result<u128> {
let rtx = self.database.begin_read()?;

let outpoint_to_balances = rtx.open_table(OUTPOINT_TO_RUNE_BALANCES)?;

let Some(balances) = outpoint_to_balances.get(&outpoint.store())? else {
return Ok(0);
};

let balances_buffer = balances.value();

let mut i = 0;
while i < balances_buffer.len() {
let (balance_id, length) = runes::varint::decode(&balances_buffer[i..]);
i += length;
let (amount, length) = runes::varint::decode(&balances_buffer[i..]);
i += length;

if RuneId::try_from(balance_id).unwrap() == id {
return Ok(amount);
}
}

Ok(0)
}

pub(crate) fn get_rune_balances_for_outpoint(
&self,
outpoint: OutPoint,
Expand Down
36 changes: 32 additions & 4 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ use {
charm::Charm,
config::Config,
decimal::Decimal,
decimal_sat::DecimalSat,
degree::Degree,
deserialize_from_str::DeserializeFromStr,
envelope::ParsedEnvelope,
Expand All @@ -28,22 +29,25 @@ use {
options::Options,
outgoing::Outgoing,
representation::Representation,
runes::{Edict, Etching, Pile, SpacedRune},
runes::{Etching, Pile, SpacedRune},
subcommand::{Subcommand, SubcommandResult},
tally::Tally,
},
anyhow::{anyhow, bail, ensure, Context, Error},
bip39::Mnemonic,
bitcoin::{
address::{Address, NetworkUnchecked},
blockdata::constants::COIN_VALUE,
blockdata::constants::{DIFFCHANGE_INTERVAL, SUBSIDY_HALVING_INTERVAL},
blockdata::{
constants::{COIN_VALUE, DIFFCHANGE_INTERVAL, SUBSIDY_HALVING_INTERVAL},
locktime::absolute::LockTime,
},
consensus::{self, Decodable, Encodable},
hash_types::BlockHash,
hashes::Hash,
opcodes,
script::{self, Instruction},
Amount, Block, Network, OutPoint, Script, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Txid,
Witness,
},
bitcoincore_rpc::{Client, RpcApi},
chain::Chain,
Expand Down Expand Up @@ -85,7 +89,7 @@ pub use self::{
inscription::Inscription,
object::Object,
rarity::Rarity,
runes::{Rune, RuneId, Runestone},
runes::{Edict, Rune, RuneId, Runestone},
sat::Sat,
sat_point::SatPoint,
subcommand::wallet::transaction_builder::{Target, TransactionBuilder},
Expand Down Expand Up @@ -114,6 +118,7 @@ mod chain;
mod charm;
mod config;
mod decimal;
mod decimal_sat;
mod degree;
mod deserialize_from_str;
mod envelope;
Expand Down Expand Up @@ -149,6 +154,29 @@ static INDEXER: Mutex<Option<thread::JoinHandle<()>>> = Mutex::new(Option::None)

const TARGET_POSTAGE: Amount = Amount::from_sat(10_000);

#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
fn fund_raw_transaction(
client: &Client,
fee_rate: FeeRate,
unfunded_transaction: &Transaction,
) -> Result<Vec<u8>> {
Ok(
client
.fund_raw_transaction(
unfunded_transaction,
Some(&bitcoincore_rpc::json::FundRawTransactionOptions {
// NB. This is `fundrawtransaction`'s `feeRate`, which is fee per kvB
// and *not* fee per vB. So, we multiply the fee rate given by the user
// by 1000.
fee_rate: Some(Amount::from_sat((fee_rate.n() * 1000.0).ceil() as u64)),
..Default::default()
}),
Some(false),
)?
.hex,
)
}

fn integration_test() -> bool {
env::var_os("ORD_INTEGRATION_TEST")
.map(|value| value.len() > 0)
Expand Down
Loading

0 comments on commit c6a87ab

Please sign in to comment.