Skip to content

Commit f6bfb21

Browse files
authored
[Bitcoin/Rust] Add support for creating Ordinal NFT inscriptions (trustwallet#3297)
* 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
1 parent 7e9576b commit f6bfb21

File tree

23 files changed

+2236
-47
lines changed

23 files changed

+2236
-47
lines changed

include/TrustWalletCore/TWBitcoinScript.h

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,14 +203,24 @@ struct TWBitcoinScript* _Nonnull TWBitcoinScriptBuildPayToWitnessScriptHash(TWDa
203203
TW_EXPORT_STATIC_METHOD
204204
TWData* _Nonnull TWBitcoinScriptBuildBRC20InscribeTransfer(TWString* _Nonnull ticker, TWString* _Nonnull amount, TWData* _Nonnull pubkey);
205205

206+
/// Builds the Ordinals inscripton for NFT construction.
207+
///
208+
/// \param mimeType the MIME type of the payload
209+
/// \param payload the payload to inscribe
210+
/// \param pubkey Non-null pointer to a pubkey
211+
/// \note Must be deleted with \TWBitcoinScriptDelete
212+
/// \return A pointer to the built script
213+
TW_EXPORT_STATIC_METHOD
214+
TWData* _Nonnull TWBitcoinScriptBuildOrdinalNftInscription(TWString* _Nonnull mimeType, TWData* _Nonnull payload, TWData* _Nonnull pubkey);
215+
206216
/// Builds a appropriate lock script for the given address..
207217
///
208218
/// \param address Non-null pointer to an address
209219
/// \param coin coin type
210220
/// \note Must be deleted with \TWBitcoinScriptDelete
211221
/// \return A pointer to the built script
212222
TW_EXPORT_STATIC_METHOD
213-
struct TWBitcoinScript* _Nonnull TWBitcoinScriptLockScriptForAddress(TWString* _Nonnull address, enum TWCoinType coin);
223+
struct TWBitcoinScript *_Nonnull TWBitcoinScriptLockScriptForAddress(TWString* _Nonnull address, enum TWCoinType coin);
214224

215225
/// Builds a appropriate lock script for the given address with replay.
216226
TW_EXPORT_STATIC_METHOD

rust/coverage.stats

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
87.8
1+
86.4

rust/tw_bitcoin/src/ffi/mod.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ pub(crate) fn taproot_build_and_sign_transaction(proto: SigningInput) -> Result<
101101
script_buf,
102102
)
103103
.into(),
104-
TrVariant::BRC20TRANSFER => {
104+
TrVariant::BRC20TRANSFER | TrVariant::NFTINSCRIPTION => {
105105
// We construct the merkle root for the given spending script.
106106
let spending_script = ScriptBuf::from_bytes(input.spendingScript.to_vec());
107107
let merkle_root = TapNodeHash::from_script(
@@ -155,9 +155,14 @@ pub(crate) fn taproot_build_and_sign_transaction(proto: SigningInput) -> Result<
155155
TrVariant::P2TRKEYPATH => {
156156
TxOutputP2TRKeyPath::new_with_script(satoshis, script_buf).into()
157157
},
158+
// We're keeping those two variants separate for now, we're planning
159+
// on writing a new interface as part of a larger task anyway.
158160
TrVariant::BRC20TRANSFER => {
159161
TXOutputP2TRScriptPath::new_with_script(satoshis, script_buf).into()
160162
},
163+
TrVariant::NFTINSCRIPTION => {
164+
TXOutputP2TRScriptPath::new_with_script(satoshis, script_buf).into()
165+
}
161166
};
162167

163168
builder = builder.add_output(tx);

rust/tw_bitcoin/src/ffi/scripts.rs

Lines changed: 59 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
use crate::brc20::{BRC20TransferInscription, Ticker};
2+
use crate::nft::OrdinalNftInscription;
23
use crate::{
34
Recipient, TXOutputP2TRScriptPath, TxOutputP2PKH, TxOutputP2TRKeyPath, TxOutputP2WPKH,
45
};
56
use bitcoin::{PublicKey, WPubkeyHash};
67
use std::borrow::Cow;
8+
use std::ffi::{c_char, CStr};
79
use tw_memory::ffi::c_byte_array::CByteArray;
810
use tw_memory::ffi::c_byte_array_ref::CByteArrayRef;
911
use tw_misc::try_or_else;
@@ -98,23 +100,25 @@ pub unsafe extern "C" fn tw_build_p2tr_key_path_script(
98100

99101
#[no_mangle]
100102
// Builds the Ordinals inscripton for BRC20 transfer.
101-
pub unsafe extern "C" fn tw_build_brc20_inscribe_transfer(
103+
pub unsafe extern "C" fn tw_build_brc20_transfer_inscription(
102104
// The 4-byte ticker.
103-
ticker: *const u8,
105+
ticker: *const c_char,
104106
amount: u64,
105107
satoshis: i64,
106108
pubkey: *const u8,
107109
pubkey_len: usize,
108110
) -> CByteArray {
109111
// Convert ticket.
110-
let slice = try_or_else!(CByteArrayRef::new(ticker, 4).as_slice(), CByteArray::null);
112+
let ticker = match CStr::from_ptr(ticker).to_str() {
113+
Ok(input) => input,
114+
Err(_) => return CByteArray::null(),
115+
};
111116

112-
if slice.len() != 4 {
117+
if ticker.len() != 4 {
113118
return CByteArray::null();
114119
}
115120

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

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

143147
CByteArray::from(serialized)
144148
}
149+
150+
#[no_mangle]
151+
// Builds the Ordinals inscripton for BRC20 transfer.
152+
pub unsafe extern "C" fn tw_bitcoin_build_nft_inscription(
153+
mime_type: *const c_char,
154+
data: *const u8,
155+
data_len: usize,
156+
satoshis: i64,
157+
pubkey: *const u8,
158+
pubkey_len: usize,
159+
) -> CByteArray {
160+
// Convert mimeType.
161+
let mime_type = match CStr::from_ptr(mime_type).to_str() {
162+
Ok(input) => input,
163+
Err(_) => return CByteArray::null(),
164+
};
165+
166+
// Convert data to inscribe.
167+
let data = try_or_else!(
168+
CByteArrayRef::new(data, data_len).as_slice(),
169+
CByteArray::null
170+
);
171+
172+
// Convert Recipient.
173+
let slice = try_or_else!(
174+
CByteArrayRef::new(pubkey, pubkey_len).as_slice(),
175+
CByteArray::null
176+
);
177+
178+
let recipient = try_or_else!(Recipient::<PublicKey>::from_slice(slice), CByteArray::null);
179+
180+
// Inscribe NFT data.
181+
let nft = OrdinalNftInscription::new(mime_type.as_bytes(), data, recipient)
182+
.expect("Ordinal NFT inscription incorrectly constructed");
183+
184+
let tx_out = TXOutputP2TRScriptPath::new(satoshis as u64, nft.inscription().recipient());
185+
let spending_script = nft.inscription().taproot_program();
186+
187+
// Prepare and serialize protobuf structure.
188+
let proto = TransactionOutput {
189+
value: satoshis,
190+
script: Cow::from(tx_out.script_pubkey.as_bytes()),
191+
spendingScript: Cow::from(spending_script.as_bytes()),
192+
};
193+
194+
let serialized = tw_proto::serialize(&proto).expect("failed to serialized transaction output");
195+
196+
CByteArray::from(serialized)
197+
}

rust/tw_bitcoin/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ pub mod brc20;
44
pub mod claim;
55
pub mod ffi;
66
pub mod input;
7+
pub mod nft;
78
pub mod ordinals;
89
pub mod output;
910
pub mod recipient;

rust/tw_bitcoin/src/nft.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
use crate::ordinals::OrdinalsInscription;
2+
use crate::{Recipient, Result};
3+
use bitcoin::PublicKey;
4+
5+
pub struct OrdinalNftInscription(OrdinalsInscription);
6+
7+
impl OrdinalNftInscription {
8+
// Constructs an [Ordinal inscription] with a given MIME type. Common MIME
9+
// types are:
10+
// * "application/json",
11+
// * "application/pdf",
12+
// * "image/gif",
13+
// * "image/jpeg",
14+
// * "image/png",
15+
// * "text/plain;charset=utf-8"
16+
// * ...
17+
//
18+
// [Ordinal inscription]: https://docs.ordinals.com/inscriptions.html
19+
pub fn new(mime_type: &[u8], data: &[u8], recipient: Recipient<PublicKey>) -> Result<Self> {
20+
OrdinalsInscription::new(mime_type, data, recipient).map(OrdinalNftInscription)
21+
}
22+
pub fn inscription(&self) -> &OrdinalsInscription {
23+
&self.0
24+
}
25+
}

rust/tw_bitcoin/src/ordinals.rs

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -65,31 +65,38 @@ fn create_envelope(mime: &[u8], data: &[u8], internal_key: PublicKey) -> Result<
6565
let mut mime_buf = PushBytesBuf::new();
6666
mime_buf.extend_from_slice(mime).map_err(|_| Error::Todo)?;
6767

68-
// Create data buffer.
69-
let mut data_buf = PushBytesBuf::new();
70-
data_buf.extend_from_slice(data).map_err(|_| Error::Todo)?;
71-
7268
// Create an Ordinals Inscription.
73-
let script = ScriptBuf::builder()
69+
let mut builder = ScriptBuf::builder()
7470
.push_opcode(OP_FALSE)
7571
.push_opcode(OP_IF)
7672
.push_slice(b"ord")
7773
// Separator.
7874
.push_opcode(OP_PUSHBYTES_1)
79-
// This seems to be necessary for now and indicates the size of the
80-
// length indicator. The method `push_slice` prefixes the data with the
81-
// length, but does not specify how many bytes that prefix requires.
82-
//
83-
// TODO: Look up if this could be somehow improved or if the `bitcoin`
84-
// crate can/should handle that.
75+
// MIME types require this addtional push. It seems that the original
76+
// creator inadvertently used `push_slice(&[1])`, which leads to
77+
// `<1><1>`, which denotes a length prefix followed by the value. On the
78+
// other hand, for the data, `push_slice(&[])` is used, producing `<0>`.
79+
// This denotes a length prefix followed by no data, as opposed to
80+
// '<1><0>', which would be a reasonable assumption. While this appears
81+
// inconsistent, it's the current requirement.
8582
.push_opcode(OP_PUSHBYTES_1)
83+
// MIME type identifying the data
8684
.push_slice(mime_buf.as_push_bytes())
8785
// Separator.
88-
.push_opcode(OP_PUSHBYTES_0)
89-
// The payload itself.
90-
.push_slice(data_buf)
91-
.push_opcode(OP_ENDIF)
92-
.into_script();
86+
.push_opcode(OP_PUSHBYTES_0);
87+
88+
// Push the actual data in chunks.
89+
for chunk in data.chunks(520) {
90+
// Create data buffer.
91+
let mut data_buf = PushBytesBuf::new();
92+
data_buf.extend_from_slice(chunk).map_err(|_| Error::Todo)?;
93+
94+
// Push buffer
95+
builder = builder.push_slice(data_buf);
96+
}
97+
98+
// Finalize scripts.
99+
let script = builder.push_opcode(OP_ENDIF).into_script();
93100

94101
// Generate the necessary spending information. As mentioned in the
95102
// documentation of this function at the top, this serves two purposes;

rust/tw_bitcoin/src/tests/brc20_transfer.rs

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,6 @@ pub const COMMIT_TX_RAW: &str = "02000000000101089098890d2653567b9e8df2d1fbe5c3c
3131
// https://www.blockchain.com/explorer/transactions/btc/7046dc2689a27e143ea2ad1039710885147e9485ab6453fa7e87464aa7dd3eca
3232
pub const REVEAL_TXID: &str = "797d17d47ae66e598341f9dfdea020b04d4017dcf9cc33f0e51f7a6082171fb1";
3333
pub const REVEAL_RAW: &str = "02000000000101b11f1782607a1fe5f033ccf9dc17404db020a0dedff94183596ee67ad4177d790000000000ffffffff012202000000000000160014e311b8d6ddff856ce8e9a4e03bc6d4fe5050a83d0340de6fd13e43700f59876d305e5a4a5c41ad7ada10bc5a4e4bdd779eb0060c0a78ebae9c33daf77bb3725172edb5bd12e26f00c08f9263e480d53b93818138ad0b5b0063036f7264010118746578742f706c61696e3b636861727365743d7574662d3800377b2270223a226272632d3230222c226f70223a227472616e73666572222c227469636b223a226f616466222c22616d74223a223230227d6821c00f209b6ada5edb42c77fd2bc64ad650ae38314c8f451f3e36d80bc8e26f132cb00000000";
34-
pub const REVEAL_RAW_P1: &str = "02000000000101b11f1782607a1fe5f033ccf9dc17404db020a0dedff94183596ee67ad4177d790000000000ffffffff012202000000000000160014e311b8d6ddff856ce8e9a4e03bc6d4fe5050a83d0340";
35-
pub const REVEAL_RAW_SCHNORR: &str = "de6fd13e43700f59876d305e5a4a5c41ad7ada10bc5a4e4bdd779eb0060c0a78ebae9c33daf77bb3725172edb5bd12e26f00c08f9263e480d53b93818138ad0b";
36-
pub const REVEAL_RAW_P2: &str = "5b0063036f7264010118746578742f706c61696e3b636861727365743d7574662d3800377b2270223a226272632d3230222c226f70223a227472616e73666572222c227469636b223a226f616466222c22616d74223a223230227d6821c00f209b6ada5edb42c77fd2bc64ad650ae38314c8f451f3e36d80bc8e26f132cb00000000";
3734

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

132-
assert_eq!(
133-
REVEAL_RAW,
134-
[REVEAL_RAW_P1, REVEAL_RAW_SCHNORR, REVEAL_RAW_P2].concat()
135-
);
136-
137-
assert_eq!(&hex[..164], REVEAL_RAW_P1);
129+
assert_eq!(hex[..164], REVEAL_RAW[..164]);
138130
// We ignore the 64-byte Schnorr signature, since it uses random data for
139131
// signing on each construction and is therefore not reproducible.
140-
assert_ne!(&hex[164..292], REVEAL_RAW_SCHNORR);
141-
assert_eq!(&hex[292..], REVEAL_RAW_P2);
132+
assert_ne!(hex[164..292], REVEAL_RAW[164..292]);
133+
assert_eq!(hex[292..], REVEAL_RAW[292..]);
142134

143135
// # Actually transfer the "transferable" tokens.
144136
// Based on Bitcoin transaction:

0 commit comments

Comments
 (0)