common.rs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443
  1. /* This file is part of DarkFi (https://dark.fi)
  2. *
  3. * Copyright (C) 2020-2026 Dyne.org foundation
  4. *
  5. * This program is free software: you can redistribute it and/or modify
  6. * it under the terms of the GNU Affero General Public License as
  7. * published by the Free Software Foundation, either version 3 of the
  8. * License, or (at your option) any later version.
  9. *
  10. * This program is distributed in the hope that it will be useful,
  11. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. * GNU Affero General Public License for more details.
  14. *
  15. * You should have received a copy of the GNU Affero General Public License
  16. * along with this program. If not, see <https://www.gnu.org/licenses/>.
  17. */
  18. use std::collections::HashMap;
  19. use darkfi::{tx::Transaction, util::parse::encode_base10, zk::halo2::Field};
  20. use darkfi_money_contract::{
  21. client::OwnCoin,
  22. model::{
  23. Coin, MoneyAuthTokenFreezeParamsV1, MoneyAuthTokenMintParamsV1, MoneyBurnParamsV1,
  24. MoneyFeeParamsV1, MoneyGenesisMintParamsV1, MoneyPoWRewardParamsV1, MoneyTokenMintParamsV1,
  25. MoneyTransferParamsV1, TokenId,
  26. },
  27. MoneyFunction,
  28. };
  29. use darkfi_sdk::{
  30. crypto::{
  31. keypair::{Address, Network, PublicKey, SecretKey, StandardAddress},
  32. BaseBlind, ContractId, FuncId, DAO_CONTRACT_ID, DEPLOYOOOR_CONTRACT_ID, MONEY_CONTRACT_ID,
  33. },
  34. pasta::pallas,
  35. };
  36. use darkfi_serial::{deserialize, serialize};
  37. use prettytable::{format, row, Table};
  38. use crate::{money::BALANCE_BASE10_DECIMALS, Drk};
  39. pub fn prettytable_addrs(
  40. network: Network,
  41. addresses: &[(u64, PublicKey, SecretKey, u64)],
  42. ) -> Table {
  43. let mut table = Table::new();
  44. table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR);
  45. table.set_titles(row!["Key ID", "Address", "Public Key", "Secret Key", "Is Default"]);
  46. for (key_id, public_key, secret_key, is_default) in addresses {
  47. let is_default = match is_default {
  48. 1 => "*",
  49. _ => "",
  50. };
  51. let address: Address = StandardAddress::from_public(network, *public_key).into();
  52. table.add_row(row![key_id, address, public_key, secret_key, is_default]);
  53. }
  54. table
  55. }
  56. pub fn prettytable_balance(
  57. balmap: &HashMap<String, u64>,
  58. alimap: &HashMap<String, String>,
  59. ) -> Table {
  60. let mut table = Table::new();
  61. table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR);
  62. table.set_titles(row!["Token ID", "Aliases", "Balance"]);
  63. for (token_id, balance) in balmap.iter() {
  64. let alias = match alimap.get(token_id) {
  65. Some(v) => v,
  66. None => "-",
  67. };
  68. table.add_row(row![token_id, alias, encode_base10(*balance, BALANCE_BASE10_DECIMALS)]);
  69. }
  70. table
  71. }
  72. pub fn prettytable_coins(
  73. coins: &[(OwnCoin, u32, bool, Option<u32>, String)],
  74. alimap: &HashMap<String, String>,
  75. ) -> Table {
  76. let mut table = Table::new();
  77. table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR);
  78. table.set_titles(row![
  79. "Coin",
  80. "Token ID",
  81. "Aliases",
  82. "Value",
  83. "Spend Hook",
  84. "User Data",
  85. "Creation Height",
  86. "Spent",
  87. "Spent Height",
  88. "Spent TX",
  89. ]);
  90. for coin in coins {
  91. let alias = match alimap.get(&coin.0.note.token_id.to_string()) {
  92. Some(v) => v,
  93. None => "-",
  94. };
  95. let spend_hook = if coin.0.note.spend_hook != FuncId::none() {
  96. format!("{}", coin.0.note.spend_hook)
  97. } else {
  98. String::from("-")
  99. };
  100. let user_data = if coin.0.note.user_data != pallas::Base::ZERO {
  101. bs58::encode(serialize(&coin.0.note.user_data)).into_string().to_string()
  102. } else {
  103. String::from("-")
  104. };
  105. let spent_height = match coin.3 {
  106. Some(spent_height) => spent_height.to_string(),
  107. None => String::from("-"),
  108. };
  109. table.add_row(row![
  110. bs58::encode(&serialize(&coin.0.coin.inner())).into_string().to_string(),
  111. coin.0.note.token_id,
  112. alias,
  113. format!(
  114. "{} ({})",
  115. coin.0.note.value,
  116. encode_base10(coin.0.note.value, BALANCE_BASE10_DECIMALS)
  117. ),
  118. spend_hook,
  119. user_data,
  120. coin.1,
  121. coin.2,
  122. spent_height,
  123. coin.4,
  124. ]);
  125. }
  126. table
  127. }
  128. pub fn prettytable_tokenlist(
  129. tokens: &[(TokenId, SecretKey, BaseBlind, bool, Option<u32>)],
  130. alimap: &HashMap<String, String>,
  131. ) -> Table {
  132. let mut table = Table::new();
  133. table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR);
  134. table.set_titles(row![
  135. "Token ID",
  136. "Aliases",
  137. "Mint Authority",
  138. "Token Blind",
  139. "Frozen",
  140. "Freeze Height",
  141. ]);
  142. for (token_id, authority, blind, frozen, freeze_height) in tokens {
  143. let alias = match alimap.get(&token_id.to_string()) {
  144. Some(v) => v,
  145. None => "-",
  146. };
  147. let freeze_height = match freeze_height {
  148. Some(freeze_height) => freeze_height.to_string(),
  149. None => String::from("-"),
  150. };
  151. table.add_row(row![token_id, alias, authority, blind, frozen, freeze_height]);
  152. }
  153. table
  154. }
  155. pub fn prettytable_contract_history(deploy_history: &[(String, String, u32)]) -> Table {
  156. let mut table = Table::new();
  157. table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR);
  158. table.set_titles(row!["Transaction Hash", "Type", "Block Height"]);
  159. for (tx_hash, tx_type, block_height) in deploy_history {
  160. table.add_row(row![tx_hash, tx_type, block_height]);
  161. }
  162. table
  163. }
  164. pub fn prettytable_contract_auth(auths: &[(ContractId, SecretKey, bool, Option<u32>)]) -> Table {
  165. let mut table = Table::new();
  166. table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR);
  167. table.set_titles(row!["Contract ID", "Secret Key", "Locked", "Lock Height"]);
  168. for (contract_id, secret_key, is_locked, lock_height) in auths {
  169. let lock_height = match lock_height {
  170. Some(lock_height) => lock_height.to_string(),
  171. None => String::from("-"),
  172. };
  173. table.add_row(row![contract_id, secret_key, is_locked, lock_height]);
  174. }
  175. table
  176. }
  177. pub fn prettytable_aliases(alimap: &HashMap<String, TokenId>) -> Table {
  178. let mut table = Table::new();
  179. table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR);
  180. table.set_titles(row!["Alias", "Token ID"]);
  181. for (alias, token_id) in alimap.iter() {
  182. table.add_row(row![alias, token_id]);
  183. }
  184. table
  185. }
  186. pub fn prettytable_scanned_blocks(scanned_blocks: &[(u32, String, String)]) -> Table {
  187. let mut table = Table::new();
  188. table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR);
  189. table.set_titles(row!["Height", "Hash", "Signing Key"]);
  190. for (height, hash, signing_key) in scanned_blocks {
  191. table.add_row(row![height, hash, signing_key]);
  192. }
  193. table
  194. }
  195. /// Parse money call data and return formatted inputs/outputs string
  196. async fn format_money_call_data(
  197. calldata: &[u8],
  198. nullifier_map: &HashMap<[u8; 32], Coin>,
  199. ) -> String {
  200. if calldata.is_empty() {
  201. return "-".to_string();
  202. }
  203. let coin_display = |tx_local: bool, nullifier: &[u8; 32]| -> String {
  204. if tx_local {
  205. return "tx-local".to_string();
  206. }
  207. nullifier_map.get(nullifier).map(|c| c.to_string()).unwrap_or_else(|| "unknown".to_string())
  208. };
  209. let output_display = |tx_local: bool, coin: &Coin| -> String {
  210. if tx_local {
  211. "tx-local".to_string()
  212. } else {
  213. coin.to_string()
  214. }
  215. };
  216. let func_code = calldata[0];
  217. match MoneyFunction::try_from(func_code) {
  218. Ok(MoneyFunction::TransferV1) => {
  219. let Some(params) = deserialize::<MoneyTransferParamsV1>(&calldata[1..]).ok() else {
  220. return "Failed to parse TransferV1 data".to_string()
  221. };
  222. let mut parts = vec![];
  223. if !params.inputs.is_empty() {
  224. parts.push("Inputs:".to_string());
  225. parts.extend(params.inputs.iter().enumerate().map(|(i, input)| {
  226. format!(
  227. " [{}] {}",
  228. i,
  229. coin_display(input.tx_local, &input.nullifier.to_bytes())
  230. )
  231. }));
  232. }
  233. if !params.outputs.is_empty() {
  234. parts.push("Outputs:".to_string());
  235. parts.extend(params.outputs.iter().enumerate().map(|(i, output)| {
  236. format!(" [{}] {}", i, output_display(output.tx_local, &output.coin))
  237. }));
  238. }
  239. parts.join("\n")
  240. }
  241. Ok(MoneyFunction::FeeV1) => {
  242. if calldata.len() <= 9 {
  243. return "Invalid FeeV1 data length".to_string()
  244. }
  245. let Some(params) = deserialize::<MoneyFeeParamsV1>(&calldata[9..]).ok() else {
  246. return "Failed to parse FeeV1 data".to_string()
  247. };
  248. format!(
  249. "Input: {}\nOutput: {}",
  250. coin_display(params.input.tx_local, &params.input.nullifier.to_bytes()),
  251. output_display(params.output.tx_local, &params.output.coin)
  252. )
  253. }
  254. Ok(MoneyFunction::BurnV1) => {
  255. let Some(params) = deserialize::<MoneyBurnParamsV1>(&calldata[1..]).ok() else {
  256. return "Failed to parse BurnV1 data".to_string()
  257. };
  258. let mut parts = vec!["Inputs:".to_string()];
  259. parts.extend(params.inputs.iter().enumerate().map(|(i, input)| {
  260. format!(" [{}] {}", i, coin_display(input.tx_local, &input.nullifier.to_bytes()))
  261. }));
  262. parts.join("\n")
  263. }
  264. Ok(MoneyFunction::GenesisMintV1) => {
  265. let Some(params) = deserialize::<MoneyGenesisMintParamsV1>(&calldata[1..]).ok() else {
  266. return "Failed to parse GenesisMintV1 data".to_string()
  267. };
  268. let mut parts = vec![format!(
  269. "Input: Value: {}, Token: {}",
  270. params.input.value, params.input.token_id
  271. )];
  272. if !params.outputs.is_empty() {
  273. parts.push("Outputs:".to_string());
  274. parts.extend(params.outputs.iter().enumerate().map(|(i, output)| {
  275. format!(" [{}] Coin: {}", i, output_display(output.tx_local, &output.coin))
  276. }));
  277. }
  278. parts.join("\n")
  279. }
  280. Ok(MoneyFunction::PoWRewardV1) => {
  281. let Some(params) = deserialize::<MoneyPoWRewardParamsV1>(&calldata[1..]).ok() else {
  282. return "Failed to parse PoWRewardV1 data".to_string()
  283. };
  284. format!(
  285. "Input: Value: {}, Token: {}\nOutput: Coin: {}",
  286. params.input.value,
  287. params.input.token_id,
  288. output_display(params.output.tx_local, &params.output.coin)
  289. )
  290. }
  291. Ok(MoneyFunction::TokenMintV1) => {
  292. let Some(params) = deserialize::<MoneyTokenMintParamsV1>(&calldata[1..]).ok() else {
  293. return "Failed to parse TokenMintV1 data".to_string()
  294. };
  295. format!("Coin: {}", params.coin)
  296. }
  297. Ok(MoneyFunction::AuthTokenMintV1) => {
  298. let Some(params) = deserialize::<MoneyAuthTokenMintParamsV1>(&calldata[1..]).ok()
  299. else {
  300. return "Failed to parse AuthTokenMintV1 data".to_string()
  301. };
  302. format!("Token ID: {}, Mint Pubkey: {}", params.token_id, params.mint_pubkey)
  303. }
  304. Ok(MoneyFunction::AuthTokenFreezeV1) => {
  305. let Some(params) = deserialize::<MoneyAuthTokenFreezeParamsV1>(&calldata[1..]).ok()
  306. else {
  307. return "Failed to parse AuthTokenFreezeV1 data".to_string()
  308. };
  309. format!("Token ID: {}, Mint Public: {}", params.token_id, params.mint_public)
  310. }
  311. Err(_) => format!("Invalid function code: {func_code}"),
  312. }
  313. }
  314. pub async fn pretty_tx(tx: &Transaction, drk: &Drk) -> String {
  315. let hash = tx.hash().to_string();
  316. let mut fees: Vec<String> = vec![];
  317. let mut fees_total: u64 = 0;
  318. let mut fees_overflow = false;
  319. let mut table = Table::new();
  320. table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR);
  321. table.set_titles(row!["", "Contract", "Function", "Details"]);
  322. let nullifier_map = drk.get_coins_by_nullifier().await.unwrap_or_default();
  323. for (i, call) in tx.calls.iter().enumerate() {
  324. if call.data.is_money_fee() {
  325. if let Ok(fee) = deserialize(&call.data.data[1..9]) {
  326. fees.push(format!("{} DRK", encode_base10(fee, BALANCE_BASE10_DECIMALS)));
  327. fees_total = fees_total.checked_add(fee).unwrap_or_else(|| {
  328. fees_overflow = true;
  329. u64::MAX
  330. });
  331. } else {
  332. fees.push("invalid".to_string());
  333. }
  334. }
  335. let contract_name = match call.data.contract_id {
  336. id if id == *MONEY_CONTRACT_ID => "Money",
  337. id if id == *DAO_CONTRACT_ID => "DAO",
  338. id if id == *DEPLOYOOOR_CONTRACT_ID => "Deployooor",
  339. _ => "Custom",
  340. };
  341. let calldata = &call.data.data;
  342. let fn_name = match (call.data.contract_id, calldata[0]) {
  343. (id, 0x00) if id == *MONEY_CONTRACT_ID => "Fee",
  344. (id, 0x01) if id == *MONEY_CONTRACT_ID => "GenesisMint",
  345. (id, 0x02) if id == *MONEY_CONTRACT_ID => "PoWReward",
  346. (id, 0x03) if id == *MONEY_CONTRACT_ID => "Transfer",
  347. (id, 0x04) if id == *MONEY_CONTRACT_ID => "AuthTokenMint",
  348. (id, 0x05) if id == *MONEY_CONTRACT_ID => "AuthTokenFreeze",
  349. (id, 0x06) if id == *MONEY_CONTRACT_ID => "TokenMint",
  350. (id, 0x07) if id == *MONEY_CONTRACT_ID => "Burn",
  351. (id, 0x00) if id == *DAO_CONTRACT_ID => "Mint",
  352. (id, 0x01) if id == *DAO_CONTRACT_ID => "Propose",
  353. (id, 0x02) if id == *DAO_CONTRACT_ID => "Vote",
  354. (id, 0x03) if id == *DAO_CONTRACT_ID => "Exec",
  355. (id, 0x04) if id == *DAO_CONTRACT_ID => "AuthMoneyTransfer",
  356. (id, 0x00) if id == *DEPLOYOOOR_CONTRACT_ID => "Deploy",
  357. (id, 0x01) if id == *DEPLOYOOOR_CONTRACT_ID => "Lock",
  358. _ => "",
  359. };
  360. let fn_name = Some(fn_name).filter(|x| !x.is_empty());
  361. let details = if call.data.contract_id == *MONEY_CONTRACT_ID {
  362. format_money_call_data(calldata, &nullifier_map).await
  363. } else {
  364. "-".to_string()
  365. };
  366. table.add_row(row![
  367. i.to_string(),
  368. format!("{} [{}]", call.data.contract_id.to_string(), contract_name),
  369. if !calldata.is_empty() {
  370. fn_name
  371. .map(|n| format!("{} [{}]", calldata[0], n))
  372. .unwrap_or_else(|| calldata[0].to_string())
  373. } else {
  374. "-".to_string()
  375. },
  376. details,
  377. ]);
  378. }
  379. let fee = match fees.len() {
  380. 0 => "-".to_string(),
  381. 1 => fees[0].clone(),
  382. _ => format!(
  383. "{} [TOTAL: {}]",
  384. fees.join(", "),
  385. if fees_overflow {
  386. "OVERFLOW".to_string()
  387. } else {
  388. format!("{} DRK", encode_base10(fees_total, BALANCE_BASE10_DECIMALS))
  389. }
  390. ),
  391. };
  392. format!("Hash: {hash}\nFee: {fee}\n\n{table}")
  393. }