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

drk2: Transfer functionality added

aggstam 2 лет назад
Родитель
Сommit
31dd5d6208
4 измененных файлов с 247 добавлено и 3 удалено
  1. 52 2
      bin/drk2/src/main.rs
  2. 42 1
      bin/drk2/src/money.rs
  3. 22 0
      bin/drk2/src/rpc.rs
  4. 131 0
      bin/drk2/src/transfer.rs

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

@@ -39,7 +39,7 @@ use darkfi::{
 };
 use darkfi_money_contract::model::Coin;
 use darkfi_sdk::{
-    crypto::TokenId,
+    crypto::{PublicKey, TokenId},
     pasta::{group::ff::PrimeField, pallas},
 };
 use darkfi_serial::{deserialize, serialize};
@@ -50,6 +50,9 @@ mod error;
 /// darkfid JSON-RPC related methods
 mod rpc;
 
+/// Payment methods
+mod transfer;
+
 /// CLI utility functions
 mod cli_util;
 use cli_util::kaching;
@@ -162,7 +165,17 @@ enum Subcmd {
         coin: String,
     },
 
-    // TODO: Transfer
+    /// Create a payment transaction
+    Transfer {
+        /// Amount to send
+        amount: String,
+
+        /// Token ID to send
+        token: String,
+
+        /// Recipient address
+        recipient: String,
+    },
 
     // TODO: OTC
     /// Inspect a transaction from stdin
@@ -603,6 +616,43 @@ async fn realmain(args: Args, ex: Arc<smol::Executor<'static>>) -> Result<()> {
             Ok(())
         }
 
+        Subcmd::Transfer { amount, token, 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 alias: {e:?}");
+                    exit(2);
+                }
+            };
+
+            let tx = match drk.transfer(&amount, token_id, rcpt).await {
+                Ok(t) => t,
+                Err(e) => {
+                    eprintln!("Failed to create payment transaction: {e:?}");
+                    exit(2);
+                }
+            };
+
+            println!("{}", bs58::encode(&serialize(&tx)).into_string());
+
+            Ok(())
+        }
+
         Subcmd::Inspect => {
             let mut buf = String::new();
             stdin().read_to_string(&mut buf)?;

+ 42 - 1
bin/drk2/src/money.rs

@@ -16,7 +16,7 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 
-use std::collections::HashMap;
+use std::{collections::HashMap, str::FromStr};
 
 use rand::rngs::OsRng;
 use rusqlite::types::Value;
@@ -124,6 +124,33 @@ impl Drk {
         Ok(())
     }
 
+    /// Fetch default secret key from the wallet.
+    pub async fn default_secret(&self) -> Result<SecretKey> {
+        let row = match self
+            .wallet
+            .query_single(
+                MONEY_KEYS_TABLE,
+                &[MONEY_KEYS_COL_SECRET],
+                convert_named_params! {(MONEY_KEYS_COL_IS_DEFAULT, 1)},
+            )
+            .await
+        {
+            Ok(r) => r,
+            Err(e) => {
+                return Err(Error::RusqliteError(format!(
+                    "[default_secret] Default secret key retrieval failed: {e:?}"
+                )))
+            }
+        };
+
+        let Value::Blob(ref key_bytes) = row[0] else {
+            return Err(Error::ParseFailed("[default_secret] Key bytes parsing failed"))
+        };
+        let secret_key: SecretKey = deserialize(key_bytes)?;
+
+        Ok(secret_key)
+    }
+
     /// Fetch default pubkey from the wallet.
     pub async fn default_address(&self) -> Result<PublicKey> {
         let row = match self
@@ -764,4 +791,18 @@ impl Drk {
 
         Ok(())
     }
+
+    /// Retrieve token by provided string.
+    /// Input string represents either an alias or a token id.
+    pub async fn get_token(&self, input: String) -> Result<TokenId> {
+        // Check if input is an alias(max 5 characters)
+        if input.chars().count() <= 5 {
+            let aliases = self.get_aliases(Some(input.clone()), None).await?;
+            if let Some(token_id) = aliases.get(&input) {
+                return Ok(*token_id)
+            }
+        }
+        // Else parse input
+        Ok(TokenId::from_str(input.as_str())?)
+    }
 }

+ 22 - 0
bin/drk2/src/rpc.rs

@@ -33,6 +33,7 @@ use darkfi::{
     Error, Result,
 };
 use darkfi_money_contract::client::{MONEY_INFO_COL_LAST_SCANNED_SLOT, MONEY_INFO_TABLE};
+use darkfi_sdk::crypto::ContractId;
 use darkfi_serial::{deserialize, serialize};
 
 use super::{
@@ -345,4 +346,25 @@ impl Drk {
         let is_valid = *rep.get::<bool>().unwrap();
         Ok(is_valid)
     }
+
+    /// Try to fetch zkas bincodes for the given `ContractId`.
+    pub async fn lookup_zkas(&self, contract_id: &ContractId) -> Result<Vec<(String, Vec<u8>)>> {
+        eprintln!("Querying zkas bincode for {contract_id}");
+
+        let params = JsonValue::Array(vec![JsonValue::String(format!("{contract_id}"))]);
+        let req = JsonRequest::new("blockchain.lookup_zkas", params);
+
+        let rep = self.rpc_client.request(req).await?;
+        let params = rep.get::<Vec<JsonValue>>().unwrap();
+
+        let mut ret = Vec::with_capacity(params.len());
+        for param in params {
+            let zkas_ns = param[0].get::<String>().unwrap().clone();
+            let zkas_bincode_bytes = base64::decode(param.get::<String>().unwrap()).unwrap();
+            let zkas_bincode = deserialize(&zkas_bincode_bytes)?;
+            ret.push((zkas_ns, zkas_bincode));
+        }
+
+        Ok(ret)
+    }
 }

+ 131 - 0
bin/drk2/src/transfer.rs

@@ -0,0 +1,131 @@
+/* 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 darkfi::{
+    tx::{ContractCallLeaf, Transaction, TransactionBuilder},
+    util::parse::{decode_base10, encode_base10},
+    zk::{proof::ProvingKey, vm::ZkCircuit, vm_heap::empty_witnesses},
+    zkas::ZkBinary,
+    Error, Result,
+};
+use darkfi_money_contract::{
+    client::{transfer_v1::make_transfer_call, OwnCoin},
+    MoneyFunction, MONEY_CONTRACT_ZKAS_BURN_NS_V1, MONEY_CONTRACT_ZKAS_MINT_NS_V1,
+};
+use darkfi_sdk::{
+    crypto::{contract_id::MONEY_CONTRACT_ID, Keypair, PublicKey, TokenId},
+    pasta::pallas,
+    tx::ContractCall,
+};
+use darkfi_serial::Encodable;
+use rand::rngs::OsRng;
+
+use crate::{money::BALANCE_BASE10_DECIMALS, Drk};
+
+impl Drk {
+    /// Create a payment transaction. Returns the transaction object on success.
+    pub async fn transfer(
+        &self,
+        amount: &str,
+        token_id: TokenId,
+        recipient: PublicKey,
+    ) -> Result<Transaction> {
+        // First get all unspent OwnCoins to see what our balance is.
+        eprintln!("Fetching OwnCoins");
+        let owncoins = self.get_coins(false).await?;
+        let mut owncoins: Vec<OwnCoin> = owncoins.iter().map(|x| x.0.clone()).collect();
+        // We're only interested in the ones for the token_id we're sending
+        // And the ones not owned by some protocol (meaning spend-hook should be 0)
+        owncoins.retain(|x| x.note.token_id == token_id);
+        owncoins.retain(|x| x.note.spend_hook == pallas::Base::zero());
+        if owncoins.is_empty() {
+            return Err(Error::Custom(format!("Did not find any coins with token ID: {token_id}")))
+        }
+
+        let amount = decode_base10(amount, BALANCE_BASE10_DECIMALS, false)?;
+        let mut balance = 0;
+        for coin in owncoins.iter() {
+            balance += coin.note.value;
+        }
+
+        if balance < amount {
+            return Err(Error::Custom(format!(
+                "Not enough balance for token ID: {token_id}, found: {}",
+                encode_base10(balance, BALANCE_BASE10_DECIMALS)
+            )))
+        }
+
+        // We'll also need our Merkle tree
+        let tree = self.get_money_tree().await?;
+
+        let secret = self.default_secret().await?;
+        let keypair = Keypair::new(secret);
+
+        let contract_id = *MONEY_CONTRACT_ID;
+
+        // 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(&contract_id).await?;
+
+        let Some(mint_zkbin) = zkas_bins.iter().find(|x| x.0 == MONEY_CONTRACT_ZKAS_MINT_NS_V1)
+        else {
+            return Err(Error::Custom("Mint circuit not found".to_string()))
+        };
+
+        let Some(burn_zkbin) = zkas_bins.iter().find(|x| x.0 == MONEY_CONTRACT_ZKAS_BURN_NS_V1)
+        else {
+            return Err(Error::Custom("Burn circuit not found".to_string()))
+        };
+
+        let mint_zkbin = ZkBinary::decode(&mint_zkbin.1)?;
+        let burn_zkbin = ZkBinary::decode(&burn_zkbin.1)?;
+
+        let mint_circuit = ZkCircuit::new(empty_witnesses(&mint_zkbin)?, &mint_zkbin);
+        let burn_circuit = ZkCircuit::new(empty_witnesses(&burn_zkbin)?, &burn_zkbin);
+
+        eprintln!("Creating Mint and Burn circuit proving keys");
+        let mint_pk = ProvingKey::build(mint_zkbin.k, &mint_circuit);
+        let burn_pk = ProvingKey::build(burn_zkbin.k, &burn_circuit);
+
+        eprintln!("Building transaction parameters");
+        let (params, secrets, spent_coins) = make_transfer_call(
+            keypair, recipient, amount, token_id, owncoins, tree, mint_zkbin, mint_pk, burn_zkbin,
+            burn_pk,
+        )?;
+
+        // Encode and sign the transaction
+        let mut data = vec![MoneyFunction::TransferV1 as u8];
+        params.encode(&mut data)?;
+        let call = ContractCall { contract_id: *MONEY_CONTRACT_ID, data };
+        let mut tx_builder =
+            TransactionBuilder::new(ContractCallLeaf { call, proofs: secrets.proofs }, vec![])?;
+        let mut tx = tx_builder.build()?;
+        let sigs = tx.create_sigs(&mut OsRng, &secrets.signature_secrets)?;
+        tx.signatures = vec![sigs];
+
+        // We need to mark the coins we've spent in our wallet
+        for spent_coin in spent_coins {
+            if let Err(e) = self.mark_spent_coin(&spent_coin.coin).await {
+                return Err(Error::Custom(format!("Mark spent coin {spent_coin:?} failed: {e:?}")))
+            };
+        }
+
+        Ok(tx)
+    }
+}