Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Send runes with ord wallet send #2858

Merged
merged 18 commits into from
Dec 15, 2023
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
Loading