Skip to content

Commit d6af757

Browse files
committed
feat: Expose psbt output
1 parent 1753965 commit d6af757

File tree

2 files changed

+306
-0
lines changed

2 files changed

+306
-0
lines changed

bdk-ffi/src/bitcoin.rs

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,12 @@ use bdk_wallet::bitcoin::hashes::sha256::Hash as BitcoinSha256Hash;
1919
use bdk_wallet::bitcoin::hashes::sha256d::Hash as BitcoinDoubleSha256Hash;
2020
use bdk_wallet::bitcoin::io::Cursor;
2121
use bdk_wallet::bitcoin::psbt::Input as BdkInput;
22+
use bdk_wallet::bitcoin::psbt::Output as BdkOutput;
2223
use bdk_wallet::bitcoin::secp256k1::Secp256k1;
24+
25+
use bdk_wallet::bitcoin::taproot::LeafNode as BdkLeafNode;
26+
use bdk_wallet::bitcoin::taproot::NodeInfo as BdkNodeInfo;
27+
use bdk_wallet::bitcoin::taproot::TapTree as BdkTapTree;
2328
use bdk_wallet::bitcoin::Amount as BdkAmount;
2429
use bdk_wallet::bitcoin::BlockHash as BitcoinBlockHash;
2530
use bdk_wallet::bitcoin::FeeRate as BdkFeeRate;
@@ -804,6 +809,218 @@ impl From<&BdkInput> for Input {
804809
}
805810
}
806811

812+
/// Store information about taproot leaf node.
813+
#[derive(Debug, uniffi::Object)]
814+
#[uniffi::export(Display)]
815+
pub struct LeafNode(BdkLeafNode);
816+
817+
#[uniffi::export]
818+
impl LeafNode {
819+
/// Returns the depth of this script leaf in the tap tree.
820+
pub fn depth(&self) -> u8 {
821+
self.0.depth()
822+
}
823+
824+
/// Computes a leaf hash for this ScriptLeaf if the leaf is known.
825+
/// This TapLeafHash is useful while signing taproot script spends.
826+
/// See LeafNode::node_hash for computing the TapNodeHash which returns the hidden node hash if the node is hidden.
827+
pub fn leaf_hash(&self) -> Option<String> {
828+
self.0.leaf_hash().map(|h| h.to_string())
829+
}
830+
831+
/// Computes the [`TapNodeHash`] for this [`ScriptLeaf`]. This returns the
832+
/// leaf hash if the leaf is known and the hidden node hash if the leaf is
833+
/// hidden.
834+
/// See also, [`bdk_electrum::bdk_core::bitcoin::taproot::LeafNode::leaf_hash`].
835+
pub fn node_hash(&self) -> String {
836+
self.0.node_hash().to_string()
837+
}
838+
839+
/// Returns reference to the leaf script if the leaf is known.
840+
pub fn script(&self) -> Option<Arc<Script>> {
841+
self.0.script().map(|s| Arc::new(Script(s.to_owned())))
842+
}
843+
844+
/// Returns leaf version of the script if the leaf is known.
845+
pub fn leaf_version(&self) -> Option<u8> {
846+
self.0.leaf_version().map(|n| n.to_consensus())
847+
}
848+
849+
/// Returns reference to the merkle proof (hashing partners) to get this
850+
/// node in form of [`TaprootMerkleBranch`].
851+
pub fn merkle_branch(&self) -> Vec<String> {
852+
self.0
853+
.merkle_branch()
854+
.to_vec()
855+
.iter()
856+
.map(|h| h.to_string())
857+
.collect()
858+
}
859+
}
860+
861+
impl Display for LeafNode {
862+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
863+
write!(f, "{:?}", self)
864+
}
865+
}
866+
867+
/// Taproot Tree representing a complete binary tree without any hidden nodes.
868+
///
869+
/// This is in contrast to NodeInfo, which allows hidden nodes. The implementations for Eq, PartialEq and Hash compare the merkle root of the tree
870+
#[derive(Debug, uniffi::Object)]
871+
#[uniffi::export(Display)]
872+
pub struct TapTree(BdkTapTree);
873+
874+
#[uniffi::export]
875+
impl TapTree {
876+
/// Returns the root TapNodeHash of this tree.
877+
pub fn root_hash(&self) -> String {
878+
self.0.root_hash().to_string()
879+
}
880+
881+
/// Gets the reference to inner NodeInfo of this tree root.
882+
pub fn node_info(&self) -> Arc<NodeInfo> {
883+
Arc::new(NodeInfo(self.0.node_info().clone()))
884+
}
885+
}
886+
887+
impl Display for TapTree {
888+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
889+
write!(f, "{:?}", self)
890+
}
891+
}
892+
893+
/// Represents the node information in taproot tree. In contrast to TapTree, this is allowed to have hidden leaves as children.
894+
///
895+
/// Helper type used in merkle tree construction allowing one to build sparse merkle trees. The node represents part of the tree that has information about all of its descendants. See how TaprootBuilder works for more details.
896+
/// You can use TaprootSpendInfo::from_node_info to a get a TaprootSpendInfo from the merkle root NodeInfo.
897+
#[derive(Debug, uniffi::Object)]
898+
#[uniffi::export(Display)]
899+
pub struct NodeInfo(BdkNodeInfo);
900+
901+
#[uniffi::export]
902+
impl NodeInfo {
903+
/// Creates an iterator over all leaves (including hidden leaves) in the tree.
904+
pub fn leaf_nodes(&self) -> Vec<Arc<LeafNode>> {
905+
self.0
906+
.leaf_nodes()
907+
.map(|ln| Arc::new(LeafNode(ln.clone())))
908+
.collect()
909+
}
910+
911+
/// Returns the root TapNodeHash of this node info.
912+
pub fn node_hash(&self) -> String {
913+
self.0.node_hash().to_string()
914+
}
915+
}
916+
917+
impl Display for NodeInfo {
918+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
919+
write!(f, "{:?}", self)
920+
}
921+
}
922+
923+
/// A key-value map for an output of the corresponding index in the unsigned
924+
/// transaction.
925+
#[derive(Debug, uniffi::Record)]
926+
pub struct Output {
927+
/// The redeem script for this output.
928+
pub redeem_script: Option<Arc<Script>>,
929+
/// The witness script for this output.
930+
pub witness_script: Option<Arc<Script>>,
931+
/// Map of public keys needed to spend this output to their corresponding
932+
/// master key fingerprints and derivation paths.
933+
pub bip32_derivation: HashMap<String, KeySource>,
934+
/// Taproot Internal key.
935+
pub tap_internal_key: Option<String>,
936+
/// Taproot Output tree (structured record).
937+
pub tap_tree: Option<Arc<TapTree>>,
938+
/// Map of tap root x only keys to origin info and leaf hashes contained in it.
939+
pub tap_key_origins: HashMap<String, TapKeyOrigin>,
940+
/// Proprietary key-value pairs for this output.
941+
pub proprietary: HashMap<ProprietaryKey, Vec<u8>>,
942+
/// Unknown key-value pairs for this output.
943+
pub unknown: HashMap<Key, Vec<u8>>,
944+
}
945+
946+
impl From<&BdkOutput> for Output {
947+
fn from(output: &BdkOutput) -> Self {
948+
Output {
949+
redeem_script: output
950+
.redeem_script
951+
.as_ref()
952+
.map(|s| Arc::new(Script(s.clone()))),
953+
witness_script: output
954+
.witness_script
955+
.as_ref()
956+
.map(|s| Arc::new(Script(s.clone()))),
957+
bip32_derivation: output
958+
.bip32_derivation
959+
.iter()
960+
.map(|(pk, (fingerprint, deriv_path))| {
961+
(
962+
pk.to_string(),
963+
KeySource {
964+
fingerprint: fingerprint.to_string(),
965+
path: Arc::new(deriv_path.clone().into()),
966+
},
967+
)
968+
})
969+
.collect(),
970+
tap_internal_key: output.tap_internal_key.as_ref().map(|k| k.to_string()),
971+
tap_tree: output
972+
.tap_tree
973+
.as_ref()
974+
.map(|t| Arc::new(TapTree(t.clone()))),
975+
tap_key_origins: output
976+
.tap_key_origins
977+
.iter()
978+
.map(|(k, v)| {
979+
let key = k.to_string();
980+
let value = TapKeyOrigin {
981+
tap_leaf_hashes: v.0.iter().map(|h| h.to_string()).collect(),
982+
key_source: KeySource {
983+
// Unnecessary spaces being added by fmt. We use #[rustfmt::skip] to avoid them for now.
984+
#[rustfmt::skip]
985+
fingerprint: v.1.0.to_string(),
986+
#[rustfmt::skip]
987+
path: Arc::new(v.1.1.clone().into()),
988+
},
989+
};
990+
(key, value)
991+
})
992+
.collect(),
993+
proprietary: output
994+
.proprietary
995+
.iter()
996+
.map(|(k, v)| {
997+
(
998+
ProprietaryKey {
999+
prefix: k.prefix.clone(),
1000+
subtype: k.subtype,
1001+
key: k.key.clone(),
1002+
},
1003+
v.to_vec(),
1004+
)
1005+
})
1006+
.collect(),
1007+
unknown: output
1008+
.unknown
1009+
.iter()
1010+
.map(|(k, v)| {
1011+
(
1012+
Key {
1013+
key: k.key.clone(),
1014+
type_value: k.type_value,
1015+
},
1016+
v.to_vec(),
1017+
)
1018+
})
1019+
.collect(),
1020+
}
1021+
}
1022+
}
1023+
8071024
/// A Partially Signed Transaction.
8081025
#[derive(uniffi::Object)]
8091026
pub struct Psbt(pub(crate) Mutex<BdkPsbt>);
@@ -935,6 +1152,12 @@ impl Psbt {
9351152
let psbt = self.0.lock().unwrap();
9361153
psbt.inputs.iter().map(|input| input.into()).collect()
9371154
}
1155+
1156+
/// The corresponding key-value map for each output in the unsigned transaction.
1157+
pub fn output(&self) -> Vec<Output> {
1158+
let psbt = self.0.lock().unwrap();
1159+
psbt.outputs.iter().map(|o| o.into()).collect()
1160+
}
9381161
}
9391162

9401163
impl From<BdkPsbt> for Psbt {

bdk-ffi/src/tests/bitcoin.rs

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -524,6 +524,84 @@ fn test_psbt_input_proprietary() {
524524
);
525525
}
526526

527+
#[test]
528+
fn test_psbt_output_length() {
529+
let psbt = sample_psbt();
530+
let psbt_outputs = psbt.output();
531+
println!("Psbt Output: {:?}", psbt_outputs);
532+
533+
assert_eq!(psbt_outputs.len(), 2);
534+
}
535+
536+
#[test]
537+
fn test_psbt_output_witness_script() {
538+
let psbt = sample_psbt();
539+
println!("Psbt: {:?}", psbt.json_serialize());
540+
let psbt_outputs = psbt.output();
541+
assert!(!psbt_outputs.is_empty(), "Output should not be empty");
542+
println!("Psbt Output: {:?}", psbt_outputs);
543+
let output = &psbt_outputs[0];
544+
let witness_script = output
545+
.witness_script
546+
.as_ref()
547+
.expect("Witness script should be present");
548+
let byte_witness = witness_script.to_bytes();
549+
let witness_hex = DisplayHex::to_lower_hex_string(&byte_witness);
550+
let expected_witness_hex = "522103b72bf1f4c738fb44fadd3333789626fa5f3efb0d695c90d126abea721ef6d417210326ee4ece63eabe2ec81eddb5400ae49af6bd7d26cfa536e4ed1217a15a4a5ed621027a51e6ce68730ec4130e702921c9d6473de8151ebc517d5a83c8df93f48aba8a53ae";
551+
assert_eq!(
552+
witness_hex, expected_witness_hex,
553+
"Witness script hex does not match the expected value"
554+
);
555+
}
556+
557+
#[test]
558+
fn test_psbt_output_tap_tree() {
559+
let psbt = sample_psbt_taproot();
560+
println!("Psbt: {:?}", psbt.json_serialize());
561+
let psbt_outputs = psbt.output();
562+
assert!(!psbt_outputs.is_empty(), "Output should not be empty");
563+
println!("Psbt Output: {:?}", psbt_outputs);
564+
let output = &psbt_outputs[1];
565+
let tap_tree = output
566+
.tap_tree
567+
.as_ref()
568+
.expect("Tap tree should be present");
569+
let tap_tree_root_hash = tap_tree.root_hash();
570+
571+
let expected_tap_tree_root_hash =
572+
"a5cc5e9312d2a08787c6597d71ba00733d0b13357aac952ce4b9519c72ffc2c5";
573+
assert_eq!(
574+
tap_tree_root_hash, expected_tap_tree_root_hash,
575+
"Tap tree root hash does not match the expected value"
576+
);
577+
578+
//Check script and version
579+
let expected_script_hex = "200f7e1b4af070857b37c203c8759915b7cb97ef99f7d3d9c51eb516791cdb7145ac20a2574e343ae4bcee78c2b061e508e5e817bfa7d8ac5a07f20ac9b39e9933df20ba20400d4657d75ff6b396b59c496f61e25d4d2fe489c792a777682f32655387cbcaba529c";
580+
let expected_leaf_version = Some(192u8); // 0xc0 which is default for Taproot scripts. 192 in decimal
581+
582+
// Get script and version from the first leaf node
583+
let node_info = tap_tree.node_info();
584+
println!("Tap tree node infos: {:?}", node_info);
585+
586+
let leaf_nodes = node_info.leaf_nodes();
587+
let leaf_node = &leaf_nodes[0];
588+
let script = leaf_node.script();
589+
590+
let script_byte = script.unwrap().to_bytes();
591+
let script_hex = DisplayHex::to_lower_hex_string(&script_byte);
592+
593+
let leaf_version = leaf_node.leaf_version();
594+
595+
assert_eq!(
596+
script_hex, expected_script_hex,
597+
"Tap tree script hex does not match the expected value"
598+
);
599+
assert_eq!(
600+
leaf_version, expected_leaf_version,
601+
"Tap tree leaf version does not match the expected value"
602+
);
603+
}
604+
527605
fn sample_psbt() -> Psbt {
528606
Psbt::new("cHNidP8BAH0CAAAAAXHl8cCbj84lm1v42e54IGI6CQru/nBXwrPE3q2fiGO4AAAAAAD9////Ar4DAAAAAAAAIgAgYw/rnGd4Bifj8s7TaMgR2tal/lq+L1jVv2Sqd1mxMbJEEQAAAAAAABYAFNVpt8vHYUPZNSF6Hu07uP1YeHts4QsAAAABALUCAAAAAAEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////BAJ+CwD/////AkAlAAAAAAAAIgAgQyrnn86L9D3vDiH959KJbPudDHc/bp6nI9E5EBLQD1YAAAAAAAAAACZqJKohqe3i9hw/cdHe/T+pmd+jaVN1XGkGiXmZYrSL69g2l06M+QEgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQErQCUAAAAAAAAiACBDKuefzov0Pe8OIf3n0ols+50Mdz9unqcj0TkQEtAPViICAy4V+d/Qff71zzPXxK4FWG5x+wL/Ku93y/LG5p+0rI2xSDBFAiEA9b0OdASAs0P2uhQinjN7QGP5jX/b32LcShBmny8U0RUCIBebxvCDbpchCjqLAhOMjydT80DAzokaalGzV7XVTsbiASICA1tMY+46EgxIHU18bgHnUvAAlAkMq5LfwkpOGZ97sDKRRzBEAiBpmlZwJocNEiKLxexEX0Par6UgG8a89AklTG3/z9AHlAIgQH/ybCvfKJzr2dq0+IyueDebm7FamKIJdzBYWMXRr/wBIgID+aCzK9nclwhbbN7KbIVGUQGLWZsjcaqWPxk9gFeG+FxIMEUCIQDRPBzb0i9vaUmxCcs1yz8uq4tq1mdDAYvvYn3isKEhFAIgfmeTLLzMo0mmQ23ooMnyx6iPceE8xV5CvARuJsd88tEBAQVpUiEDW0xj7joSDEgdTXxuAedS8ACUCQyrkt/CSk4Zn3uwMpEhAy4V+d/Qff71zzPXxK4FWG5x+wL/Ku93y/LG5p+0rI2xIQP5oLMr2dyXCFts3spshUZRAYtZmyNxqpY/GT2AV4b4XFOuIgYDLhX539B9/vXPM9fErgVYbnH7Av8q73fL8sbmn7SsjbEYCapBE1QAAIABAACAAAAAgAAAAAAAAAAAIgYDW0xj7joSDEgdTXxuAedS8ACUCQyrkt/CSk4Zn3uwMpEY2bvrelQAAIABAACAAAAAgAAAAAAAAAAAIgYD+aCzK9nclwhbbN7KbIVGUQGLWZsjcaqWPxk9gFeG+FwYAKVFVFQAAIABAACAAAAAgAAAAAAAAAAAAAEBaVIhA7cr8fTHOPtE+t0zM3iWJvpfPvsNaVyQ0Sar6nIe9tQXIQMm7k7OY+q+Lsge3bVACuSa9r19Js+lNuTtEhehWkpe1iECelHmzmhzDsQTDnApIcnWRz3oFR68UX1ag8jfk/SKuopTriICAnpR5s5ocw7EEw5wKSHJ1kc96BUevFF9WoPI35P0irqKGAClRVRUAACAAQAAgAAAAIABAAAAAAAAACICAybuTs5j6r4uyB7dtUAK5Jr2vX0mz6U25O0SF6FaSl7WGAmqQRNUAACAAQAAgAAAAIABAAAAAAAAACICA7cr8fTHOPtE+t0zM3iWJvpfPvsNaVyQ0Sar6nIe9tQXGNm763pUAACAAQAAgAAAAIABAAAAAAAAAAAA".to_string())
529607
.unwrap()
@@ -533,3 +611,8 @@ fn sample_psbt2() -> Psbt {
533611
Psbt::new("cHNidP8BAFMBAAAAATkUkZZWjQ4TAMqaOkez2dl2+5yBsfd38qS6x8fkjesmAQAAAAD/////AXL++E4sAAAAF6kUM5cluiHv1irHU6m80GfWx6ajnQWHAAAAAE8BBIiyHgAAAAAAAAAAAIc9/4HAL1JWI/0f5RZ+rDpVoEnePTFLtC7iJ//tN9UIAzmjYBMwFZfa70H75ZOgLMUT0LVVJ+wt8QUOLo/0nIXCDN6tvu8AAACAAQAAABD8BXByZWZ4KnRlc3Rfa2V5AwUGBwMJAAEDAwQFAAEAjwEAAAAAAQGJo8ceq00g4Dcbu6TMaY+ilclGOvouOX+FM8y2L5Vn5QEAAAAXFgAUvhjRUqmwEgOdrz2n3k9TNJ7suYX/////AXL++E4sAAAAF6kUM5cluiHv1irHU6m80GfWx6ajnQWHASED0uFWdJQbrUqZY3LLh+GFbTZSYG2YVi/jnF6efkE/IQUAAAAAAQEgcv74TiwAAAAXqRQzlyW6Ie/WKsdTqbzQZ9bHpqOdBYciAgM5iA3JI5S3NV49BDn6KDwx3nWQgS6gEcQkXAZ0poXog0cwRAIgT2fir7dhQtRPrliiSV0zo0GdqibNDbjQTzRStjKJrA8CIBB2Kp+2fpTMXK2QJvbcmf9/Bw9CeNMPvH0Mhp3TjH/nAQEDBIMAAAABBAFRIgYDOYgNySOUtzVePQQ5+ig8Md51kIEuoBHEJFwGdKaF6IMM3q2+7wAAAIABAAAAAQgGAgIBAwEFFQoYn3yLGjhv/o7tkbODDHp7zR53jAIBAiELoShx/uIQ+4YZKR6uoZRYHL0lMeSyN1nSJfaAaSP2MiICAQIVDBXMSeGRy8Ug2RlEYApct3r2qjKRAgECIQ12pWrO2RXSUT3NhMLDeLLoqlzWMrW3HKLyrFsOOmSb2wIBAhD8BXByZWZ4KnRlc3Rfa2V5AwUGBwMJAAEDAwQFACICAzmIDckjlLc1Xj0EOfooPDHedZCBLqARxCRcBnSmheiDDN6tvu8AAACAAQAAABD8BXByZWZ4KnRlc3Rfa2V5AwUGBwMJAAEDAwQFAA==".to_string())
534612
.unwrap()
535613
}
614+
615+
fn sample_psbt_taproot() -> Psbt {
616+
Psbt::new("cHNidP8BAH0CAAAAARblbcPN67JMY1pAsqbkYuqfh+OffiMD1PXBKuohxHUhAAAAAAD9////AkQRAAAAAAAAFgAU1Wm3y8dhQ9k1IXoe7Tu4/Vh4e2wVv/UFAAAAACJRILJL6QjSVc9B74yO2wV9qJ1D2HkxpgKV/LRX3dOOV+uMOAMAAAABASsA4fUFAAAAACJRIMSkYUKwqnaNBsaJxcZ1MKFYDd+ZEmqOaLTAGheYLSWeQRQZmDg8WRPva5p6l4cMrRyqdLSCYC74Gk1Mn1aimc9eDHAZu3+0gymYN/cLd5pvviwpc9YiW6HwxS7yCJ5umnS6QFa6MEJrll8dUVdGve8T2Q7nNfN27yTe0dWHAMEL4AvvpJddyZugvr1WuK5CfNdNvHUfuHsWalE8dXsM2XYvy4UiFcEZmDg8WRPva5p6l4cMrRyqdLSCYC74Gk1Mn1aimc9eDGkgGZg4PFkT72uaepeHDK0cqnS0gmAu+BpNTJ9WopnPXgysIGzdf1E91bpWIz3gwC+dFe5OS1a+SUQsP12wvvnaryY4uiCU7qCfqcnKJ7j6aL4hZr1iSn3Rrt04wcmnQwovyqPzWbpSnMAhFhmYODxZE+9rmnqXhwytHKp0tIJgLvgaTUyfVqKZz14MOQFwGbt/tIMpmDf3C3eab74sKXPWIluh8MUu8giebpp0ur6IapxWAACAAQAAgAAAAIAAAAAAAAAAACEWbN1/UT3VulYjPeDAL50V7k5LVr5JRCw/XbC++dqvJjg5AXAZu3+0gymYN/cLd5pvviwpc9YiW6HwxS7yCJ5umnS6WyNan1YAAIABAACAAAAAgAAAAAAAAAAAIRaU7qCfqcnKJ7j6aL4hZr1iSn3Rrt04wcmnQwovyqPzWTkBcBm7f7SDKZg39wt3mm++LClz1iJbofDFLvIInm6adLqsWpreVgAAgAEAAIAAAACAAAAAAAAAAAABFyAZmDg8WRPva5p6l4cMrRyqdLSCYC74Gk1Mn1aimc9eDAEYIHAZu3+0gymYN/cLd5pvviwpc9YiW6HwxS7yCJ5umnS6AAABBSAPfhtK8HCFezfCA8h1mRW3y5fvmffT2cUetRZ5HNtxRQEGawDAaCAPfhtK8HCFezfCA8h1mRW3y5fvmffT2cUetRZ5HNtxRawgoldONDrkvO54wrBh5Qjl6Be/p9isWgfyCsmznpkz3yC6IEANRlfXX/azlrWcSW9h4l1NL+SJx5Knd2gvMmVTh8vKulKcIQcPfhtK8HCFezfCA8h1mRW3y5fvmffT2cUetRZ5HNtxRTkBpcxekxLSoIeHxll9cboAcz0LEzV6rJUs5LlRnHL/wsW+iGqcVgAAgAEAAIAAAACAAQAAAAAAAAAhB0ANRlfXX/azlrWcSW9h4l1NL+SJx5Knd2gvMmVTh8vKOQGlzF6TEtKgh4fGWX1xugBzPQsTNXqslSzkuVGccv/Cxaxamt5WAACAAQAAgAAAAIABAAAAAAAAACEHoldONDrkvO54wrBh5Qjl6Be/p9isWgfyCsmznpkz3yA5AaXMXpMS0qCHh8ZZfXG6AHM9CxM1eqyVLOS5UZxy/8LFWyNan1YAAIABAACAAAAAgAEAAAAAAAAAAA==".to_string())
617+
.unwrap()
618+
}

0 commit comments

Comments
 (0)