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
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())),
raphjaph marked this conversation as resolved.
Show resolved Hide resolved
.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 {
raphjaph marked this conversation as resolved.
Show resolved Hide resolved
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())))
);
}

raphjaph marked this conversation as resolved.
Show resolved Hide resolved
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