use crate::overlay::{
LookupAnswer, LookupQuestion, LookupResolver, LookupResolverConfig, NetworkPreset,
OutputListItem, TopicBroadcaster, TopicBroadcasterConfig,
};
use crate::primitives::{from_hex, to_hex, PublicKey};
use crate::registry::types::{
BasketDefinitionData, BasketQuery, BroadcastFailure, BroadcastSuccess,
CertificateDefinitionData, CertificateQuery, DefinitionData, DefinitionType,
ProtocolDefinitionData, ProtocolQuery, RegisterDefinitionResult, RegistryRecord,
RevokeDefinitionResult, TokenData, UpdateDefinitionResult,
};
use crate::registry::REGISTRANT_TOKEN_AMOUNT;
use crate::script::templates::PushDrop;
use crate::script::LockingScript;
use crate::transaction::Transaction;
use crate::wallet::interface::WalletInterface;
use crate::wallet::{
CreateActionArgs, CreateActionInput, CreateActionOutput, GetPublicKeyArgs, ListOutputsArgs,
Outpoint, OutputInclude, SignActionArgs, SignActionSpend,
};
use crate::{Error, Result};
use std::collections::HashMap;
use std::sync::Arc;
#[derive(Clone)]
pub struct RegistryClientConfig {
pub network_preset: NetworkPreset,
pub resolver: Option<Arc<LookupResolver>>,
pub originator: Option<String>,
pub accept_delayed_broadcast: bool,
}
impl Default for RegistryClientConfig {
fn default() -> Self {
Self {
network_preset: NetworkPreset::Mainnet,
resolver: None,
originator: None,
accept_delayed_broadcast: false,
}
}
}
impl RegistryClientConfig {
pub fn new() -> Self {
Self::default()
}
pub fn with_network(mut self, preset: NetworkPreset) -> Self {
self.network_preset = preset;
self
}
pub fn with_resolver(mut self, resolver: Arc<LookupResolver>) -> Self {
self.resolver = Some(resolver);
self
}
pub fn with_originator(mut self, originator: impl Into<String>) -> Self {
self.originator = Some(originator.into());
self
}
pub fn with_delayed_broadcast(mut self, accept: bool) -> Self {
self.accept_delayed_broadcast = accept;
self
}
}
pub struct RegistryClient<W: WalletInterface> {
wallet: W,
config: RegistryClientConfig,
resolver: Arc<LookupResolver>,
}
impl<W: WalletInterface> RegistryClient<W> {
pub fn new(wallet: W, config: RegistryClientConfig) -> Self {
let resolver = config.resolver.clone().unwrap_or_else(|| {
Arc::new(LookupResolver::new(LookupResolverConfig {
network_preset: config.network_preset,
..Default::default()
}))
});
Self {
wallet,
config,
resolver,
}
}
fn originator(&self) -> &str {
self.config
.originator
.as_deref()
.unwrap_or("registry-client")
}
async fn get_identity_key_hex(&self) -> Result<String> {
let result = self
.wallet
.get_public_key(
GetPublicKeyArgs {
identity_key: true,
protocol_id: None,
key_id: None,
counterparty: None,
for_self: None,
},
self.originator(),
)
.await?;
Ok(result.public_key)
}
async fn get_identity_public_key(&self) -> Result<PublicKey> {
let hex = self.get_identity_key_hex().await?;
PublicKey::from_hex(&hex)
}
pub async fn register_definition(
&self,
mut data: DefinitionData,
) -> Result<RegisterDefinitionResult> {
let registry_operator = self.get_identity_key_hex().await?;
data.set_registry_operator(registry_operator.clone());
let def_type = data.get_definition_type();
let identity_pubkey = self.get_identity_public_key().await?;
let fields = data.to_pushdrop_fields(®istry_operator)?;
let pushdrop = PushDrop::new(identity_pubkey, fields);
let locking_script = pushdrop.lock();
let create_result = self
.wallet
.create_action(
CreateActionArgs {
description: format!("Register a new {} item", def_type.as_str()),
inputs: None,
input_beef: None,
outputs: Some(vec![CreateActionOutput {
locking_script: locking_script.to_binary(),
satoshis: REGISTRANT_TOKEN_AMOUNT,
output_description: format!("New {} registration token", def_type.as_str()),
basket: Some(def_type.wallet_basket().to_string()),
custom_instructions: None,
tags: None,
}]),
lock_time: None,
version: None,
labels: Some(vec!["registry".to_string(), def_type.as_str().to_string()]),
options: None,
},
self.originator(),
)
.await?;
if create_result.tx.is_none() {
return Err(Error::RegistryError(format!(
"Failed to create {} registration transaction",
def_type.as_str()
)));
}
let txid = create_result.txid.ok_or_else(|| {
Error::RegistryError("No transaction ID in create result".to_string())
})?;
let topic = def_type.broadcast_topic().to_string();
let mut broadcast_success = None;
let mut broadcast_failure = None;
if let Some(ref tx_bytes) = create_result.tx {
if let Ok(tx) = Transaction::from_beef(tx_bytes, None) {
match TopicBroadcaster::new(
vec![topic],
TopicBroadcasterConfig {
network_preset: self.config.network_preset,
..Default::default()
},
) {
Ok(broadcaster) => {
let result = broadcaster.broadcast_tx(&tx).await;
if result.is_ok() {
broadcast_success = Some(BroadcastSuccess {
txid: to_hex(&txid),
message: "success".to_string(),
});
} else {
broadcast_failure = Some(BroadcastFailure {
code: "BROADCAST_ERROR".to_string(),
description: "Failed to broadcast to overlay".to_string(),
});
}
}
Err(e) => {
broadcast_failure = Some(BroadcastFailure {
code: "BROADCASTER_ERROR".to_string(),
description: e.to_string(),
});
}
}
}
}
if broadcast_success.is_none() && broadcast_failure.is_none() {
broadcast_success = Some(BroadcastSuccess {
txid: to_hex(&txid),
message: "created".to_string(),
});
}
Ok(RegisterDefinitionResult {
success: broadcast_success,
failure: broadcast_failure,
})
}
pub async fn resolve_basket(&self, query: BasketQuery) -> Result<Vec<BasketDefinitionData>> {
let service = DefinitionType::Basket.lookup_service();
let query_json = serde_json::to_value(&query)
.map_err(|e| Error::RegistryError(format!("Failed to serialize query: {}", e)))?;
let question = LookupQuestion::new(service, query_json);
let answer = self.resolver.query(&question, None).await?;
match answer {
LookupAnswer::OutputList { outputs } => {
let mut results = Vec::new();
for output in outputs {
if let Ok(Some(data)) = self.parse_basket_from_output(&output) {
results.push(data);
}
}
Ok(results)
}
_ => Ok(Vec::new()),
}
}
pub async fn resolve_protocol(
&self,
query: ProtocolQuery,
) -> Result<Vec<ProtocolDefinitionData>> {
let service = DefinitionType::Protocol.lookup_service();
let query_json = serde_json::to_value(&query)
.map_err(|e| Error::RegistryError(format!("Failed to serialize query: {}", e)))?;
let question = LookupQuestion::new(service, query_json);
let answer = self.resolver.query(&question, None).await?;
match answer {
LookupAnswer::OutputList { outputs } => {
let mut results = Vec::new();
for output in outputs {
if let Ok(Some(data)) = self.parse_protocol_from_output(&output) {
results.push(data);
}
}
Ok(results)
}
_ => Ok(Vec::new()),
}
}
pub async fn resolve_certificate(
&self,
query: CertificateQuery,
) -> Result<Vec<CertificateDefinitionData>> {
let service = DefinitionType::Certificate.lookup_service();
let query_json = serde_json::to_value(&query)
.map_err(|e| Error::RegistryError(format!("Failed to serialize query: {}", e)))?;
let question = LookupQuestion::new(service, query_json);
let answer = self.resolver.query(&question, None).await?;
match answer {
LookupAnswer::OutputList { outputs } => {
let mut results = Vec::new();
for output in outputs {
if let Ok(Some(data)) = self.parse_certificate_from_output(&output) {
results.push(data);
}
}
Ok(results)
}
_ => Ok(Vec::new()),
}
}
fn parse_basket_from_output(
&self,
output: &OutputListItem,
) -> Result<Option<BasketDefinitionData>> {
let tx = Transaction::from_beef(&output.beef, None)
.map_err(|e| Error::RegistryError(format!("Failed to parse BEEF: {}", e)))?;
let tx_output = tx
.outputs
.get(output.output_index as usize)
.ok_or_else(|| Error::RegistryError("Output index out of bounds".into()))?;
let pushdrop = PushDrop::decode(&tx_output.locking_script)
.map_err(|e| Error::RegistryError(format!("Failed to decode PushDrop: {}", e)))?;
if pushdrop.fields.len() != 6 {
return Ok(None);
}
BasketDefinitionData::from_pushdrop_fields(&pushdrop.fields).map(Some)
}
fn parse_protocol_from_output(
&self,
output: &OutputListItem,
) -> Result<Option<ProtocolDefinitionData>> {
let tx = Transaction::from_beef(&output.beef, None)
.map_err(|e| Error::RegistryError(format!("Failed to parse BEEF: {}", e)))?;
let tx_output = tx
.outputs
.get(output.output_index as usize)
.ok_or_else(|| Error::RegistryError("Output index out of bounds".into()))?;
let pushdrop = PushDrop::decode(&tx_output.locking_script)
.map_err(|e| Error::RegistryError(format!("Failed to decode PushDrop: {}", e)))?;
if pushdrop.fields.len() != 6 {
return Ok(None);
}
ProtocolDefinitionData::from_pushdrop_fields(&pushdrop.fields).map(Some)
}
fn parse_certificate_from_output(
&self,
output: &OutputListItem,
) -> Result<Option<CertificateDefinitionData>> {
let tx = Transaction::from_beef(&output.beef, None)
.map_err(|e| Error::RegistryError(format!("Failed to parse BEEF: {}", e)))?;
let tx_output = tx
.outputs
.get(output.output_index as usize)
.ok_or_else(|| Error::RegistryError("Output index out of bounds".into()))?;
let pushdrop = PushDrop::decode(&tx_output.locking_script)
.map_err(|e| Error::RegistryError(format!("Failed to decode PushDrop: {}", e)))?;
if pushdrop.fields.len() != 7 {
return Ok(None);
}
CertificateDefinitionData::from_pushdrop_fields(&pushdrop.fields).map(Some)
}
pub async fn list_own_registry_entries(
&self,
definition_type: DefinitionType,
) -> Result<Vec<RegistryRecord>> {
let basket = definition_type.wallet_basket().to_string();
let list_result = self
.wallet
.list_outputs(
ListOutputsArgs {
basket,
include: Some(OutputInclude::EntireTransactions),
include_custom_instructions: Some(true),
include_tags: Some(true),
include_labels: Some(true),
tags: None,
tag_query_mode: None,
limit: None,
offset: None,
seek_permission: None,
},
self.originator(),
)
.await?;
let mut records = Vec::new();
for output in list_result.outputs {
if !output.spendable {
continue;
}
if let Some(ref script_bytes) = output.locking_script {
if let Ok(script) = LockingScript::from_binary(script_bytes) {
if let Ok(pushdrop) = PushDrop::decode(&script) {
if pushdrop.fields.len() != definition_type.expected_field_count() {
continue;
}
let token = TokenData::with_beef(
to_hex(&output.outpoint.txid),
output.outpoint.vout,
output.satoshis,
script.to_hex(),
list_result.beef.clone().unwrap_or_default(),
);
let record = match definition_type {
DefinitionType::Basket => {
BasketDefinitionData::from_pushdrop_fields(&pushdrop.fields)
.ok()
.map(|def| RegistryRecord::basket(def, token))
}
DefinitionType::Protocol => {
ProtocolDefinitionData::from_pushdrop_fields(&pushdrop.fields)
.ok()
.map(|def| RegistryRecord::protocol(def, token))
}
DefinitionType::Certificate => {
CertificateDefinitionData::from_pushdrop_fields(&pushdrop.fields)
.ok()
.map(|def| RegistryRecord::certificate(def, token))
}
};
if let Some(r) = record {
records.push(r);
}
}
}
}
}
Ok(records)
}
pub async fn revoke_own_registry_entry(
&self,
record: &RegistryRecord,
) -> Result<RevokeDefinitionResult> {
if record.txid().is_empty() || record.token.locking_script.is_empty() {
return Err(Error::RegistryError(
"Invalid registry record - missing txid or lockingScript".to_string(),
));
}
let identity_key = self.get_identity_key_hex().await?;
if record.get_registry_operator() != identity_key {
return Err(Error::RegistryError(
"This registry token does not belong to the current wallet".to_string(),
));
}
let def_type = record.get_definition_type();
let item_identifier = match &record.definition {
DefinitionData::Basket(d) => d.basket_id.clone(),
DefinitionData::Protocol(d) => d.name.clone(),
DefinitionData::Certificate(d) => {
if !d.name.is_empty() {
d.name.clone()
} else {
d.cert_type.clone()
}
}
};
let txid_bytes = from_hex(record.txid())
.map_err(|e| Error::RegistryError(format!("Invalid txid: {}", e)))?;
let mut txid_arr = [0u8; 32];
if txid_bytes.len() != 32 {
return Err(Error::RegistryError("Invalid txid length".to_string()));
}
txid_arr.copy_from_slice(&txid_bytes);
let create_result = self
.wallet
.create_action(
CreateActionArgs {
description: format!("Revoke {} item: {}", def_type.as_str(), item_identifier),
inputs: Some(vec![CreateActionInput {
outpoint: Outpoint {
txid: txid_arr,
vout: record.output_index(),
},
unlocking_script: None,
unlocking_script_length: Some(74), input_description: format!("Revoking {} token", def_type.as_str()),
sequence_number: None,
}]),
input_beef: record.token.beef.clone(),
outputs: None, lock_time: None,
version: None,
labels: Some(vec![
"registry".to_string(),
"revoke".to_string(),
def_type.as_str().to_string(),
]),
options: None,
},
self.originator(),
)
.await?;
if create_result.signable_transaction.is_none() {
return Err(Error::RegistryError(
"Failed to create signable transaction".to_string(),
));
}
if let Some(ref signable_tx) = create_result.signable_transaction {
let mut spends = HashMap::new();
spends.insert(
record.output_index(),
SignActionSpend {
unlocking_script: vec![],
sequence_number: None,
},
);
let sign_result = self
.wallet
.sign_action(
SignActionArgs {
reference: String::from_utf8(signable_tx.reference.clone())
.unwrap_or_default(),
spends,
options: None,
},
self.originator(),
)
.await?;
if sign_result.tx.is_none() {
return Err(Error::RegistryError(
"Failed to get signed transaction".to_string(),
));
}
let topic = def_type.broadcast_topic().to_string();
let mut broadcast_success = None;
let mut broadcast_failure = None;
if let Some(ref tx_bytes) = sign_result.tx {
if let Ok(tx) = Transaction::from_beef(tx_bytes, None) {
match TopicBroadcaster::new(
vec![topic],
TopicBroadcasterConfig {
network_preset: self.config.network_preset,
..Default::default()
},
) {
Ok(broadcaster) => {
let result = broadcaster.broadcast_tx(&tx).await;
if result.is_ok() {
broadcast_success = Some(BroadcastSuccess {
txid: tx.id(),
message: "success".to_string(),
});
} else {
broadcast_failure = Some(BroadcastFailure {
code: "BROADCAST_ERROR".to_string(),
description: "Failed to broadcast revocation".to_string(),
});
}
}
Err(e) => {
broadcast_failure = Some(BroadcastFailure {
code: "BROADCASTER_ERROR".to_string(),
description: e.to_string(),
});
}
}
}
}
if broadcast_success.is_none() && broadcast_failure.is_none() {
broadcast_success = Some(BroadcastSuccess {
txid: "signed".to_string(),
message: "created".to_string(),
});
}
return Ok(RevokeDefinitionResult {
success: broadcast_success,
failure: broadcast_failure,
});
}
Err(Error::RegistryError(
"Failed to sign revocation transaction".to_string(),
))
}
pub async fn update_definition(
&self,
record: &RegistryRecord,
mut updated_data: DefinitionData,
) -> Result<UpdateDefinitionResult> {
if record.txid().is_empty() || record.token.locking_script.is_empty() {
return Err(Error::RegistryError(
"Invalid registry record - missing txid or lockingScript".to_string(),
));
}
if record.get_definition_type() != updated_data.get_definition_type() {
return Err(Error::RegistryError(format!(
"Cannot change definition type from {} to {}",
record.get_definition_type(),
updated_data.get_definition_type()
)));
}
let identity_key = self.get_identity_key_hex().await?;
if record.get_registry_operator() != identity_key {
return Err(Error::RegistryError(
"This registry token does not belong to the current wallet".to_string(),
));
}
let def_type = record.get_definition_type();
let item_identifier = match &record.definition {
DefinitionData::Basket(d) => d.basket_id.clone(),
DefinitionData::Protocol(d) => d.name.clone(),
DefinitionData::Certificate(d) => {
if !d.name.is_empty() {
d.name.clone()
} else {
d.cert_type.clone()
}
}
};
updated_data.set_registry_operator(identity_key.clone());
let identity_pubkey = self.get_identity_public_key().await?;
let fields = updated_data.to_pushdrop_fields(&identity_key)?;
let pushdrop = PushDrop::new(identity_pubkey, fields);
let new_locking_script = pushdrop.lock();
let txid_bytes = from_hex(record.txid())
.map_err(|e| Error::RegistryError(format!("Invalid txid: {}", e)))?;
let mut txid_arr = [0u8; 32];
if txid_bytes.len() != 32 {
return Err(Error::RegistryError("Invalid txid length".to_string()));
}
txid_arr.copy_from_slice(&txid_bytes);
let create_result = self
.wallet
.create_action(
CreateActionArgs {
description: format!("Update {} item: {}", def_type.as_str(), item_identifier),
inputs: Some(vec![CreateActionInput {
outpoint: Outpoint {
txid: txid_arr,
vout: record.output_index(),
},
unlocking_script: None,
unlocking_script_length: Some(74), input_description: format!("Updating {} token", def_type.as_str()),
sequence_number: None,
}]),
input_beef: record.token.beef.clone(),
outputs: Some(vec![CreateActionOutput {
locking_script: new_locking_script.to_binary(),
satoshis: REGISTRANT_TOKEN_AMOUNT,
output_description: format!(
"Updated {} registration token",
def_type.as_str()
),
basket: Some(def_type.wallet_basket().to_string()),
custom_instructions: None,
tags: None,
}]),
lock_time: None,
version: None,
labels: Some(vec![
"registry".to_string(),
"update".to_string(),
def_type.as_str().to_string(),
]),
options: None,
},
self.originator(),
)
.await?;
if create_result.signable_transaction.is_none() {
return Err(Error::RegistryError(
"Failed to create signable transaction".to_string(),
));
}
if let Some(ref signable_tx) = create_result.signable_transaction {
let mut spends = HashMap::new();
spends.insert(
record.output_index(),
SignActionSpend {
unlocking_script: vec![],
sequence_number: None,
},
);
let sign_result = self
.wallet
.sign_action(
SignActionArgs {
reference: String::from_utf8(signable_tx.reference.clone())
.unwrap_or_default(),
spends,
options: None,
},
self.originator(),
)
.await?;
if sign_result.tx.is_none() {
return Err(Error::RegistryError(
"Failed to get signed transaction".to_string(),
));
}
let topic = def_type.broadcast_topic().to_string();
let mut broadcast_success = None;
let mut broadcast_failure = None;
if let Some(ref tx_bytes) = sign_result.tx {
if let Ok(tx) = Transaction::from_beef(tx_bytes, None) {
match TopicBroadcaster::new(
vec![topic],
TopicBroadcasterConfig {
network_preset: self.config.network_preset,
..Default::default()
},
) {
Ok(broadcaster) => {
let result = broadcaster.broadcast_tx(&tx).await;
if result.is_ok() {
broadcast_success = Some(BroadcastSuccess {
txid: tx.id(),
message: "success".to_string(),
});
} else {
broadcast_failure = Some(BroadcastFailure {
code: "BROADCAST_ERROR".to_string(),
description: "Failed to broadcast update".to_string(),
});
}
}
Err(e) => {
broadcast_failure = Some(BroadcastFailure {
code: "BROADCASTER_ERROR".to_string(),
description: e.to_string(),
});
}
}
}
}
if broadcast_success.is_none() && broadcast_failure.is_none() {
broadcast_success = Some(BroadcastSuccess {
txid: "signed".to_string(),
message: "created".to_string(),
});
}
return Ok(UpdateDefinitionResult {
success: broadcast_success,
failure: broadcast_failure,
});
}
Err(Error::RegistryError(
"Failed to sign update transaction".to_string(),
))
}
pub fn set_network(&mut self, network: NetworkPreset) {
self.config.network_preset = network;
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::wallet::{Protocol as WalletProtocol, SecurityLevel};
#[test]
fn test_config_defaults() {
let config = RegistryClientConfig::default();
assert_eq!(config.network_preset, NetworkPreset::Mainnet);
assert!(config.resolver.is_none());
assert!(config.originator.is_none());
assert!(!config.accept_delayed_broadcast);
}
#[test]
fn test_config_builder() {
let config = RegistryClientConfig::new()
.with_network(NetworkPreset::Testnet)
.with_originator("myapp.com")
.with_delayed_broadcast(true);
assert_eq!(config.network_preset, NetworkPreset::Testnet);
assert_eq!(config.originator, Some("myapp.com".to_string()));
assert!(config.accept_delayed_broadcast);
}
#[test]
fn test_pushdrop_fields_format_basket() {
let data = BasketDefinitionData::new("my_basket", "My Basket")
.with_icon_url("icon.png")
.with_description("desc")
.with_documentation_url("docs.html");
let fields = data.to_pushdrop_fields("02abc123");
assert_eq!(fields.len(), 6);
assert_eq!(String::from_utf8(fields[0].clone()).unwrap(), "my_basket");
assert_eq!(String::from_utf8(fields[1].clone()).unwrap(), "My Basket");
assert_eq!(String::from_utf8(fields[2].clone()).unwrap(), "icon.png");
assert_eq!(String::from_utf8(fields[3].clone()).unwrap(), "desc");
assert_eq!(String::from_utf8(fields[4].clone()).unwrap(), "docs.html");
assert_eq!(String::from_utf8(fields[5].clone()).unwrap(), "02abc123");
}
#[test]
fn test_pushdrop_fields_format_protocol() {
let protocol = WalletProtocol::new(SecurityLevel::App, "my_protocol");
let data = ProtocolDefinitionData::new(protocol, "My Protocol").with_description("desc");
let fields = data.to_pushdrop_fields("02abc123").unwrap();
assert_eq!(fields.len(), 6);
let protocol_json = String::from_utf8(fields[0].clone()).unwrap();
assert!(protocol_json.contains("my_protocol"));
assert_eq!(String::from_utf8(fields[1].clone()).unwrap(), "My Protocol");
assert_eq!(String::from_utf8(fields[5].clone()).unwrap(), "02abc123");
}
#[test]
fn test_pushdrop_fields_format_certificate() {
use crate::registry::CertificateFieldDescriptor;
let data = CertificateDefinitionData::new("cert_type", "My Cert")
.with_description("desc")
.with_field("email", CertificateFieldDescriptor::text("Email"));
let fields = data.to_pushdrop_fields("02abc123").unwrap();
assert_eq!(fields.len(), 7);
assert_eq!(String::from_utf8(fields[0].clone()).unwrap(), "cert_type");
assert_eq!(String::from_utf8(fields[1].clone()).unwrap(), "My Cert");
let fields_json = String::from_utf8(fields[5].clone()).unwrap();
assert!(fields_json.contains("email"));
assert_eq!(String::from_utf8(fields[6].clone()).unwrap(), "02abc123");
}
#[test]
fn test_definition_data_conversion() {
let basket = BasketDefinitionData::new("b", "Basket");
let data: DefinitionData = basket.into();
assert_eq!(data.get_definition_type(), DefinitionType::Basket);
let protocol =
ProtocolDefinitionData::new(WalletProtocol::new(SecurityLevel::App, "p"), "Protocol");
let data: DefinitionData = protocol.into();
assert_eq!(data.get_definition_type(), DefinitionType::Protocol);
}
#[test]
fn test_update_definition_result() {
use crate::registry::UpdateDefinitionResult;
let success_result = UpdateDefinitionResult {
success: Some(BroadcastSuccess {
txid: "abc123".to_string(),
message: "success".to_string(),
}),
failure: None,
};
assert!(success_result.is_success());
assert!(!success_result.is_failure());
let failure_result = UpdateDefinitionResult {
success: None,
failure: Some(BroadcastFailure {
code: "ERR".to_string(),
description: "Failed".to_string(),
}),
};
assert!(!failure_result.is_success());
assert!(failure_result.is_failure());
}
}