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			},
110			token if *token == GasToken::<P>::new(None).script_hash() => {
111				Self::GAS_TOKEN_STRING.to_owned()
112			},
113			_ => ScriptHashExtension::to_bs58_string(token),
114		})
115	}
116
117	// Builders
118
119	pub async fn build_transfer_from(
120		&self,
121		sender: &Account,
122	) -> Result<TransactionBuilder<P>, ContractError> {
123		let recipient = self
124			.recipient
125			.ok_or_else(|| ContractError::InvalidStateError("Recipient not set".to_string()))?;
126		let amount = self
127			.amount
128			.ok_or_else(|| ContractError::InvalidStateError("Amount not set".to_string()))?;
129		let token_hash = self
130			.token
131			.ok_or_else(|| ContractError::InvalidStateError("Token not set".to_string()))?;
132
133		// Validate amount precision
134		let amount_scale = (amount as f64).log10().floor() as u32 + 1;
135
136		if Self::is_neo_token(&token_hash) && amount_scale > 0 {
137			return Err(ContractError::InvalidArgError(
138				"NEO does not support decimals".to_string(),
139			));
140		}
141
142		if Self::is_gas_token(&token_hash)
143			&& amount_scale > GasToken::<P>::new(None).decimals().unwrap() as u32
144		{
145			return Err(ContractError::InvalidArgError(
146				"Too many decimal places for GAS".to_string(),
147			));
148		}
149
150		let mut token = FungibleTokenContract::new(&token_hash, self.provider);
151
152		let decimals = token.get_decimals().await?;
153		if amount_scale > decimals as u32 {
154			return Err(ContractError::InvalidArgError(
155				"Too many decimal places for token".to_string(),
156			));
157		}
158
159		let amt = token.to_fractions(amount, 0)?;
160
161		// Create a new TransactionBuilder
162		let mut tx_builder = TransactionBuilder::new();
163
164		// Build the script for the transfer
165		let script = ScriptBuilder::new()
166			.contract_call(
167				&token_hash,
168				"transfer",
169				&[
170					ContractParameter::h160(&sender.get_script_hash()),
171					ContractParameter::h160(&recipient),
172					ContractParameter::integer(amt as i64),
173					ContractParameter::any(),
174				],
175				None,
176			)
177			.map_err(|err| ContractError::RuntimeError(err.to_string()))?
178			.to_bytes();
179
180		// Set up the TransactionBuilder
181		tx_builder
182			.set_script(Some(script))
183			.set_signers(vec![AccountSigner::called_by_entry(sender).unwrap().into()]);
184
185		Ok(tx_builder)
186	}
187
188	// Helpers
189
190	fn is_neo_token(token: &H160) -> bool {
191		token == &NeoToken::<P>::new(None).script_hash()
192	}
193
194	fn is_gas_token(token: &H160) -> bool {
195		token == &GasToken::<P>::new(None).script_hash()
196	}
197
198	// Setters
199
200	pub fn token_str(&mut self, token_str: &str) {
201		self.token = match token_str {
202			Self::NEO_TOKEN_STRING => Some(NeoToken::new(self.provider).script_hash()),
203			Self::GAS_TOKEN_STRING => Some(GasToken::new(self.provider).script_hash()),
204			_ => Some(token_str.parse().unwrap()),
205		};
206	}
207
208	// URI builder
209
210	fn build_query(&self) -> String {
211		let mut parts = Vec::new();
212
213		if let Some(token) = &self.token {
214			let token_str = match token {
215				token if *token == NeoToken::new(self.provider).script_hash() => {
216					Self::NEO_TOKEN_STRING.to_owned()
217				},
218				token if *token == GasToken::new(self.provider).script_hash() => {
219					Self::GAS_TOKEN_STRING.to_owned()
220				},
221				_ => ScriptHashExtension::to_bs58_string(token),
222			};
223
224			parts.push(format!("asset={}", token_str));
225		}
226
227		if let Some(amount) = &self.amount {
228			parts.push(format!("amount={}", amount));
229		}
230
231		parts.join("&")
232	}
233
234	pub fn build_uri(&mut self) -> Result<Url, ContractError> {
235		let recipient = self
236			.recipient
237			.ok_or(ContractError::InvalidStateError("No recipient set".to_string()))
238			.unwrap();
239
240		let base = format!("{}:{}", Self::NEO_SCHEME, recipient.to_address());
241		let query = self.build_query();
242		let uri_str = if query.is_empty() { base } else { format!("{}.unwrap(){}", base, query) };
243
244		self.uri = Some(uri_str.parse().unwrap());
245
246		Ok(self.uri.clone().unwrap())
247	}
248}