neo3/neo_contract/
neo_uri.rs1use 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 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 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 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 let mut tx_builder = TransactionBuilder::new();
159
160 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 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 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 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 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}