Skip to content

Commit

Permalink
Add PNG inscriptions (ordinals#800)
Browse files Browse the repository at this point in the history
  • Loading branch information
raphjaph authored Nov 17, 2022
1 parent 60b1f7d commit f8d8206
Show file tree
Hide file tree
Showing 9 changed files with 174 additions and 31 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions 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 Down
5 changes: 4 additions & 1 deletion src/index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,10 @@ impl Index {
.begin_read()?
.open_table(ORDINAL_TO_INSCRIPTION)?
.get(&ordinal.n())?
.map(|inscription| Inscription(inscription.to_owned())),
.map(|inscription| {
serde_json::from_str(inscription)
.expect("failed to deserialize inscription (JSON) from database")
}),
)
}

Expand Down
4 changes: 3 additions & 1 deletion src/index/updater.rs
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,9 @@ impl Updater {
if self.chain != Chain::Mainnet {
if let Some((ordinal, inscription)) = Inscription::from_transaction(tx, input_ordinal_ranges)
{
ordinal_to_inscription.insert(&ordinal.n(), &inscription.0)?;
let json = serde_json::to_string(&inscription)
.expect("Inscription serialization should always succeed");
ordinal_to_inscription.insert(&ordinal.n(), &json)?;
}
}

Expand Down
100 changes: 88 additions & 12 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 {
pub(crate) fn from_transaction(
Expand All @@ -25,6 +28,41 @@ impl Inscription {

Some((Ordinal(*start), inscription))
}

pub(crate) fn from_file(path: PathBuf) -> Result<Self, Error> {
let file = fs::read(&path).with_context(|| format!("io error reading {}", path.display()))?;

if file.len() > 520 {
bail!("file size exceeds 520 bytes");
}

match path
.extension()
.ok_or_else(|| anyhow!("file must have extension"))?
.to_str()
.ok_or_else(|| anyhow!("unrecognized extension"))?
{
"txt" => Ok(Inscription::Text(String::from_utf8(file)?)),
"png" => Ok(Inscription::Png(file)),
other => Err(anyhow!(
"unrecognized file extension `.{other}`, only .txt and .png accepted"
)),
}
}

pub(crate) fn media_type(&self) -> &str {
match self {
Inscription::Text(_) => "text/plain;charset=utf-8",
Inscription::Png(_) => "image/png",
}
}

pub(crate) fn content(&self) -> &[u8] {
match self {
Inscription::Text(text) => text.as_bytes(),
Inscription::Png(png) => png.as_ref(),
}
}
}

#[derive(Debug, PartialEq)]
Expand Down Expand Up @@ -99,19 +137,33 @@ 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 content = if let Instruction::PushBytes(bytes) = content {
let media_type = if let Instruction::PushBytes(bytes) = self.advance()? {
str::from_utf8(bytes).map_err(InscriptionError::Utf8Decode)?
} else {
return Err(InscriptionError::InvalidInscription);
};

let content = if let Instruction::PushBytes(bytes) = self.advance()? {
bytes
} else {
return Err(InscriptionError::InvalidInscription);
};

let inscription = match media_type {
"text/plain;charset=utf-8" => Some(Inscription::Text(
str::from_utf8(content)
.map_err(InscriptionError::Utf8Decode)?
.into(),
)),
"image/png" => Some(Inscription::Png(content.to_vec())),
_ => 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 +219,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".into()))
);
}

Expand All @@ -182,14 +235,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".into()))
);
}

Expand All @@ -199,13 +253,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 +269,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 +290,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 @@ -278,12 +336,13 @@ mod tests {
.push_opcode(opcodes::all::OP_IF)
.push_slice("ord".as_bytes())
.push_slice("ord".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![]])),
Err(InscriptionError::InvalidInscription)
Err(InscriptionError::InvalidInscription),
);
}

Expand All @@ -292,6 +351,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 +373,7 @@ mod tests {

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

Expand Down Expand Up @@ -377,4 +437,20 @@ mod tests {

assert_eq!(Inscription::from_transaction(&tx, &ranges), None,);
}

#[test]
fn inscribe_png() {
let script = script::Builder::new()
.push_opcode(opcodes::OP_FALSE)
.push_opcode(opcodes::all::OP_IF)
.push_slice("image/png".as_bytes())
.push_slice(&[1; 100])
.push_opcode(opcodes::all::OP_ENDIF)
.into_script();

assert_eq!(
InscriptionParser::parse(&Witness::from_vec(vec![script.into_bytes(), vec![]])),
Ok(Inscription::Png(vec![1; 100]))
);
}
}
6 changes: 3 additions & 3 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".into())),
}
.to_string(),
"
Expand Down Expand Up @@ -117,8 +117,8 @@ 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>".into()
)),
}
.to_string(),
Expand Down
25 changes: 15 additions & 10 deletions src/subcommand/wallet/inscribe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,18 @@ use {

#[derive(Debug, Parser)]
pub(crate) struct Inscribe {
#[clap(long, help = "Inscribe <ORDINAL>")]
ordinal: Ordinal,
content: String,
#[clap(long, help = "Inscribe ordinal with contents of <FILE>")]
file: PathBuf,
}

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

let inscription = Inscription::from_file(self.file)?;

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

Expand All @@ -33,7 +37,7 @@ impl Inscribe {

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

fn create_inscription_transactions(
ordinal: Ordinal,
content: &[u8],
inscription: Inscription,
network: bitcoin::Network,
utxos: Vec<(OutPoint, Vec<(u64, u64)>)>,
change: Vec<Address>,
Expand All @@ -74,7 +78,8 @@ impl Inscribe {
.push_opcode(opcodes::all::OP_CHECKSIG)
.push_opcode(opcodes::OP_FALSE)
.push_opcode(opcodes::all::OP_IF)
.push_slice(content)
.push_slice(inscription.media_type().as_bytes())
.push_slice(inscription.content())
.push_opcode(opcodes::all::OP_ENDIF)
.into_script();

Expand Down Expand Up @@ -180,14 +185,14 @@ mod tests {
#[test]
fn reveal_transaction_pays_fee() {
let utxos = vec![(outpoint(1), vec![(10_000, 15_000)])];
let content = b"ord";
let inscription = Inscription::Text("ord".into());
let ordinal = Ordinal(10_000);
let commit_address = change(0);
let reveal_address = recipient();

let (commit_tx, reveal_tx) = Inscribe::create_inscription_transactions(
ordinal,
content,
inscription,
bitcoin::Network::Signet,
utxos,
vec![commit_address, change(1)],
Expand All @@ -206,14 +211,14 @@ mod tests {
#[test]
fn reveal_transaction_value_insufficient_to_pay_fee() {
let utxos = vec![(outpoint(1), vec![(10_000, 11_000)])];
let content = [b'a'; 5000];
let ordinal = Ordinal(10_000);
let inscription = Inscription::Png([1; 10_000].to_vec());
let commit_address = change(0);
let reveal_address = recipient();

assert!(Inscribe::create_inscription_transactions(
ordinal,
&content,
inscription,
bitcoin::Network::Signet,
utxos,
vec![commit_address, change(1)],
Expand All @@ -227,14 +232,14 @@ mod tests {
#[test]
fn reveal_transaction_would_create_dust() {
let utxos = vec![(outpoint(1), vec![(10_000, 10_600)])];
let content = [b'a'; 1];
let inscription = Inscription::Text("ord".into());
let ordinal = Ordinal(10_000);
let commit_address = change(0);
let reveal_address = recipient();

let error = Inscribe::create_inscription_transactions(
ordinal,
&content,
inscription,
bitcoin::Network::Signet,
utxos,
vec![commit_address, change(1)],
Expand Down
Loading

0 comments on commit f8d8206

Please sign in to comment.