Просмотр исходного кода

drk2: Token functionality added

aggstam 2 лет назад
Родитель
Сommit
f9683b867e
3 измененных файлов с 404 добавлено и 3 удалено
  1. 177 2
      bin/drk2/src/main.rs
  2. 225 0
      bin/drk2/src/token.rs
  3. 2 1
      bin/drk2/src/transfer.rs

+ 177 - 2
bin/drk2/src/main.rs

@@ -26,6 +26,7 @@ use std::{
 };
 
 use prettytable::{format, row, Table};
+use rand::rngs::OsRng;
 use smol::stream::StreamExt;
 use structopt_toml::{serde::Deserialize, structopt::StructOpt, StructOptToml};
 use url::Url;
@@ -39,7 +40,7 @@ use darkfi::{
 };
 use darkfi_money_contract::model::Coin;
 use darkfi_sdk::{
-    crypto::{PublicKey, TokenId},
+    crypto::{PublicKey, SecretKey, TokenId},
     pasta::{group::ff::PrimeField, pallas},
 };
 use darkfi_serial::{deserialize, serialize};
@@ -57,6 +58,9 @@ mod transfer;
 mod swap;
 use swap::PartialSwapData;
 
+/// Token methods
+mod token;
+
 /// CLI utility functions
 mod cli_util;
 use cli_util::{kaching, parse_token_pair, parse_value_pair};
@@ -229,7 +233,13 @@ enum Subcmd {
         /// Sub command to execute
         command: AliasSubcmd,
     },
-    // TODO: Token
+
+    /// Token functionalities
+    Token {
+        #[structopt(subcommand)]
+        /// Sub command to execute
+        command: TokenSubcmd,
+    },
 }
 
 #[derive(Clone, Debug, Deserialize, StructOpt)]
@@ -316,6 +326,36 @@ enum AliasSubcmd {
     },
 }
 
+#[derive(Clone, Debug, Deserialize, StructOpt)]
+enum TokenSubcmd {
+    /// Import a mint authority secret from stdin
+    Import,
+
+    /// Generate a new mint authority
+    GenerateMint,
+
+    /// List token IDs with available mint authorities
+    List,
+
+    /// Mint tokens
+    Mint {
+        /// Token ID to mint
+        token: String,
+
+        /// Amount to mint
+        amount: String,
+
+        /// Recipient of the minted tokens
+        recipient: String,
+    },
+
+    /// Freeze a token mint
+    Freeze {
+        /// Token ID mint to freeze
+        token: String,
+    },
+}
+
 /// CLI-util structure
 pub struct Drk {
     /// Wallet database operations handler
@@ -1011,5 +1051,140 @@ async fn realmain(args: Args, ex: Arc<smol::Executor<'static>>) -> Result<()> {
                 Ok(())
             }
         },
+
+        Subcmd::Token { command } => match command {
+            TokenSubcmd::Import => {
+                let mut buf = String::new();
+                stdin().read_to_string(&mut buf)?;
+                let mint_authority = match SecretKey::from_str(buf.trim()) {
+                    Ok(ma) => ma,
+                    Err(e) => {
+                        eprintln!("Invalid secret key: {e:?}");
+                        exit(2);
+                    }
+                };
+
+                let drk = Drk::new(args.wallet_path, args.wallet_pass, args.endpoint, ex).await?;
+                if let Err(e) = drk.import_mint_authority(mint_authority).await {
+                    eprintln!("Importing mint authority failed: {e:?}");
+                    exit(2);
+                };
+
+                let token_id = TokenId::derive(mint_authority);
+                eprintln!("Successfully imported mint authority for token ID: {token_id}");
+
+                Ok(())
+            }
+
+            TokenSubcmd::GenerateMint => {
+                let mint_authority = SecretKey::random(&mut OsRng);
+
+                let drk = Drk::new(args.wallet_path, args.wallet_pass, args.endpoint, ex).await?;
+
+                if let Err(e) = drk.import_mint_authority(mint_authority).await {
+                    eprintln!("Importing mint authority failed: {e:?}");
+                    exit(2);
+                };
+
+                let token_id = TokenId::derive(mint_authority);
+                eprintln!("Successfully imported mint authority for token ID: {token_id}");
+
+                Ok(())
+            }
+
+            TokenSubcmd::List => {
+                let drk = Drk::new(args.wallet_path, args.wallet_pass, args.endpoint, ex).await?;
+                let tokens = drk.list_tokens().await?;
+                let aliases_map = match drk.get_aliases_mapped_by_token().await {
+                    Ok(map) => map,
+                    Err(e) => {
+                        eprintln!("Failed to fetch wallet aliases: {e:?}");
+                        exit(2);
+                    }
+                };
+
+                let mut table = Table::new();
+                table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR);
+                table.set_titles(row!["Token ID", "Aliases", "Mint Authority", "Frozen"]);
+
+                for (token_id, authority, frozen) in tokens {
+                    let aliases = match aliases_map.get(&token_id.to_string()) {
+                        Some(a) => a,
+                        None => "-",
+                    };
+
+                    table.add_row(row![token_id, aliases, authority, frozen]);
+                }
+
+                if table.is_empty() {
+                    eprintln!("No tokens found");
+                } else {
+                    eprintln!("{table}");
+                }
+
+                Ok(())
+            }
+
+            // TODO: Mint directly into DAO treasury
+            TokenSubcmd::Mint { token, amount, recipient } => {
+                let drk = Drk::new(args.wallet_path, args.wallet_pass, args.endpoint, ex).await?;
+
+                if let Err(e) = f64::from_str(&amount) {
+                    eprintln!("Invalid amount: {e:?}");
+                    exit(2);
+                }
+
+                let rcpt = match PublicKey::from_str(&recipient) {
+                    Ok(r) => r,
+                    Err(e) => {
+                        eprintln!("Invalid recipient: {e:?}");
+                        exit(2);
+                    }
+                };
+
+                let token_id = match drk.get_token(token).await {
+                    Ok(t) => t,
+                    Err(e) => {
+                        eprintln!("Invalid Token ID: {e:?}");
+                        exit(2);
+                    }
+                };
+
+                let tx = match drk.mint_token(&amount, rcpt, token_id).await {
+                    Ok(tx) => tx,
+                    Err(e) => {
+                        eprintln!("Failed to create token mint transaction: {e:?}");
+                        exit(2);
+                    }
+                };
+
+                eprintln!("{}", bs58::encode(&serialize(&tx)).into_string());
+
+                Ok(())
+            }
+
+            TokenSubcmd::Freeze { token } => {
+                let drk = Drk::new(args.wallet_path, args.wallet_pass, args.endpoint, ex).await?;
+                let token_id = match drk.get_token(token).await {
+                    Ok(t) => t,
+                    Err(e) => {
+                        eprintln!("Invalid Token ID: {e:?}");
+                        exit(2);
+                    }
+                };
+
+                let tx = match drk.freeze_token(token_id).await {
+                    Ok(tx) => tx,
+                    Err(e) => {
+                        eprintln!("Failed to create token freeze transaction: {e:?}");
+                        exit(2);
+                    }
+                };
+
+                eprintln!("{}", bs58::encode(&serialize(&tx)).into_string());
+
+                Ok(())
+            }
+        },
     }
 }

+ 225 - 0
bin/drk2/src/token.rs

@@ -0,0 +1,225 @@
+/* This file is part of DarkFi (https://dark.fi)
+ *
+ * Copyright (C) 2020-2024 Dyne.org foundation
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use rand::rngs::OsRng;
+use rusqlite::types::Value;
+
+use darkfi::{
+    tx::{ContractCallLeaf, Transaction, TransactionBuilder},
+    util::parse::decode_base10,
+    zk::{proof::ProvingKey, vm::ZkCircuit, vm_heap::empty_witnesses},
+    zkas::ZkBinary,
+    Error, Result,
+};
+use darkfi_money_contract::{
+    client::{
+        token_freeze_v1::TokenFreezeCallBuilder, token_mint_v1::TokenMintCallBuilder,
+        MONEY_TOKENS_COL_IS_FROZEN, MONEY_TOKENS_COL_MINT_AUTHORITY, MONEY_TOKENS_COL_TOKEN_ID,
+        MONEY_TOKENS_TABLE,
+    },
+    MoneyFunction, MONEY_CONTRACT_ZKAS_TOKEN_FRZ_NS_V1, MONEY_CONTRACT_ZKAS_TOKEN_MINT_NS_V1,
+};
+use darkfi_sdk::{
+    crypto::{contract_id::MONEY_CONTRACT_ID, Keypair, PublicKey, SecretKey, TokenId},
+    pasta::pallas,
+    tx::ContractCall,
+};
+use darkfi_serial::{deserialize, serialize, Encodable};
+
+use crate::{error::WalletDbResult, Drk};
+
+impl Drk {
+    /// Import a token mint authority into the wallet
+    pub async fn import_mint_authority(&self, mint_authority: SecretKey) -> WalletDbResult<()> {
+        let token_id = TokenId::derive(mint_authority);
+        let is_frozen = 0;
+
+        let query = format!(
+            "INSERT INTO {} ({}, {}, {}) VALUES (?1, ?2, ?3);",
+            MONEY_TOKENS_TABLE,
+            MONEY_TOKENS_COL_MINT_AUTHORITY,
+            MONEY_TOKENS_COL_TOKEN_ID,
+            MONEY_TOKENS_COL_IS_FROZEN,
+        );
+
+        self.wallet
+            .exec_sql(
+                &query,
+                rusqlite::params![serialize(&mint_authority), serialize(&token_id), is_frozen,],
+            )
+            .await
+    }
+
+    pub async fn list_tokens(&self) -> Result<Vec<(TokenId, SecretKey, bool)>> {
+        let rows = match self.wallet.query_multiple(MONEY_TOKENS_TABLE, &[], &[]).await {
+            Ok(r) => r,
+            Err(e) => {
+                return Err(Error::RusqliteError(format!(
+                    "[list_tokens] Tokens retrieval failed: {e:?}"
+                )))
+            }
+        };
+
+        let mut ret = Vec::with_capacity(rows.len());
+        for row in rows {
+            let Value::Blob(ref auth_bytes) = row[0] else {
+                return Err(Error::ParseFailed("[list_tokens] Mint authority bytes parsing failed"))
+            };
+            let mint_authority = deserialize(auth_bytes)?;
+
+            let Value::Blob(ref token_bytes) = row[1] else {
+                return Err(Error::ParseFailed("[list_tokens] Token ID bytes parsing failed"))
+            };
+            let token_id = deserialize(token_bytes)?;
+
+            let Value::Integer(frozen) = row[2] else {
+                return Err(Error::ParseFailed("[list_tokens] Is frozen parsing failed"))
+            };
+            let Ok(frozen) = i32::try_from(frozen) else {
+                return Err(Error::ParseFailed("[list_tokens] Is frozen parsing failed"))
+            };
+
+            ret.push((token_id, mint_authority, frozen != 0));
+        }
+
+        Ok(ret)
+    }
+
+    /// Create a token mint transaction. Returns the transaction object on success.
+    pub async fn mint_token(
+        &self,
+        amount: &str,
+        recipient: PublicKey,
+        token_id: TokenId,
+    ) -> Result<Transaction> {
+        // TODO: Mint directly into DAO treasury
+        let spend_hook = pallas::Base::zero();
+        let user_data = pallas::Base::zero();
+
+        let amount = decode_base10(amount, 8, false)?;
+
+        let mut tokens = self.list_tokens().await?;
+        tokens.retain(|x| x.0 == token_id);
+        if tokens.is_empty() {
+            return Err(Error::Custom(format!(
+                "Did not find mint authority for token ID {token_id}"
+            )))
+        }
+        assert!(tokens.len() == 1);
+
+        let mint_authority = Keypair::new(tokens[0].1);
+
+        if tokens[0].2 {
+            return Err(Error::Custom(
+                "This token mint is marked as frozen in the wallet".to_string(),
+            ))
+        }
+
+        // Now we need to do a lookup for the zkas proof bincodes, and create
+        // the circuit objects and proving keys so we can build the transaction.
+        // We also do this through the RPC.
+        let zkas_bins = self.lookup_zkas(&MONEY_CONTRACT_ID).await?;
+        let zkas_ns = MONEY_CONTRACT_ZKAS_TOKEN_MINT_NS_V1;
+
+        let Some(token_mint_zkbin) = zkas_bins.iter().find(|x| x.0 == zkas_ns) else {
+            return Err(Error::Custom("Token mint circuit not found".to_string()))
+        };
+
+        let token_mint_zkbin = ZkBinary::decode(&token_mint_zkbin.1)?;
+        let token_mint_circuit =
+            ZkCircuit::new(empty_witnesses(&token_mint_zkbin)?, &token_mint_zkbin);
+
+        eprintln!("Creating token mint circuit proving keys");
+        let token_mint_pk = ProvingKey::build(token_mint_zkbin.k, &token_mint_circuit);
+        let mint_builder = TokenMintCallBuilder {
+            mint_authority,
+            recipient,
+            amount,
+            spend_hook,
+            user_data,
+            token_mint_zkbin,
+            token_mint_pk,
+        };
+
+        eprintln!("Building transaction parameters");
+        let debris = mint_builder.build()?;
+
+        // Encode and sign the transaction
+        let mut data = vec![MoneyFunction::TokenMintV1 as u8];
+        debris.params.encode(&mut data)?;
+        let call = ContractCall { contract_id: *MONEY_CONTRACT_ID, data };
+        let mut tx_builder =
+            TransactionBuilder::new(ContractCallLeaf { call, proofs: debris.proofs }, vec![])?;
+        let mut tx = tx_builder.build()?;
+        let sigs = tx.create_sigs(&mut OsRng, &[mint_authority.secret])?;
+        tx.signatures = vec![sigs];
+
+        Ok(tx)
+    }
+
+    /// Create a token freeze transaction. Returns the transaction object on success.
+    pub async fn freeze_token(&self, token_id: TokenId) -> Result<Transaction> {
+        let mut tokens = self.list_tokens().await?;
+        tokens.retain(|x| x.0 == token_id);
+        if tokens.is_empty() {
+            return Err(Error::Custom(format!(
+                "Did not find mint authority for token ID {token_id}"
+            )))
+        }
+        assert!(tokens.len() == 1);
+
+        let mint_authority = Keypair::new(tokens[0].1);
+
+        if tokens[0].2 {
+            return Err(Error::Custom(
+                "This token is already marked as frozen in the wallet".to_string(),
+            ))
+        }
+
+        let zkas_bins = self.lookup_zkas(&MONEY_CONTRACT_ID).await?;
+        let zkas_ns = MONEY_CONTRACT_ZKAS_TOKEN_FRZ_NS_V1;
+
+        let Some(token_freeze_zkbin) = zkas_bins.iter().find(|x| x.0 == zkas_ns) else {
+            return Err(Error::Custom("Token freeze circuit not found".to_string()))
+        };
+
+        let token_freeze_zkbin = ZkBinary::decode(&token_freeze_zkbin.1)?;
+        let token_freeze_circuit =
+            ZkCircuit::new(empty_witnesses(&token_freeze_zkbin)?, &token_freeze_zkbin);
+
+        eprintln!("Creating token freeze circuit proving keys");
+        let token_freeze_pk = ProvingKey::build(token_freeze_zkbin.k, &token_freeze_circuit);
+        let freeze_builder =
+            TokenFreezeCallBuilder { mint_authority, token_freeze_zkbin, token_freeze_pk };
+
+        eprintln!("Building transaction parameters");
+        let debris = freeze_builder.build()?;
+
+        // Encode and sign the transaction
+        let mut data = vec![MoneyFunction::TokenFreezeV1 as u8];
+        debris.params.encode(&mut data)?;
+        let call = ContractCall { contract_id: *MONEY_CONTRACT_ID, data };
+        let mut tx_builder =
+            TransactionBuilder::new(ContractCallLeaf { call, proofs: debris.proofs }, vec![])?;
+        let mut tx = tx_builder.build()?;
+        let sigs = tx.create_sigs(&mut OsRng, &[mint_authority.secret])?;
+        tx.signatures = vec![sigs];
+
+        Ok(tx)
+    }
+}

+ 2 - 1
bin/drk2/src/transfer.rs

@@ -16,6 +16,8 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 
+use rand::rngs::OsRng;
+
 use darkfi::{
     tx::{ContractCallLeaf, Transaction, TransactionBuilder},
     util::parse::{decode_base10, encode_base10},
@@ -33,7 +35,6 @@ use darkfi_sdk::{
     tx::ContractCall,
 };
 use darkfi_serial::Encodable;
-use rand::rngs::OsRng;
 
 use crate::{money::BALANCE_BASE10_DECIMALS, Drk};