ns_inscriber/
inscriber.rs

1use bitcoin::{
2    address::NetworkChecked,
3    blockdata::{opcodes, script::PushBytesBuf},
4    ecdsa,
5    hashes::Hash,
6    key::{
7        constants::{SCHNORR_PUBLIC_KEY_SIZE, SCHNORR_SIGNATURE_SIZE},
8        TapTweak, TweakedKeypair,
9    },
10    locktime::absolute::LockTime,
11    secp256k1::{rand, All, Keypair, Message, Secp256k1, SecretKey},
12    sighash::{EcdsaSighashType, Prevouts, SighashCache, TapSighashType},
13    taproot::{
14        self, LeafVersion, TapLeafHash, TaprootBuilder, TAPROOT_CONTROL_BASE_SIZE,
15        TAPROOT_CONTROL_NODE_SIZE,
16    },
17    transaction::Version,
18    Address, Amount, Network, OutPoint, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Txid,
19    Witness,
20};
21use serde::{Deserialize, Serialize};
22use std::{collections::HashSet, str::FromStr};
23
24use ns_protocol::ns::{Name, MAX_NAME_BYTES};
25
26use crate::bitcoin::{BitCoinRPCOptions, BitcoinRPC};
27
28pub struct InscriberOptions {
29    pub bitcoin: BitCoinRPCOptions,
30}
31
32pub struct Inscriber {
33    secp: Secp256k1<All>,
34    pub bitcoin: BitcoinRPC,
35    pub network: Network,
36}
37
38#[derive(Clone, PartialEq, Debug)]
39pub struct UnspentTxOut {
40    pub txid: Txid,
41    pub vout: u32,
42    pub amount: Amount,
43    pub script_pubkey: ScriptBuf,
44}
45
46impl Inscriber {
47    pub fn new(opts: &InscriberOptions) -> anyhow::Result<Self> {
48        let secp = Secp256k1::new();
49
50        Ok(Self {
51            secp,
52            bitcoin: BitcoinRPC::new(&opts.bitcoin)?,
53            network: opts.bitcoin.network,
54        })
55    }
56
57    pub async fn inscribe(
58        &self,
59        names: &Vec<Name>,
60        fee_rate: Amount,
61        secret: &SecretKey,
62        unspent_txout: &UnspentTxOut,
63    ) -> anyhow::Result<Txid> {
64        let (signed_commit_tx, signed_reveal_tx) = self
65            .build_signed_inscription(names, fee_rate, secret, unspent_txout)
66            .await?;
67
68        let commit = self.bitcoin.send_transaction(&signed_commit_tx).await?;
69        let reveal = self
70            .bitcoin
71            .send_transaction(&signed_reveal_tx)
72            .await
73            .map_err(|err| {
74                anyhow::anyhow!("failed to send reveal transaction: {err}\ncommit tx: {commit}")
75            })?;
76
77        Ok(reveal)
78    }
79
80    pub async fn build_signed_inscription(
81        &self,
82        names: &Vec<Name>,
83        fee_rate: Amount,
84        secret: &SecretKey,
85        unspent_txout: &UnspentTxOut,
86    ) -> anyhow::Result<(Transaction, Transaction)> {
87        let keypair = Keypair::from_secret_key(&self.secp, secret);
88
89        let (unsigned_commit_tx, signed_reveal_tx) = self
90            .build_inscription_transactions(names, fee_rate, unspent_txout, Some(keypair))
91            .await?;
92
93        let mut signed_commit_tx = unsigned_commit_tx;
94        // sigh commit_tx
95        if unspent_txout.script_pubkey.is_p2tr() {
96            let mut sighasher = SighashCache::new(&mut signed_commit_tx);
97            let prevouts = vec![TxOut {
98                value: unspent_txout.amount,
99                script_pubkey: unspent_txout.script_pubkey.clone(),
100            }];
101            let sighash = sighasher
102                .taproot_key_spend_signature_hash(
103                    0,
104                    &Prevouts::All(&prevouts),
105                    TapSighashType::Default,
106                )
107                .expect("failed to construct sighash");
108
109            let tweaked: TweakedKeypair = keypair.tap_tweak(&self.secp, None);
110            let sig = self.secp.sign_schnorr(
111                &Message::from_digest(sighash.to_byte_array()),
112                &tweaked.to_inner(),
113            );
114
115            let signature = taproot::Signature {
116                sig,
117                hash_ty: TapSighashType::Default,
118            };
119
120            sighasher
121                .witness_mut(0)
122                .expect("getting mutable witness reference should work")
123                .push(&signature.to_vec());
124        } else if unspent_txout.script_pubkey.is_p2wpkh() {
125            let mut sighasher = SighashCache::new(&mut signed_commit_tx);
126            let sighash = sighasher
127                .p2wpkh_signature_hash(
128                    0,
129                    &unspent_txout.script_pubkey,
130                    unspent_txout.amount,
131                    EcdsaSighashType::All,
132                )
133                .expect("failed to create sighash");
134
135            let sig = self
136                .secp
137                .sign_ecdsa(&Message::from(sighash), &keypair.secret_key());
138            let signature = ecdsa::Signature {
139                sig,
140                hash_ty: EcdsaSighashType::All,
141            };
142            signed_commit_tx.input[0].witness = Witness::p2wpkh(&signature, &keypair.public_key());
143        } else {
144            anyhow::bail!("unsupported script_pubkey");
145        }
146
147        let test_txs = self
148            .bitcoin
149            .test_mempool_accept(&[&signed_commit_tx, &signed_reveal_tx])
150            .await?;
151        for r in &test_txs {
152            if !r.allowed {
153                anyhow::bail!("failed to accept transaction: {:?}", &test_txs);
154            }
155        }
156
157        Ok((signed_commit_tx, signed_reveal_tx))
158    }
159
160    pub async fn send_sats(
161        &self,
162        fee_rate: Amount,
163        secret: &SecretKey,
164        unspent_txout: &UnspentTxOut,
165        to: &Address<NetworkChecked>,
166        amount: Amount,
167    ) -> anyhow::Result<Txid> {
168        let keypair = Keypair::from_secret_key(&self.secp, secret);
169
170        let mut tx = Transaction {
171            version: Version::TWO,
172            lock_time: LockTime::ZERO,
173            input: vec![TxIn {
174                previous_output: OutPoint {
175                    txid: unspent_txout.txid,
176                    vout: unspent_txout.vout,
177                },
178                script_sig: ScriptBuf::new(),
179                sequence: Sequence::ENABLE_RBF_NO_LOCKTIME,
180                witness: Witness::new(),
181            }],
182            output: vec![
183                TxOut {
184                    value: amount,
185                    script_pubkey: to.script_pubkey(),
186                },
187                TxOut {
188                    // change
189                    value: unspent_txout.amount, // will update later
190                    script_pubkey: unspent_txout.script_pubkey.clone(),
191                },
192            ],
193        };
194
195        let fee = {
196            let mut v_tx = tx.clone();
197            v_tx.input[0].witness = Witness::from_slice(&[&[0; SCHNORR_SIGNATURE_SIZE]]);
198            fee_rate
199                .checked_mul(v_tx.vsize() as u64)
200                .expect("should compute commit_tx fee")
201        };
202
203        let change_value = unspent_txout
204            .amount
205            .checked_sub(amount)
206            .ok_or_else(|| anyhow::anyhow!("should compute amount"))?;
207        if change_value > fee {
208            tx.output[1].value = change_value - fee;
209        } else {
210            tx.output.pop(); // no change
211        }
212
213        if unspent_txout.script_pubkey.is_p2tr() {
214            let mut sighasher = SighashCache::new(&mut tx);
215            let prevouts = vec![TxOut {
216                value: unspent_txout.amount,
217                script_pubkey: unspent_txout.script_pubkey.clone(),
218            }];
219            let sighash = sighasher
220                .taproot_key_spend_signature_hash(
221                    0,
222                    &Prevouts::All(&prevouts),
223                    TapSighashType::Default,
224                )
225                .expect("failed to construct sighash");
226
227            let tweaked: TweakedKeypair = keypair.tap_tweak(&self.secp, None);
228            let sig = self.secp.sign_schnorr(
229                &Message::from_digest(sighash.to_byte_array()),
230                &tweaked.to_inner(),
231            );
232
233            let signature = taproot::Signature {
234                sig,
235                hash_ty: TapSighashType::Default,
236            };
237
238            sighasher
239                .witness_mut(0)
240                .expect("getting mutable witness reference should work")
241                .push(&signature.to_vec());
242        } else if unspent_txout.script_pubkey.is_p2wpkh() {
243            let mut sighasher = SighashCache::new(&mut tx);
244            let sighash = sighasher
245                .p2wpkh_signature_hash(
246                    0,
247                    &unspent_txout.script_pubkey,
248                    unspent_txout.amount,
249                    EcdsaSighashType::All,
250                )
251                .expect("failed to create sighash");
252
253            let sig = self
254                .secp
255                .sign_ecdsa(&Message::from(sighash), &keypair.secret_key());
256            let signature = ecdsa::Signature {
257                sig,
258                hash_ty: EcdsaSighashType::All,
259            };
260            tx.input[0].witness = Witness::p2wpkh(&signature, &keypair.public_key());
261        } else {
262            anyhow::bail!("unsupported script_pubkey");
263        }
264
265        let test_txs = self.bitcoin.test_mempool_accept(&[&tx]).await?;
266        for r in &test_txs {
267            if !r.allowed {
268                anyhow::bail!("failed to accept transaction: {:?}", &test_txs);
269            }
270        }
271
272        let txid = self.bitcoin.send_transaction(&tx).await?;
273        Ok(txid)
274    }
275
276    pub async fn collect_sats(
277        &self,
278        fee_rate: Amount,
279        unspent_txouts: &[(SecretKey, UnspentTxOut)],
280        to: &Address<NetworkChecked>,
281    ) -> anyhow::Result<Txid> {
282        let amount = unspent_txouts.iter().map(|(_, v)| v.amount).sum::<Amount>();
283
284        let mut tx = Transaction {
285            version: Version::TWO,
286            lock_time: LockTime::ZERO,
287            input: unspent_txouts
288                .iter()
289                .map(|(_, v)| TxIn {
290                    previous_output: OutPoint {
291                        txid: v.txid,
292                        vout: v.vout,
293                    },
294                    script_sig: ScriptBuf::new(),
295                    sequence: Sequence::ENABLE_RBF_NO_LOCKTIME,
296                    witness: Witness::new(),
297                })
298                .collect(),
299            output: vec![TxOut {
300                value: amount,
301                script_pubkey: to.script_pubkey(),
302            }],
303        };
304
305        let fee = {
306            let mut v_tx = tx.clone();
307            for input in v_tx.input.iter_mut() {
308                input.witness = Witness::from_slice(&[&[0; SCHNORR_SIGNATURE_SIZE]]);
309            }
310            fee_rate
311                .checked_mul(v_tx.vsize() as u64)
312                .expect("should compute commit_tx fee")
313        };
314
315        let change_value = amount
316            .checked_sub(fee)
317            .ok_or_else(|| anyhow::anyhow!("should compute change value"))?;
318        tx.output[0].value = change_value;
319
320        let mut sighasher = SighashCache::new(&mut tx);
321
322        for (i, (secret, unspent_txout)) in unspent_txouts.iter().enumerate() {
323            let keypair = Keypair::from_secret_key(&self.secp, secret);
324
325            if unspent_txout.script_pubkey.is_p2tr() {
326                let tweaked: TweakedKeypair = keypair.tap_tweak(&self.secp, None);
327                let sighash = sighasher
328                    .taproot_key_spend_signature_hash(
329                        0,
330                        &Prevouts::All(&[TxOut {
331                            value: unspent_txout.amount,
332                            script_pubkey: unspent_txout.script_pubkey.clone(),
333                        }]),
334                        TapSighashType::Default,
335                    )
336                    .expect("failed to construct sighash");
337
338                let sig = self.secp.sign_schnorr(
339                    &Message::from_digest(sighash.to_byte_array()),
340                    &tweaked.to_inner(),
341                );
342
343                let signature = taproot::Signature {
344                    sig,
345                    hash_ty: TapSighashType::Default,
346                };
347
348                sighasher
349                    .witness_mut(i)
350                    .expect("getting mutable witness reference should work")
351                    .push(&signature.to_vec());
352            } else if unspent_txout.script_pubkey.is_p2wpkh() {
353                let sighash = sighasher
354                    .p2wpkh_signature_hash(
355                        0,
356                        &unspent_txout.script_pubkey,
357                        unspent_txout.amount,
358                        EcdsaSighashType::All,
359                    )
360                    .expect("failed to create sighash");
361
362                let sig = self
363                    .secp
364                    .sign_ecdsa(&Message::from(sighash), &keypair.secret_key());
365                let signature = ecdsa::Signature {
366                    sig,
367                    hash_ty: EcdsaSighashType::All,
368                };
369                *sighasher
370                    .witness_mut(i)
371                    .expect("getting mutable witness reference should work") =
372                    Witness::p2wpkh(&signature, &keypair.public_key());
373            } else {
374                anyhow::bail!("unsupported script_pubkey");
375            }
376        }
377
378        let test_txs = self.bitcoin.test_mempool_accept(&[&tx]).await?;
379        for r in &test_txs {
380            if !r.allowed {
381                anyhow::bail!("failed to accept transaction: {:?}", &test_txs);
382            }
383        }
384
385        let txid = self.bitcoin.send_transaction(&tx).await?;
386        Ok(txid)
387    }
388
389    // return (unsigned_commit_tx, signed_reveal_tx)
390    pub async fn build_inscription_transactions(
391        &self,
392        names: &Vec<Name>,
393        fee_rate: Amount,
394        unspent_txout: &UnspentTxOut,
395        inscription_keypair: Option<Keypair>,
396    ) -> anyhow::Result<(Transaction, Transaction)> {
397        if names.is_empty() {
398            anyhow::bail!("no names to inscribe");
399        }
400        if fee_rate.to_sat() == 0 {
401            anyhow::bail!("fee rate cannot be zero");
402        }
403
404        if let Some(name) = check_duplicate(names) {
405            anyhow::bail!("duplicate name {}", name);
406        }
407
408        for name in names {
409            if let Err(err) = name.validate() {
410                anyhow::bail!("invalid name {}: {}", name.name, err);
411            }
412        }
413
414        let keypair = inscription_keypair
415            .unwrap_or_else(|| Keypair::new(&self.secp, &mut rand::thread_rng()));
416
417        let (unsigned_commit_tx, signed_reveal_tx) =
418            self.create_inscription_transactions(names, fee_rate, unspent_txout, &keypair)?;
419        Ok((unsigned_commit_tx, signed_reveal_tx))
420    }
421
422    pub fn preview_inscription_transactions(
423        names: &Vec<Name>,
424        fee_rate: Amount,
425    ) -> anyhow::Result<(Transaction, Transaction, Amount)> {
426        if let Some(name) = check_duplicate(names) {
427            anyhow::bail!("duplicate name {}", name);
428        }
429
430        let mut reveal_script = ScriptBuf::builder()
431            .push_slice([0; SCHNORR_PUBLIC_KEY_SIZE])
432            .push_opcode(opcodes::all::OP_CHECKSIG)
433            .push_opcode(opcodes::OP_FALSE)
434            .push_opcode(opcodes::all::OP_IF);
435        for name in names {
436            let data = name.to_bytes()?;
437            if data.len() > MAX_NAME_BYTES {
438                anyhow::bail!("name {} is too large", name.name);
439            }
440            reveal_script = reveal_script.push_slice(PushBytesBuf::try_from(data)?);
441        }
442        let reveal_script = reveal_script
443            .push_opcode(opcodes::all::OP_ENDIF)
444            .into_script();
445
446        let dust_value = reveal_script.dust_value();
447        let dust_value = fee_rate
448            .checked_mul(42)
449            .expect("should compute amount")
450            .checked_add(dust_value)
451            .expect("should compute amount");
452
453        let mut witness = Witness::default();
454        witness.push(
455            taproot::Signature::from_slice(&[0; SCHNORR_SIGNATURE_SIZE])
456                .unwrap()
457                .to_vec(),
458        );
459        witness.push(reveal_script);
460        witness.push([0; TAPROOT_CONTROL_BASE_SIZE + TAPROOT_CONTROL_NODE_SIZE]);
461        let script_pubkey = ScriptBuf::from_bytes([0; SCHNORR_PUBLIC_KEY_SIZE].to_vec());
462        let p_reveal_tx = Transaction {
463            version: Version::TWO,
464            lock_time: LockTime::ZERO,
465            input: vec![TxIn {
466                previous_output: OutPoint::null(),
467                script_sig: ScriptBuf::default(),
468                sequence: Sequence::ENABLE_RBF_NO_LOCKTIME,
469                witness,
470            }],
471            output: vec![TxOut {
472                value: dust_value,
473                script_pubkey: script_pubkey.clone(),
474            }],
475        };
476
477        let reveal_tx_fee = fee_rate
478            .checked_mul(p_reveal_tx.vsize() as u64)
479            .expect("should compute reveal_tx fee");
480        let total_value = reveal_tx_fee
481            .checked_add(dust_value)
482            .expect("should compute amount");
483
484        let p_commit_tx = Transaction {
485            version: Version::TWO,
486            lock_time: LockTime::ZERO,
487            input: vec![TxIn {
488                previous_output: OutPoint::null(),
489                script_sig: ScriptBuf::new(),
490                sequence: Sequence::ENABLE_RBF_NO_LOCKTIME,
491                witness: Witness::from_slice(&[&[0; SCHNORR_SIGNATURE_SIZE]]),
492            }],
493            output: vec![TxOut {
494                value: total_value,
495                script_pubkey: script_pubkey.clone(),
496            }],
497        };
498        let total_value = fee_rate
499            .checked_mul(p_commit_tx.vsize() as u64)
500            .expect("should compute amount")
501            .checked_add(total_value)
502            .expect("should compute amount");
503
504        Ok((p_commit_tx, p_reveal_tx, total_value))
505    }
506
507    // return (unsigned_commit_tx, signed_reveal_tx)
508    fn create_inscription_transactions(
509        &self,
510        names: &Vec<Name>,
511        fee_rate: Amount,
512        unspent_txout: &UnspentTxOut,
513        keypair: &Keypair,
514    ) -> anyhow::Result<(Transaction, Transaction)> {
515        // or use one-time KeyPair
516
517        let (public_key, _parity) = keypair.x_only_public_key();
518
519        let mut reveal_script = ScriptBuf::builder()
520            .push_slice(public_key.serialize())
521            .push_opcode(opcodes::all::OP_CHECKSIG)
522            .push_opcode(opcodes::OP_FALSE)
523            .push_opcode(opcodes::all::OP_IF);
524        for name in names {
525            let data = name.to_bytes()?;
526            if data.len() > MAX_NAME_BYTES {
527                anyhow::bail!("name {} is too large", name.name);
528            }
529            reveal_script = reveal_script.push_slice(PushBytesBuf::try_from(data)?);
530        }
531        let reveal_script = reveal_script
532            .push_opcode(opcodes::all::OP_ENDIF)
533            .into_script();
534
535        let dust_value = reveal_script.dust_value();
536
537        let taproot_spend_info = TaprootBuilder::new()
538            .add_leaf(0, reveal_script.clone())
539            .expect("adding leaf should work")
540            .finalize(&self.secp, public_key)
541            .expect("finalizing taproot builder should work");
542
543        let control_block = taproot_spend_info
544            .control_block(&(reveal_script.clone(), LeafVersion::TapScript))
545            .expect("should compute control block");
546        let control_block_bytes = &control_block.serialize();
547
548        let commit_tx_address =
549            Address::p2tr_tweaked(taproot_spend_info.output_key(), self.network);
550
551        let mut reveal_tx = Transaction {
552            version: Version::TWO,     // Post BIP-68.
553            lock_time: LockTime::ZERO, // Ignore the locktime.
554            input: vec![TxIn {
555                previous_output: OutPoint::null(), // Filled in after signing.
556                script_sig: ScriptBuf::default(),  // For a p2tr script_sig is empty.
557                sequence: Sequence::ENABLE_RBF_NO_LOCKTIME,
558                witness: Witness::default(), // Filled in after signing.
559            }],
560            output: vec![TxOut {
561                value: unspent_txout.amount,
562                script_pubkey: unspent_txout.script_pubkey.clone(),
563            }],
564        };
565
566        let reveal_tx_fee = {
567            let mut v_reveal_tx = reveal_tx.clone();
568            let mut witness = Witness::default();
569            witness.push(
570                taproot::Signature::from_slice(&[0; SCHNORR_SIGNATURE_SIZE])
571                    .unwrap()
572                    .to_vec(),
573            );
574            witness.push(reveal_script.clone());
575            witness.push(control_block_bytes);
576            v_reveal_tx.input[0].witness = witness;
577            fee_rate
578                .checked_mul(v_reveal_tx.vsize() as u64)
579                .expect("should compute reveal_tx fee")
580        };
581
582        let mut commit_tx = Transaction {
583            version: Version::TWO,
584            lock_time: LockTime::ZERO,
585            input: vec![TxIn {
586                previous_output: OutPoint {
587                    txid: unspent_txout.txid,
588                    vout: unspent_txout.vout,
589                },
590                script_sig: ScriptBuf::new(),
591                sequence: Sequence::ENABLE_RBF_NO_LOCKTIME,
592                witness: Witness::new(),
593            }],
594            output: vec![TxOut {
595                value: unspent_txout.amount,
596                script_pubkey: commit_tx_address.script_pubkey(),
597            }],
598        };
599
600        let commit_tx_fee = {
601            let mut v_commit_tx = commit_tx.clone();
602            v_commit_tx.input[0].witness = Witness::from_slice(&[&[0; SCHNORR_SIGNATURE_SIZE]]);
603            fee_rate
604                .checked_mul(v_commit_tx.vsize() as u64)
605                .expect("should compute commit_tx fee")
606        };
607        let change_value = unspent_txout
608            .amount
609            .checked_sub(commit_tx_fee)
610            .ok_or_else(|| anyhow::anyhow!("should compute commit_tx fee"))?;
611
612        commit_tx.output[0].value = change_value;
613
614        let change_value = change_value
615            .checked_sub(reveal_tx_fee)
616            .ok_or_else(|| anyhow::anyhow!("should compute reveal_tx fee"))?;
617        if change_value < dust_value {
618            anyhow::bail!(
619                "input value is too small, need another {} sats",
620                dust_value - change_value
621            );
622        }
623
624        reveal_tx.output[0].value = change_value;
625        reveal_tx.input[0].previous_output = OutPoint {
626            txid: commit_tx.txid(),
627            vout: 0,
628        };
629
630        // sigh reveal_tx
631        {
632            let mut sighasher = SighashCache::new(&mut reveal_tx);
633            let prevouts = vec![commit_tx.output[0].clone()];
634            let sighash = sighasher
635                .taproot_script_spend_signature_hash(
636                    0,
637                    &Prevouts::All(&prevouts),
638                    TapLeafHash::from_script(&reveal_script, LeafVersion::TapScript),
639                    TapSighashType::Default,
640                )
641                .expect("failed to construct sighash");
642
643            let sig = self
644                .secp
645                .sign_schnorr(&Message::from_digest(sighash.to_byte_array()), keypair);
646
647            let witness = sighasher
648                .witness_mut(0)
649                .expect("getting mutable witness reference should work");
650            witness.push(
651                taproot::Signature {
652                    sig,
653                    hash_ty: TapSighashType::Default,
654                }
655                .to_vec(),
656            );
657            witness.push(reveal_script);
658            witness.push(control_block_bytes);
659        }
660
661        Ok((commit_tx, reveal_tx))
662    }
663}
664
665pub fn check_duplicate(names: &Vec<Name>) -> Option<String> {
666    let mut set: HashSet<String> = HashSet::new();
667    for name in names {
668        if set.contains(&name.name) {
669            return Some(name.name.clone());
670        }
671        set.insert(name.name.clone());
672    }
673    None
674}
675
676#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
677pub struct UnspentTxOutJSON {
678    pub txid: String,
679    pub vout: u32,
680    pub amount: u64,
681    pub script_pubkey: String,
682}
683
684impl UnspentTxOutJSON {
685    pub fn to(&self) -> anyhow::Result<UnspentTxOut> {
686        Ok(UnspentTxOut {
687            txid: Txid::from_str(&self.txid)?,
688            vout: self.vout,
689            amount: Amount::from_sat(self.amount),
690            script_pubkey: ScriptBuf::from_hex(&self.script_pubkey)?,
691        })
692    }
693
694    pub fn from(tx: UnspentTxOut) -> Self {
695        Self {
696            txid: tx.txid.to_string(),
697            vout: tx.vout,
698            amount: tx.amount.to_sat(),
699            script_pubkey: tx.script_pubkey.to_hex_string(),
700        }
701    }
702}
703
704#[cfg(test)]
705mod tests {
706    use super::*;
707    use hex_literal::hex;
708    use serde_json::to_value;
709
710    use ns_indexer::envelope::Envelope;
711    use ns_protocol::{
712        ed25519,
713        ns::{Bytes32, Operation, PublicKeyParams, Service, ThresholdLevel, Value},
714    };
715
716    fn get_name(name: &str) -> Name {
717        let secret_key = Bytes32(hex!(
718            "7ef3811aabb916dc2f646ef1a371b90adec91bc07992cd4d44c156c42fc1b300"
719        ));
720        let public_key = Bytes32(hex!(
721            "ee90735ac719e85dc2f3e5974036387fdf478af7d9d1f8480e97eee601890266"
722        ));
723        let params = PublicKeyParams {
724            public_keys: vec![public_key],
725            threshold: Some(1),
726            kind: None,
727        };
728
729        let signer = ed25519::SigningKey::from_bytes(&secret_key.0);
730        let signers = vec![signer];
731
732        let mut name = Name {
733            name: name.to_string(),
734            sequence: 0,
735            service: Service {
736                code: 0,
737                operations: vec![Operation {
738                    subcode: 1,
739                    params: Value::from(&params),
740                }],
741                attesters: None,
742            },
743            signatures: vec![],
744        };
745        name.sign(&params, ThresholdLevel::Default, &signers)
746            .unwrap();
747        assert!(name.validate().is_ok());
748        name
749    }
750
751    #[test]
752    fn unspent_txout_json_works() {
753        let json_str = serde_json::json!({
754            "txid": "8e9d3e0d762c1d2348a2ca046b36f8de001f740c976b09c046ee1f09a8680131",
755            "vout": 0,
756            "amount": 4929400,
757            "script_pubkey": "0014d37960b3783772f0b6e5a0917f163fa642b3a7fc"
758        });
759        let json: UnspentTxOutJSON = serde_json::from_value(json_str).unwrap();
760        let tx = json.to().unwrap();
761        let json2 = UnspentTxOutJSON::from(tx);
762        assert_eq!(json, json2)
763    }
764
765    #[test]
766    fn preview_inscription_transactions_works() {
767        let names = vec![get_name("0"), get_name("a")];
768        let fee_rate = Amount::from_sat(10);
769        let (commit_tx, reveal_tx, total_value) =
770            Inscriber::preview_inscription_transactions(&names, fee_rate).unwrap();
771
772        assert!(fee_rate.checked_mul(commit_tx.vsize() as u64).unwrap() > Amount::from_sat(0));
773        assert!(fee_rate.checked_mul(reveal_tx.vsize() as u64).unwrap() > Amount::from_sat(0));
774        assert!(total_value > Amount::from_sat(0));
775
776        let envelopes = Envelope::from_transaction(&commit_tx);
777        assert!(envelopes.is_empty());
778
779        let envelopes = Envelope::from_transaction(&reveal_tx);
780
781        assert_eq!(1, envelopes.len());
782        assert_eq!(reveal_tx.txid(), envelopes[0].txid);
783        assert_eq!(0, envelopes[0].vin);
784        assert_eq!(names, envelopes[0].payload);
785    }
786
787    #[tokio::test(flavor = "current_thread")]
788    #[ignore]
789    async fn inscriber_works() {
790        dotenvy::from_filename("sample.env").expect(".env file not found");
791
792        let rpcurl = std::env::var("BITCOIN_RPC_URL").unwrap();
793        let rpcuser = std::env::var("BITCOIN_RPC_USER").unwrap_or_default();
794        let rpcpassword = std::env::var("BITCOIN_RPC_PASSWORD").unwrap_or_default();
795        let rpctoken = std::env::var("BITCOIN_RPC_TOKEN").unwrap_or_default();
796        let network = Network::from_core_arg(&std::env::var("BITCOIN_NETWORK").unwrap_or_default())
797            .unwrap_or(Network::Regtest);
798
799        let secp = Secp256k1::new();
800        let keypair = Keypair::new(&secp, &mut rand::thread_rng());
801        let (public_key, _parity) = keypair.x_only_public_key();
802        let script_pubkey = ScriptBuf::new_p2tr(&secp, public_key, None);
803        let address: Address<NetworkChecked> =
804            Address::from_script(&script_pubkey, network).unwrap();
805
806        println!("rpcurl: {}, network: {}", rpcurl, network);
807        println!("address: {}", address);
808
809        let inscriber = Inscriber::new(&InscriberOptions {
810            bitcoin: BitCoinRPCOptions {
811                rpcurl,
812                rpcuser,
813                rpcpassword,
814                rpctoken,
815                network,
816            },
817        })
818        .unwrap();
819
820        inscriber.bitcoin.ping().await.unwrap();
821
822        // wallet
823        // bitcoind -regtest -txindex -rpcuser=test -rpcpassword=123456 -fallbackfee=0.00001
824        let _ = inscriber
825            .bitcoin
826            .call::<serde_json::Value>("createwallet", &["testwallet".into()])
827            .await;
828        let _ = inscriber
829            .bitcoin
830            .call::<serde_json::Value>("loadwallet", &["testwallet".into()])
831            .await;
832        let _ = inscriber
833            .bitcoin
834            .call::<serde_json::Value>(
835                "generatetoaddress",
836                &[1.into(), to_value(&address).unwrap()],
837            )
838            .await
839            .unwrap();
840        let txid: Txid = inscriber
841            .bitcoin
842            .call("sendtoaddress", &[to_value(&address).unwrap(), 1.into()])
843            .await
844            .unwrap();
845
846        // load unspent from wallet
847        let tx = inscriber.bitcoin.get_transaction(&txid).await.unwrap();
848        println!("tx: {:?}", tx);
849
850        let unspent_txs = tx
851            .output
852            .iter()
853            .enumerate()
854            .filter_map(|(i, v)| {
855                if v.script_pubkey == address.script_pubkey() {
856                    Some(UnspentTxOut {
857                        txid: tx.txid(),
858                        vout: i as u32,
859                        amount: v.value,
860                        script_pubkey: v.script_pubkey.clone(),
861                    })
862                } else {
863                    None
864                }
865            })
866            .collect::<Vec<_>>();
867        println!("unspent_txs: {:#?}", unspent_txs);
868
869        let names = vec![get_name("0")];
870        let fee_rate = Amount::from_sat(20);
871        let txid = inscriber
872            .inscribe(
873                &names,
874                fee_rate,
875                &keypair.secret_key(),
876                unspent_txs.first().unwrap(),
877            )
878            .await
879            .unwrap();
880        println!("txid: {}", txid);
881
882        // load inscription tx
883        let tx = inscriber.bitcoin.get_transaction(&txid).await.unwrap();
884        println!("tx: {:?}", tx);
885
886        let envelopes = Envelope::from_transaction(&tx);
887        assert_eq!(1, envelopes.len());
888        assert_eq!(txid, envelopes[0].txid);
889        assert_eq!(0, envelopes[0].vin);
890        assert_eq!(names, envelopes[0].payload);
891
892        // trigger block building
893        let _ = inscriber
894            .bitcoin
895            .call::<serde_json::Value>(
896                "generatetoaddress",
897                &[1.into(), to_value(&address).unwrap()],
898            )
899            .await
900            .unwrap();
901
902        let tx_info = inscriber.bitcoin.get_transaction_info(&txid).await.unwrap();
903        println!("tx_info: {:?}", tx_info);
904        assert!(tx_info.blockhash.is_some());
905
906        // inscribe from previous inscription tx
907        let unspent_txs = tx
908            .output
909            .iter()
910            .enumerate()
911            .filter_map(|(i, v)| {
912                if v.script_pubkey == address.script_pubkey() {
913                    Some(UnspentTxOut {
914                        txid: tx.txid(),
915                        vout: i as u32,
916                        amount: v.value,
917                        script_pubkey: v.script_pubkey.clone(),
918                    })
919                } else {
920                    None
921                }
922            })
923            .collect::<Vec<_>>();
924        println!("unspent_txs: {:#?}", unspent_txs);
925
926        let names = "0123456789abcdefghijklmnopqrstuvwxyz"
927            .split("")
928            .skip(1)
929            .take(36)
930            .map(get_name)
931            .collect::<Vec<_>>();
932        assert_eq!(36, names.len());
933        assert_eq!("0", names[0].name);
934        assert_eq!("z", names[35].name);
935
936        let txid = inscriber
937            .inscribe(
938                &names,
939                fee_rate,
940                &keypair.secret_key(),
941                unspent_txs.first().unwrap(),
942            )
943            .await
944            .unwrap();
945        println!("txid: {}", txid);
946
947        // trigger block building
948        let _ = inscriber
949            .bitcoin
950            .call::<serde_json::Value>(
951                "generatetoaddress",
952                &[1.into(), to_value(&address).unwrap()],
953            )
954            .await
955            .unwrap();
956
957        let tx_info = inscriber.bitcoin.get_transaction_info(&txid).await.unwrap();
958        assert!(tx_info.blockhash.is_some());
959    }
960}