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

[Bitcoin/Rust] Add support for creating Ordinal NFT inscriptions #3297

Merged
merged 53 commits into from
Jul 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
7f26b64
expand comment on MIME data prefix in the construction of ordinal ins…
lamafab Jul 5, 2023
3f47bd6
add NFT module
lamafab Jul 5, 2023
657560c
update comment on push_opcode
lamafab Jul 5, 2023
e8bdb90
add test for NFT inscription
lamafab Jul 5, 2023
7f47e36
expand full list of inscription types
lamafab Jul 6, 2023
5f8eca8
compare NFT inscription test data with expected values
lamafab Jul 6, 2023
d378fcb
avoid splitting expected values into individual consts, use slices di…
lamafab Jul 6, 2023
6964c73
fix warnings
lamafab Jul 6, 2023
d0f6b46
rename new_image to just new
lamafab Jul 6, 2023
2cf3146
add tw_build_nft_inscription FFI function
lamafab Jul 6, 2023
f638ad6
test tw_build_nft_inscription FFI function
lamafab Jul 6, 2023
ec17d25
test protobuf NFT inscriptions
lamafab Jul 6, 2023
2e86ffc
rename ImageType to MimeType
lamafab Jul 6, 2023
30d0324
rename TW::Rust::tw_build_brc20_inscribe_transfer to TW::Rust::tw_bui…
lamafab Jul 7, 2023
ed59769
bitcoin-nft-inscriptions
lamafab Jul 7, 2023
d277601
add Script::buildNftInscription to CXX files, include correct path to…
lamafab Jul 10, 2023
ab2e9dc
add CXX test for SignNftInscription
lamafab Jul 10, 2023
6457bee
add CXX test SignNftInscriptionReveal
lamafab Jul 10, 2023
cf9cd63
expand C interfaces with NFT inscription construction
lamafab Jul 10, 2023
86f3af6
track TWBitcoinOrdinalsMimeType.h
lamafab Jul 10, 2023
7b8c2ad
add Swift tests for NFT inscription
lamafab Jul 11, 2023
f11a7c3
track TWOrdMimeType.h
lamafab Jul 11, 2023
fbcb90e
correctly set payload when reading from file
lamafab Jul 11, 2023
982cf10
compare substrings
lamafab Jul 11, 2023
c51f42c
avoid var name reuse
lamafab Jul 11, 2023
60a725c
add nft inscription hex data of image and expected output
lamafab Jul 11, 2023
c001d94
small cleanup
lamafab Jul 11, 2023
46a3a2d
replace TW::Rust::MimeType with TWOrdMimeType
lamafab Jul 11, 2023
1f8fdf7
pass raw integer to Rust, derived from enum variant
lamafab Jul 12, 2023
e914d59
reverse txId for Swift tests
lamafab Jul 12, 2023
8907a12
trigger CI
lamafab Jul 12, 2023
a1e327a
run cargo fmt
lamafab Jul 12, 2023
55fdd9d
revert Podlock and update rust/coverage.stats
lamafab Jul 12, 2023
1ec2683
update wallet-core-kotlin version in kmp
lamafab Jul 12, 2023
cfb82d6
clear todos
lamafab Jul 13, 2023
565bafb
rename tw_build_nft_inscription to tw_build_ordinal_nft_inscription
lamafab Jul 13, 2023
01ae022
remove Foundation import in Swift tests
lamafab Jul 13, 2023
7955882
add Ordinal prefix to Nft inscriptions where appropriate, deprecate M…
lamafab Jul 13, 2023
4ad8656
pass mime type as string from C++ to Rust
lamafab Jul 13, 2023
3513a67
rename tw_build_ordinal_nft_inscription to tw_bitcoin_build_nft_inscr…
lamafab Jul 13, 2023
d327527
update wallet-core-kotlin
lamafab Jul 13, 2023
3d7e93f
update Pods in samples
lamafab Jul 13, 2023
c2af9e4
Merge branch 'master' into bitcoin-nft-inscriptions
lamafab Jul 13, 2023
27606a5
run cargo fmt
lamafab Jul 13, 2023
5668faa
update pods in swift/ and swift/Example
lamafab Jul 13, 2023
f7c3385
fix how mime type is passed in swift tests
lamafab Jul 13, 2023
2b642ad
pass string directly to Rust functions
lamafab Jul 14, 2023
876bdf0
undo pod changes in samples
lamafab Jul 14, 2023
5c9f7cd
run cargo fmt
lamafab Jul 14, 2023
a72678f
embed image content and raw transaction in CPP file directly
lamafab Jul 17, 2023
9a0b6b9
embed image as raw hex in Rust
lamafab Jul 17, 2023
647bc65
add embedded data in CPP files into separate file
lamafab Jul 17, 2023
03e8607
Merge branch 'embed-nft-data' into bitcoin-nft-inscriptions
lamafab Jul 17, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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