Skip to content

Commit

Permalink
[Bitcoin/Rust] Add support for creating Ordinal NFT inscriptions (#3297)
Browse files Browse the repository at this point in the history
* expand comment on MIME data prefix in the construction of ordinal inscription

* add NFT module

* update comment on push_opcode

* add test for NFT inscription

* expand full list of inscription types

* compare NFT inscription test data with expected values

* avoid splitting expected values into individual consts, use slices directly

* fix warnings

* rename new_image to just new

* add tw_build_nft_inscription FFI function

* test tw_build_nft_inscription FFI function

* test protobuf NFT inscriptions

* rename ImageType to MimeType

* rename TW::Rust::tw_build_brc20_inscribe_transfer to TW::Rust::tw_build_brc20_transfer_inscription in Script.cpp

* bitcoin-nft-inscriptions

* add Script::buildNftInscription to CXX files, include correct path to MimeType

* add CXX test for SignNftInscription

* add CXX test SignNftInscriptionReveal

* expand C interfaces with NFT inscription construction

* track TWBitcoinOrdinalsMimeType.h

* add Swift tests for NFT inscription

* track TWOrdMimeType.h

* correctly set payload when reading from file

* compare substrings

* avoid var name reuse

* add nft inscription hex data of image and expected output

* small cleanup

* replace TW::Rust::MimeType with TWOrdMimeType

* pass raw integer to Rust, derived from enum variant

* reverse txId for Swift tests

* trigger CI

* run cargo fmt

* revert Podlock and update rust/coverage.stats

* update wallet-core-kotlin version in kmp

* clear todos

* rename tw_build_nft_inscription to tw_build_ordinal_nft_inscription

* remove Foundation import in Swift tests

* add Ordinal prefix to Nft inscriptions where appropriate, deprecate MimeType enum and use strings directly

* pass mime type as string from C++ to Rust

* rename tw_build_ordinal_nft_inscription to tw_bitcoin_build_nft_inscription, fix CoinType import issue

* update wallet-core-kotlin

* update Pods in samples

* run cargo fmt

* update pods in swift/ and swift/Example

* fix how mime type is passed in swift tests

* pass string directly to Rust functions

* undo pod changes in samples

* run cargo fmt

* embed image content and raw transaction in CPP file directly

* embed image as raw hex in Rust

* add embedded data in CPP files into separate file
  • Loading branch information
lamafab authored Jul 17, 2023
1 parent f266567 commit 53c1b1d
Show file tree
Hide file tree
Showing 23 changed files with 2,236 additions and 47 deletions.
12 changes: 11 additions & 1 deletion include/TrustWalletCore/TWBitcoinScript.h
Original file line number Diff line number Diff line change
Expand Up @@ -203,14 +203,24 @@ struct TWBitcoinScript* _Nonnull TWBitcoinScriptBuildPayToWitnessScriptHash(TWDa
TW_EXPORT_STATIC_METHOD
TWData* _Nonnull TWBitcoinScriptBuildBRC20InscribeTransfer(TWString* _Nonnull ticker, TWString* _Nonnull amount, TWData* _Nonnull pubkey);

/// Builds the Ordinals inscripton for NFT construction.
///
/// \param mimeType the MIME type of the payload
/// \param payload the payload to inscribe
/// \param pubkey Non-null pointer to a pubkey
/// \note Must be deleted with \TWBitcoinScriptDelete
/// \return A pointer to the built script
TW_EXPORT_STATIC_METHOD
TWData* _Nonnull TWBitcoinScriptBuildOrdinalNftInscription(TWString* _Nonnull mimeType, TWData* _Nonnull payload, TWData* _Nonnull pubkey);

/// Builds a appropriate lock script for the given address..
///
/// \param address Non-null pointer to an address
/// \param coin coin type
/// \note Must be deleted with \TWBitcoinScriptDelete
/// \return A pointer to the built script
TW_EXPORT_STATIC_METHOD
struct TWBitcoinScript* _Nonnull TWBitcoinScriptLockScriptForAddress(TWString* _Nonnull address, enum TWCoinType coin);
struct TWBitcoinScript *_Nonnull TWBitcoinScriptLockScriptForAddress(TWString* _Nonnull address, enum TWCoinType coin);

/// Builds a appropriate lock script for the given address with replay.
TW_EXPORT_STATIC_METHOD
Expand Down
2 changes: 1 addition & 1 deletion rust/coverage.stats
Original file line number Diff line number Diff line change
@@ -1 +1 @@
87.8
86.4
7 changes: 6 additions & 1 deletion rust/tw_bitcoin/src/ffi/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ pub(crate) fn taproot_build_and_sign_transaction(proto: SigningInput) -> Result<
script_buf,
)
.into(),
TrVariant::BRC20TRANSFER => {
TrVariant::BRC20TRANSFER | TrVariant::NFTINSCRIPTION => {
// We construct the merkle root for the given spending script.
let spending_script = ScriptBuf::from_bytes(input.spendingScript.to_vec());
let merkle_root = TapNodeHash::from_script(
Expand Down Expand Up @@ -155,9 +155,14 @@ pub(crate) fn taproot_build_and_sign_transaction(proto: SigningInput) -> Result<
TrVariant::P2TRKEYPATH => {
TxOutputP2TRKeyPath::new_with_script(satoshis, script_buf).into()
},
// We're keeping those two variants separate for now, we're planning
// on writing a new interface as part of a larger task anyway.
TrVariant::BRC20TRANSFER => {
TXOutputP2TRScriptPath::new_with_script(satoshis, script_buf).into()
},
TrVariant::NFTINSCRIPTION => {
TXOutputP2TRScriptPath::new_with_script(satoshis, script_buf).into()
}
};

builder = builder.add_output(tx);
Expand Down
65 changes: 59 additions & 6 deletions rust/tw_bitcoin/src/ffi/scripts.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
use crate::brc20::{BRC20TransferInscription, Ticker};
use crate::nft::OrdinalNftInscription;
use crate::{
Recipient, TXOutputP2TRScriptPath, TxOutputP2PKH, TxOutputP2TRKeyPath, TxOutputP2WPKH,
};
use bitcoin::{PublicKey, WPubkeyHash};
use std::borrow::Cow;
use std::ffi::{c_char, CStr};
use tw_memory::ffi::c_byte_array::CByteArray;
use tw_memory::ffi::c_byte_array_ref::CByteArrayRef;
use tw_misc::try_or_else;
Expand Down Expand Up @@ -98,23 +100,25 @@ pub unsafe extern "C" fn tw_build_p2tr_key_path_script(

#[no_mangle]
// Builds the Ordinals inscripton for BRC20 transfer.
pub unsafe extern "C" fn tw_build_brc20_inscribe_transfer(
pub unsafe extern "C" fn tw_build_brc20_transfer_inscription(
// The 4-byte ticker.
ticker: *const u8,
ticker: *const c_char,
amount: u64,
satoshis: i64,
pubkey: *const u8,
pubkey_len: usize,
) -> CByteArray {
// Convert ticket.
let slice = try_or_else!(CByteArrayRef::new(ticker, 4).as_slice(), CByteArray::null);
let ticker = match CStr::from_ptr(ticker).to_str() {
Ok(input) => input,
Err(_) => return CByteArray::null(),
};

if slice.len() != 4 {
if ticker.len() != 4 {
return CByteArray::null();
}

let string = try_or_else!(String::from_utf8(slice.to_vec()), CByteArray::null);
let ticker = Ticker::new(string).expect("ticker must be 4 bytes");
let ticker = Ticker::new(ticker.to_string()).expect("ticker must be 4 bytes");

// Convert Recipient
let slice = try_or_else!(
Expand Down Expand Up @@ -142,3 +146,52 @@ pub unsafe extern "C" fn tw_build_brc20_inscribe_transfer(

CByteArray::from(serialized)
}

#[no_mangle]
// Builds the Ordinals inscripton for BRC20 transfer.
pub unsafe extern "C" fn tw_bitcoin_build_nft_inscription(
mime_type: *const c_char,
data: *const u8,
data_len: usize,
satoshis: i64,
pubkey: *const u8,
pubkey_len: usize,
) -> CByteArray {
// Convert mimeType.
let mime_type = match CStr::from_ptr(mime_type).to_str() {
Ok(input) => input,
Err(_) => return CByteArray::null(),
};

// Convert data to inscribe.
let data = try_or_else!(
CByteArrayRef::new(data, data_len).as_slice(),
CByteArray::null
);

// Convert Recipient.
let slice = try_or_else!(
CByteArrayRef::new(pubkey, pubkey_len).as_slice(),
CByteArray::null
);

let recipient = try_or_else!(Recipient::<PublicKey>::from_slice(slice), CByteArray::null);

// Inscribe NFT data.
let nft = OrdinalNftInscription::new(mime_type.as_bytes(), data, recipient)
.expect("Ordinal NFT inscription incorrectly constructed");

let tx_out = TXOutputP2TRScriptPath::new(satoshis as u64, nft.inscription().recipient());
let spending_script = nft.inscription().taproot_program();

// Prepare and serialize protobuf structure.
let proto = TransactionOutput {
value: satoshis,
script: Cow::from(tx_out.script_pubkey.as_bytes()),
spendingScript: Cow::from(spending_script.as_bytes()),
};

let serialized = tw_proto::serialize(&proto).expect("failed to serialized transaction output");

CByteArray::from(serialized)
}
1 change: 1 addition & 0 deletions rust/tw_bitcoin/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pub mod brc20;
pub mod claim;
pub mod ffi;
pub mod input;
pub mod nft;
pub mod ordinals;
pub mod output;
pub mod recipient;
Expand Down
25 changes: 25 additions & 0 deletions rust/tw_bitcoin/src/nft.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
use crate::ordinals::OrdinalsInscription;
use crate::{Recipient, Result};
use bitcoin::PublicKey;

pub struct OrdinalNftInscription(OrdinalsInscription);

impl OrdinalNftInscription {
// Constructs an [Ordinal inscription] with a given MIME type. Common MIME
// types are:
// * "application/json",
// * "application/pdf",
// * "image/gif",
// * "image/jpeg",
// * "image/png",
// * "text/plain;charset=utf-8"
// * ...
//
// [Ordinal inscription]: https://docs.ordinals.com/inscriptions.html
pub fn new(mime_type: &[u8], data: &[u8], recipient: Recipient<PublicKey>) -> Result<Self> {
OrdinalsInscription::new(mime_type, data, recipient).map(OrdinalNftInscription)
}
pub fn inscription(&self) -> &OrdinalsInscription {
&self.0
}
}
39 changes: 23 additions & 16 deletions rust/tw_bitcoin/src/ordinals.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,31 +65,38 @@ fn create_envelope(mime: &[u8], data: &[u8], internal_key: PublicKey) -> Result<
let mut mime_buf = PushBytesBuf::new();
mime_buf.extend_from_slice(mime).map_err(|_| Error::Todo)?;

// Create data buffer.
let mut data_buf = PushBytesBuf::new();
data_buf.extend_from_slice(data).map_err(|_| Error::Todo)?;

// Create an Ordinals Inscription.
let script = ScriptBuf::builder()
let mut builder = ScriptBuf::builder()
.push_opcode(OP_FALSE)
.push_opcode(OP_IF)
.push_slice(b"ord")
// Separator.
.push_opcode(OP_PUSHBYTES_1)
// This seems to be necessary for now and indicates the size of the
// length indicator. The method `push_slice` prefixes the data with the
// length, but does not specify how many bytes that prefix requires.
//
// TODO: Look up if this could be somehow improved or if the `bitcoin`
// crate can/should handle that.
// MIME types require this addtional push. It seems that the original
// creator inadvertently used `push_slice(&[1])`, which leads to
// `<1><1>`, which denotes a length prefix followed by the value. On the
// other hand, for the data, `push_slice(&[])` is used, producing `<0>`.
// This denotes a length prefix followed by no data, as opposed to
// '<1><0>', which would be a reasonable assumption. While this appears
// inconsistent, it's the current requirement.
.push_opcode(OP_PUSHBYTES_1)
// MIME type identifying the data
.push_slice(mime_buf.as_push_bytes())
// Separator.
.push_opcode(OP_PUSHBYTES_0)
// The payload itself.
.push_slice(data_buf)
.push_opcode(OP_ENDIF)
.into_script();
.push_opcode(OP_PUSHBYTES_0);

// Push the actual data in chunks.
for chunk in data.chunks(520) {
// Create data buffer.
let mut data_buf = PushBytesBuf::new();
data_buf.extend_from_slice(chunk).map_err(|_| Error::Todo)?;

// Push buffer
builder = builder.push_slice(data_buf);
}

// Finalize scripts.
let script = builder.push_opcode(OP_ENDIF).into_script();

// Generate the necessary spending information. As mentioned in the
// documentation of this function at the top, this serves two purposes;
Expand Down
14 changes: 3 additions & 11 deletions rust/tw_bitcoin/src/tests/brc20_transfer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,6 @@ pub const COMMIT_TX_RAW: &str = "02000000000101089098890d2653567b9e8df2d1fbe5c3c
// https://www.blockchain.com/explorer/transactions/btc/7046dc2689a27e143ea2ad1039710885147e9485ab6453fa7e87464aa7dd3eca
pub const REVEAL_TXID: &str = "797d17d47ae66e598341f9dfdea020b04d4017dcf9cc33f0e51f7a6082171fb1";
pub const REVEAL_RAW: &str = "02000000000101b11f1782607a1fe5f033ccf9dc17404db020a0dedff94183596ee67ad4177d790000000000ffffffff012202000000000000160014e311b8d6ddff856ce8e9a4e03bc6d4fe5050a83d0340de6fd13e43700f59876d305e5a4a5c41ad7ada10bc5a4e4bdd779eb0060c0a78ebae9c33daf77bb3725172edb5bd12e26f00c08f9263e480d53b93818138ad0b5b0063036f7264010118746578742f706c61696e3b636861727365743d7574662d3800377b2270223a226272632d3230222c226f70223a227472616e73666572222c227469636b223a226f616466222c22616d74223a223230227d6821c00f209b6ada5edb42c77fd2bc64ad650ae38314c8f451f3e36d80bc8e26f132cb00000000";
pub const REVEAL_RAW_P1: &str = "02000000000101b11f1782607a1fe5f033ccf9dc17404db020a0dedff94183596ee67ad4177d790000000000ffffffff012202000000000000160014e311b8d6ddff856ce8e9a4e03bc6d4fe5050a83d0340";
pub const REVEAL_RAW_SCHNORR: &str = "de6fd13e43700f59876d305e5a4a5c41ad7ada10bc5a4e4bdd779eb0060c0a78ebae9c33daf77bb3725172edb5bd12e26f00c08f9263e480d53b93818138ad0b";
pub const REVEAL_RAW_P2: &str = "5b0063036f7264010118746578742f706c61696e3b636861727365743d7574662d3800377b2270223a226272632d3230222c226f70223a227472616e73666572222c227469636b223a226f616466222c22616d74223a223230227d6821c00f209b6ada5edb42c77fd2bc64ad650ae38314c8f451f3e36d80bc8e26f132cb00000000";

// Used for transfering the Inscription ("BRC20 transfer").
// https://www.blockchain.com/explorer/transactions/btc/3e3576eb02667fac284a5ecfcb25768969680cc4c597784602d0a33ba7c654b7
Expand Down Expand Up @@ -129,16 +126,11 @@ fn brc20_transfer() {
// Encode the signed transaction.
let hex = hex::encode(&transaction, false);

assert_eq!(
REVEAL_RAW,
[REVEAL_RAW_P1, REVEAL_RAW_SCHNORR, REVEAL_RAW_P2].concat()
);

assert_eq!(&hex[..164], REVEAL_RAW_P1);
assert_eq!(hex[..164], REVEAL_RAW[..164]);
// We ignore the 64-byte Schnorr signature, since it uses random data for
// signing on each construction and is therefore not reproducible.
assert_ne!(&hex[164..292], REVEAL_RAW_SCHNORR);
assert_eq!(&hex[292..], REVEAL_RAW_P2);
assert_ne!(hex[164..292], REVEAL_RAW[164..292]);
assert_eq!(hex[292..], REVEAL_RAW[292..]);

// # Actually transfer the "transferable" tokens.
// Based on Bitcoin transaction:
Expand Down
Loading

0 comments on commit 53c1b1d

Please sign in to comment.