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 },
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 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 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 let mut tx_builder = TransactionBuilder::new();
163
164 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 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 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 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 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}