/ bin / drk / src / token.rs
token.rs
  1  /* This file is part of DarkFi (https://dark.fi)
  2   *
  3   * Copyright (C) 2020-2025 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  
 19  use rand::rngs::OsRng;
 20  use rusqlite::types::Value;
 21  
 22  use darkfi::{
 23      tx::{ContractCallLeaf, Transaction, TransactionBuilder},
 24      util::parse::decode_base10,
 25      zk::{halo2::Field, proof::ProvingKey, vm::ZkCircuit, vm_heap::empty_witnesses},
 26      zkas::ZkBinary,
 27      Error, Result,
 28  };
 29  use darkfi_money_contract::{
 30      client::{
 31          auth_token_freeze_v1::AuthTokenFreezeCallBuilder,
 32          auth_token_mint_v1::AuthTokenMintCallBuilder, token_mint_v1::TokenMintCallBuilder,
 33      },
 34      model::{CoinAttributes, TokenAttributes, TokenId},
 35      MoneyFunction, MONEY_CONTRACT_ZKAS_AUTH_TOKEN_MINT_NS_V1, MONEY_CONTRACT_ZKAS_FEE_NS_V1,
 36      MONEY_CONTRACT_ZKAS_TOKEN_MINT_NS_V1,
 37  };
 38  use darkfi_sdk::{
 39      crypto::{
 40          contract_id::MONEY_CONTRACT_ID, poseidon_hash, BaseBlind, Blind, FuncId, FuncRef, Keypair,
 41          PublicKey, SecretKey,
 42      },
 43      dark_tree::DarkTree,
 44      pasta::pallas,
 45      tx::ContractCall,
 46  };
 47  use darkfi_serial::{deserialize_async, serialize_async, AsyncEncodable};
 48  
 49  use crate::{
 50      convert_named_params,
 51      error::WalletDbResult,
 52      money::{
 53          BALANCE_BASE10_DECIMALS, MONEY_TOKENS_COL_FREEZE_HEIGHT, MONEY_TOKENS_COL_IS_FROZEN,
 54          MONEY_TOKENS_COL_MINT_AUTHORITY, MONEY_TOKENS_COL_TOKEN_BLIND, MONEY_TOKENS_COL_TOKEN_ID,
 55          MONEY_TOKENS_TABLE,
 56      },
 57      Drk,
 58  };
 59  
 60  impl Drk {
 61      /// Auxiliary function to derive `TokenAttributes` for provided secret key and token blind.
 62      fn derive_token_attributes(
 63          &self,
 64          mint_authority: SecretKey,
 65          token_blind: BaseBlind,
 66      ) -> TokenAttributes {
 67          // Create the Auth FuncID
 68          let auth_func_id = FuncRef {
 69              contract_id: *MONEY_CONTRACT_ID,
 70              func_code: MoneyFunction::AuthTokenMintV1 as u8,
 71          }
 72          .to_func_id();
 73  
 74          // Grab the mint authority key public coordinates
 75          let (mint_auth_x, mint_auth_y) = PublicKey::from_secret(mint_authority).xy();
 76  
 77          // Generate the token attributes
 78          TokenAttributes {
 79              auth_parent: auth_func_id,
 80              user_data: poseidon_hash([mint_auth_x, mint_auth_y]),
 81              blind: token_blind,
 82          }
 83      }
 84  
 85      /// Import a token mint authority into the wallet.
 86      pub async fn import_mint_authority(
 87          &self,
 88          mint_authority: SecretKey,
 89          token_blind: BaseBlind,
 90      ) -> Result<TokenId> {
 91          let token_id = self.derive_token_attributes(mint_authority, token_blind).to_token_id();
 92          let is_frozen = 0;
 93          let freeze_height: Option<u32> = None;
 94  
 95          let query = format!(
 96              "INSERT INTO {} ({}, {}, {}, {}, {}) VALUES (?1, ?2, ?3, ?4, ?5);",
 97              *MONEY_TOKENS_TABLE,
 98              MONEY_TOKENS_COL_TOKEN_ID,
 99              MONEY_TOKENS_COL_MINT_AUTHORITY,
100              MONEY_TOKENS_COL_TOKEN_BLIND,
101              MONEY_TOKENS_COL_IS_FROZEN,
102              MONEY_TOKENS_COL_FREEZE_HEIGHT,
103          );
104  
105          if let Err(e) = self.wallet.exec_sql(
106              &query,
107              rusqlite::params![
108                  serialize_async(&token_id).await,
109                  serialize_async(&mint_authority).await,
110                  serialize_async(&token_blind).await,
111                  is_frozen,
112                  freeze_height,
113              ],
114          ) {
115              return Err(Error::DatabaseError(format!(
116                  "[import_mint_authority] Inserting mint authority failed: {e}"
117              )))
118          };
119  
120          Ok(token_id)
121      }
122  
123      /// Auxiliary function to parse a `MONEY_TOKENS_TABLE` records.
124      /// The boolean in the returned tuples notes if the token mint authority is frozen.
125      async fn parse_mint_authority_record(
126          &self,
127          row: &[Value],
128      ) -> Result<(TokenId, SecretKey, BaseBlind, bool, Option<u32>)> {
129          let Value::Blob(ref token_bytes) = row[0] else {
130              return Err(Error::ParseFailed(
131                  "[parse_mint_authority_record] Token ID bytes parsing failed",
132              ))
133          };
134          let token_id = deserialize_async(token_bytes).await?;
135  
136          let Value::Blob(ref auth_bytes) = row[1] else {
137              return Err(Error::ParseFailed(
138                  "[parse_mint_authority_record] Mint authority bytes parsing failed",
139              ))
140          };
141          let mint_authority = deserialize_async(auth_bytes).await?;
142  
143          let Value::Blob(ref token_blind_bytes) = row[2] else {
144              return Err(Error::ParseFailed(
145                  "[parse_mint_authority_record] Token blind bytes parsing failed",
146              ))
147          };
148          let token_blind: BaseBlind = deserialize_async(token_blind_bytes).await?;
149  
150          let Value::Integer(frozen) = row[3] else {
151              return Err(Error::ParseFailed("[parse_mint_authority_record] Is frozen parsing failed"))
152          };
153          let Ok(frozen) = i32::try_from(frozen) else {
154              return Err(Error::ParseFailed("[parse_mint_authority_record] Is frozen parsing failed"))
155          };
156  
157          let freeze_height = match row[4] {
158              Value::Integer(freeze_height) => {
159                  let Ok(freeze_height) = u32::try_from(freeze_height) else {
160                      return Err(Error::ParseFailed(
161                          "[parse_mint_authority_record] Freeze height parsing failed",
162                      ))
163                  };
164                  Some(freeze_height)
165              }
166              Value::Null => None,
167              _ => {
168                  return Err(Error::ParseFailed(
169                      "[parse_mint_authority_record] Freeze height parsing failed",
170                  ))
171              }
172          };
173  
174          Ok((token_id, mint_authority, token_blind, frozen != 0, freeze_height))
175      }
176  
177      /// Reset all token mint authorities frozen status in the wallet.
178      pub fn reset_mint_authorities(&self, output: &mut Vec<String>) -> WalletDbResult<()> {
179          output.push(String::from("Resetting mint authorities frozen status"));
180          let query = format!(
181              "UPDATE {} SET {} = 0, {} = NULL;",
182              *MONEY_TOKENS_TABLE, MONEY_TOKENS_COL_IS_FROZEN, MONEY_TOKENS_COL_FREEZE_HEIGHT
183          );
184          self.wallet.exec_sql(&query, &[])?;
185          output.push(String::from("Successfully reset mint authorities frozen status"));
186  
187          Ok(())
188      }
189  
190      /// Remove token mint authorities frozen status in the wallet that
191      /// where frozen after provided height.
192      pub fn unfreeze_mint_authorities_after(
193          &self,
194          height: &u32,
195          output: &mut Vec<String>,
196      ) -> WalletDbResult<()> {
197          output.push(format!("Resetting mint authorities frozen status after: {height}"));
198          let query = format!(
199              "UPDATE {} SET {} = 0, {} = NULL WHERE {} > ?1;",
200              *MONEY_TOKENS_TABLE,
201              MONEY_TOKENS_COL_IS_FROZEN,
202              MONEY_TOKENS_COL_FREEZE_HEIGHT,
203              MONEY_TOKENS_COL_FREEZE_HEIGHT
204          );
205          self.wallet.exec_sql(&query, rusqlite::params![Some(*height)])?;
206          output.push(String::from("Successfully reset mint authorities frozen status"));
207  
208          Ok(())
209      }
210  
211      /// Fetch all token mint authorities from the wallet.
212      pub async fn get_mint_authorities(
213          &self,
214      ) -> Result<Vec<(TokenId, SecretKey, BaseBlind, bool, Option<u32>)>> {
215          let rows = match self.wallet.query_multiple(&MONEY_TOKENS_TABLE, &[], &[]) {
216              Ok(r) => r,
217              Err(e) => {
218                  return Err(Error::DatabaseError(format!(
219                      "[get_mint_authorities] Tokens mint autorities retrieval failed: {e}"
220                  )))
221              }
222          };
223  
224          let mut ret = Vec::with_capacity(rows.len());
225          for row in rows {
226              ret.push(self.parse_mint_authority_record(&row).await?);
227          }
228  
229          Ok(ret)
230      }
231  
232      /// Fetch provided token unfrozen mint authority from the wallet.
233      async fn get_token_mint_authority(
234          &self,
235          token_id: &TokenId,
236      ) -> Result<(TokenId, SecretKey, BaseBlind, bool, Option<u32>)> {
237          let row = match self.wallet.query_single(
238              &MONEY_TOKENS_TABLE,
239              &[],
240              convert_named_params! {(MONEY_TOKENS_COL_TOKEN_ID, serialize_async(token_id).await)},
241          ) {
242              Ok(r) => r,
243              Err(e) => {
244                  return Err(Error::DatabaseError(format!(
245                      "[get_token_mint_authority] Token mint autority retrieval failed: {e}"
246                  )))
247              }
248          };
249  
250          let token = self.parse_mint_authority_record(&row).await?;
251  
252          if token.3 {
253              return Err(Error::Custom(
254                  "This token mint is marked as frozen in the wallet".to_string(),
255              ))
256          }
257  
258          Ok(token)
259      }
260  
261      /// Create a token mint transaction. Returns the transaction object on success.
262      pub async fn mint_token(
263          &self,
264          amount: &str,
265          recipient: PublicKey,
266          token_id: TokenId,
267          spend_hook: Option<FuncId>,
268          user_data: Option<pallas::Base>,
269      ) -> Result<Transaction> {
270          // Decode provided amount
271          let amount = decode_base10(amount, BALANCE_BASE10_DECIMALS, false)?;
272  
273          // Grab token ID mint authority and attributes
274          let token_mint_authority = self.get_token_mint_authority(&token_id).await?;
275          let token_attrs =
276              self.derive_token_attributes(token_mint_authority.1, token_mint_authority.2);
277          let mint_authority = Keypair::new(token_mint_authority.1);
278  
279          // Sanity check
280          assert_eq!(token_id, token_attrs.to_token_id());
281  
282          // Now we need to do a lookup for the zkas proof bincodes, and create
283          // the circuit objects and proving keys so we can build the transaction.
284          // We also do this through the RPC.
285          let zkas_bins = self.lookup_zkas(&MONEY_CONTRACT_ID).await?;
286  
287          let Some(mint_zkbin) =
288              zkas_bins.iter().find(|x| x.0 == MONEY_CONTRACT_ZKAS_TOKEN_MINT_NS_V1)
289          else {
290              return Err(Error::Custom("Token mint circuit not found".to_string()))
291          };
292  
293          let Some(auth_mint_zkbin) =
294              zkas_bins.iter().find(|x| x.0 == MONEY_CONTRACT_ZKAS_AUTH_TOKEN_MINT_NS_V1)
295          else {
296              return Err(Error::Custom("Auth token mint circuit not found".to_string()))
297          };
298  
299          let Some(fee_zkbin) = zkas_bins.iter().find(|x| x.0 == MONEY_CONTRACT_ZKAS_FEE_NS_V1)
300          else {
301              return Err(Error::Custom("Fee circuit not found".to_string()))
302          };
303  
304          let mint_zkbin = ZkBinary::decode(&mint_zkbin.1)?;
305          let auth_mint_zkbin = ZkBinary::decode(&auth_mint_zkbin.1)?;
306          let fee_zkbin = ZkBinary::decode(&fee_zkbin.1)?;
307  
308          let mint_circuit = ZkCircuit::new(empty_witnesses(&mint_zkbin)?, &mint_zkbin);
309          let auth_mint_circuit =
310              ZkCircuit::new(empty_witnesses(&auth_mint_zkbin)?, &auth_mint_zkbin);
311          let fee_circuit = ZkCircuit::new(empty_witnesses(&fee_zkbin)?, &fee_zkbin);
312  
313          // Creating TokenMint, AuthTokenMint and Fee circuits proving keys
314          let mint_pk = ProvingKey::build(mint_zkbin.k, &mint_circuit);
315          let auth_mint_pk = ProvingKey::build(auth_mint_zkbin.k, &auth_mint_circuit);
316          let fee_pk = ProvingKey::build(fee_zkbin.k, &fee_circuit);
317  
318          // Build the coin attributes
319          let coin_attrs = CoinAttributes {
320              public_key: recipient,
321              value: amount,
322              token_id,
323              spend_hook: spend_hook.unwrap_or(FuncId::none()),
324              user_data: user_data.unwrap_or(pallas::Base::ZERO),
325              blind: Blind::random(&mut OsRng),
326          };
327  
328          // Create the auth call
329          let builder = AuthTokenMintCallBuilder {
330              coin_attrs: coin_attrs.clone(),
331              token_attrs: token_attrs.clone(),
332              mint_keypair: mint_authority,
333              auth_mint_zkbin,
334              auth_mint_pk,
335          };
336          let auth_debris = builder.build()?;
337          let mut data = vec![MoneyFunction::AuthTokenMintV1 as u8];
338          auth_debris.params.encode_async(&mut data).await?;
339          let auth_call = ContractCall { contract_id: *MONEY_CONTRACT_ID, data };
340  
341          // Create the minting call
342          let builder = TokenMintCallBuilder { coin_attrs, token_attrs, mint_zkbin, mint_pk };
343          let mint_debris = builder.build()?;
344          let mut data = vec![MoneyFunction::TokenMintV1 as u8];
345          mint_debris.params.encode_async(&mut data).await?;
346          let mint_call = ContractCall { contract_id: *MONEY_CONTRACT_ID, data };
347  
348          // Create the TransactionBuilder containing above calls
349          let mut tx_builder = TransactionBuilder::new(
350              ContractCallLeaf { call: mint_call, proofs: mint_debris.proofs },
351              vec![DarkTree::new(
352                  ContractCallLeaf { call: auth_call, proofs: auth_debris.proofs },
353                  vec![],
354                  None,
355                  None,
356              )],
357          )?;
358  
359          // We first have to execute the fee-less tx to gather its used gas, and then we feed
360          // it into the fee-creating function.
361          let mut tx = tx_builder.build()?;
362          let auth_sigs = tx.create_sigs(&[mint_authority.secret])?;
363          let mint_sigs = tx.create_sigs(&[])?;
364          tx.signatures = vec![auth_sigs, mint_sigs];
365  
366          let tree = self.get_money_tree().await?;
367          let (fee_call, fee_proofs, fee_secrets) =
368              self.append_fee_call(&tx, &tree, &fee_pk, &fee_zkbin, None).await?;
369  
370          // Append the fee call to the transaction
371          tx_builder.append(ContractCallLeaf { call: fee_call, proofs: fee_proofs }, vec![])?;
372  
373          // Now build the actual transaction and sign it with all necessary keys.
374          let mut tx = tx_builder.build()?;
375          let sigs = tx.create_sigs(&[mint_authority.secret])?;
376          tx.signatures.push(sigs);
377          let sigs = tx.create_sigs(&[])?;
378          tx.signatures.push(sigs);
379          let sigs = tx.create_sigs(&fee_secrets)?;
380          tx.signatures.push(sigs);
381  
382          Ok(tx)
383      }
384  
385      /// Create a token freeze transaction. Returns the transaction object on success.
386      pub async fn freeze_token(&self, token_id: TokenId) -> Result<Transaction> {
387          // Grab token ID mint authority and attributes
388          let token_mint_authority = self.get_token_mint_authority(&token_id).await?;
389          let token_attrs =
390              self.derive_token_attributes(token_mint_authority.1, token_mint_authority.2);
391          let mint_authority = Keypair::new(token_mint_authority.1);
392  
393          // Sanity check
394          assert_eq!(token_id, token_attrs.to_token_id());
395  
396          // Now we need to do a lookup for the zkas proof bincodes, and create
397          // the circuit objects and proving keys so we can build the transaction.
398          // We also do this through the RPC.
399          let zkas_bins = self.lookup_zkas(&MONEY_CONTRACT_ID).await?;
400  
401          let Some(auth_mint_zkbin) =
402              zkas_bins.iter().find(|x| x.0 == MONEY_CONTRACT_ZKAS_AUTH_TOKEN_MINT_NS_V1)
403          else {
404              return Err(Error::Custom("Auth token mint circuit not found".to_string()))
405          };
406  
407          let Some(fee_zkbin) = zkas_bins.iter().find(|x| x.0 == MONEY_CONTRACT_ZKAS_FEE_NS_V1)
408          else {
409              return Err(Error::Custom("Fee circuit not found".to_string()))
410          };
411  
412          let auth_mint_zkbin = ZkBinary::decode(&auth_mint_zkbin.1)?;
413          let fee_zkbin = ZkBinary::decode(&fee_zkbin.1)?;
414  
415          let auth_mint_circuit =
416              ZkCircuit::new(empty_witnesses(&auth_mint_zkbin)?, &auth_mint_zkbin);
417          let fee_circuit = ZkCircuit::new(empty_witnesses(&fee_zkbin)?, &fee_zkbin);
418  
419          // Creating AuthTokenMint and Fee circuits proving keys
420          let auth_mint_pk = ProvingKey::build(auth_mint_zkbin.k, &auth_mint_circuit);
421          let fee_pk = ProvingKey::build(fee_zkbin.k, &fee_circuit);
422  
423          // Create the freeze call
424          let builder = AuthTokenFreezeCallBuilder {
425              mint_keypair: mint_authority,
426              token_attrs,
427              auth_mint_zkbin,
428              auth_mint_pk,
429          };
430          let freeze_debris = builder.build()?;
431          let mut data = vec![MoneyFunction::AuthTokenFreezeV1 as u8];
432          freeze_debris.params.encode_async(&mut data).await?;
433          let freeze_call = ContractCall { contract_id: *MONEY_CONTRACT_ID, data };
434  
435          // Create the TransactionBuilder containing above call
436          let mut tx_builder = TransactionBuilder::new(
437              ContractCallLeaf { call: freeze_call, proofs: freeze_debris.proofs },
438              vec![],
439          )?;
440  
441          // We first have to execute the fee-less tx to gather its used gas, and then we feed
442          // it into the fee-creating function.
443          let mut tx = tx_builder.build()?;
444          let sigs = tx.create_sigs(&[mint_authority.secret])?;
445          tx.signatures.push(sigs);
446  
447          let tree = self.get_money_tree().await?;
448          let (fee_call, fee_proofs, fee_secrets) =
449              self.append_fee_call(&tx, &tree, &fee_pk, &fee_zkbin, None).await?;
450  
451          // Append the fee call to the transaction
452          tx_builder.append(ContractCallLeaf { call: fee_call, proofs: fee_proofs }, vec![])?;
453  
454          // Now build the actual transaction and sign it with all necessary keys.
455          let mut tx = tx_builder.build()?;
456          let sigs = tx.create_sigs(&[mint_authority.secret])?;
457          tx.signatures.push(sigs);
458          let sigs = tx.create_sigs(&fee_secrets)?;
459          tx.signatures.push(sigs);
460  
461          Ok(tx)
462      }
463  }