neo3/neo_contract/
neo_uri.rs

1use std::str::FromStr;
2
3use crate::{
4	neo_builder::{AccountSigner, ScriptBuilder, TransactionBuilder},
5	neo_clients::{JsonRpcProvider, RpcClient},
6	neo_contract::{
7		ContractError, FungibleTokenContract, GasToken, NeoToken, SmartContractTrait, TokenTrait,
8	},
9	neo_protocol::Account,
10	neo_types::{
11		serde_with_utils::{
12			deserialize_script_hash_option, deserialize_url_option, serialize_script_hash_option,
13			serialize_url_option,
14		},
15		ContractParameter, ScriptHash, ScriptHashExtension,
16	},
17};
18use getset::{Getters, Setters};
19use primitive_types::H160;
20use reqwest::Url;
21use serde::{Deserialize, Serialize};
22
23#[derive(Debug, Clone, Serialize, Deserialize, Getters, Setters)]
24pub struct NeoURI<'a, P: JsonRpcProvider> {
25	#[serde(skip_serializing_if = "Option::is_none")]
26	#[serde(deserialize_with = "deserialize_url_option")]
27	#[serde(serialize_with = "serialize_url_option")]
28	#[getset(get = "pub", set = "pub")]
29	uri: Option<Url>,
30	#[serde(skip_serializing_if = "Option::is_none")]
31	#[serde(deserialize_with = "deserialize_script_hash_option")]
32	#[serde(serialize_with = "serialize_script_hash_option")]
33	#[getset(get = "pub", set = "pub")]
34	recipient: Option<ScriptHash>,
35	#[serde(skip_serializing_if = "Option::is_none")]
36	#[serde(deserialize_with = "deserialize_script_hash_option")]
37	#[serde(serialize_with = "serialize_script_hash_option")]
38	#[getset(get = "pub", set = "pub")]
39	token: Option<ScriptHash>,
40	#[serde(skip_serializing_if = "Option::is_none")]
41	#[getset(get = "pub", set = "pub")]
42	amount: Option<u64>,
43	#[serde(skip)]
44	provider: Option<&'a RpcClient<P>>,
45}
46
47impl<'a, P: JsonRpcProvider + 'static> NeoURI<'a, P> {
48	const NEO_SCHEME: &'static str = "neo";
49	const MIN_NEP9_URI_LENGTH: usize = 38;
50	const NEO_TOKEN_STRING: &'static str = "neo";
51	const GAS_TOKEN_STRING: &'static str = "gas";
52
53	pub fn new(provider: Option<&'a RpcClient<P>>) -> Self {
54		Self { uri: None, recipient: None, token: None, amount: None, provider }
55	}
56
57	pub fn from_uri(uri_string: &str) -> Result<Self, ContractError> {
58		let parts: Vec<&str> = uri_string.split(".unwrap()").collect();
59		let base = parts[0];
60		let query = if parts.len() > 1 { Some(parts[1]) } else { None };
61
62		let base_parts: Vec<&str> = base.split(":").collect();
63		if base_parts.len() != 2
64			|| base_parts[0] != Self::NEO_SCHEME
65			|| uri_string.len() < Self::MIN_NEP9_URI_LENGTH
66		{
67			return Err(ContractError::InvalidNeoName("Invalid NEP-9 URI".to_string()));
68		}
69
70		let mut neo_uri = Self::new(None);
71		neo_uri.set_recipient(ScriptHash::from_address(base_parts[1]).ok());
72
73		if let Some(query_str) = query {
74			for part in query_str.split("&") {
75				let kv: Vec<&str> = part.split("=").collect();
76				if kv.len() != 2 {
77					return Err(ContractError::InvalidNeoName("Invalid query".to_string()));
78				}
79
80				match kv[0] {
81					"asset" if neo_uri.token().is_none() => {
82						&neo_uri.set_token(H160::from_str(kv[1]).ok());
83					},
84					"amount" if neo_uri.amount.is_none() => {
85						neo_uri.amount = Some(kv[1].parse().unwrap());
86					},
87					_ => {},
88				}
89			}
90		}
91
92		Ok(neo_uri)
93	}
94
95	// Getters
96
97	pub fn uri_string(&self) -> Option<String> {
98		self.uri.as_ref().map(|uri| uri.to_string())
99	}
100
101	pub fn recipient_address(&self) -> Option<String> {
102		self.recipient.as_ref().map(H160::to_address)
103	}
104
105	pub fn token_string(&self) -> Option<String> {
106		self.token.as_ref().map(|token| match token {
107			token if *token == NeoToken::<P>::new(None).script_hash() =>
108				Self::NEO_TOKEN_STRING.to_owned(),
109			token if *token == GasToken::<P>::new(None).script_hash() =>
110				Self::GAS_TOKEN_STRING.to_owned(),
111			_ => ScriptHashExtension::to_bs58_string(token),
112		})
113	}
114
115	// Builders
116
117	pub async fn build_transfer_from(
118		&self,
119		sender: &Account,
120	) -> Result<TransactionBuilder<P>, ContractError> {
121		let recipient = self
122			.recipient
123			.ok_or_else(|| ContractError::InvalidStateError("Recipient not set".to_string()))?;
124		let amount = self
125			.amount
126			.ok_or_else(|| ContractError::InvalidStateError("Amount not set".to_string()))?;
127		let token_hash = self
128			.token
129			.ok_or_else(|| ContractError::InvalidStateError("Token not set".to_string()))?;
130
131		// Validate amount precision
132		let amount_scale = (amount as f64).log10().floor() as u32 + 1;
133
134		if Self::is_neo_token(&token_hash) && amount_scale > 0 {
135			return Err(ContractError::InvalidArgError("NEO does not support decimals".to_string()));
136		}
137
138		if Self::is_gas_token(&token_hash)
139			&& amount_scale > GasToken::<P>::new(None).decimals().unwrap() as u32
140		{
141			return Err(ContractError::InvalidArgError(
142				"Too many decimal places for GAS".to_string(),
143			));
144		}
145
146		let mut token = FungibleTokenContract::new(&token_hash, self.provider);
147
148		let decimals = token.get_decimals().await?;
149		if amount_scale > decimals as u32 {
150			return Err(ContractError::InvalidArgError(
151				"Too many decimal places for token".to_string(),
152			));
153		}
154
155		let amt = token.to_fractions(amount, 0)?;
156
157		// Create a new TransactionBuilder
158		let mut tx_builder = TransactionBuilder::new();
159
160		// Build the script for the transfer
161		let script = ScriptBuilder::new()
162			.contract_call(
163				&token_hash,
164				"transfer",
165				&[
166					ContractParameter::h160(&sender.get_script_hash()),
167					ContractParameter::h160(&recipient),
168					ContractParameter::integer(amt as i64),
169					ContractParameter::any(),
170				],
171				None,
172			)
173			.map_err(|err| ContractError::RuntimeError(err.to_string()))?
174			.to_bytes();
175
176		// Set up the TransactionBuilder
177		tx_builder
178			.set_script(Some(script))
179			.set_signers(vec![AccountSigner::called_by_entry(sender).unwrap().into()]);
180
181		Ok(tx_builder)
182	}
183
184	// Helpers
185
186	fn is_neo_token(token: &H160) -> bool {
187		token == &NeoToken::<P>::new(None).script_hash()
188	}
189
190	fn is_gas_token(token: &H160) -> bool {
191		token == &GasToken::<P>::new(None).script_hash()
192	}
193
194	// Setters
195
196	pub fn token_str(&mut self, token_str: &str) {
197		self.token = match token_str {
198			Self::NEO_TOKEN_STRING => Some(NeoToken::new(self.provider).script_hash()),
199			Self::GAS_TOKEN_STRING => Some(GasToken::new(self.provider).script_hash()),
200			_ => Some(token_str.parse().unwrap()),
201		};
202	}
203
204	// URI builder
205
206	fn build_query(&self) -> String {
207		let mut parts = Vec::new();
208
209		if let Some(token) = &self.token {
210			let token_str = match token {
211				token if *token == NeoToken::new(self.provider).script_hash() =>
212					Self::NEO_TOKEN_STRING.to_owned(),
213				token if *token == GasToken::new(self.provider).script_hash() =>
214					Self::GAS_TOKEN_STRING.to_owned(),
215				_ => ScriptHashExtension::to_bs58_string(token),
216			};
217
218			parts.push(format!("asset={}", token_str));
219		}
220
221		if let Some(amount) = &self.amount {
222			parts.push(format!("amount={}", amount));
223		}
224
225		parts.join("&")
226	}
227
228	pub fn build_uri(&mut self) -> Result<Url, ContractError> {
229		let recipient = self
230			.recipient
231			.ok_or(ContractError::InvalidStateError("No recipient set".to_string()))
232			.unwrap();
233
234		let base = format!("{}:{}", Self::NEO_SCHEME, recipient.to_address());
235		let query = self.build_query();
236		let uri_str = if query.is_empty() { base } else { format!("{}.unwrap(){}", base, query) };
237
238		self.uri = Some(uri_str.parse().unwrap());
239
240		Ok(self.uri.clone().unwrap())
241	}
242}