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

Inscribe png and text #800

Merged
merged 14 commits into from
Nov 17, 2022
146 changes: 76 additions & 70 deletions Cargo.lock

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ members = [".", "test-bitcoincore-rpc"]
anyhow = { version = "1.0.56", features = ["backtrace"] }
axum = "0.5.6"
axum-server = "0.4.0"
base64 = "0.13.1"
bitcoin = { version = "0.29.1", features = ["rand"] }
boilerplate = { version = "0.2.1", features = ["axum"] }
chrono = "0.4.19"
Expand All @@ -33,7 +34,7 @@ log = "0.4.14"
mime = "0.3.16"
mime_guess = "2.0.4"
ord-bitcoincore-rpc = "0.16.3"
redb = "0.8.0"
redb = "0.9.0"
raphjaph marked this conversation as resolved.
Show resolved Hide resolved
regex = "1.6.0"
reqwest = { version = "0.11.10", features = ["blocking"] }
rust-embed = "6.4.0"
Expand Down
2 changes: 1 addition & 1 deletion src/index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,7 @@ impl Index {
.begin_read()?
.open_table(ORDINAL_TO_INSCRIPTION)?
.get(&ordinal.n())?
.map(|inscription| Inscription(inscription.to_owned())),
raphjaph marked this conversation as resolved.
Show resolved Hide resolved
.map(|inscription| serde_json::from_str(inscription).unwrap())
)
}

Expand Down
7 changes: 6 additions & 1 deletion src/index/updater.rs
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,12 @@ impl Updater {
outputs_traversed: &mut u64,
) -> Result {
if let Some((ordinal, inscription)) = Inscription::from_transaction(tx, input_ordinal_ranges) {
ordinal_to_inscription.insert(&ordinal.n(), &inscription.0)?;
// let json = match inscription {
// Inscription::Text(content) => serde_json::json!({ "media_type": "text/plain;charset=utf-8", "content": content,}),
// Inscription::Png(content) => serde_json::json!({ "media_type": "image/png", "content": content,}),
// };
raphjaph marked this conversation as resolved.
Show resolved Hide resolved
let json = serde_json::to_string(&inscription).unwrap();
raphjaph marked this conversation as resolved.
Show resolved Hide resolved
ordinal_to_inscription.insert(&ordinal.n(), &json)?;
}

for (vout, output) in tx.output.iter().enumerate() {
Expand Down
52 changes: 42 additions & 10 deletions src/inscription.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,11 @@ use {
std::str::{self, Utf8Error},
};

#[derive(Debug, PartialEq)]
pub(crate) struct Inscription(pub(crate) String);
#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub(crate) enum Inscription {
Text(String),
Png(Vec<u8>),
}

impl Inscription {
raphjaph marked this conversation as resolved.
Show resolved Hide resolved
pub(crate) fn from_transaction(
Expand Down Expand Up @@ -99,19 +102,41 @@ impl<'a> InscriptionParser<'a> {

fn parse_inscription(&mut self) -> Result<Option<Inscription>> {
if self.advance()? == Instruction::Op(opcodes::all::OP_IF) {
let content = self.advance()?;
let media_type = self.advance()?;

let content = if let Instruction::PushBytes(bytes) = content {
let media_type = if let Instruction::PushBytes(bytes) = media_type {
raphjaph marked this conversation as resolved.
Show resolved Hide resolved
str::from_utf8(bytes).map_err(InscriptionError::Utf8Decode)?
} else {
return Err(InscriptionError::InvalidInscription);
};

let inscription = match media_type {
"text/plain;charset=utf-8" => {
let content = if let Instruction::PushBytes(bytes) = self.advance()? {
raphjaph marked this conversation as resolved.
Show resolved Hide resolved
str::from_utf8(bytes).map_err(InscriptionError::Utf8Decode)?
} else {
return Err(InscriptionError::InvalidInscription);
};

Some(Inscription::Text(content.to_string()))
}
"image/png" => {
let content = if let Instruction::PushBytes(bytes) = self.advance()? {
bytes.to_vec()
} else {
return Err(InscriptionError::InvalidInscription);
};

Some(Inscription::Png(content))
}
_ => None,
};

if self.advance()? != Instruction::Op(opcodes::all::OP_ENDIF) {
return Err(InscriptionError::InvalidInscription);
}

return Ok(Some(Inscription(content.to_string())));
return Ok(inscription);
}

Ok(None)
Expand Down Expand Up @@ -167,13 +192,14 @@ mod tests {
let script = script::Builder::new()
.push_opcode(opcodes::OP_FALSE)
.push_opcode(opcodes::all::OP_IF)
.push_slice("text/plain;charset=utf-8".as_bytes())
.push_slice("ord".as_bytes())
.push_opcode(opcodes::all::OP_ENDIF)
.into_script();

assert_eq!(
InscriptionParser::parse(&Witness::from_vec(vec![script.into_bytes(), vec![]])),
Ok(Inscription("ord".into()))
Ok(Inscription::Text("ord".to_string()))
raphjaph marked this conversation as resolved.
Show resolved Hide resolved
raphjaph marked this conversation as resolved.
Show resolved Hide resolved
);
}

Expand All @@ -182,14 +208,15 @@ mod tests {
let script = script::Builder::new()
.push_opcode(opcodes::OP_FALSE)
.push_opcode(opcodes::all::OP_IF)
.push_slice("text/plain;charset=utf-8".as_bytes())
.push_slice("ord".as_bytes())
.push_opcode(opcodes::all::OP_ENDIF)
.push_opcode(opcodes::all::OP_CHECKSIG)
.into_script();

assert_eq!(
InscriptionParser::parse(&Witness::from_vec(vec![script.into_bytes(), vec![]])),
Ok(Inscription("ord".into()))
Ok(Inscription::Text("ord".to_string()))
raphjaph marked this conversation as resolved.
Show resolved Hide resolved
);
}

Expand All @@ -199,13 +226,14 @@ mod tests {
.push_opcode(opcodes::all::OP_CHECKSIG)
.push_opcode(opcodes::OP_FALSE)
.push_opcode(opcodes::all::OP_IF)
.push_slice("text/plain;charset=utf-8".as_bytes())
.push_slice("ord".as_bytes())
.push_opcode(opcodes::all::OP_ENDIF)
.into_script();

assert_eq!(
InscriptionParser::parse(&Witness::from_vec(vec![script.into_bytes(), vec![]])),
Ok(Inscription("ord".into()))
Ok(Inscription::Text("ord".into()))
);
}

Expand All @@ -214,17 +242,19 @@ mod tests {
let script = script::Builder::new()
.push_opcode(opcodes::OP_FALSE)
.push_opcode(opcodes::all::OP_IF)
.push_slice("text/plain;charset=utf-8".as_bytes())
.push_slice("foo".as_bytes())
.push_opcode(opcodes::all::OP_ENDIF)
.push_opcode(opcodes::OP_FALSE)
.push_opcode(opcodes::all::OP_IF)
.push_slice("text/plain;charset=utf-8".as_bytes())
.push_slice("bar".as_bytes())
.push_opcode(opcodes::all::OP_ENDIF)
.into_script();

assert_eq!(
InscriptionParser::parse(&Witness::from_vec(vec![script.into_bytes(), vec![]])),
Ok(Inscription("foo".into()))
Ok(Inscription::Text("foo".into()))
);
}

Expand All @@ -233,6 +263,7 @@ mod tests {
let script = script::Builder::new()
.push_opcode(opcodes::OP_FALSE)
.push_opcode(opcodes::all::OP_IF)
.push_slice("text/plain;charset=utf-8".as_bytes())
.push_slice(&[0b10000000])
.push_opcode(opcodes::all::OP_ENDIF)
.into_script();
Expand Down Expand Up @@ -292,6 +323,7 @@ mod tests {
let script = script::Builder::new()
.push_opcode(opcodes::OP_FALSE)
.push_opcode(opcodes::all::OP_IF)
.push_slice("text/plain;charset=utf-8".as_bytes())
.push_slice("ord".as_bytes())
.push_opcode(opcodes::all::OP_ENDIF)
.into_script();
Expand All @@ -313,7 +345,7 @@ mod tests {

assert_eq!(
Inscription::from_transaction(&tx, &ranges),
Some((Ordinal(1), Inscription("ord".into())))
Some((Ordinal(1), Inscription::Text("ord".into())))
);
}

raphjaph marked this conversation as resolved.
Show resolved Hide resolved
Expand Down
6 changes: 2 additions & 4 deletions src/subcommand/server/templates/ordinal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ mod tests {
OrdinalHtml {
ordinal: Ordinal(0),
blocktime: Blocktime::Confirmed(0),
inscription: Some(Inscription("HELLOWORLD".to_string())),
inscription: Some(Inscription::Text("HELLOWORLD".to_string())),
raphjaph marked this conversation as resolved.
Show resolved Hide resolved
}
.to_string(),
"
Expand Down Expand Up @@ -117,9 +117,7 @@ mod tests {
OrdinalHtml {
ordinal: Ordinal(0),
blocktime: Blocktime::Confirmed(0),
inscription: Some(Inscription(
"<script>alert('HELLOWORLD');</script>".to_string()
)),
inscription: Some(Inscription::Text("<script>alert('HELLOWORLD');</script>".to_string())),
}
.to_string(),
"
Expand Down
28 changes: 26 additions & 2 deletions src/subcommand/wallet/inscribe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,33 @@ use {
util::taproot::{LeafVersion, TapLeafHash, TaprootBuilder},
PackedLockTime, SchnorrSighashType, Witness,
},
mime::Mime,
};

#[derive(Debug, Parser)]
pub(crate) struct Inscribe {
#[clap(long, help = "Inscribe on <ORDINAL>")]
raphjaph marked this conversation as resolved.
Show resolved Hide resolved
ordinal: Ordinal,
content: String,
#[clap(long, help = "Inscribe contents of this <FILE>")]
content: PathBuf,
#[clap(long, help = "Set the media type of the contents")]
media_type: Mime,
raphjaph marked this conversation as resolved.
Show resolved Hide resolved
}

impl Inscribe {
pub(crate) fn run(self, options: Options) -> Result {
let client = options.bitcoin_rpc_client_mainnet_forbidden("ord wallet inscribe")?;

if !matches!(self.media_type.as_ref(), "text/plain;charset=utf-8")
& !matches!(self.media_type.as_ref(), "image/png")
{
return Err(anyhow!(
"inscribe only accepts text/plain;charset=utf-8 and image/png"
));
}

let content = fs::read(self.content).unwrap();

let index = Index::open(&options)?;
index.update()?;

Expand All @@ -33,7 +48,8 @@ impl Inscribe {

let (unsigned_commit_tx, reveal_tx) = Inscribe::create_inscription_transactions(
self.ordinal,
self.content.as_bytes(),
self.media_type,
&content,
options.chain.network(),
utxos,
commit_tx_change,
Expand All @@ -59,6 +75,7 @@ impl Inscribe {

fn create_inscription_transactions(
ordinal: Ordinal,
media_type: Mime,
content: &[u8],
raphjaph marked this conversation as resolved.
Show resolved Hide resolved
network: bitcoin::Network,
utxos: Vec<(OutPoint, Vec<(u64, u64)>)>,
Expand All @@ -74,6 +91,7 @@ impl Inscribe {
.push_opcode(opcodes::all::OP_CHECKSIG)
.push_opcode(opcodes::OP_FALSE)
.push_opcode(opcodes::all::OP_IF)
.push_slice(media_type.as_ref().as_bytes())
.push_slice(content)
.push_opcode(opcodes::all::OP_ENDIF)
.into_script();
Expand Down Expand Up @@ -180,13 +198,15 @@ mod tests {
#[test]
fn reveal_transaction_pays_fee() {
let utxos = vec![(outpoint(1), vec![(10_000, 15_000)])];
let media_type = Mime::from_str("text/plain").unwrap();
let content = b"ord";
let ordinal = Ordinal(10_000);
let commit_address = change(0);
let reveal_address = recipient();

let (commit_tx, reveal_tx) = Inscribe::create_inscription_transactions(
ordinal,
media_type,
content,
bitcoin::Network::Signet,
utxos,
Expand All @@ -206,13 +226,15 @@ mod tests {
#[test]
fn reveal_transaction_value_insufficient_to_pay_fee() {
let utxos = vec![(outpoint(1), vec![(10_000, 11_000)])];
let media_type = Mime::from_str("text/plain").unwrap();
let content = [b'a'; 5000];
let ordinal = Ordinal(10_000);
let commit_address = change(0);
let reveal_address = recipient();

assert!(Inscribe::create_inscription_transactions(
ordinal,
media_type,
&content,
bitcoin::Network::Signet,
utxos,
Expand All @@ -227,13 +249,15 @@ mod tests {
#[test]
fn reveal_transaction_would_create_dust() {
let utxos = vec![(outpoint(1), vec![(10_000, 10_600)])];
let media_type = Mime::from_str("text/plain").unwrap();
let content = [b'a'; 1];
let ordinal = Ordinal(10_000);
let commit_address = change(0);
let reveal_address = recipient();

let error = Inscribe::create_inscription_transactions(
ordinal,
media_type,
&content,
bitcoin::Network::Signet,
utxos,
Expand Down
12 changes: 9 additions & 3 deletions templates/ordinal.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,15 @@ <h1>Ordinal {{ self.ordinal.n() }}</h1>
<dt>offset</dt><dd>{{ self.ordinal.third() }}</dd>
<dt>rarity</dt><dd><span class={{self.ordinal.rarity()}}>{{ self.ordinal.rarity() }}</span></dd>
<dt>time</dt><dd>{{ self.blocktime }}</dd>
%% if let Some(Inscription(inscription)) = &self.inscription {
<dt>inscription</dt><dd>{{inscription}}</dd>
%% }
%% match &self.inscription {
raphjaph marked this conversation as resolved.
Show resolved Hide resolved
%% Some(Inscription::Text(content)) => {
<dt>inscription</dt><dd>{{ content }}</dd>
%% },
%% Some(Inscription::Png(content)) => {
<dt>inscription</dt><dd><img src="data:image/png;base64,{{ base64::encode(&content) }}"></dd>
raphjaph marked this conversation as resolved.
Show resolved Hide resolved
%% },
%% None => {}
%% };
</dl>
%% if self.ordinal.n() > 0 {
<a href=/ordinal/{{self.ordinal.n() - 1}}>prev</a>
Expand Down
Loading