dao.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 std::{collections::HashMap, fmt, str::FromStr}; 20 21 use lazy_static::lazy_static; 22 use num_bigint::BigUint; 23 use rand::rngs::OsRng; 24 use rusqlite::types::Value; 25 26 use darkfi::{ 27 tx::{ContractCallLeaf, Transaction, TransactionBuilder}, 28 util::parse::{decode_base10, encode_base10}, 29 zk::{empty_witnesses, halo2::Field, ProvingKey, ZkCircuit}, 30 zkas::ZkBinary, 31 Error, Result, 32 }; 33 use darkfi_dao_contract::{ 34 blockwindow, 35 client::{ 36 make_mint_call, DaoAuthMoneyTransferCall, DaoExecCall, DaoProposeCall, 37 DaoProposeStakeInput, DaoVoteCall, DaoVoteInput, 38 }, 39 model::{ 40 Dao, DaoAuthCall, DaoBulla, DaoExecParams, DaoMintParams, DaoProposal, DaoProposalBulla, 41 DaoProposeParams, DaoVoteParams, 42 }, 43 DaoFunction, DAO_CONTRACT_ZKAS_DAO_AUTH_MONEY_TRANSFER_ENC_COIN_NS, 44 DAO_CONTRACT_ZKAS_DAO_AUTH_MONEY_TRANSFER_NS, DAO_CONTRACT_ZKAS_DAO_EARLY_EXEC_NS, 45 DAO_CONTRACT_ZKAS_DAO_EXEC_NS, DAO_CONTRACT_ZKAS_DAO_MINT_NS, 46 DAO_CONTRACT_ZKAS_DAO_PROPOSE_INPUT_NS, DAO_CONTRACT_ZKAS_DAO_PROPOSE_MAIN_NS, 47 DAO_CONTRACT_ZKAS_DAO_VOTE_INPUT_NS, DAO_CONTRACT_ZKAS_DAO_VOTE_MAIN_NS, 48 }; 49 use darkfi_money_contract::{ 50 client::transfer_v1::{select_coins, TransferCallBuilder, TransferCallInput}, 51 model::{CoinAttributes, Nullifier, TokenId}, 52 MoneyFunction, MONEY_CONTRACT_ZKAS_BURN_NS_V1, MONEY_CONTRACT_ZKAS_FEE_NS_V1, 53 MONEY_CONTRACT_ZKAS_MINT_NS_V1, 54 }; 55 use darkfi_sdk::{ 56 bridgetree, 57 crypto::{ 58 poseidon_hash, 59 smt::{MemoryStorageFp, PoseidonFp, SmtMemoryFp, EMPTY_NODES_FP}, 60 util::{fp_mod_fv, fp_to_u64}, 61 BaseBlind, Blind, FuncId, FuncRef, MerkleNode, MerkleTree, PublicKey, ScalarBlind, 62 SecretKey, DAO_CONTRACT_ID, MONEY_CONTRACT_ID, 63 }, 64 dark_tree::DarkTree, 65 pasta::pallas, 66 tx::TransactionHash, 67 ContractCall, 68 }; 69 use darkfi_serial::{ 70 async_trait, deserialize_async, serialize, serialize_async, AsyncEncodable, SerialDecodable, 71 SerialEncodable, 72 }; 73 74 use crate::{ 75 cache::{CacheOverlay, CacheSmt, CacheSmtStorage, SLED_MONEY_SMT_TREE}, 76 convert_named_params, 77 error::{WalletDbError, WalletDbResult}, 78 money::BALANCE_BASE10_DECIMALS, 79 rpc::ScanCache, 80 Drk, 81 }; 82 83 // DAO Merkle trees Sled keys 84 pub const SLED_MERKLE_TREES_DAO_DAOS: &[u8] = b"_dao_daos"; 85 pub const SLED_MERKLE_TREES_DAO_PROPOSALS: &[u8] = b"_dao_proposals"; 86 87 // Wallet SQL table constant names. These have to represent the `dao.sql` 88 // SQL schema. Table names are prefixed with the contract ID to avoid collisions. 89 lazy_static! { 90 pub static ref DAO_DAOS_TABLE: String = format!("{}_dao_daos", DAO_CONTRACT_ID.to_string()); 91 pub static ref DAO_COINS_TABLE: String = format!("{}_dao_coins", DAO_CONTRACT_ID.to_string()); 92 pub static ref DAO_PROPOSALS_TABLE: String = 93 format!("{}_dao_proposals", DAO_CONTRACT_ID.to_string()); 94 pub static ref DAO_VOTES_TABLE: String = format!("{}_dao_votes", DAO_CONTRACT_ID.to_string()); 95 } 96 97 // DAO_DAOS_TABLE 98 pub const DAO_DAOS_COL_BULLA: &str = "bulla"; 99 pub const DAO_DAOS_COL_NAME: &str = "name"; 100 pub const DAO_DAOS_COL_PARAMS: &str = "params"; 101 pub const DAO_DAOS_COL_LEAF_POSITION: &str = "leaf_position"; 102 pub const DAO_DAOS_COL_MINT_HEIGHT: &str = "mint_height"; 103 pub const DAO_DAOS_COL_TX_HASH: &str = "tx_hash"; 104 pub const DAO_DAOS_COL_CALL_INDEX: &str = "call_index"; 105 106 // DAO_PROPOSALS_TABLE 107 pub const DAO_PROPOSALS_COL_BULLA: &str = "bulla"; 108 pub const DAO_PROPOSALS_COL_DAO_BULLA: &str = "dao_bulla"; 109 pub const DAO_PROPOSALS_COL_PROPOSAL: &str = "proposal"; 110 pub const DAO_PROPOSALS_COL_DATA: &str = "data"; 111 pub const DAO_PROPOSALS_COL_LEAF_POSITION: &str = "leaf_position"; 112 pub const DAO_PROPOSALS_COL_MONEY_SNAPSHOT_TREE: &str = "money_snapshot_tree"; 113 pub const DAO_PROPOSALS_COL_NULLIFIERS_SMT_SNAPSHOT: &str = "nullifiers_smt_snapshot"; 114 pub const DAO_PROPOSALS_COL_MINT_HEIGHT: &str = "mint_height"; 115 pub const DAO_PROPOSALS_COL_TX_HASH: &str = "tx_hash"; 116 pub const DAO_PROPOSALS_COL_CALL_INDEX: &str = "call_index"; 117 pub const DAO_PROPOSALS_COL_EXEC_HEIGHT: &str = "exec_height"; 118 pub const DAO_PROPOSALS_COL_EXEC_TX_HASH: &str = "exec_tx_hash"; 119 120 // DAO_VOTES_TABLE 121 pub const DAO_VOTES_COL_PROPOSAL_BULLA: &str = "proposal_bulla"; 122 pub const DAO_VOTES_COL_VOTE_OPTION: &str = "vote_option"; 123 pub const DAO_VOTES_COL_YES_VOTE_BLIND: &str = "yes_vote_blind"; 124 pub const DAO_VOTES_COL_ALL_VOTE_VALUE: &str = "all_vote_value"; 125 pub const DAO_VOTES_COL_ALL_VOTE_BLIND: &str = "all_vote_blind"; 126 pub const DAO_VOTES_COL_BLOCK_HEIGHT: &str = "block_height"; 127 pub const DAO_VOTES_COL_TX_HASH: &str = "tx_hash"; 128 pub const DAO_VOTES_COL_CALL_INDEX: &str = "call_index"; 129 pub const DAO_VOTES_COL_NULLIFIERS: &str = "nullifiers"; 130 131 #[derive(Debug, Clone, SerialEncodable, SerialDecodable)] 132 /// Parameters representing a DAO to be initialized 133 pub struct DaoParams { 134 /// The on chain representation of the DAO 135 pub dao: Dao, 136 /// DAO notes decryption secret key 137 pub notes_secret_key: Option<SecretKey>, 138 /// DAO proposals creator secret key 139 pub proposer_secret_key: Option<SecretKey>, 140 /// DAO proposals viewer secret key 141 pub proposals_secret_key: Option<SecretKey>, 142 /// DAO votes viewer secret key 143 pub votes_secret_key: Option<SecretKey>, 144 /// DAO proposals executor secret key 145 pub exec_secret_key: Option<SecretKey>, 146 /// DAO strongly supported proposals executor secret key 147 pub early_exec_secret_key: Option<SecretKey>, 148 } 149 150 impl DaoParams { 151 /// Generate new `DaoParams`. If a specific secret key is provided, 152 /// the corresponding public key will be derived from it and ignore the provided one. 153 #[allow(clippy::too_many_arguments)] 154 pub fn new( 155 proposer_limit: u64, 156 quorum: u64, 157 early_exec_quorum: u64, 158 approval_ratio_base: u64, 159 approval_ratio_quot: u64, 160 gov_token_id: TokenId, 161 notes_secret_key: Option<SecretKey>, 162 notes_public_key: PublicKey, 163 proposer_secret_key: Option<SecretKey>, 164 proposer_public_key: PublicKey, 165 proposals_secret_key: Option<SecretKey>, 166 proposals_public_key: PublicKey, 167 votes_secret_key: Option<SecretKey>, 168 votes_public_key: PublicKey, 169 exec_secret_key: Option<SecretKey>, 170 exec_public_key: PublicKey, 171 early_exec_secret_key: Option<SecretKey>, 172 early_exec_public_key: PublicKey, 173 bulla_blind: BaseBlind, 174 ) -> Self { 175 // Derive corresponding keys from their secret or use the provided ones. 176 let notes_public_key = match notes_secret_key { 177 Some(secret_key) => PublicKey::from_secret(secret_key), 178 None => notes_public_key, 179 }; 180 let proposer_public_key = match proposer_secret_key { 181 Some(secret_key) => PublicKey::from_secret(secret_key), 182 None => proposer_public_key, 183 }; 184 let proposals_public_key = match proposals_secret_key { 185 Some(secret_key) => PublicKey::from_secret(secret_key), 186 None => proposals_public_key, 187 }; 188 let votes_public_key = match votes_secret_key { 189 Some(secret_key) => PublicKey::from_secret(secret_key), 190 None => votes_public_key, 191 }; 192 let exec_public_key = match exec_secret_key { 193 Some(secret_key) => PublicKey::from_secret(secret_key), 194 None => exec_public_key, 195 }; 196 let early_exec_public_key = match early_exec_secret_key { 197 Some(secret_key) => PublicKey::from_secret(secret_key), 198 None => early_exec_public_key, 199 }; 200 201 let dao = Dao { 202 proposer_limit, 203 quorum, 204 early_exec_quorum, 205 approval_ratio_base, 206 approval_ratio_quot, 207 gov_token_id, 208 notes_public_key, 209 proposer_public_key, 210 proposals_public_key, 211 votes_public_key, 212 exec_public_key, 213 early_exec_public_key, 214 bulla_blind, 215 }; 216 Self { 217 dao, 218 notes_secret_key, 219 proposer_secret_key, 220 proposals_secret_key, 221 votes_secret_key, 222 exec_secret_key, 223 early_exec_secret_key, 224 } 225 } 226 227 /// Parse provided toml string into `DaoParams`. 228 /// If a specific secret key is provided, the corresponding public key 229 /// will be derived from it and ignore the provided one. 230 pub fn from_toml_str(toml: &str) -> Result<Self> { 231 // Parse TOML file contents 232 let Ok(contents) = toml::from_str::<toml::Value>(toml) else { 233 return Err(Error::ParseFailed("Failed parsing TOML config")) 234 }; 235 let Some(table) = contents.as_table() else { 236 return Err(Error::ParseFailed("TOML not a map")) 237 }; 238 239 // Grab configuration parameters 240 let Some(proposer_limit) = table.get("proposer_limit") else { 241 return Err(Error::ParseFailed("TOML does not contain proposer limit")) 242 }; 243 let Some(proposer_limit) = proposer_limit.as_str() else { 244 return Err(Error::ParseFailed("Invalid proposer limit: Not a string")) 245 }; 246 if f64::from_str(proposer_limit).is_err() { 247 return Err(Error::ParseFailed("Invalid proposer limit: Cannot be parsed to float")) 248 } 249 let proposer_limit = decode_base10(proposer_limit, BALANCE_BASE10_DECIMALS, true)?; 250 251 let Some(quorum) = table.get("quorum") else { 252 return Err(Error::ParseFailed("TOML does not contain quorum")) 253 }; 254 let Some(quorum) = quorum.as_str() else { 255 return Err(Error::ParseFailed("Invalid quorum: Not a string")) 256 }; 257 if f64::from_str(quorum).is_err() { 258 return Err(Error::ParseFailed("Invalid quorum: Cannot be parsed to float")) 259 } 260 let quorum = decode_base10(quorum, BALANCE_BASE10_DECIMALS, true)?; 261 262 let Some(early_exec_quorum) = table.get("early_exec_quorum") else { 263 return Err(Error::ParseFailed("TOML does not contain early exec quorum")) 264 }; 265 let Some(early_exec_quorum) = early_exec_quorum.as_str() else { 266 return Err(Error::ParseFailed("Invalid early exec quorum: Not a string")) 267 }; 268 if f64::from_str(early_exec_quorum).is_err() { 269 return Err(Error::ParseFailed("Invalid early exec quorum: Cannot be parsed to float")) 270 } 271 let early_exec_quorum = decode_base10(early_exec_quorum, BALANCE_BASE10_DECIMALS, true)?; 272 273 let Some(approval_ratio) = table.get("approval_ratio") else { 274 return Err(Error::ParseFailed("TOML does not contain approval ratio")) 275 }; 276 let Some(approval_ratio) = approval_ratio.as_float() else { 277 return Err(Error::ParseFailed("Invalid approval ratio: Not a float")) 278 }; 279 if approval_ratio > 1.0 { 280 return Err(Error::ParseFailed("Approval ratio cannot be >1.0")) 281 } 282 let approval_ratio_base = 100_u64; 283 let approval_ratio_quot = (approval_ratio * approval_ratio_base as f64) as u64; 284 285 let Some(gov_token_id) = table.get("gov_token_id") else { 286 return Err(Error::ParseFailed("TOML does not contain gov token id")) 287 }; 288 let Some(gov_token_id) = gov_token_id.as_str() else { 289 return Err(Error::ParseFailed("Invalid gov token id: Not a string")) 290 }; 291 let gov_token_id = TokenId::from_str(gov_token_id)?; 292 293 let Some(bulla_blind) = table.get("bulla_blind") else { 294 return Err(Error::ParseFailed("TOML does not contain bulla blind")) 295 }; 296 let Some(bulla_blind) = bulla_blind.as_str() else { 297 return Err(Error::ParseFailed("Invalid bulla blind: Not a string")) 298 }; 299 let bulla_blind = BaseBlind::from_str(bulla_blind)?; 300 301 // Grab DAO actions keypairs 302 let notes_secret_key = match table.get("notes_secret_key") { 303 Some(notes_secret_key) => { 304 let Some(notes_secret_key) = notes_secret_key.as_str() else { 305 return Err(Error::ParseFailed("Invalid notes secret key: Not a string")) 306 }; 307 let Ok(notes_secret_key) = SecretKey::from_str(notes_secret_key) else { 308 return Err(Error::ParseFailed("Invalid notes secret key: Decoding failed")) 309 }; 310 Some(notes_secret_key) 311 } 312 None => None, 313 }; 314 let notes_public_key = match notes_secret_key { 315 Some(notes_secret_key) => PublicKey::from_secret(notes_secret_key), 316 None => { 317 let Some(notes_public_key) = table.get("notes_public_key") else { 318 return Err(Error::ParseFailed("TOML does not contain notes public key")) 319 }; 320 let Some(notes_public_key) = notes_public_key.as_str() else { 321 return Err(Error::ParseFailed("Invalid notes public key: Not a string")) 322 }; 323 let Ok(notes_public_key) = PublicKey::from_str(notes_public_key) else { 324 return Err(Error::ParseFailed("Invalid notes public key: Decoding failed")) 325 }; 326 notes_public_key 327 } 328 }; 329 330 let proposer_secret_key = match table.get("proposer_secret_key") { 331 Some(proposer_secret_key) => { 332 let Some(proposer_secret_key) = proposer_secret_key.as_str() else { 333 return Err(Error::ParseFailed("Invalid proposer secret key: Not a string")) 334 }; 335 let Ok(proposer_secret_key) = SecretKey::from_str(proposer_secret_key) else { 336 return Err(Error::ParseFailed("Invalid proposer secret key: Decoding failed")) 337 }; 338 Some(proposer_secret_key) 339 } 340 None => None, 341 }; 342 let proposer_public_key = match proposer_secret_key { 343 Some(proposer_secret_key) => PublicKey::from_secret(proposer_secret_key), 344 None => { 345 let Some(proposer_public_key) = table.get("proposer_public_key") else { 346 return Err(Error::ParseFailed("TOML does not contain proposer public key")) 347 }; 348 let Some(proposer_public_key) = proposer_public_key.as_str() else { 349 return Err(Error::ParseFailed("Invalid proposer public key: Not a string")) 350 }; 351 let Ok(proposer_public_key) = PublicKey::from_str(proposer_public_key) else { 352 return Err(Error::ParseFailed("Invalid proposer public key: Decoding failed")) 353 }; 354 proposer_public_key 355 } 356 }; 357 358 let proposals_secret_key = match table.get("proposals_secret_key") { 359 Some(proposals_secret_key) => { 360 let Some(proposals_secret_key) = proposals_secret_key.as_str() else { 361 return Err(Error::ParseFailed("Invalid proposals secret key: Not a string")) 362 }; 363 let Ok(proposals_secret_key) = SecretKey::from_str(proposals_secret_key) else { 364 return Err(Error::ParseFailed("Invalid proposals secret key: Decoding failed")) 365 }; 366 Some(proposals_secret_key) 367 } 368 None => None, 369 }; 370 let proposals_public_key = match proposals_secret_key { 371 Some(proposals_secret_key) => PublicKey::from_secret(proposals_secret_key), 372 None => { 373 let Some(proposals_public_key) = table.get("proposals_public_key") else { 374 return Err(Error::ParseFailed("TOML does not contain proposals public key")) 375 }; 376 let Some(proposals_public_key) = proposals_public_key.as_str() else { 377 return Err(Error::ParseFailed("Invalid proposals public key: Not a string")) 378 }; 379 let Ok(proposals_public_key) = PublicKey::from_str(proposals_public_key) else { 380 return Err(Error::ParseFailed("Invalid proposals public key: Decoding failed")) 381 }; 382 proposals_public_key 383 } 384 }; 385 386 let votes_secret_key = match table.get("votes_secret_key") { 387 Some(votes_secret_key) => { 388 let Some(votes_secret_key) = votes_secret_key.as_str() else { 389 return Err(Error::ParseFailed("Invalid votes secret key: Not a string")) 390 }; 391 let Ok(votes_secret_key) = SecretKey::from_str(votes_secret_key) else { 392 return Err(Error::ParseFailed("Invalid votes secret key: Decoding failed")) 393 }; 394 Some(votes_secret_key) 395 } 396 None => None, 397 }; 398 let votes_public_key = match votes_secret_key { 399 Some(votes_secret_key) => PublicKey::from_secret(votes_secret_key), 400 None => { 401 let Some(votes_public_key) = table.get("votes_public_key") else { 402 return Err(Error::ParseFailed("TOML does not contain votes public key")) 403 }; 404 let Some(votes_public_key) = votes_public_key.as_str() else { 405 return Err(Error::ParseFailed("Invalid votes public key: Not a string")) 406 }; 407 let Ok(votes_public_key) = PublicKey::from_str(votes_public_key) else { 408 return Err(Error::ParseFailed("Invalid votes public key: Decoding failed")) 409 }; 410 votes_public_key 411 } 412 }; 413 414 let exec_secret_key = match table.get("exec_secret_key") { 415 Some(exec_secret_key) => { 416 let Some(exec_secret_key) = exec_secret_key.as_str() else { 417 return Err(Error::ParseFailed("Invalid exec secret key: Not a string")) 418 }; 419 let Ok(exec_secret_key) = SecretKey::from_str(exec_secret_key) else { 420 return Err(Error::ParseFailed("Invalid exec secret key: Decoding failed")) 421 }; 422 Some(exec_secret_key) 423 } 424 None => None, 425 }; 426 let exec_public_key = match exec_secret_key { 427 Some(exec_secret_key) => PublicKey::from_secret(exec_secret_key), 428 None => { 429 let Some(exec_public_key) = table.get("exec_public_key") else { 430 return Err(Error::ParseFailed("TOML does not contain exec public key")) 431 }; 432 let Some(exec_public_key) = exec_public_key.as_str() else { 433 return Err(Error::ParseFailed("Invalid exec public key: Not a string")) 434 }; 435 let Ok(exec_public_key) = PublicKey::from_str(exec_public_key) else { 436 return Err(Error::ParseFailed("Invalid exec public key: Decoding failed")) 437 }; 438 exec_public_key 439 } 440 }; 441 442 let early_exec_secret_key = match table.get("early_exec_secret_key") { 443 Some(early_exec_secret_key) => { 444 let Some(early_exec_secret_key) = early_exec_secret_key.as_str() else { 445 return Err(Error::ParseFailed("Invalid early exec secret key: Not a string")) 446 }; 447 let Ok(early_exec_secret_key) = SecretKey::from_str(early_exec_secret_key) else { 448 return Err(Error::ParseFailed("Invalid early exec secret key: Decoding failed")) 449 }; 450 Some(early_exec_secret_key) 451 } 452 None => None, 453 }; 454 let early_exec_public_key = match early_exec_secret_key { 455 Some(early_exec_secret_key) => PublicKey::from_secret(early_exec_secret_key), 456 None => { 457 let Some(early_exec_public_key) = table.get("early_exec_public_key") else { 458 return Err(Error::ParseFailed("TOML does not contain early exec public key")) 459 }; 460 let Some(early_exec_public_key) = early_exec_public_key.as_str() else { 461 return Err(Error::ParseFailed("Invalid early exec public key: Not a string")) 462 }; 463 let Ok(early_exec_public_key) = PublicKey::from_str(early_exec_public_key) else { 464 return Err(Error::ParseFailed("Invalid early exec public key: Decoding failed")) 465 }; 466 early_exec_public_key 467 } 468 }; 469 470 Ok(Self::new( 471 proposer_limit, 472 quorum, 473 early_exec_quorum, 474 approval_ratio_base, 475 approval_ratio_quot, 476 gov_token_id, 477 notes_secret_key, 478 notes_public_key, 479 proposer_secret_key, 480 proposer_public_key, 481 proposals_secret_key, 482 proposals_public_key, 483 votes_secret_key, 484 votes_public_key, 485 exec_secret_key, 486 exec_public_key, 487 early_exec_secret_key, 488 early_exec_public_key, 489 bulla_blind, 490 )) 491 } 492 493 /// Generate a toml string containing the DAO configuration. 494 pub fn toml_str(&self) -> String { 495 // Header comments 496 let mut toml = String::from( 497 "## DAO configuration file\n\ 498 ##\n\ 499 ## Please make sure you go through all the settings so you can configure\n\ 500 ## your DAO properly.\n\ 501 ##\n\ 502 ## If you want to restrict access to certain actions, the corresponding\n\ 503 ## secret key can be omitted. All public keys, along with the DAO configuration\n\ 504 ## parameters must be shared.\n\ 505 ##\n\ 506 ## If you want to combine access to certain actions, you can use the same\n\ 507 ## secret and public key combination for them.\n\n", 508 ); 509 510 // Configuration parameters 511 toml += &format!( 512 "## ====== DAO configuration parameters =====\n\n\ 513 ## The minimum amount of governance tokens needed to open a proposal for this DAO\n\ 514 proposer_limit = \"{}\"\n\n\ 515 ## Minimal threshold of participating total tokens needed for a proposal to pass\n\ 516 quorum = \"{}\"\n\n\ 517 ## Minimal threshold of participating total tokens needed for a proposal to\n\ 518 ## be considered as strongly supported, enabling early execution.\n\ 519 ## Must be greater or equal to normal quorum.\n\ 520 early_exec_quorum = \"{}\"\n\n\ 521 ## The ratio of winning votes/total votes needed for a proposal to pass (2 decimals)\n\ 522 approval_ratio = {}\n\n\ 523 ## DAO's governance token ID\n\ 524 gov_token_id = \"{}\"\n\n\ 525 ## Bulla blind\n\ 526 bulla_blind = \"{}\"\n\n", 527 encode_base10(self.dao.proposer_limit, BALANCE_BASE10_DECIMALS), 528 encode_base10(self.dao.quorum, BALANCE_BASE10_DECIMALS), 529 encode_base10(self.dao.early_exec_quorum, BALANCE_BASE10_DECIMALS), 530 self.dao.approval_ratio_quot as f64 / self.dao.approval_ratio_base as f64, 531 self.dao.gov_token_id, 532 self.dao.bulla_blind, 533 ); 534 535 // DAO actions keypairs 536 toml += &format!( 537 "## ====== DAO actions keypairs =====\n\n\ 538 ## DAO notes decryption keypair\n\ 539 notes_public_key = \"{}\"\n", 540 self.dao.notes_public_key, 541 ); 542 match self.notes_secret_key { 543 Some(secret_key) => toml += &format!("notes_secret_key = \"{secret_key}\"\n\n"), 544 None => toml += "\n", 545 } 546 toml += &format!( 547 "## DAO proposals creator keypair\n\ 548 proposer_public_key = \"{}\"\n", 549 self.dao.proposer_public_key, 550 ); 551 match self.proposer_secret_key { 552 Some(secret_key) => toml += &format!("proposer_secret_key = \"{secret_key}\"\n\n"), 553 None => toml += "\n", 554 } 555 toml += &format!( 556 "## DAO proposals viewer keypair\n\ 557 proposals_public_key = \"{}\"\n", 558 self.dao.proposals_public_key, 559 ); 560 match self.proposals_secret_key { 561 Some(secret_key) => toml += &format!("proposals_secret_key = \"{secret_key}\"\n\n"), 562 None => toml += "\n", 563 } 564 toml += &format!( 565 "## DAO votes viewer keypair\n\ 566 votes_public_key = \"{}\"\n", 567 self.dao.votes_public_key, 568 ); 569 match self.votes_secret_key { 570 Some(secret_key) => toml += &format!("votes_secret_key = \"{secret_key}\"\n\n"), 571 None => toml += "\n", 572 } 573 toml += &format!( 574 "## DAO proposals executor keypair\n\ 575 exec_public_key = \"{}\"\n", 576 self.dao.exec_public_key, 577 ); 578 match self.exec_secret_key { 579 Some(secret_key) => toml += &format!("exec_secret_key = \"{secret_key}\"\n\n"), 580 None => toml += "\n", 581 } 582 toml += &format!( 583 "## DAO strongly supported proposals executor keypair\n\ 584 early_exec_public_key = \"{}\"", 585 self.dao.early_exec_public_key, 586 ); 587 if let Some(secret_key) = self.early_exec_secret_key { 588 toml += &format!("\nearly_exec_secret_key = \"{secret_key}\"") 589 } 590 591 toml 592 } 593 } 594 595 impl fmt::Display for DaoParams { 596 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 597 // Grab known secret keys 598 let notes_secret_key = match self.notes_secret_key { 599 Some(secret_key) => format!("{secret_key}"), 600 None => "None".to_string(), 601 }; 602 let proposer_secret_key = match self.proposer_secret_key { 603 Some(secret_key) => format!("{secret_key}"), 604 None => "None".to_string(), 605 }; 606 let proposals_secret_key = match self.proposals_secret_key { 607 Some(secret_key) => format!("{secret_key}"), 608 None => "None".to_string(), 609 }; 610 let votes_secret_key = match self.votes_secret_key { 611 Some(secret_key) => format!("{secret_key}"), 612 None => "None".to_string(), 613 }; 614 let exec_secret_key = match self.exec_secret_key { 615 Some(secret_key) => format!("{secret_key}"), 616 None => "None".to_string(), 617 }; 618 let early_exec_secret_key = match self.early_exec_secret_key { 619 Some(secret_key) => format!("{secret_key}"), 620 None => "None".to_string(), 621 }; 622 623 let s = format!( 624 "{}\n{}\n{}: {} ({})\n{}: {} ({})\n{}: {} ({})\n{}: {}\n{}: {}\n{}: {}\n{}: {}\n{}: {}\n{}: {}\n{}: {}\n{}: {}\n{}: {}\n{}: {}\n{}: {}\n{}: {}\n{}: {}\n{}: {}\n{}: {}", 625 "DAO Parameters", 626 "==============", 627 "Proposer limit", 628 encode_base10(self.dao.proposer_limit, BALANCE_BASE10_DECIMALS), 629 self.dao.proposer_limit, 630 "Quorum", 631 encode_base10(self.dao.quorum, BALANCE_BASE10_DECIMALS), 632 self.dao.quorum, 633 "Early Exec Quorum", 634 encode_base10(self.dao.early_exec_quorum, BALANCE_BASE10_DECIMALS), 635 self.dao.early_exec_quorum, 636 "Approval ratio", 637 self.dao.approval_ratio_quot as f64 / self.dao.approval_ratio_base as f64, 638 "Governance Token ID", 639 self.dao.gov_token_id, 640 "Notes Public key", 641 self.dao.notes_public_key, 642 "Notes Secret key", 643 notes_secret_key, 644 "Proposer Public key", 645 self.dao.proposer_public_key, 646 "Proposer Secret key", 647 proposer_secret_key, 648 "Proposals Public key", 649 self.dao.proposals_public_key, 650 "Proposals Secret key", 651 proposals_secret_key, 652 "Votes Public key", 653 self.dao.votes_public_key, 654 "Votes Secret key", 655 votes_secret_key, 656 "Exec Public key", 657 self.dao.exec_public_key, 658 "Exec Secret key", 659 exec_secret_key, 660 "Early Exec Public key", 661 self.dao.early_exec_public_key, 662 "Early Exec Secret key", 663 early_exec_secret_key, 664 "Bulla blind", 665 self.dao.bulla_blind, 666 ); 667 668 write!(f, "{s}") 669 } 670 } 671 672 #[derive(Debug, Clone)] 673 /// Structure representing a `DAO_DAOS_TABLE` record. 674 pub struct DaoRecord { 675 /// Name identifier for the DAO 676 pub name: String, 677 /// DAO parameters 678 pub params: DaoParams, 679 /// Leaf position of the DAO in the Merkle tree of DAOs 680 pub leaf_position: Option<bridgetree::Position>, 681 /// Block height of the transaction this DAO was deployed 682 pub mint_height: Option<u32>, 683 /// The transaction hash where the DAO was deployed 684 pub tx_hash: Option<TransactionHash>, 685 /// The call index in the transaction where the DAO was deployed 686 pub call_index: Option<u8>, 687 } 688 689 impl DaoRecord { 690 pub fn new( 691 name: String, 692 params: DaoParams, 693 leaf_position: Option<bridgetree::Position>, 694 mint_height: Option<u32>, 695 tx_hash: Option<TransactionHash>, 696 call_index: Option<u8>, 697 ) -> Self { 698 Self { name, params, leaf_position, mint_height, tx_hash, call_index } 699 } 700 701 pub fn bulla(&self) -> DaoBulla { 702 self.params.dao.to_bulla() 703 } 704 } 705 706 impl fmt::Display for DaoRecord { 707 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 708 // Grab known secret keys 709 let notes_secret_key = match self.params.notes_secret_key { 710 Some(secret_key) => format!("{secret_key}"), 711 None => "None".to_string(), 712 }; 713 let proposer_secret_key = match self.params.proposer_secret_key { 714 Some(secret_key) => format!("{secret_key}"), 715 None => "None".to_string(), 716 }; 717 let proposals_secret_key = match self.params.proposals_secret_key { 718 Some(secret_key) => format!("{secret_key}"), 719 None => "None".to_string(), 720 }; 721 let votes_secret_key = match self.params.votes_secret_key { 722 Some(secret_key) => format!("{secret_key}"), 723 None => "None".to_string(), 724 }; 725 let exec_secret_key = match self.params.exec_secret_key { 726 Some(secret_key) => format!("{secret_key}"), 727 None => "None".to_string(), 728 }; 729 let early_exec_secret_key = match self.params.early_exec_secret_key { 730 Some(secret_key) => format!("{secret_key}"), 731 None => "None".to_string(), 732 }; 733 734 // Grab mint information 735 let leaf_position = match self.leaf_position { 736 Some(p) => format!("{p:?}"), 737 None => "None".to_string(), 738 }; 739 let mint_height = match self.mint_height { 740 Some(h) => format!("{h}"), 741 None => "None".to_string(), 742 }; 743 let tx_hash = match self.tx_hash { 744 Some(t) => format!("{t}"), 745 None => "None".to_string(), 746 }; 747 let call_index = match self.call_index { 748 Some(c) => format!("{c}"), 749 None => "None".to_string(), 750 }; 751 752 let s = format!( 753 "{}\n{}\n{}: {}\n{}: {}\n{}: {} ({})\n{}: {} ({})\n{}: {} ({})\n{}: {}\n{}: {}\n{}: {}\n{}: {}\n{}: {}\n{}: {}\n{}: {}\n{}: {}\n{}: {}\n{}: {}\n{}: {}\n{}: {}\n{}: {}\n{}: {}\n{}: {}\n{}: {}\n{}: {}\n{}: {}\n{}: {}", 754 "DAO Parameters", 755 "==============", 756 "Name", 757 self.name, 758 "Bulla", 759 self.bulla(), 760 "Proposer limit", 761 encode_base10(self.params.dao.proposer_limit, BALANCE_BASE10_DECIMALS), 762 self.params.dao.proposer_limit, 763 "Quorum", 764 encode_base10(self.params.dao.quorum, BALANCE_BASE10_DECIMALS), 765 self.params.dao.quorum, 766 "Early Exec Quorum", 767 encode_base10(self.params.dao.early_exec_quorum, BALANCE_BASE10_DECIMALS), 768 self.params.dao.early_exec_quorum, 769 "Approval ratio", 770 self.params.dao.approval_ratio_quot as f64 / self.params.dao.approval_ratio_base as f64, 771 "Governance Token ID", 772 self.params.dao.gov_token_id, 773 "Notes Public key", 774 self.params.dao.notes_public_key, 775 "Notes Secret key", 776 notes_secret_key, 777 "Proposer Public key", 778 self.params.dao.proposer_public_key, 779 "Proposer Secret key", 780 proposer_secret_key, 781 "Proposals Public key", 782 self.params.dao.proposals_public_key, 783 "Proposals Secret key", 784 proposals_secret_key, 785 "Votes Public key", 786 self.params.dao.votes_public_key, 787 "Votes Secret key", 788 votes_secret_key, 789 "Exec Public key", 790 self.params.dao.exec_public_key, 791 "Exec Secret key", 792 exec_secret_key, 793 "Early Exec Public key", 794 self.params.dao.early_exec_public_key, 795 "Early Exec Secret key", 796 early_exec_secret_key, 797 "Bulla blind", 798 self.params.dao.bulla_blind, 799 "Leaf position", 800 leaf_position, 801 "Mint height", 802 mint_height, 803 "Transaction hash", 804 tx_hash, 805 "Call index", 806 call_index, 807 ); 808 809 write!(f, "{s}") 810 } 811 } 812 813 #[derive(Debug, Clone, SerialEncodable, SerialDecodable)] 814 /// Structure representing a `DAO_PROPOSALS_TABLE` record. 815 pub struct ProposalRecord { 816 /// The on chain representation of the proposal 817 pub proposal: DaoProposal, 818 /// Plaintext proposal call data the members share between them 819 pub data: Option<Vec<u8>>, 820 /// Leaf position of the proposal in the Merkle tree of proposals 821 pub leaf_position: Option<bridgetree::Position>, 822 /// Money merkle tree snapshot for reproducing the snapshot Merkle root 823 pub money_snapshot_tree: Option<MerkleTree>, 824 /// Money nullifiers SMT snapshot for reproducing the snapshot Merkle root 825 pub nullifiers_smt_snapshot: Option<HashMap<BigUint, pallas::Base>>, 826 /// Block height of the transaction this proposal was deployed 827 pub mint_height: Option<u32>, 828 /// The transaction hash where the proposal was deployed 829 pub tx_hash: Option<TransactionHash>, 830 /// The call index in the transaction where the proposal was deployed 831 pub call_index: Option<u8>, 832 /// Block height of the transaction this proposal was executed 833 pub exec_height: Option<u32>, 834 /// The transaction hash where the proposal was executed 835 pub exec_tx_hash: Option<TransactionHash>, 836 } 837 838 impl ProposalRecord { 839 pub fn bulla(&self) -> DaoProposalBulla { 840 self.proposal.to_bulla() 841 } 842 } 843 844 impl fmt::Display for ProposalRecord { 845 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 846 let leaf_position = match self.leaf_position { 847 Some(p) => format!("{p:?}"), 848 None => "None".to_string(), 849 }; 850 let mint_height = match self.mint_height { 851 Some(h) => format!("{h}"), 852 None => "None".to_string(), 853 }; 854 let tx_hash = match self.tx_hash { 855 Some(t) => format!("{t}"), 856 None => "None".to_string(), 857 }; 858 let call_index = match self.call_index { 859 Some(c) => format!("{c}"), 860 None => "None".to_string(), 861 }; 862 863 let s = format!( 864 "{}\n{}\n{}: {}\n{}: {}\n{}: {}\n{}: {}\n{}: {}\n{}: {}\n{}: {}\n{}: {} ({})", 865 "Proposal parameters", 866 "===================", 867 "Bulla", 868 self.bulla(), 869 "DAO Bulla", 870 self.proposal.dao_bulla, 871 "Proposal leaf position", 872 leaf_position, 873 "Proposal mint height", 874 mint_height, 875 "Proposal transaction hash", 876 tx_hash, 877 "Proposal call index", 878 call_index, 879 "Creation block window", 880 self.proposal.creation_blockwindow, 881 "Duration", 882 self.proposal.duration_blockwindows, 883 "Block windows" 884 ); 885 886 write!(f, "{s}") 887 } 888 } 889 890 #[derive(Debug, Clone)] 891 /// Structure representing a `DAO_VOTES_TABLE` record. 892 pub struct VoteRecord { 893 /// Numeric identifier for the vote 894 pub id: u64, 895 /// Bulla identifier of the proposal this vote is for 896 pub proposal: DaoProposalBulla, 897 /// The vote 898 pub vote_option: bool, 899 /// Blinding factor for the yes vote 900 pub yes_vote_blind: ScalarBlind, 901 /// Value of all votes 902 pub all_vote_value: u64, 903 /// Blinding facfor of all votes 904 pub all_vote_blind: ScalarBlind, 905 /// Block height of the transaction this vote was casted 906 pub block_height: u32, 907 /// Transaction hash where this vote was casted 908 pub tx_hash: TransactionHash, 909 /// Call index in the transaction where this vote was casted 910 pub call_index: u8, 911 /// Vote input nullifiers 912 pub nullifiers: Vec<Nullifier>, 913 } 914 915 impl Drk { 916 /// Initialize wallet with tables for the DAO contract. 917 pub async fn initialize_dao(&self) -> WalletDbResult<()> { 918 // Initialize DAO wallet schema 919 let wallet_schema = include_str!("../dao.sql"); 920 self.wallet.exec_batch_sql(wallet_schema)?; 921 922 Ok(()) 923 } 924 925 /// Fetch DAO Merkle trees from the wallet. 926 /// If a tree doesn't exists a new Merkle Tree is returned. 927 pub async fn get_dao_trees(&self) -> Result<(MerkleTree, MerkleTree)> { 928 let daos_tree = match self.cache.merkle_trees.get(SLED_MERKLE_TREES_DAO_DAOS)? { 929 Some(tree_bytes) => deserialize_async(&tree_bytes).await?, 930 None => MerkleTree::new(u32::MAX as usize), 931 }; 932 let proposals_tree = match self.cache.merkle_trees.get(SLED_MERKLE_TREES_DAO_PROPOSALS)? { 933 Some(tree_bytes) => deserialize_async(&tree_bytes).await?, 934 None => MerkleTree::new(u32::MAX as usize), 935 }; 936 Ok((daos_tree, proposals_tree)) 937 } 938 939 /// Auxiliary function to parse a `DAO_DAOS_TABLE` record. 940 async fn parse_dao_record(&self, row: &[Value]) -> Result<DaoRecord> { 941 let Value::Text(ref name) = row[1] else { 942 return Err(Error::ParseFailed("[parse_dao_record] Name parsing failed")) 943 }; 944 let name = name.clone(); 945 946 let Value::Blob(ref params_bytes) = row[2] else { 947 return Err(Error::ParseFailed("[parse_dao_record] Params bytes parsing failed")) 948 }; 949 let params = deserialize_async(params_bytes).await?; 950 951 let leaf_position = match row[3] { 952 Value::Blob(ref leaf_position_bytes) => { 953 Some(deserialize_async(leaf_position_bytes).await?) 954 } 955 Value::Null => None, 956 _ => { 957 return Err(Error::ParseFailed( 958 "[parse_dao_record] Leaf position bytes parsing failed", 959 )) 960 } 961 }; 962 963 let mint_height = match row[4] { 964 Value::Integer(mint_height) => { 965 let Ok(mint_height) = u32::try_from(mint_height) else { 966 return Err(Error::ParseFailed("[parse_dao_record] Mint height parsing failed")) 967 }; 968 Some(mint_height) 969 } 970 Value::Null => None, 971 _ => return Err(Error::ParseFailed("[parse_dao_record] Mint height parsing failed")), 972 }; 973 974 let tx_hash = match row[5] { 975 Value::Blob(ref tx_hash_bytes) => Some(deserialize_async(tx_hash_bytes).await?), 976 Value::Null => None, 977 _ => { 978 return Err(Error::ParseFailed( 979 "[parse_dao_record] Transaction hash bytes parsing failed", 980 )) 981 } 982 }; 983 984 let call_index = match row[6] { 985 Value::Integer(call_index) => { 986 let Ok(call_index) = u8::try_from(call_index) else { 987 return Err(Error::ParseFailed("[parse_dao_record] Call index parsing failed")) 988 }; 989 Some(call_index) 990 } 991 Value::Null => None, 992 _ => return Err(Error::ParseFailed("[parse_dao_record] Call index parsing failed")), 993 }; 994 995 let dao = DaoRecord::new(name, params, leaf_position, mint_height, tx_hash, call_index); 996 997 Ok(dao) 998 } 999 1000 /// Fetch all known DAOs from the wallet. 1001 pub async fn get_daos(&self) -> Result<Vec<DaoRecord>> { 1002 let rows = match self.wallet.query_multiple(&DAO_DAOS_TABLE, &[], &[]) { 1003 Ok(r) => r, 1004 Err(e) => { 1005 return Err(Error::DatabaseError(format!("[get_daos] DAOs retrieval failed: {e}"))) 1006 } 1007 }; 1008 1009 let mut daos = Vec::with_capacity(rows.len()); 1010 for row in rows { 1011 daos.push(self.parse_dao_record(&row).await?); 1012 } 1013 1014 Ok(daos) 1015 } 1016 1017 /// Auxiliary function to parse a proposal record row. 1018 async fn parse_dao_proposal(&self, row: &[Value]) -> Result<ProposalRecord> { 1019 let Value::Blob(ref proposal_bytes) = row[2] else { 1020 return Err(Error::ParseFailed( 1021 "[parse_dao_proposal] Proposal bytes bytes parsing failed", 1022 )) 1023 }; 1024 let proposal = deserialize_async(proposal_bytes).await?; 1025 1026 let data = match row[3] { 1027 Value::Blob(ref data_bytes) => Some(data_bytes.clone()), 1028 Value::Null => None, 1029 _ => return Err(Error::ParseFailed("[parse_dao_proposal] Data bytes parsing failed")), 1030 }; 1031 1032 let leaf_position = match row[4] { 1033 Value::Blob(ref leaf_position_bytes) => { 1034 Some(deserialize_async(leaf_position_bytes).await?) 1035 } 1036 Value::Null => None, 1037 _ => { 1038 return Err(Error::ParseFailed( 1039 "[parse_dao_proposal] Leaf position bytes parsing failed", 1040 )) 1041 } 1042 }; 1043 1044 let money_snapshot_tree = match row[5] { 1045 Value::Blob(ref money_snapshot_tree_bytes) => { 1046 Some(deserialize_async(money_snapshot_tree_bytes).await?) 1047 } 1048 Value::Null => None, 1049 _ => { 1050 return Err(Error::ParseFailed( 1051 "[parse_dao_proposal] Money snapshot tree bytes parsing failed", 1052 )) 1053 } 1054 }; 1055 1056 let nullifiers_smt_snapshot = match row[6] { 1057 Value::Blob(ref nullifiers_smt_snapshot_bytes) => { 1058 Some(deserialize_async(nullifiers_smt_snapshot_bytes).await?) 1059 } 1060 Value::Null => None, 1061 _ => { 1062 return Err(Error::ParseFailed( 1063 "[parse_dao_proposal] Nullifiers SMT snapshot bytes parsing failed", 1064 )) 1065 } 1066 }; 1067 1068 let mint_height = match row[7] { 1069 Value::Integer(mint_height) => { 1070 let Ok(mint_height) = u32::try_from(mint_height) else { 1071 return Err(Error::ParseFailed( 1072 "[parse_dao_proposal] Mint height parsing failed", 1073 )) 1074 }; 1075 Some(mint_height) 1076 } 1077 Value::Null => None, 1078 _ => return Err(Error::ParseFailed("[parse_dao_proposal] Mint height parsing failed")), 1079 }; 1080 1081 let tx_hash = match row[8] { 1082 Value::Blob(ref tx_hash_bytes) => Some(deserialize_async(tx_hash_bytes).await?), 1083 Value::Null => None, 1084 _ => { 1085 return Err(Error::ParseFailed( 1086 "[parse_dao_proposal] Transaction hash bytes parsing failed", 1087 )) 1088 } 1089 }; 1090 1091 let call_index = match row[9] { 1092 Value::Integer(call_index) => { 1093 let Ok(call_index) = u8::try_from(call_index) else { 1094 return Err(Error::ParseFailed("[parse_dao_proposal] Call index parsing failed")) 1095 }; 1096 Some(call_index) 1097 } 1098 Value::Null => None, 1099 _ => return Err(Error::ParseFailed("[parse_dao_proposal] Call index parsing failed")), 1100 }; 1101 1102 let exec_height = match row[10] { 1103 Value::Integer(exec_height) => { 1104 let Ok(exec_height) = u32::try_from(exec_height) else { 1105 return Err(Error::ParseFailed( 1106 "[parse_dao_proposal] Execution height parsing failed", 1107 )) 1108 }; 1109 Some(exec_height) 1110 } 1111 Value::Null => None, 1112 _ => { 1113 return Err(Error::ParseFailed( 1114 "[parse_dao_proposal] Execution height parsing failed", 1115 )) 1116 } 1117 }; 1118 1119 let exec_tx_hash = match row[11] { 1120 Value::Blob(ref exec_tx_hash_bytes) => { 1121 Some(deserialize_async(exec_tx_hash_bytes).await?) 1122 } 1123 Value::Null => None, 1124 _ => { 1125 return Err(Error::ParseFailed( 1126 "[parse_dao_proposal] Execution transaction hash bytes parsing failed", 1127 )) 1128 } 1129 }; 1130 1131 Ok(ProposalRecord { 1132 proposal, 1133 data, 1134 leaf_position, 1135 money_snapshot_tree, 1136 nullifiers_smt_snapshot, 1137 mint_height, 1138 tx_hash, 1139 call_index, 1140 exec_height, 1141 exec_tx_hash, 1142 }) 1143 } 1144 1145 /// Fetch all known DAO proposals from the wallet given a DAO name. 1146 pub async fn get_dao_proposals(&self, name: &str) -> Result<Vec<ProposalRecord>> { 1147 let Ok(dao) = self.get_dao_by_name(name).await else { 1148 return Err(Error::DatabaseError(format!( 1149 "[get_dao_proposals] DAO with name {name} not found in wallet" 1150 ))) 1151 }; 1152 1153 let rows = match self.wallet.query_multiple( 1154 &DAO_PROPOSALS_TABLE, 1155 &[], 1156 convert_named_params! {(DAO_PROPOSALS_COL_DAO_BULLA, serialize_async(&dao.bulla()).await)}, 1157 ) { 1158 Ok(r) => r, 1159 Err(e) => { 1160 return Err(Error::DatabaseError(format!( 1161 "[get_dao_proposals] Proposals retrieval failed: {e}" 1162 ))) 1163 } 1164 }; 1165 1166 let mut proposals = Vec::with_capacity(rows.len()); 1167 for row in rows { 1168 let proposal = self.parse_dao_proposal(&row).await?; 1169 proposals.push(proposal); 1170 } 1171 1172 Ok(proposals) 1173 } 1174 1175 /// Auxiliary function to apply `DaoFunction::Mint` call data to 1176 /// the wallet and update the provided scan cache. 1177 /// Returns a flag indicating if the provided call refers to our 1178 /// own wallet. 1179 async fn apply_dao_mint_data( 1180 &self, 1181 scan_cache: &mut ScanCache, 1182 new_bulla: &DaoBulla, 1183 tx_hash: &TransactionHash, 1184 call_index: &u8, 1185 mint_height: &u32, 1186 ) -> Result<bool> { 1187 // Append the new dao bulla to the Merkle tree. 1188 // Every dao bulla has to be added. 1189 scan_cache.dao_daos_tree.append(MerkleNode::from(new_bulla.inner())); 1190 1191 // Check if we have the DAO 1192 if !scan_cache.own_daos.contains_key(new_bulla) { 1193 return Ok(false) 1194 } 1195 1196 // Confirm it 1197 scan_cache.log(format!( 1198 "[apply_dao_mint_data] Found minted DAO {new_bulla}, noting down for wallet update" 1199 )); 1200 if let Err(e) = self 1201 .confirm_dao( 1202 new_bulla, 1203 &scan_cache.dao_daos_tree.mark().unwrap(), 1204 tx_hash, 1205 call_index, 1206 mint_height, 1207 ) 1208 .await 1209 { 1210 return Err(Error::DatabaseError(format!( 1211 "[apply_dao_mint_data] Confirm DAO failed: {e}" 1212 ))) 1213 } 1214 1215 Ok(true) 1216 } 1217 1218 /// Auxiliary function to apply `DaoFunction::Propose` call data to 1219 /// the wallet and update the provided scan cache. 1220 /// Returns a flag indicating if the provided call refers to our 1221 /// own wallet. 1222 async fn apply_dao_propose_data( 1223 &self, 1224 scan_cache: &mut ScanCache, 1225 params: &DaoProposeParams, 1226 tx_hash: &TransactionHash, 1227 call_index: &u8, 1228 mint_height: &u32, 1229 ) -> Result<bool> { 1230 // Append the new proposal bulla to the Merkle tree. 1231 // Every proposal bulla has to be added. 1232 scan_cache.dao_proposals_tree.append(MerkleNode::from(params.proposal_bulla.inner())); 1233 1234 // If we're able to decrypt this note, that's the way to link it 1235 // to a specific DAO. 1236 for (dao, (proposals_secret_key, _)) in &scan_cache.own_daos { 1237 // Check if we have the proposals key 1238 let Some(proposals_secret_key) = proposals_secret_key else { continue }; 1239 1240 // Try to decrypt the proposal note 1241 let Ok(note) = params.note.decrypt::<DaoProposal>(proposals_secret_key) else { 1242 continue 1243 }; 1244 1245 // We managed to decrypt it. Let's place this in a proper ProposalRecord object 1246 scan_cache.messages_buffer.push(format!( 1247 "[apply_dao_propose_data] Managed to decrypt proposal note for DAO: {dao}" 1248 )); 1249 1250 // Check if we already got the record 1251 let our_proposal = if scan_cache.own_proposals.contains_key(¶ms.proposal_bulla) { 1252 // Grab the record from the db 1253 let mut our_proposal = 1254 self.get_dao_proposal_by_bulla(¶ms.proposal_bulla).await?; 1255 our_proposal.leaf_position = scan_cache.dao_proposals_tree.mark(); 1256 our_proposal.money_snapshot_tree = Some(scan_cache.money_tree.clone()); 1257 our_proposal.nullifiers_smt_snapshot = Some(scan_cache.money_smt.store.snapshot()?); 1258 our_proposal.mint_height = Some(*mint_height); 1259 our_proposal.tx_hash = Some(*tx_hash); 1260 our_proposal.call_index = Some(*call_index); 1261 our_proposal 1262 } else { 1263 let our_proposal = ProposalRecord { 1264 proposal: note, 1265 data: None, 1266 leaf_position: scan_cache.dao_proposals_tree.mark(), 1267 money_snapshot_tree: Some(scan_cache.money_tree.clone()), 1268 nullifiers_smt_snapshot: Some(scan_cache.money_smt.store.snapshot()?), 1269 mint_height: Some(*mint_height), 1270 tx_hash: Some(*tx_hash), 1271 call_index: Some(*call_index), 1272 exec_height: None, 1273 exec_tx_hash: None, 1274 }; 1275 scan_cache.own_proposals.insert(params.proposal_bulla, *dao); 1276 our_proposal 1277 }; 1278 1279 // Update/store our record 1280 if let Err(e) = self.put_dao_proposal(&our_proposal).await { 1281 return Err(Error::DatabaseError(format!( 1282 "[apply_dao_propose_data] Put DAO proposals failed: {e}" 1283 ))) 1284 } 1285 1286 return Ok(true) 1287 } 1288 1289 Ok(false) 1290 } 1291 1292 /// Auxiliary function to apply `DaoFunction::Vote` call data to 1293 /// the wallet. 1294 /// Returns a flag indicating if the provided call refers to our 1295 /// own wallet. 1296 async fn apply_dao_vote_data( 1297 &self, 1298 scan_cache: &ScanCache, 1299 params: &DaoVoteParams, 1300 tx_hash: &TransactionHash, 1301 call_index: &u8, 1302 block_height: &u32, 1303 ) -> Result<bool> { 1304 // Check if we got the corresponding proposal 1305 let Some(dao_bulla) = scan_cache.own_proposals.get(¶ms.proposal_bulla) else { 1306 return Ok(false) 1307 }; 1308 1309 // Grab the proposal DAO votes key 1310 let Some((_, votes_secret_key)) = scan_cache.own_daos.get(dao_bulla) else { 1311 return Err(Error::DatabaseError(format!( 1312 "[apply_dao_vote_data] Couldn't find proposal {} DAO {}", 1313 params.proposal_bulla, dao_bulla, 1314 ))) 1315 }; 1316 1317 // Check if we actually have the votes key 1318 let Some(votes_secret_key) = votes_secret_key else { return Ok(false) }; 1319 1320 // Decrypt the vote note 1321 let note = match params.note.decrypt_unsafe(votes_secret_key) { 1322 Ok(n) => n, 1323 Err(e) => { 1324 return Err(Error::DatabaseError(format!( 1325 "[apply_dao_vote_data] Couldn't decrypt proposal {} vote with DAO {} keys: {e}", 1326 params.proposal_bulla, dao_bulla, 1327 ))) 1328 } 1329 }; 1330 1331 // Create the DAO vote record 1332 let vote_option = fp_to_u64(note[0]).unwrap(); 1333 if vote_option > 1 { 1334 return Err(Error::DatabaseError(format!( 1335 "[apply_dao_vote_data] Malformed vote for proposal {}: {vote_option}", 1336 params.proposal_bulla, 1337 ))) 1338 } 1339 let vote_option = vote_option != 0; 1340 let yes_vote_blind = Blind(fp_mod_fv(note[1])); 1341 let all_vote_value = fp_to_u64(note[2]).unwrap(); 1342 let all_vote_blind = Blind(fp_mod_fv(note[3])); 1343 1344 let v = VoteRecord { 1345 id: 0, // This will be set by SQLite AUTOINCREMENT 1346 proposal: params.proposal_bulla, 1347 vote_option, 1348 yes_vote_blind, 1349 all_vote_value, 1350 all_vote_blind, 1351 block_height: *block_height, 1352 tx_hash: *tx_hash, 1353 call_index: *call_index, 1354 nullifiers: params.inputs.iter().map(|i| i.vote_nullifier).collect(), 1355 }; 1356 1357 if let Err(e) = self.put_dao_vote(&v).await { 1358 return Err(Error::DatabaseError(format!( 1359 "[apply_dao_vote_data] Put DAO votes failed: {e}" 1360 ))) 1361 } 1362 1363 Ok(true) 1364 } 1365 1366 /// Auxiliary function to apply `DaoFunction::Exec` call data to 1367 /// the wallet and update the provided scan cache. 1368 /// Returns a flag indicating if the provided call refers to our 1369 /// own wallet. 1370 async fn apply_dao_exec_data( 1371 &self, 1372 scan_cache: &ScanCache, 1373 params: &DaoExecParams, 1374 tx_hash: &TransactionHash, 1375 exec_height: &u32, 1376 ) -> Result<bool> { 1377 // Check if we got the corresponding proposal 1378 if !scan_cache.own_proposals.contains_key(¶ms.proposal_bulla) { 1379 return Ok(false) 1380 } 1381 1382 // Grab proposal record key 1383 let key = serialize_async(¶ms.proposal_bulla).await; 1384 1385 // Create an SQL `UPDATE` query to update proposal exec transaction hash 1386 let query = format!( 1387 "UPDATE {} SET {} = ?1, {} = ?2 WHERE {} = ?3;", 1388 *DAO_PROPOSALS_TABLE, 1389 DAO_PROPOSALS_COL_EXEC_HEIGHT, 1390 DAO_PROPOSALS_COL_EXEC_TX_HASH, 1391 DAO_PROPOSALS_COL_BULLA, 1392 ); 1393 1394 // Execute the query 1395 if let Err(e) = self 1396 .wallet 1397 .exec_sql(&query, rusqlite::params![Some(*exec_height), Some(serialize(tx_hash)), key]) 1398 { 1399 return Err(Error::DatabaseError(format!( 1400 "[apply_dao_exec_data] Update DAO proposal failed: {e}" 1401 ))) 1402 } 1403 1404 Ok(true) 1405 } 1406 1407 /// Append data related to DAO contract transactions into the 1408 /// wallet database and update the provided scan cache. 1409 /// Returns a flag indicating if provided data refer to our own 1410 /// wallet. 1411 pub async fn apply_tx_dao_data( 1412 &self, 1413 scan_cache: &mut ScanCache, 1414 data: &[u8], 1415 tx_hash: &TransactionHash, 1416 call_idx: &u8, 1417 block_height: &u32, 1418 ) -> Result<bool> { 1419 // Run through the transaction call data and see what we got: 1420 match DaoFunction::try_from(data[0])? { 1421 DaoFunction::Mint => { 1422 scan_cache.log(String::from("[apply_tx_dao_data] Found Dao::Mint call")); 1423 let params: DaoMintParams = deserialize_async(&data[1..]).await?; 1424 self.apply_dao_mint_data( 1425 scan_cache, 1426 ¶ms.dao_bulla, 1427 tx_hash, 1428 call_idx, 1429 block_height, 1430 ) 1431 .await 1432 } 1433 DaoFunction::Propose => { 1434 scan_cache.log(String::from("[apply_tx_dao_data] Found Dao::Propose call")); 1435 let params: DaoProposeParams = deserialize_async(&data[1..]).await?; 1436 self.apply_dao_propose_data(scan_cache, ¶ms, tx_hash, call_idx, block_height) 1437 .await 1438 } 1439 DaoFunction::Vote => { 1440 scan_cache.log(String::from("[apply_tx_dao_data] Found Dao::Vote call")); 1441 let params: DaoVoteParams = deserialize_async(&data[1..]).await?; 1442 self.apply_dao_vote_data(scan_cache, ¶ms, tx_hash, call_idx, block_height).await 1443 } 1444 DaoFunction::Exec => { 1445 scan_cache.log(String::from("[apply_tx_dao_data] Found Dao::Exec call")); 1446 let params: DaoExecParams = deserialize_async(&data[1..]).await?; 1447 self.apply_dao_exec_data(scan_cache, ¶ms, tx_hash, block_height).await 1448 } 1449 DaoFunction::AuthMoneyTransfer => { 1450 scan_cache 1451 .log(String::from("[apply_tx_dao_data] Found Dao::AuthMoneyTransfer call")); 1452 // Does nothing, just verifies the other calls are correct 1453 Ok(false) 1454 } 1455 } 1456 } 1457 1458 /// Confirm already imported DAO metadata into the wallet. 1459 /// Here we just write the leaf position, mint height, tx hash, 1460 /// and call index. 1461 pub async fn confirm_dao( 1462 &self, 1463 dao: &DaoBulla, 1464 leaf_position: &bridgetree::Position, 1465 tx_hash: &TransactionHash, 1466 call_index: &u8, 1467 mint_height: &u32, 1468 ) -> WalletDbResult<()> { 1469 // Grab dao record key 1470 let key = serialize_async(dao).await; 1471 1472 // Create an SQL `UPDATE` query 1473 let query = format!( 1474 "UPDATE {} SET {} = ?1, {} = ?2, {} = ?3, {} = ?4 WHERE {} = ?5;", 1475 *DAO_DAOS_TABLE, 1476 DAO_DAOS_COL_LEAF_POSITION, 1477 DAO_DAOS_COL_MINT_HEIGHT, 1478 DAO_DAOS_COL_TX_HASH, 1479 DAO_DAOS_COL_CALL_INDEX, 1480 DAO_DAOS_COL_BULLA 1481 ); 1482 1483 // Create its params 1484 let params = rusqlite::params![ 1485 serialize(leaf_position), 1486 Some(*mint_height), 1487 serialize(tx_hash), 1488 call_index, 1489 key, 1490 ]; 1491 1492 // Execute the query 1493 self.wallet.exec_sql(&query, params) 1494 } 1495 1496 /// Import given DAO proposal into the wallet. 1497 pub async fn put_dao_proposal(&self, proposal: &ProposalRecord) -> Result<()> { 1498 // Check that we already have the proposal DAO 1499 if let Err(e) = self.get_dao_by_bulla(&proposal.proposal.dao_bulla).await { 1500 return Err(Error::DatabaseError(format!( 1501 "[put_dao_proposal] Couldn't find proposal {} DAO {}: {e}", 1502 proposal.bulla(), 1503 proposal.proposal.dao_bulla 1504 ))) 1505 } 1506 1507 // Grab proposal record key 1508 let key = serialize_async(&proposal.bulla()).await; 1509 1510 // Create an SQL `INSERT OR REPLACE` query 1511 let query = format!( 1512 "INSERT OR REPLACE INTO {} ({}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12);", 1513 *DAO_PROPOSALS_TABLE, 1514 DAO_PROPOSALS_COL_BULLA, 1515 DAO_PROPOSALS_COL_DAO_BULLA, 1516 DAO_PROPOSALS_COL_PROPOSAL, 1517 DAO_PROPOSALS_COL_DATA, 1518 DAO_PROPOSALS_COL_LEAF_POSITION, 1519 DAO_PROPOSALS_COL_MONEY_SNAPSHOT_TREE, 1520 DAO_PROPOSALS_COL_NULLIFIERS_SMT_SNAPSHOT, 1521 DAO_PROPOSALS_COL_MINT_HEIGHT, 1522 DAO_PROPOSALS_COL_TX_HASH, 1523 DAO_PROPOSALS_COL_CALL_INDEX, 1524 DAO_PROPOSALS_COL_EXEC_HEIGHT, 1525 DAO_PROPOSALS_COL_EXEC_TX_HASH, 1526 ); 1527 1528 // Create its params 1529 let data = match &proposal.data { 1530 Some(data) => Some(data), 1531 None => None, 1532 }; 1533 1534 let leaf_position = match &proposal.leaf_position { 1535 Some(leaf_position) => Some(serialize_async(leaf_position).await), 1536 None => None, 1537 }; 1538 1539 let money_snapshot_tree = match &proposal.money_snapshot_tree { 1540 Some(money_snapshot_tree) => Some(serialize_async(money_snapshot_tree).await), 1541 None => None, 1542 }; 1543 1544 let nullifiers_smt_snapshot = match &proposal.nullifiers_smt_snapshot { 1545 Some(nullifiers_smt_snapshot) => Some(serialize_async(nullifiers_smt_snapshot).await), 1546 None => None, 1547 }; 1548 1549 let tx_hash = match &proposal.tx_hash { 1550 Some(tx_hash) => Some(serialize_async(tx_hash).await), 1551 None => None, 1552 }; 1553 1554 let exec_tx_hash = match &proposal.exec_tx_hash { 1555 Some(exec_tx_hash) => Some(serialize_async(exec_tx_hash).await), 1556 None => None, 1557 }; 1558 1559 let params = rusqlite::params![ 1560 key, 1561 serialize(&proposal.proposal.dao_bulla), 1562 serialize(&proposal.proposal), 1563 data, 1564 leaf_position, 1565 money_snapshot_tree, 1566 nullifiers_smt_snapshot, 1567 proposal.mint_height, 1568 tx_hash, 1569 proposal.call_index, 1570 proposal.exec_height, 1571 exec_tx_hash, 1572 ]; 1573 1574 // Execute the query 1575 if let Err(e) = self.wallet.exec_sql(&query, params) { 1576 return Err(Error::DatabaseError(format!( 1577 "[put_dao_proposal] Proposal insert failed: {e}" 1578 ))) 1579 } 1580 1581 Ok(()) 1582 } 1583 1584 /// Import given DAO vote into the wallet. 1585 pub async fn put_dao_vote(&self, vote: &VoteRecord) -> WalletDbResult<()> { 1586 println!("Importing DAO vote into wallet"); 1587 1588 // Create an SQL `INSERT OR REPLACE` query 1589 let query = format!( 1590 "INSERT INTO {} ({}, {}, {}, {}, {}, {}, {}, {}, {}) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9);", 1591 *DAO_VOTES_TABLE, 1592 DAO_VOTES_COL_PROPOSAL_BULLA, 1593 DAO_VOTES_COL_VOTE_OPTION, 1594 DAO_VOTES_COL_YES_VOTE_BLIND, 1595 DAO_VOTES_COL_ALL_VOTE_VALUE, 1596 DAO_VOTES_COL_ALL_VOTE_BLIND, 1597 DAO_VOTES_COL_BLOCK_HEIGHT, 1598 DAO_VOTES_COL_TX_HASH, 1599 DAO_VOTES_COL_CALL_INDEX, 1600 DAO_VOTES_COL_NULLIFIERS, 1601 ); 1602 1603 // Create its params 1604 let params = rusqlite::params![ 1605 serialize(&vote.proposal), 1606 vote.vote_option as u64, 1607 serialize(&vote.yes_vote_blind), 1608 serialize(&vote.all_vote_value), 1609 serialize(&vote.all_vote_blind), 1610 vote.block_height, 1611 serialize(&vote.tx_hash), 1612 vote.call_index, 1613 serialize(&vote.nullifiers), 1614 ]; 1615 1616 // Execute the query 1617 self.wallet.exec_sql(&query, params)?; 1618 1619 println!("DAO vote added to wallet"); 1620 1621 Ok(()) 1622 } 1623 1624 /// Reset the DAO Merkle trees in the cache. 1625 pub fn reset_dao_trees(&self, output: &mut Vec<String>) -> WalletDbResult<()> { 1626 output.push(String::from("Resetting DAO Merkle trees")); 1627 if let Err(e) = self.cache.merkle_trees.remove(SLED_MERKLE_TREES_DAO_DAOS) { 1628 output.push(format!("[reset_dao_trees] Resetting DAO DAOs Merkle tree failed: {e}")); 1629 return Err(WalletDbError::GenericError) 1630 } 1631 if let Err(e) = self.cache.merkle_trees.remove(SLED_MERKLE_TREES_DAO_PROPOSALS) { 1632 output 1633 .push(format!("[reset_dao_trees] Resetting DAO Proposals Merkle tree failed: {e}")); 1634 return Err(WalletDbError::GenericError) 1635 } 1636 output.push(String::from("Successfully reset DAO Merkle trees")); 1637 1638 Ok(()) 1639 } 1640 1641 /// Reset confirmed DAOs in the wallet. 1642 pub fn reset_daos(&self, output: &mut Vec<String>) -> WalletDbResult<()> { 1643 output.push(String::from("Resetting DAO confirmations")); 1644 let query = format!( 1645 "UPDATE {} SET {} = NULL, {} = NULL, {} = NULL, {} = NULL;", 1646 *DAO_DAOS_TABLE, 1647 DAO_DAOS_COL_LEAF_POSITION, 1648 DAO_DAOS_COL_MINT_HEIGHT, 1649 DAO_DAOS_COL_TX_HASH, 1650 DAO_DAOS_COL_CALL_INDEX, 1651 ); 1652 self.wallet.exec_sql(&query, &[])?; 1653 output.push(String::from("Successfully unconfirmed DAOs")); 1654 1655 Ok(()) 1656 } 1657 1658 /// Reset confirmed DAOs in the wallet that were minted after 1659 /// provided height. 1660 pub fn unconfirm_daos_after( 1661 &self, 1662 height: &u32, 1663 output: &mut Vec<String>, 1664 ) -> WalletDbResult<()> { 1665 output.push(format!("Resetting DAO confirmations after: {height}")); 1666 let query = format!( 1667 "UPDATE {} SET {} = NULL, {} = NULL, {} = NULL, {} = NULL WHERE {} > ?1;", 1668 *DAO_DAOS_TABLE, 1669 DAO_DAOS_COL_LEAF_POSITION, 1670 DAO_DAOS_COL_MINT_HEIGHT, 1671 DAO_DAOS_COL_TX_HASH, 1672 DAO_DAOS_COL_CALL_INDEX, 1673 DAO_DAOS_COL_MINT_HEIGHT, 1674 ); 1675 self.wallet.exec_sql(&query, rusqlite::params![Some(*height)])?; 1676 output.push(String::from("Successfully unconfirmed DAOs")); 1677 1678 Ok(()) 1679 } 1680 1681 /// Reset all DAO proposals in the wallet. 1682 pub fn reset_dao_proposals(&self, output: &mut Vec<String>) -> WalletDbResult<()> { 1683 output.push(String::from("Resetting DAO proposals confirmations")); 1684 let query = format!( 1685 "UPDATE {} SET {} = NULL, {} = NULL, {} = NULL, {} = NULL, {} = NULL, {} = NULL, {} = NULL, {} = NULL;", 1686 *DAO_PROPOSALS_TABLE, 1687 DAO_PROPOSALS_COL_LEAF_POSITION, 1688 DAO_PROPOSALS_COL_MONEY_SNAPSHOT_TREE, 1689 DAO_PROPOSALS_COL_NULLIFIERS_SMT_SNAPSHOT, 1690 DAO_PROPOSALS_COL_MINT_HEIGHT, 1691 DAO_PROPOSALS_COL_TX_HASH, 1692 DAO_PROPOSALS_COL_CALL_INDEX, 1693 DAO_PROPOSALS_COL_EXEC_HEIGHT, 1694 DAO_PROPOSALS_COL_EXEC_TX_HASH, 1695 ); 1696 self.wallet.exec_sql(&query, &[])?; 1697 output.push(String::from("Successfully unconfirmed DAO proposals")); 1698 1699 Ok(()) 1700 } 1701 1702 /// Reset DAO proposals in the wallet that were minted after 1703 /// provided height. 1704 pub fn unconfirm_dao_proposals_after( 1705 &self, 1706 height: &u32, 1707 output: &mut Vec<String>, 1708 ) -> WalletDbResult<()> { 1709 output.push(format!("Resetting DAO proposals confirmations after: {height}")); 1710 let query = format!( 1711 "UPDATE {} SET {} = NULL, {} = NULL, {} = NULL, {} = NULL, {} = NULL, {} = NULL, {} = NULL, {} = NULL WHERE {} > ?1;", 1712 *DAO_PROPOSALS_TABLE, 1713 DAO_PROPOSALS_COL_LEAF_POSITION, 1714 DAO_PROPOSALS_COL_MONEY_SNAPSHOT_TREE, 1715 DAO_PROPOSALS_COL_NULLIFIERS_SMT_SNAPSHOT, 1716 DAO_PROPOSALS_COL_MINT_HEIGHT, 1717 DAO_PROPOSALS_COL_TX_HASH, 1718 DAO_PROPOSALS_COL_CALL_INDEX, 1719 DAO_PROPOSALS_COL_EXEC_HEIGHT, 1720 DAO_PROPOSALS_COL_EXEC_TX_HASH, 1721 DAO_PROPOSALS_COL_MINT_HEIGHT, 1722 ); 1723 self.wallet.exec_sql(&query, rusqlite::params![Some(*height)])?; 1724 output.push(String::from("Successfully unconfirmed DAO proposals")); 1725 1726 Ok(()) 1727 } 1728 1729 /// Reset execution information in the wallet for DAO proposals 1730 /// that were executed after provided height. 1731 pub fn unexec_dao_proposals_after( 1732 &self, 1733 height: &u32, 1734 output: &mut Vec<String>, 1735 ) -> WalletDbResult<()> { 1736 output.push(format!("Resetting DAO proposals execution information after: {height}")); 1737 let query = format!( 1738 "UPDATE {} SET {} = NULL, {} = NULL WHERE {} > ?1;", 1739 *DAO_PROPOSALS_TABLE, 1740 DAO_PROPOSALS_COL_EXEC_HEIGHT, 1741 DAO_PROPOSALS_COL_EXEC_TX_HASH, 1742 DAO_PROPOSALS_COL_EXEC_HEIGHT, 1743 ); 1744 self.wallet.exec_sql(&query, rusqlite::params![Some(*height)])?; 1745 output.push(String::from("Successfully reset DAO proposals execution information")); 1746 1747 Ok(()) 1748 } 1749 1750 /// Reset all DAO votes in the wallet. 1751 pub fn reset_dao_votes(&self, output: &mut Vec<String>) -> WalletDbResult<()> { 1752 output.push(String::from("Resetting DAO votes")); 1753 let query = format!("DELETE FROM {};", *DAO_VOTES_TABLE); 1754 self.wallet.exec_sql(&query, &[])?; 1755 output.push(String::from("Successfully reset DAO votes")); 1756 1757 Ok(()) 1758 } 1759 1760 /// Remove the DAO votes in the wallet that were created after 1761 /// provided height. 1762 pub fn remove_dao_votes_after( 1763 &self, 1764 height: &u32, 1765 output: &mut Vec<String>, 1766 ) -> WalletDbResult<()> { 1767 output.push(format!("Removing DAO votes after: {height}")); 1768 let query = 1769 format!("DELETE FROM {} WHERE {} > ?1;", *DAO_VOTES_TABLE, DAO_VOTES_COL_BLOCK_HEIGHT); 1770 self.wallet.exec_sql(&query, rusqlite::params![height])?; 1771 output.push(String::from("Successfully removed DAO votes")); 1772 1773 Ok(()) 1774 } 1775 1776 /// Import given DAO params into the wallet with a given name. 1777 pub async fn import_dao( 1778 &self, 1779 name: &str, 1780 params: &DaoParams, 1781 output: &mut Vec<String>, 1782 ) -> Result<()> { 1783 // Grab the params DAO 1784 let bulla = params.dao.to_bulla(); 1785 1786 // Check if we already have imported the DAO so we retain its 1787 // mint information. 1788 match self.get_dao_by_bulla(&bulla).await { 1789 Ok(dao) => { 1790 output.push(format!("Updating \"{}\" DAO keys into the wallet", dao.name)); 1791 let query = format!( 1792 "UPDATE {} SET {} = ?1 WHERE {} = ?2;", 1793 *DAO_DAOS_TABLE, DAO_DAOS_COL_PARAMS, DAO_DAOS_COL_BULLA, 1794 ); 1795 if let Err(e) = 1796 self.wallet.exec_sql( 1797 &query, 1798 rusqlite::params![ 1799 serialize_async(params).await, 1800 serialize_async(&bulla).await, 1801 ], 1802 ) 1803 { 1804 return Err(Error::DatabaseError(format!("[import_dao] DAO update failed: {e}"))) 1805 }; 1806 } 1807 Err(_) => { 1808 output.push(format!("Importing \"{name}\" DAO into the wallet")); 1809 let query = format!( 1810 "INSERT INTO {} ({}, {}, {}) VALUES (?1, ?2, ?3);", 1811 *DAO_DAOS_TABLE, DAO_DAOS_COL_BULLA, DAO_DAOS_COL_NAME, DAO_DAOS_COL_PARAMS, 1812 ); 1813 if let Err(e) = self.wallet.exec_sql( 1814 &query, 1815 rusqlite::params![ 1816 serialize_async(¶ms.dao.to_bulla()).await, 1817 name, 1818 serialize_async(params).await, 1819 ], 1820 ) { 1821 return Err(Error::DatabaseError(format!("[import_dao] DAO insert failed: {e}"))) 1822 }; 1823 } 1824 }; 1825 1826 Ok(()) 1827 } 1828 1829 /// Fetch a DAO given its bulla. 1830 pub async fn get_dao_by_bulla(&self, bulla: &DaoBulla) -> Result<DaoRecord> { 1831 let row = match self.wallet.query_single( 1832 &DAO_DAOS_TABLE, 1833 &[], 1834 convert_named_params! {(DAO_DAOS_COL_BULLA, serialize_async(bulla).await)}, 1835 ) { 1836 Ok(r) => r, 1837 Err(e) => { 1838 return Err(Error::DatabaseError(format!( 1839 "[get_dao_by_bulla] DAO retrieval failed: {e}" 1840 ))) 1841 } 1842 }; 1843 1844 self.parse_dao_record(&row).await 1845 } 1846 1847 /// Fetch a DAO given its name. 1848 pub async fn get_dao_by_name(&self, name: &str) -> Result<DaoRecord> { 1849 let row = match self.wallet.query_single( 1850 &DAO_DAOS_TABLE, 1851 &[], 1852 convert_named_params! {(DAO_DAOS_COL_NAME, name)}, 1853 ) { 1854 Ok(r) => r, 1855 Err(e) => { 1856 return Err(Error::DatabaseError(format!( 1857 "[get_dao_by_name] DAO retrieval failed: {e}" 1858 ))) 1859 } 1860 }; 1861 1862 self.parse_dao_record(&row).await 1863 } 1864 1865 /// List DAO(s) imported in the wallet. If a name is given, just print the 1866 /// metadata for that specific one, if found. 1867 pub async fn dao_list(&self, name: &Option<String>, output: &mut Vec<String>) -> Result<()> { 1868 if let Some(name) = name { 1869 let dao = self.get_dao_by_name(name).await?; 1870 output.push(format!("{dao}")); 1871 return Ok(()); 1872 } 1873 1874 let daos = self.get_daos().await?; 1875 for (i, dao) in daos.iter().enumerate() { 1876 output.push(format!("{i}. {}", dao.name)); 1877 } 1878 1879 Ok(()) 1880 } 1881 1882 /// Fetch known unspent balances from the wallet for the given DAO name. 1883 pub async fn dao_balance(&self, name: &str) -> Result<HashMap<String, u64>> { 1884 let dao = self.get_dao_by_name(name).await?; 1885 1886 let dao_spend_hook = 1887 FuncRef { contract_id: *DAO_CONTRACT_ID, func_code: DaoFunction::Exec as u8 } 1888 .to_func_id(); 1889 1890 let mut coins = self.get_coins(false).await?; 1891 coins.retain(|x| x.0.note.spend_hook == dao_spend_hook); 1892 coins.retain(|x| x.0.note.user_data == dao.bulla().inner()); 1893 1894 // Fill this map with balances 1895 let mut balmap: HashMap<String, u64> = HashMap::new(); 1896 1897 for coin in coins { 1898 let mut value = coin.0.note.value; 1899 1900 if let Some(prev) = balmap.get(&coin.0.note.token_id.to_string()) { 1901 value += prev; 1902 } 1903 1904 balmap.insert(coin.0.note.token_id.to_string(), value); 1905 } 1906 1907 Ok(balmap) 1908 } 1909 1910 /// Fetch all known DAO proposalss from the wallet. 1911 pub async fn get_proposals(&self) -> Result<Vec<ProposalRecord>> { 1912 let rows = match self.wallet.query_multiple(&DAO_PROPOSALS_TABLE, &[], &[]) { 1913 Ok(r) => r, 1914 Err(e) => { 1915 return Err(Error::DatabaseError(format!( 1916 "[get_proposals] DAO proposalss retrieval failed: {e}" 1917 ))) 1918 } 1919 }; 1920 1921 let mut daos = Vec::with_capacity(rows.len()); 1922 for row in rows { 1923 daos.push(self.parse_dao_proposal(&row).await?); 1924 } 1925 1926 Ok(daos) 1927 } 1928 1929 /// Fetch a DAO proposal by its bulla. 1930 pub async fn get_dao_proposal_by_bulla( 1931 &self, 1932 bulla: &DaoProposalBulla, 1933 ) -> Result<ProposalRecord> { 1934 // Grab the proposal record 1935 let row = match self.wallet.query_single( 1936 &DAO_PROPOSALS_TABLE, 1937 &[], 1938 convert_named_params! {(DAO_PROPOSALS_COL_BULLA, serialize_async(bulla).await)}, 1939 ) { 1940 Ok(r) => r, 1941 Err(e) => { 1942 return Err(Error::DatabaseError(format!( 1943 "[get_dao_proposal_by_bulla] DAO proposal retrieval failed: {e}" 1944 ))) 1945 } 1946 }; 1947 1948 // Parse rest of the record 1949 self.parse_dao_proposal(&row).await 1950 } 1951 1952 // Fetch all known DAO proposal votes from the wallet given a proposal ID. 1953 pub async fn get_dao_proposal_votes( 1954 &self, 1955 proposal: &DaoProposalBulla, 1956 ) -> Result<Vec<VoteRecord>> { 1957 let rows = match self.wallet.query_multiple( 1958 &DAO_VOTES_TABLE, 1959 &[], 1960 convert_named_params! {(DAO_VOTES_COL_PROPOSAL_BULLA, serialize_async(proposal).await)}, 1961 ) { 1962 Ok(r) => r, 1963 Err(e) => { 1964 return Err(Error::DatabaseError(format!( 1965 "[get_dao_proposal_votes] Votes retrieval failed: {e}" 1966 ))) 1967 } 1968 }; 1969 1970 let mut votes = Vec::with_capacity(rows.len()); 1971 for row in rows { 1972 let Value::Integer(id) = row[0] else { 1973 return Err(Error::ParseFailed("[get_dao_proposal_votes] ID parsing failed")) 1974 }; 1975 let Ok(id) = u64::try_from(id) else { 1976 return Err(Error::ParseFailed("[get_dao_proposal_votes] ID parsing failed")) 1977 }; 1978 1979 let Value::Blob(ref proposal_bytes) = row[1] else { 1980 return Err(Error::ParseFailed( 1981 "[get_dao_proposal_votes] Proposal bytes bytes parsing failed", 1982 )) 1983 }; 1984 let proposal = deserialize_async(proposal_bytes).await?; 1985 1986 let Value::Integer(vote_option) = row[2] else { 1987 return Err(Error::ParseFailed( 1988 "[get_dao_proposal_votes] Vote option parsing failed", 1989 )) 1990 }; 1991 let Ok(vote_option) = u32::try_from(vote_option) else { 1992 return Err(Error::ParseFailed( 1993 "[get_dao_proposal_votes] Vote option parsing failed", 1994 )) 1995 }; 1996 let vote_option = vote_option != 0; 1997 1998 let Value::Blob(ref yes_vote_blind_bytes) = row[3] else { 1999 return Err(Error::ParseFailed( 2000 "[get_dao_proposal_votes] Yes vote blind bytes parsing failed", 2001 )) 2002 }; 2003 let yes_vote_blind = deserialize_async(yes_vote_blind_bytes).await?; 2004 2005 let Value::Blob(ref all_vote_value_bytes) = row[4] else { 2006 return Err(Error::ParseFailed( 2007 "[get_dao_proposal_votes] All vote value bytes parsing failed", 2008 )) 2009 }; 2010 let all_vote_value = deserialize_async(all_vote_value_bytes).await?; 2011 2012 let Value::Blob(ref all_vote_blind_bytes) = row[5] else { 2013 return Err(Error::ParseFailed( 2014 "[get_dao_proposal_votes] All vote blind bytes parsing failed", 2015 )) 2016 }; 2017 let all_vote_blind = deserialize_async(all_vote_blind_bytes).await?; 2018 2019 let Value::Integer(block_height) = row[6] else { 2020 return Err(Error::ParseFailed( 2021 "[get_dao_proposal_votes] Block height parsing failed", 2022 )) 2023 }; 2024 let Ok(block_height) = u32::try_from(block_height) else { 2025 return Err(Error::ParseFailed( 2026 "[get_dao_proposal_votes] Block height parsing failed", 2027 )) 2028 }; 2029 2030 let Value::Blob(ref tx_hash_bytes) = row[7] else { 2031 return Err(Error::ParseFailed( 2032 "[get_dao_proposal_votes] Transaction hash bytes parsing failed", 2033 )) 2034 }; 2035 let tx_hash = deserialize_async(tx_hash_bytes).await?; 2036 2037 let Value::Integer(call_index) = row[8] else { 2038 return Err(Error::ParseFailed("[get_dao_proposal_votes] Call index parsing failed")) 2039 }; 2040 let Ok(call_index) = u8::try_from(call_index) else { 2041 return Err(Error::ParseFailed("[get_dao_proposal_votes] Call index parsing failed")) 2042 }; 2043 2044 let Value::Blob(ref nullifiers_bytes) = row[9] else { 2045 return Err(Error::ParseFailed( 2046 "[get_dao_proposal_votes] Nullifiers bytes parsing failed", 2047 )) 2048 }; 2049 let nullifiers = deserialize_async(nullifiers_bytes).await?; 2050 2051 let vote = VoteRecord { 2052 id, 2053 proposal, 2054 vote_option, 2055 yes_vote_blind, 2056 all_vote_value, 2057 all_vote_blind, 2058 block_height, 2059 tx_hash, 2060 call_index, 2061 nullifiers, 2062 }; 2063 2064 votes.push(vote); 2065 } 2066 2067 Ok(votes) 2068 } 2069 2070 /// Mint a DAO on-chain. 2071 pub async fn dao_mint(&self, name: &str) -> Result<Transaction> { 2072 // Retrieve the dao record 2073 let dao = self.get_dao_by_name(name).await?; 2074 2075 // Check its not already minted 2076 if dao.tx_hash.is_some() { 2077 return Err(Error::Custom( 2078 "[dao_mint] This DAO seems to have already been minted on-chain".to_string(), 2079 )) 2080 } 2081 2082 // Check that we have all the keys 2083 if dao.params.notes_secret_key.is_none() || 2084 dao.params.proposer_secret_key.is_none() || 2085 dao.params.proposals_secret_key.is_none() || 2086 dao.params.votes_secret_key.is_none() || 2087 dao.params.exec_secret_key.is_none() || 2088 dao.params.early_exec_secret_key.is_none() 2089 { 2090 return Err(Error::Custom( 2091 "[dao_mint] We need all the secrets key to mint the DAO on-chain".to_string(), 2092 )) 2093 } 2094 2095 // Now we need to do a lookup for the zkas proof bincodes, and create 2096 // the circuit objects and proving keys so we can build the transaction. 2097 // We also do this through the RPC. First we grab the fee call from money. 2098 let zkas_bins = self.lookup_zkas(&MONEY_CONTRACT_ID).await?; 2099 2100 let Some(fee_zkbin) = zkas_bins.iter().find(|x| x.0 == MONEY_CONTRACT_ZKAS_FEE_NS_V1) 2101 else { 2102 return Err(Error::Custom("Fee circuit not found".to_string())) 2103 }; 2104 2105 let fee_zkbin = ZkBinary::decode(&fee_zkbin.1)?; 2106 2107 let fee_circuit = ZkCircuit::new(empty_witnesses(&fee_zkbin)?, &fee_zkbin); 2108 2109 // Creating Fee circuit proving key 2110 let fee_pk = ProvingKey::build(fee_zkbin.k, &fee_circuit); 2111 2112 // Now we grab the DAO mint 2113 let zkas_bins = self.lookup_zkas(&DAO_CONTRACT_ID).await?; 2114 2115 let Some(dao_mint_zkbin) = zkas_bins.iter().find(|x| x.0 == DAO_CONTRACT_ZKAS_DAO_MINT_NS) 2116 else { 2117 return Err(Error::DatabaseError("[dao_mint] DAO Mint circuit not found".to_string())) 2118 }; 2119 2120 let dao_mint_zkbin = ZkBinary::decode(&dao_mint_zkbin.1)?; 2121 2122 let dao_mint_circuit = ZkCircuit::new(empty_witnesses(&dao_mint_zkbin)?, &dao_mint_zkbin); 2123 2124 // Creating DAO Mint circuit proving key 2125 let dao_mint_pk = ProvingKey::build(dao_mint_zkbin.k, &dao_mint_circuit); 2126 2127 // Create the DAO mint call 2128 let notes_secret_key = dao.params.notes_secret_key.unwrap(); 2129 let (params, proofs) = make_mint_call( 2130 &dao.params.dao, 2131 ¬es_secret_key, 2132 &dao.params.proposer_secret_key.unwrap(), 2133 &dao.params.proposals_secret_key.unwrap(), 2134 &dao.params.votes_secret_key.unwrap(), 2135 &dao.params.exec_secret_key.unwrap(), 2136 &dao.params.early_exec_secret_key.unwrap(), 2137 &dao_mint_zkbin, 2138 &dao_mint_pk, 2139 )?; 2140 let mut data = vec![DaoFunction::Mint as u8]; 2141 params.encode_async(&mut data).await?; 2142 let call = ContractCall { contract_id: *DAO_CONTRACT_ID, data }; 2143 2144 // Create the TransactionBuilder containing above call 2145 let mut tx_builder = TransactionBuilder::new(ContractCallLeaf { call, proofs }, vec![])?; 2146 2147 // We first have to execute the fee-less tx to gather its used gas, and then we feed 2148 // it into the fee-creating function. 2149 let mut tx = tx_builder.build()?; 2150 let sigs = tx.create_sigs(&[notes_secret_key])?; 2151 tx.signatures.push(sigs); 2152 2153 let tree = self.get_money_tree().await?; 2154 let (fee_call, fee_proofs, fee_secrets) = 2155 self.append_fee_call(&tx, &tree, &fee_pk, &fee_zkbin, None).await?; 2156 2157 // Append the fee call to the transaction 2158 tx_builder.append(ContractCallLeaf { call: fee_call, proofs: fee_proofs }, vec![])?; 2159 2160 // Now build the actual transaction and sign it with all necessary keys. 2161 let mut tx = tx_builder.build()?; 2162 let sigs = tx.create_sigs(&[notes_secret_key])?; 2163 tx.signatures.push(sigs); 2164 let sigs = tx.create_sigs(&fee_secrets)?; 2165 tx.signatures.push(sigs); 2166 2167 Ok(tx) 2168 } 2169 2170 /// Create a DAO transfer proposal. 2171 #[allow(clippy::too_many_arguments)] 2172 pub async fn dao_propose_transfer( 2173 &self, 2174 name: &str, 2175 duration_blockwindows: u64, 2176 amount: &str, 2177 token_id: TokenId, 2178 recipient: PublicKey, 2179 spend_hook: Option<FuncId>, 2180 user_data: Option<pallas::Base>, 2181 ) -> Result<ProposalRecord> { 2182 // Fetch DAO and check its deployed 2183 let dao = self.get_dao_by_name(name).await?; 2184 if dao.leaf_position.is_none() || dao.tx_hash.is_none() || dao.call_index.is_none() { 2185 return Err(Error::Custom( 2186 "[dao_propose_transfer] DAO seems to not have been deployed yet".to_string(), 2187 )) 2188 } 2189 2190 // Check that we have the proposer key 2191 if dao.params.proposer_secret_key.is_none() { 2192 return Err(Error::Custom( 2193 "[dao_propose_transfer] We need the proposer secret key to create proposals for this DAO".to_string(), 2194 )) 2195 } 2196 2197 // Fetch DAO unspent OwnCoins to see what its balance is 2198 let dao_spend_hook = 2199 FuncRef { contract_id: *DAO_CONTRACT_ID, func_code: DaoFunction::Exec as u8 } 2200 .to_func_id(); 2201 let dao_bulla = dao.bulla(); 2202 let dao_owncoins = 2203 self.get_contract_token_coins(&token_id, &dao_spend_hook, &dao_bulla.inner()).await?; 2204 if dao_owncoins.is_empty() { 2205 return Err(Error::Custom(format!( 2206 "[dao_propose_transfer] Did not find any {token_id} unspent coins owned by this DAO" 2207 ))) 2208 } 2209 2210 // Check DAO balance is sufficient 2211 let amount = decode_base10(amount, BALANCE_BASE10_DECIMALS, false)?; 2212 if dao_owncoins.iter().map(|x| x.note.value).sum::<u64>() < amount { 2213 return Err(Error::Custom(format!( 2214 "[dao_propose_transfer] Not enough DAO balance for token ID: {token_id}", 2215 ))) 2216 } 2217 2218 // Generate proposal coin attributes 2219 let proposal_coinattrs = CoinAttributes { 2220 public_key: recipient, 2221 value: amount, 2222 token_id, 2223 spend_hook: spend_hook.unwrap_or(FuncId::none()), 2224 user_data: user_data.unwrap_or(pallas::Base::ZERO), 2225 blind: Blind::random(&mut OsRng), 2226 }; 2227 2228 // Convert coin_params to actual coins 2229 let proposal_coins = vec![proposal_coinattrs.to_coin()]; 2230 let mut proposal_data = vec![]; 2231 proposal_coins.encode_async(&mut proposal_data).await?; 2232 2233 // Create Auth calls 2234 let auth_calls = vec![ 2235 DaoAuthCall { 2236 contract_id: *DAO_CONTRACT_ID, 2237 function_code: DaoFunction::AuthMoneyTransfer as u8, 2238 auth_data: proposal_data, 2239 }, 2240 DaoAuthCall { 2241 contract_id: *MONEY_CONTRACT_ID, 2242 function_code: MoneyFunction::TransferV1 as u8, 2243 auth_data: vec![], 2244 }, 2245 ]; 2246 2247 // Retrieve next block height and current block time target, 2248 // to compute their window. 2249 let next_block_height = self.get_next_block_height().await?; 2250 let block_target = self.get_block_target().await?; 2251 let creation_blockwindow = blockwindow(next_block_height, block_target); 2252 2253 // Create the actual proposal 2254 let proposal = DaoProposal { 2255 auth_calls, 2256 creation_blockwindow, 2257 duration_blockwindows, 2258 user_data: user_data.unwrap_or(pallas::Base::ZERO), 2259 dao_bulla, 2260 blind: Blind::random(&mut OsRng), 2261 }; 2262 2263 let proposal_record = ProposalRecord { 2264 proposal, 2265 data: Some(serialize_async(&proposal_coinattrs).await), 2266 leaf_position: None, 2267 money_snapshot_tree: None, 2268 nullifiers_smt_snapshot: None, 2269 mint_height: None, 2270 tx_hash: None, 2271 call_index: None, 2272 exec_height: None, 2273 exec_tx_hash: None, 2274 }; 2275 2276 if let Err(e) = self.put_dao_proposal(&proposal_record).await { 2277 return Err(Error::DatabaseError(format!( 2278 "[dao_propose_transfer] Put DAO proposal failed: {e}" 2279 ))) 2280 } 2281 2282 Ok(proposal_record) 2283 } 2284 2285 /// Create a DAO generic proposal. 2286 pub async fn dao_propose_generic( 2287 &self, 2288 name: &str, 2289 duration_blockwindows: u64, 2290 user_data: Option<pallas::Base>, 2291 ) -> Result<ProposalRecord> { 2292 // Fetch DAO and check its deployed 2293 let dao = self.get_dao_by_name(name).await?; 2294 if dao.leaf_position.is_none() || dao.tx_hash.is_none() || dao.call_index.is_none() { 2295 return Err(Error::Custom( 2296 "[dao_propose_generic] DAO seems to not have been deployed yet".to_string(), 2297 )) 2298 } 2299 2300 // Check that we have the proposer key 2301 if dao.params.proposer_secret_key.is_none() { 2302 return Err(Error::Custom( 2303 "[dao_propose_generic] We need the proposer secret key to create proposals for this DAO".to_string(), 2304 )) 2305 } 2306 2307 // Retrieve next block height and current block time target, 2308 // to compute their window. 2309 let next_block_height = self.get_next_block_height().await?; 2310 let block_target = self.get_block_target().await?; 2311 let creation_blockwindow = blockwindow(next_block_height, block_target); 2312 2313 // Create the actual proposal 2314 let proposal = DaoProposal { 2315 auth_calls: vec![], 2316 creation_blockwindow, 2317 duration_blockwindows, 2318 user_data: user_data.unwrap_or(pallas::Base::ZERO), 2319 dao_bulla: dao.bulla(), 2320 blind: Blind::random(&mut OsRng), 2321 }; 2322 2323 let proposal_record = ProposalRecord { 2324 proposal, 2325 data: None, 2326 leaf_position: None, 2327 money_snapshot_tree: None, 2328 nullifiers_smt_snapshot: None, 2329 mint_height: None, 2330 tx_hash: None, 2331 call_index: None, 2332 exec_height: None, 2333 exec_tx_hash: None, 2334 }; 2335 2336 if let Err(e) = self.put_dao_proposal(&proposal_record).await { 2337 return Err(Error::DatabaseError(format!( 2338 "[dao_propose_generic] Put DAO proposal failed: {e}" 2339 ))) 2340 } 2341 2342 Ok(proposal_record) 2343 } 2344 2345 /// Create a DAO transfer proposal transaction. 2346 pub async fn dao_transfer_proposal_tx(&self, proposal: &ProposalRecord) -> Result<Transaction> { 2347 // Check we know the plaintext data 2348 if proposal.data.is_none() { 2349 return Err(Error::Custom( 2350 "[dao_transfer_proposal_tx] Proposal plainext data is empty".to_string(), 2351 )) 2352 } 2353 let proposal_coinattrs: CoinAttributes = 2354 deserialize_async(proposal.data.as_ref().unwrap()).await?; 2355 2356 // Fetch DAO and check its deployed 2357 let Ok(dao) = self.get_dao_by_bulla(&proposal.proposal.dao_bulla).await else { 2358 return Err(Error::Custom(format!( 2359 "[dao_transfer_proposal_tx] DAO {} was not found", 2360 proposal.proposal.dao_bulla 2361 ))) 2362 }; 2363 if dao.leaf_position.is_none() || dao.tx_hash.is_none() || dao.call_index.is_none() { 2364 return Err(Error::Custom( 2365 "[dao_transfer_proposal_tx] DAO seems to not have been deployed yet".to_string(), 2366 )) 2367 } 2368 2369 // Check that we have the proposer key 2370 if dao.params.proposer_secret_key.is_none() { 2371 return Err(Error::Custom( 2372 "[dao_transfer_proposal_tx] We need the proposer secret key to create proposals for this DAO".to_string(), 2373 )) 2374 } 2375 2376 // Fetch DAO unspent OwnCoins to see what its balance is for the coin 2377 let dao_spend_hook = 2378 FuncRef { contract_id: *DAO_CONTRACT_ID, func_code: DaoFunction::Exec as u8 } 2379 .to_func_id(); 2380 let dao_owncoins = self 2381 .get_contract_token_coins( 2382 &proposal_coinattrs.token_id, 2383 &dao_spend_hook, 2384 &proposal.proposal.dao_bulla.inner(), 2385 ) 2386 .await?; 2387 if dao_owncoins.is_empty() { 2388 return Err(Error::Custom(format!( 2389 "[dao_transfer_proposal_tx] Did not find any {} unspent coins owned by this DAO", 2390 proposal_coinattrs.token_id, 2391 ))) 2392 } 2393 2394 // Check DAO balance is sufficient 2395 if dao_owncoins.iter().map(|x| x.note.value).sum::<u64>() < proposal_coinattrs.value { 2396 return Err(Error::Custom(format!( 2397 "[dao_transfer_proposal_tx] Not enough DAO balance for token ID: {}", 2398 proposal_coinattrs.token_id, 2399 ))) 2400 } 2401 2402 // Fetch our own governance OwnCoins to see what our balance is 2403 let gov_owncoins = self.get_token_coins(&dao.params.dao.gov_token_id).await?; 2404 if gov_owncoins.is_empty() { 2405 return Err(Error::Custom(format!( 2406 "[dao_transfer_proposal_tx] Did not find any governance {} coins in wallet", 2407 dao.params.dao.gov_token_id 2408 ))) 2409 } 2410 2411 // Find which governance coins we can use 2412 let mut total_value = 0; 2413 let mut gov_owncoins_to_use = vec![]; 2414 for gov_owncoin in gov_owncoins { 2415 if total_value >= dao.params.dao.proposer_limit { 2416 break 2417 } 2418 2419 total_value += gov_owncoin.note.value; 2420 gov_owncoins_to_use.push(gov_owncoin); 2421 } 2422 2423 // Check our governance coins balance is sufficient 2424 if total_value < dao.params.dao.proposer_limit { 2425 return Err(Error::Custom(format!( 2426 "[dao_transfer_proposal_tx] Not enough gov token {} balance to propose", 2427 dao.params.dao.gov_token_id 2428 ))) 2429 } 2430 2431 // Now we need to do a lookup for the zkas proof bincodes, and create 2432 // the circuit objects and proving keys so we can build the transaction. 2433 // We also do this through the RPC. First we grab the fee call from money. 2434 let zkas_bins = self.lookup_zkas(&MONEY_CONTRACT_ID).await?; 2435 2436 let Some(fee_zkbin) = zkas_bins.iter().find(|x| x.0 == MONEY_CONTRACT_ZKAS_FEE_NS_V1) 2437 else { 2438 return Err(Error::Custom( 2439 "[dao_transfer_proposal_tx] Fee circuit not found".to_string(), 2440 )) 2441 }; 2442 2443 let fee_zkbin = ZkBinary::decode(&fee_zkbin.1)?; 2444 2445 let fee_circuit = ZkCircuit::new(empty_witnesses(&fee_zkbin)?, &fee_zkbin); 2446 2447 // Creating Fee circuit proving key 2448 let fee_pk = ProvingKey::build(fee_zkbin.k, &fee_circuit); 2449 2450 // Now we grab the DAO bins 2451 let zkas_bins = self.lookup_zkas(&DAO_CONTRACT_ID).await?; 2452 2453 let Some(propose_burn_zkbin) = 2454 zkas_bins.iter().find(|x| x.0 == DAO_CONTRACT_ZKAS_DAO_PROPOSE_INPUT_NS) 2455 else { 2456 return Err(Error::Custom( 2457 "[dao_transfer_proposal_tx] Propose Burn circuit not found".to_string(), 2458 )) 2459 }; 2460 2461 let Some(propose_main_zkbin) = 2462 zkas_bins.iter().find(|x| x.0 == DAO_CONTRACT_ZKAS_DAO_PROPOSE_MAIN_NS) 2463 else { 2464 return Err(Error::Custom( 2465 "[dao_transfer_proposal_tx] Propose Main circuit not found".to_string(), 2466 )) 2467 }; 2468 2469 let propose_burn_zkbin = ZkBinary::decode(&propose_burn_zkbin.1)?; 2470 let propose_main_zkbin = ZkBinary::decode(&propose_main_zkbin.1)?; 2471 2472 let propose_burn_circuit = 2473 ZkCircuit::new(empty_witnesses(&propose_burn_zkbin)?, &propose_burn_zkbin); 2474 let propose_main_circuit = 2475 ZkCircuit::new(empty_witnesses(&propose_main_zkbin)?, &propose_main_zkbin); 2476 2477 // Creating DAO ProposeBurn and ProposeMain circuits proving keys 2478 let propose_burn_pk = ProvingKey::build(propose_burn_zkbin.k, &propose_burn_circuit); 2479 let propose_main_pk = ProvingKey::build(propose_main_zkbin.k, &propose_main_circuit); 2480 2481 // Fetch our money Merkle tree 2482 let money_merkle_tree = self.get_money_tree().await?; 2483 2484 // Now we can create the proposal transaction parameters. 2485 // We first generate the `DaoProposeStakeInput` inputs, 2486 // using our governance OwnCoins. 2487 let mut inputs = Vec::with_capacity(gov_owncoins_to_use.len()); 2488 for gov_owncoin in gov_owncoins_to_use { 2489 let input = DaoProposeStakeInput { 2490 secret: gov_owncoin.secret, 2491 note: gov_owncoin.note.clone(), 2492 leaf_position: gov_owncoin.leaf_position, 2493 merkle_path: money_merkle_tree.witness(gov_owncoin.leaf_position, 0).unwrap(), 2494 }; 2495 inputs.push(input); 2496 } 2497 2498 // Now create the parameters for the proposal tx 2499 let signature_secret = SecretKey::random(&mut OsRng); 2500 2501 // Fetch the daos Merkle tree to compute the DAO Merkle path and root 2502 let (daos_tree, _) = self.get_dao_trees().await?; 2503 let (dao_merkle_path, dao_merkle_root) = { 2504 let root = daos_tree.root(0).unwrap(); 2505 let leaf_pos = dao.leaf_position.unwrap(); 2506 let dao_merkle_path = daos_tree.witness(leaf_pos, 0).unwrap(); 2507 (dao_merkle_path, root) 2508 }; 2509 2510 // Generate the Money nullifiers Sparse Merkle Tree 2511 let store = CacheSmtStorage::new(CacheOverlay::new(&self.cache)?, SLED_MONEY_SMT_TREE); 2512 let money_null_smt = CacheSmt::new(store, PoseidonFp::new(), &EMPTY_NODES_FP); 2513 2514 // Create the proposal call 2515 let call = DaoProposeCall { 2516 money_null_smt: &money_null_smt, 2517 inputs, 2518 proposal: proposal.proposal.clone(), 2519 dao: dao.params.dao, 2520 dao_leaf_position: dao.leaf_position.unwrap(), 2521 dao_merkle_path, 2522 dao_merkle_root, 2523 signature_secret, 2524 }; 2525 2526 let (params, proofs) = call.make( 2527 &dao.params.proposer_secret_key.unwrap(), 2528 &propose_burn_zkbin, 2529 &propose_burn_pk, 2530 &propose_main_zkbin, 2531 &propose_main_pk, 2532 )?; 2533 2534 // Encode the call 2535 let mut data = vec![DaoFunction::Propose as u8]; 2536 params.encode_async(&mut data).await?; 2537 let call = ContractCall { contract_id: *DAO_CONTRACT_ID, data }; 2538 2539 // Create the TransactionBuilder containing above call 2540 let mut tx_builder = TransactionBuilder::new(ContractCallLeaf { call, proofs }, vec![])?; 2541 2542 // We first have to execute the fee-less tx to gather its used gas, and then we feed 2543 // it into the fee-creating function. 2544 let mut tx = tx_builder.build()?; 2545 let sigs = tx.create_sigs(&[signature_secret])?; 2546 tx.signatures = vec![sigs]; 2547 2548 let tree = self.get_money_tree().await?; 2549 let (fee_call, fee_proofs, fee_secrets) = 2550 self.append_fee_call(&tx, &tree, &fee_pk, &fee_zkbin, None).await?; 2551 2552 // Append the fee call to the transaction 2553 tx_builder.append(ContractCallLeaf { call: fee_call, proofs: fee_proofs }, vec![])?; 2554 2555 // Now build the actual transaction and sign it with all necessary keys. 2556 let mut tx = tx_builder.build()?; 2557 let sigs = tx.create_sigs(&[signature_secret])?; 2558 tx.signatures.push(sigs); 2559 let sigs = tx.create_sigs(&fee_secrets)?; 2560 tx.signatures.push(sigs); 2561 2562 Ok(tx) 2563 } 2564 2565 /// Create a DAO generic proposal transaction. 2566 pub async fn dao_generic_proposal_tx(&self, proposal: &ProposalRecord) -> Result<Transaction> { 2567 // Fetch DAO and check its deployed 2568 let Ok(dao) = self.get_dao_by_bulla(&proposal.proposal.dao_bulla).await else { 2569 return Err(Error::Custom(format!( 2570 "[dao_generic_proposal_tx] DAO {} was not found", 2571 proposal.proposal.dao_bulla 2572 ))) 2573 }; 2574 if dao.leaf_position.is_none() || dao.tx_hash.is_none() || dao.call_index.is_none() { 2575 return Err(Error::Custom( 2576 "[dao_generic_proposal_tx] DAO seems to not have been deployed yet".to_string(), 2577 )) 2578 } 2579 2580 // Check that we have the proposer key 2581 if dao.params.proposer_secret_key.is_none() { 2582 return Err(Error::Custom( 2583 "[dao_generic_proposal_tx] We need the proposer secret key to create proposals for this DAO".to_string(), 2584 )) 2585 } 2586 2587 // Fetch our own governance OwnCoins to see what our balance is 2588 let gov_owncoins = self.get_token_coins(&dao.params.dao.gov_token_id).await?; 2589 if gov_owncoins.is_empty() { 2590 return Err(Error::Custom(format!( 2591 "[dao_generic_proposal_tx] Did not find any governance {} coins in wallet", 2592 dao.params.dao.gov_token_id 2593 ))) 2594 } 2595 2596 // Find which governance coins we can use 2597 let mut total_value = 0; 2598 let mut gov_owncoins_to_use = vec![]; 2599 for gov_owncoin in gov_owncoins { 2600 if total_value >= dao.params.dao.proposer_limit { 2601 break 2602 } 2603 2604 total_value += gov_owncoin.note.value; 2605 gov_owncoins_to_use.push(gov_owncoin); 2606 } 2607 2608 // Check our governance coins balance is sufficient 2609 if total_value < dao.params.dao.proposer_limit { 2610 return Err(Error::Custom(format!( 2611 "[dao_generic_proposal_tx] Not enough gov token {} balance to propose", 2612 dao.params.dao.gov_token_id 2613 ))) 2614 } 2615 2616 // Now we need to do a lookup for the zkas proof bincodes, and create 2617 // the circuit objects and proving keys so we can build the transaction. 2618 // We also do this through the RPC. First we grab the fee call from money. 2619 let zkas_bins = self.lookup_zkas(&MONEY_CONTRACT_ID).await?; 2620 2621 let Some(fee_zkbin) = zkas_bins.iter().find(|x| x.0 == MONEY_CONTRACT_ZKAS_FEE_NS_V1) 2622 else { 2623 return Err(Error::Custom("[dao_generic_proposal_tx] Fee circuit not found".to_string())) 2624 }; 2625 2626 let fee_zkbin = ZkBinary::decode(&fee_zkbin.1)?; 2627 2628 let fee_circuit = ZkCircuit::new(empty_witnesses(&fee_zkbin)?, &fee_zkbin); 2629 2630 // Creating Fee circuit proving key 2631 let fee_pk = ProvingKey::build(fee_zkbin.k, &fee_circuit); 2632 2633 // Now we grab the DAO bins 2634 let zkas_bins = self.lookup_zkas(&DAO_CONTRACT_ID).await?; 2635 2636 let Some(propose_burn_zkbin) = 2637 zkas_bins.iter().find(|x| x.0 == DAO_CONTRACT_ZKAS_DAO_PROPOSE_INPUT_NS) 2638 else { 2639 return Err(Error::Custom( 2640 "[dao_generic_proposal_tx] Propose Burn circuit not found".to_string(), 2641 )) 2642 }; 2643 2644 let Some(propose_main_zkbin) = 2645 zkas_bins.iter().find(|x| x.0 == DAO_CONTRACT_ZKAS_DAO_PROPOSE_MAIN_NS) 2646 else { 2647 return Err(Error::Custom( 2648 "[dao_generic_proposal_tx] Propose Main circuit not found".to_string(), 2649 )) 2650 }; 2651 2652 let propose_burn_zkbin = ZkBinary::decode(&propose_burn_zkbin.1)?; 2653 let propose_main_zkbin = ZkBinary::decode(&propose_main_zkbin.1)?; 2654 2655 let propose_burn_circuit = 2656 ZkCircuit::new(empty_witnesses(&propose_burn_zkbin)?, &propose_burn_zkbin); 2657 let propose_main_circuit = 2658 ZkCircuit::new(empty_witnesses(&propose_main_zkbin)?, &propose_main_zkbin); 2659 2660 // Creating DAO ProposeBurn and ProposeMain circuits proving keys 2661 let propose_burn_pk = ProvingKey::build(propose_burn_zkbin.k, &propose_burn_circuit); 2662 let propose_main_pk = ProvingKey::build(propose_main_zkbin.k, &propose_main_circuit); 2663 2664 // Fetch our money Merkle tree 2665 let money_merkle_tree = self.get_money_tree().await?; 2666 2667 // Now we can create the proposal transaction parameters. 2668 // We first generate the `DaoProposeStakeInput` inputs, 2669 // using our governance OwnCoins. 2670 let mut inputs = Vec::with_capacity(gov_owncoins_to_use.len()); 2671 for gov_owncoin in gov_owncoins_to_use { 2672 let input = DaoProposeStakeInput { 2673 secret: gov_owncoin.secret, 2674 note: gov_owncoin.note.clone(), 2675 leaf_position: gov_owncoin.leaf_position, 2676 merkle_path: money_merkle_tree.witness(gov_owncoin.leaf_position, 0).unwrap(), 2677 }; 2678 inputs.push(input); 2679 } 2680 2681 // Now create the parameters for the proposal tx 2682 let signature_secret = SecretKey::random(&mut OsRng); 2683 2684 // Fetch the daos Merkle tree to compute the DAO Merkle path and root 2685 let (daos_tree, _) = self.get_dao_trees().await?; 2686 let (dao_merkle_path, dao_merkle_root) = { 2687 let root = daos_tree.root(0).unwrap(); 2688 let leaf_pos = dao.leaf_position.unwrap(); 2689 let dao_merkle_path = daos_tree.witness(leaf_pos, 0).unwrap(); 2690 (dao_merkle_path, root) 2691 }; 2692 2693 // Generate the Money nullifiers Sparse Merkle Tree 2694 let store = CacheSmtStorage::new(CacheOverlay::new(&self.cache)?, SLED_MONEY_SMT_TREE); 2695 let money_null_smt = CacheSmt::new(store, PoseidonFp::new(), &EMPTY_NODES_FP); 2696 2697 // Create the proposal call 2698 let call = DaoProposeCall { 2699 money_null_smt: &money_null_smt, 2700 inputs, 2701 proposal: proposal.proposal.clone(), 2702 dao: dao.params.dao, 2703 dao_leaf_position: dao.leaf_position.unwrap(), 2704 dao_merkle_path, 2705 dao_merkle_root, 2706 signature_secret, 2707 }; 2708 2709 let (params, proofs) = call.make( 2710 &dao.params.proposer_secret_key.unwrap(), 2711 &propose_burn_zkbin, 2712 &propose_burn_pk, 2713 &propose_main_zkbin, 2714 &propose_main_pk, 2715 )?; 2716 2717 // Encode the call 2718 let mut data = vec![DaoFunction::Propose as u8]; 2719 params.encode_async(&mut data).await?; 2720 let call = ContractCall { contract_id: *DAO_CONTRACT_ID, data }; 2721 2722 // Create the TransactionBuilder containing above call 2723 let mut tx_builder = TransactionBuilder::new(ContractCallLeaf { call, proofs }, vec![])?; 2724 2725 // We first have to execute the fee-less tx to gather its used gas, and then we feed 2726 // it into the fee-creating function. 2727 let mut tx = tx_builder.build()?; 2728 let sigs = tx.create_sigs(&[signature_secret])?; 2729 tx.signatures = vec![sigs]; 2730 2731 let tree = self.get_money_tree().await?; 2732 let (fee_call, fee_proofs, fee_secrets) = 2733 self.append_fee_call(&tx, &tree, &fee_pk, &fee_zkbin, None).await?; 2734 2735 // Append the fee call to the transaction 2736 tx_builder.append(ContractCallLeaf { call: fee_call, proofs: fee_proofs }, vec![])?; 2737 2738 // Now build the actual transaction and sign it with all necessary keys. 2739 let mut tx = tx_builder.build()?; 2740 let sigs = tx.create_sigs(&[signature_secret])?; 2741 tx.signatures.push(sigs); 2742 let sigs = tx.create_sigs(&fee_secrets)?; 2743 tx.signatures.push(sigs); 2744 2745 Ok(tx) 2746 } 2747 2748 /// Vote on a DAO proposal 2749 pub async fn dao_vote( 2750 &self, 2751 proposal_bulla: &DaoProposalBulla, 2752 vote_option: bool, 2753 weight: Option<u64>, 2754 ) -> Result<Transaction> { 2755 // Feth the proposal and check its deployed 2756 let Ok(proposal) = self.get_dao_proposal_by_bulla(proposal_bulla).await else { 2757 return Err(Error::Custom(format!("[dao_vote] Proposal {proposal_bulla} was not found"))) 2758 }; 2759 if proposal.leaf_position.is_none() || 2760 proposal.money_snapshot_tree.is_none() || 2761 proposal.nullifiers_smt_snapshot.is_none() || 2762 proposal.tx_hash.is_none() || 2763 proposal.call_index.is_none() 2764 { 2765 return Err(Error::Custom( 2766 "[dao_vote] Proposal seems to not have been deployed yet".to_string(), 2767 )) 2768 } 2769 2770 // Check proposal is not executed 2771 if let Some(exec_tx_hash) = proposal.exec_tx_hash { 2772 return Err(Error::Custom(format!( 2773 "[dao_vote] Proposal was executed on transaction: {exec_tx_hash}" 2774 ))) 2775 } 2776 2777 // Fetch DAO and check its deployed 2778 let Ok(dao) = self.get_dao_by_bulla(&proposal.proposal.dao_bulla).await else { 2779 return Err(Error::Custom(format!( 2780 "[dao_vote] DAO {} was not found", 2781 proposal.proposal.dao_bulla 2782 ))) 2783 }; 2784 if dao.leaf_position.is_none() || dao.tx_hash.is_none() || dao.call_index.is_none() { 2785 return Err(Error::Custom( 2786 "[dao_vote] DAO seems to not have been deployed yet".to_string(), 2787 )) 2788 } 2789 2790 // Fetch all the proposal votes to check for duplicate nullifiers 2791 let votes = self.get_dao_proposal_votes(proposal_bulla).await?; 2792 let mut votes_nullifiers = vec![]; 2793 for vote in votes { 2794 for nullifier in vote.nullifiers { 2795 if !votes_nullifiers.contains(&nullifier) { 2796 votes_nullifiers.push(nullifier); 2797 } 2798 } 2799 } 2800 2801 // Fetch our own governance OwnCoins to see what our balance is 2802 let gov_owncoins = self.get_token_coins(&dao.params.dao.gov_token_id).await?; 2803 if gov_owncoins.is_empty() { 2804 return Err(Error::Custom(format!( 2805 "[dao_vote] Did not find any governance {} coins in wallet", 2806 dao.params.dao.gov_token_id 2807 ))) 2808 } 2809 2810 // Find which governance coins we can use 2811 let gov_owncoins_to_use = match weight { 2812 Some(_weight) => { 2813 // TODO: Build a proper coin selection algorithm so that we can use a 2814 // coins combination that matches the requested weight 2815 return Err(Error::Custom( 2816 "[dao_vote] Fractional vote weight not supported yet".to_string(), 2817 )) 2818 } 2819 // If no weight was specified, use them all 2820 None => gov_owncoins, 2821 }; 2822 2823 // Now we need to do a lookup for the zkas proof bincodes, and create 2824 // the circuit objects and proving keys so we can build the transaction. 2825 // We also do this through the RPC. First we grab the fee call from money. 2826 let zkas_bins = self.lookup_zkas(&MONEY_CONTRACT_ID).await?; 2827 2828 let Some(fee_zkbin) = zkas_bins.iter().find(|x| x.0 == MONEY_CONTRACT_ZKAS_FEE_NS_V1) 2829 else { 2830 return Err(Error::Custom("[dao_vote] Fee circuit not found".to_string())) 2831 }; 2832 2833 let fee_zkbin = ZkBinary::decode(&fee_zkbin.1)?; 2834 2835 let fee_circuit = ZkCircuit::new(empty_witnesses(&fee_zkbin)?, &fee_zkbin); 2836 2837 // Creating Fee circuit proving key 2838 let fee_pk = ProvingKey::build(fee_zkbin.k, &fee_circuit); 2839 2840 // Now we grab the DAO bins 2841 let zkas_bins = self.lookup_zkas(&DAO_CONTRACT_ID).await?; 2842 2843 let Some(dao_vote_burn_zkbin) = 2844 zkas_bins.iter().find(|x| x.0 == DAO_CONTRACT_ZKAS_DAO_VOTE_INPUT_NS) 2845 else { 2846 return Err(Error::Custom("[dao_vote] DAO Vote Burn circuit not found".to_string())) 2847 }; 2848 2849 let Some(dao_vote_main_zkbin) = 2850 zkas_bins.iter().find(|x| x.0 == DAO_CONTRACT_ZKAS_DAO_VOTE_MAIN_NS) 2851 else { 2852 return Err(Error::Custom("[dao_vote] DAO Vote Main circuit not found".to_string())) 2853 }; 2854 2855 let dao_vote_burn_zkbin = ZkBinary::decode(&dao_vote_burn_zkbin.1)?; 2856 let dao_vote_main_zkbin = ZkBinary::decode(&dao_vote_main_zkbin.1)?; 2857 2858 let dao_vote_burn_circuit = 2859 ZkCircuit::new(empty_witnesses(&dao_vote_burn_zkbin)?, &dao_vote_burn_zkbin); 2860 let dao_vote_main_circuit = 2861 ZkCircuit::new(empty_witnesses(&dao_vote_main_zkbin)?, &dao_vote_main_zkbin); 2862 2863 // Creating DAO VoteBurn and VoteMain circuits proving keys 2864 let dao_vote_burn_pk = ProvingKey::build(dao_vote_burn_zkbin.k, &dao_vote_burn_circuit); 2865 let dao_vote_main_pk = ProvingKey::build(dao_vote_main_zkbin.k, &dao_vote_main_circuit); 2866 2867 // Now create the parameters for the vote tx 2868 let signature_secret = SecretKey::random(&mut OsRng); 2869 let mut inputs = Vec::with_capacity(gov_owncoins_to_use.len()); 2870 for gov_owncoin in gov_owncoins_to_use { 2871 // Skip governance coins that are not part of the snapshot 2872 let Ok(merkle_path) = proposal 2873 .money_snapshot_tree 2874 .as_ref() 2875 .unwrap() 2876 .witness(gov_owncoin.leaf_position, 0) 2877 else { 2878 continue 2879 }; 2880 let nullifier = poseidon_hash([gov_owncoin.secret.inner(), gov_owncoin.coin.inner()]); 2881 let vote_nullifier = 2882 poseidon_hash([nullifier, gov_owncoin.secret.inner(), proposal_bulla.inner()]); 2883 if votes_nullifiers.contains(&vote_nullifier.into()) { 2884 return Err(Error::Custom("[dao_vote] Duplicate input nullifier found".to_string())) 2885 }; 2886 2887 let input = DaoVoteInput { 2888 secret: gov_owncoin.secret, 2889 note: gov_owncoin.note.clone(), 2890 leaf_position: gov_owncoin.leaf_position, 2891 merkle_path, 2892 signature_secret, 2893 }; 2894 inputs.push(input); 2895 } 2896 if inputs.is_empty() { 2897 return Err(Error::Custom(format!( 2898 "[dao_vote] Did not find any governance {} coins in wallet before proposal snapshot", 2899 dao.params.dao.gov_token_id 2900 ))) 2901 } 2902 2903 // Retrieve next block height and current block time target, 2904 // to compute their window. 2905 let next_block_height = self.get_next_block_height().await?; 2906 let block_target = self.get_block_target().await?; 2907 let current_blockwindow = blockwindow(next_block_height, block_target); 2908 2909 // Generate the Money nullifiers Sparse Merkle Tree 2910 let store = MemoryStorageFp { tree: proposal.nullifiers_smt_snapshot.unwrap() }; 2911 let money_null_smt = SmtMemoryFp::new(store, PoseidonFp::new(), &EMPTY_NODES_FP); 2912 2913 // Create the vote call 2914 let call = DaoVoteCall { 2915 money_null_smt: &money_null_smt, 2916 inputs, 2917 vote_option, 2918 proposal: proposal.proposal.clone(), 2919 dao: dao.params.dao.clone(), 2920 current_blockwindow, 2921 }; 2922 2923 let (params, proofs) = call.make( 2924 &dao_vote_burn_zkbin, 2925 &dao_vote_burn_pk, 2926 &dao_vote_main_zkbin, 2927 &dao_vote_main_pk, 2928 )?; 2929 2930 // Encode the call 2931 let mut data = vec![DaoFunction::Vote as u8]; 2932 params.encode_async(&mut data).await?; 2933 let call = ContractCall { contract_id: *DAO_CONTRACT_ID, data }; 2934 2935 // Create the TransactionBuilder containing above call 2936 let mut tx_builder = TransactionBuilder::new(ContractCallLeaf { call, proofs }, vec![])?; 2937 2938 // We first have to execute the fee-less tx to gather its used gas, and then we feed 2939 // it into the fee-creating function. 2940 let mut tx = tx_builder.build()?; 2941 let sigs = tx.create_sigs(&[signature_secret])?; 2942 tx.signatures = vec![sigs]; 2943 2944 let tree = self.get_money_tree().await?; 2945 let (fee_call, fee_proofs, fee_secrets) = 2946 self.append_fee_call(&tx, &tree, &fee_pk, &fee_zkbin, None).await?; 2947 2948 // Append the fee call to the transaction 2949 tx_builder.append(ContractCallLeaf { call: fee_call, proofs: fee_proofs }, vec![])?; 2950 2951 // Now build the actual transaction and sign it with all necessary keys. 2952 let mut tx = tx_builder.build()?; 2953 let sigs = tx.create_sigs(&[signature_secret])?; 2954 tx.signatures.push(sigs); 2955 let sigs = tx.create_sigs(&fee_secrets)?; 2956 tx.signatures.push(sigs); 2957 2958 Ok(tx) 2959 } 2960 2961 /// Execute a DAO transfer proposal. 2962 pub async fn dao_exec_transfer( 2963 &self, 2964 proposal: &ProposalRecord, 2965 early: bool, 2966 ) -> Result<Transaction> { 2967 if proposal.leaf_position.is_none() || 2968 proposal.money_snapshot_tree.is_none() || 2969 proposal.nullifiers_smt_snapshot.is_none() || 2970 proposal.tx_hash.is_none() || 2971 proposal.call_index.is_none() 2972 { 2973 return Err(Error::Custom( 2974 "[dao_exec_transfer] Proposal seems to not have been deployed yet".to_string(), 2975 )) 2976 } 2977 2978 // Check proposal is not executed 2979 if let Some(exec_tx_hash) = proposal.exec_tx_hash { 2980 return Err(Error::Custom(format!( 2981 "[dao_exec_transfer] Proposal was executed on transaction: {exec_tx_hash}" 2982 ))) 2983 } 2984 2985 // Check we know the plaintext data and they are valid 2986 if proposal.data.is_none() { 2987 return Err(Error::Custom( 2988 "[dao_exec_transfer] Proposal plainext data is empty".to_string(), 2989 )) 2990 } 2991 let proposal_coinattrs: CoinAttributes = 2992 deserialize_async(proposal.data.as_ref().unwrap()).await?; 2993 2994 // Fetch DAO and check its deployed 2995 let Ok(dao) = self.get_dao_by_bulla(&proposal.proposal.dao_bulla).await else { 2996 return Err(Error::Custom(format!( 2997 "[dao_exec_transfer] DAO {} was not found", 2998 proposal.proposal.dao_bulla 2999 ))) 3000 }; 3001 if dao.leaf_position.is_none() || dao.tx_hash.is_none() || dao.call_index.is_none() { 3002 return Err(Error::Custom( 3003 "[dao_exec_transfer] DAO seems to not have been deployed yet".to_string(), 3004 )) 3005 } 3006 3007 // Check that we have the exec key 3008 if dao.params.exec_secret_key.is_none() { 3009 return Err(Error::Custom( 3010 "[dao_exec_transfer] We need the exec secret key to execute proposals for this DAO" 3011 .to_string(), 3012 )) 3013 } 3014 3015 // If early flag is provided, check that we have the early exec key 3016 if early && dao.params.early_exec_secret_key.is_none() { 3017 return Err(Error::Custom( 3018 "[dao_exec_transfer] We need the early exec secret key to execute proposals early for this DAO" 3019 .to_string(), 3020 )) 3021 } 3022 3023 // Check proposal is approved 3024 let votes = self.get_dao_proposal_votes(&proposal.bulla()).await?; 3025 let mut yes_vote_value = 0; 3026 let mut yes_vote_blind = Blind::ZERO; 3027 let mut all_vote_value = 0; 3028 let mut all_vote_blind = Blind::ZERO; 3029 for vote in votes { 3030 if vote.vote_option { 3031 yes_vote_value += vote.all_vote_value; 3032 }; 3033 yes_vote_blind += vote.yes_vote_blind; 3034 all_vote_value += vote.all_vote_value; 3035 all_vote_blind += vote.all_vote_blind; 3036 } 3037 let approval_ratio = (yes_vote_value as f64 * 100.0) / all_vote_value as f64; 3038 if all_vote_value < dao.params.dao.quorum || 3039 approval_ratio < 3040 (dao.params.dao.approval_ratio_quot / dao.params.dao.approval_ratio_base) 3041 as f64 3042 { 3043 return Err(Error::Custom( 3044 "[dao_exec_transfer] Proposal is not approved yet".to_string(), 3045 )) 3046 }; 3047 3048 // Fetch DAO unspent OwnCoins to see what its balance is for the coin 3049 let dao_spend_hook = 3050 FuncRef { contract_id: *DAO_CONTRACT_ID, func_code: DaoFunction::Exec as u8 } 3051 .to_func_id(); 3052 let dao_owncoins = self 3053 .get_contract_token_coins( 3054 &proposal_coinattrs.token_id, 3055 &dao_spend_hook, 3056 &proposal.proposal.dao_bulla.inner(), 3057 ) 3058 .await?; 3059 if dao_owncoins.is_empty() { 3060 return Err(Error::Custom(format!( 3061 "[dao_exec_transfer] Did not find any {} unspent coins owned by this DAO", 3062 proposal_coinattrs.token_id, 3063 ))) 3064 } 3065 3066 // Check DAO balance is sufficient 3067 if dao_owncoins.iter().map(|x| x.note.value).sum::<u64>() < proposal_coinattrs.value { 3068 return Err(Error::Custom(format!( 3069 "[dao_exec_transfer] Not enough DAO balance for token ID: {}", 3070 proposal_coinattrs.token_id, 3071 ))) 3072 } 3073 3074 // Find which DAO coins we can use 3075 let (spent_coins, change_value) = select_coins(dao_owncoins, proposal_coinattrs.value)?; 3076 3077 // Now we need to do a lookup for the zkas proof bincodes, and create 3078 // the circuit objects and proving keys so we can build the transaction. 3079 // We also do this through the RPC. First we grab the calls from money. 3080 let zkas_bins = self.lookup_zkas(&MONEY_CONTRACT_ID).await?; 3081 3082 let Some(mint_zkbin) = zkas_bins.iter().find(|x| x.0 == MONEY_CONTRACT_ZKAS_MINT_NS_V1) 3083 else { 3084 return Err(Error::Custom("[dao_exec_transfer] Mint circuit not found".to_string())) 3085 }; 3086 3087 let Some(burn_zkbin) = zkas_bins.iter().find(|x| x.0 == MONEY_CONTRACT_ZKAS_BURN_NS_V1) 3088 else { 3089 return Err(Error::Custom("[dao_exec_transfer] Burn circuit not found".to_string())) 3090 }; 3091 3092 let Some(fee_zkbin) = zkas_bins.iter().find(|x| x.0 == MONEY_CONTRACT_ZKAS_FEE_NS_V1) 3093 else { 3094 return Err(Error::Custom("[dao_exec_transfer] Fee circuit not found".to_string())) 3095 }; 3096 3097 let mint_zkbin = ZkBinary::decode(&mint_zkbin.1)?; 3098 let burn_zkbin = ZkBinary::decode(&burn_zkbin.1)?; 3099 let fee_zkbin = ZkBinary::decode(&fee_zkbin.1)?; 3100 3101 let mint_circuit = ZkCircuit::new(empty_witnesses(&mint_zkbin)?, &mint_zkbin); 3102 let burn_circuit = ZkCircuit::new(empty_witnesses(&burn_zkbin)?, &burn_zkbin); 3103 let fee_circuit = ZkCircuit::new(empty_witnesses(&fee_zkbin)?, &fee_zkbin); 3104 3105 // Creating Mint, Burn and Fee circuits proving keys 3106 let mint_pk = ProvingKey::build(mint_zkbin.k, &mint_circuit); 3107 let burn_pk = ProvingKey::build(burn_zkbin.k, &burn_circuit); 3108 let fee_pk = ProvingKey::build(fee_zkbin.k, &fee_circuit); 3109 3110 // Now we grab the DAO bins 3111 let zkas_bins = self.lookup_zkas(&DAO_CONTRACT_ID).await?; 3112 3113 let (namespace, early_exec_secret_key) = match early { 3114 true => ( 3115 DAO_CONTRACT_ZKAS_DAO_EARLY_EXEC_NS, 3116 Some(dao.params.early_exec_secret_key.unwrap()), 3117 ), 3118 false => (DAO_CONTRACT_ZKAS_DAO_EXEC_NS, None), 3119 }; 3120 3121 let Some(dao_exec_zkbin) = zkas_bins.iter().find(|x| x.0 == namespace) else { 3122 return Err(Error::Custom(format!( 3123 "[dao_exec_transfer] DAO {namespace} circuit not found" 3124 ))) 3125 }; 3126 3127 let Some(dao_auth_transfer_zkbin) = 3128 zkas_bins.iter().find(|x| x.0 == DAO_CONTRACT_ZKAS_DAO_AUTH_MONEY_TRANSFER_NS) 3129 else { 3130 return Err(Error::Custom( 3131 "[dao_exec_transfer] DAO AuthTransfer circuit not found".to_string(), 3132 )) 3133 }; 3134 3135 let Some(dao_auth_transfer_enc_coin_zkbin) = 3136 zkas_bins.iter().find(|x| x.0 == DAO_CONTRACT_ZKAS_DAO_AUTH_MONEY_TRANSFER_ENC_COIN_NS) 3137 else { 3138 return Err(Error::Custom( 3139 "[dao_exec_transfer] DAO AuthTransferEncCoin circuit not found".to_string(), 3140 )) 3141 }; 3142 3143 let dao_exec_zkbin = ZkBinary::decode(&dao_exec_zkbin.1)?; 3144 let dao_auth_transfer_zkbin = ZkBinary::decode(&dao_auth_transfer_zkbin.1)?; 3145 let dao_auth_transfer_enc_coin_zkbin = 3146 ZkBinary::decode(&dao_auth_transfer_enc_coin_zkbin.1)?; 3147 3148 let dao_exec_circuit = ZkCircuit::new(empty_witnesses(&dao_exec_zkbin)?, &dao_exec_zkbin); 3149 let dao_auth_transfer_circuit = 3150 ZkCircuit::new(empty_witnesses(&dao_auth_transfer_zkbin)?, &dao_auth_transfer_zkbin); 3151 let dao_auth_transfer_enc_coin_circuit = ZkCircuit::new( 3152 empty_witnesses(&dao_auth_transfer_enc_coin_zkbin)?, 3153 &dao_auth_transfer_enc_coin_zkbin, 3154 ); 3155 3156 // Creating DAO Exec, AuthTransfer and AuthTransferEncCoin circuits proving keys 3157 let dao_exec_pk = ProvingKey::build(dao_exec_zkbin.k, &dao_exec_circuit); 3158 let dao_auth_transfer_pk = 3159 ProvingKey::build(dao_auth_transfer_zkbin.k, &dao_auth_transfer_circuit); 3160 let dao_auth_transfer_enc_coin_pk = ProvingKey::build( 3161 dao_auth_transfer_enc_coin_zkbin.k, 3162 &dao_auth_transfer_enc_coin_circuit, 3163 ); 3164 3165 // Fetch our money Merkle tree 3166 let tree = self.get_money_tree().await?; 3167 3168 // Retrieve next block height and current block time target, 3169 // to compute their window. 3170 let next_block_height = self.get_next_block_height().await?; 3171 let block_target = self.get_block_target().await?; 3172 let current_blockwindow = blockwindow(next_block_height, block_target); 3173 3174 // Now we can create the transfer call parameters 3175 let input_user_data_blind = Blind::random(&mut OsRng); 3176 let mut inputs = vec![]; 3177 for coin in &spent_coins { 3178 inputs.push(TransferCallInput { 3179 coin: coin.clone(), 3180 merkle_path: tree.witness(coin.leaf_position, 0).unwrap(), 3181 user_data_blind: input_user_data_blind, 3182 }); 3183 } 3184 3185 let mut outputs = vec![]; 3186 outputs.push(proposal_coinattrs.clone()); 3187 3188 let dao_coin_attrs = CoinAttributes { 3189 public_key: dao.params.dao.notes_public_key, 3190 value: change_value, 3191 token_id: proposal_coinattrs.token_id, 3192 spend_hook: dao_spend_hook, 3193 user_data: proposal.proposal.dao_bulla.inner(), 3194 blind: Blind::random(&mut OsRng), 3195 }; 3196 outputs.push(dao_coin_attrs.clone()); 3197 3198 // Create the transfer call 3199 let transfer_builder = TransferCallBuilder { 3200 clear_inputs: vec![], 3201 inputs, 3202 outputs, 3203 mint_zkbin: mint_zkbin.clone(), 3204 mint_pk: mint_pk.clone(), 3205 burn_zkbin: burn_zkbin.clone(), 3206 burn_pk: burn_pk.clone(), 3207 }; 3208 let (transfer_params, transfer_secrets) = transfer_builder.build()?; 3209 3210 // Encode the call 3211 let mut data = vec![MoneyFunction::TransferV1 as u8]; 3212 transfer_params.encode_async(&mut data).await?; 3213 let transfer_call = ContractCall { contract_id: *MONEY_CONTRACT_ID, data }; 3214 3215 // Create the exec call 3216 let exec_signature_secret = SecretKey::random(&mut OsRng); 3217 let exec_builder = DaoExecCall { 3218 proposal: proposal.proposal.clone(), 3219 dao: dao.params.dao.clone(), 3220 yes_vote_value, 3221 all_vote_value, 3222 yes_vote_blind, 3223 all_vote_blind, 3224 signature_secret: exec_signature_secret, 3225 current_blockwindow, 3226 }; 3227 let (exec_params, exec_proofs) = exec_builder.make( 3228 &dao.params.exec_secret_key.unwrap(), 3229 &early_exec_secret_key, 3230 &dao_exec_zkbin, 3231 &dao_exec_pk, 3232 )?; 3233 3234 // Encode the call 3235 let mut data = vec![DaoFunction::Exec as u8]; 3236 exec_params.encode_async(&mut data).await?; 3237 let exec_call = ContractCall { contract_id: *DAO_CONTRACT_ID, data }; 3238 3239 // Now we can create the auth call 3240 // Auth module 3241 let auth_transfer_builder = DaoAuthMoneyTransferCall { 3242 proposal: proposal.proposal.clone(), 3243 proposal_coinattrs: vec![proposal_coinattrs], 3244 dao: dao.params.dao.clone(), 3245 input_user_data_blind, 3246 dao_coin_attrs, 3247 }; 3248 let (auth_transfer_params, auth_transfer_proofs) = auth_transfer_builder.make( 3249 &dao_auth_transfer_zkbin, 3250 &dao_auth_transfer_pk, 3251 &dao_auth_transfer_enc_coin_zkbin, 3252 &dao_auth_transfer_enc_coin_pk, 3253 )?; 3254 3255 // Encode the call 3256 let mut data = vec![DaoFunction::AuthMoneyTransfer as u8]; 3257 auth_transfer_params.encode_async(&mut data).await?; 3258 let auth_transfer_call = ContractCall { contract_id: *DAO_CONTRACT_ID, data }; 3259 3260 // Create the TransactionBuilder containing above calls 3261 let mut tx_builder = TransactionBuilder::new( 3262 ContractCallLeaf { call: exec_call, proofs: exec_proofs }, 3263 vec![ 3264 DarkTree::new( 3265 ContractCallLeaf { call: auth_transfer_call, proofs: auth_transfer_proofs }, 3266 vec![], 3267 None, 3268 None, 3269 ), 3270 DarkTree::new( 3271 ContractCallLeaf { call: transfer_call, proofs: transfer_secrets.proofs }, 3272 vec![], 3273 None, 3274 None, 3275 ), 3276 ], 3277 )?; 3278 3279 // We first have to execute the fee-less tx to gather its used gas, and then we feed 3280 // it into the fee-creating function. 3281 let mut tx = tx_builder.build()?; 3282 let auth_transfer_sigs = tx.create_sigs(&[])?; 3283 let transfer_sigs = tx.create_sigs(&transfer_secrets.signature_secrets)?; 3284 let exec_sigs = tx.create_sigs(&[exec_signature_secret])?; 3285 tx.signatures = vec![auth_transfer_sigs, transfer_sigs, exec_sigs]; 3286 3287 let (fee_call, fee_proofs, fee_secrets) = 3288 self.append_fee_call(&tx, &tree, &fee_pk, &fee_zkbin, None).await?; 3289 3290 // Append the fee call to the transaction 3291 tx_builder.append(ContractCallLeaf { call: fee_call, proofs: fee_proofs }, vec![])?; 3292 3293 // Now build the actual transaction and sign it with all necessary keys. 3294 let mut tx = tx_builder.build()?; 3295 let sigs = tx.create_sigs(&[])?; 3296 tx.signatures.push(sigs); 3297 let sigs = tx.create_sigs(&transfer_secrets.signature_secrets)?; 3298 tx.signatures.push(sigs); 3299 let sigs = tx.create_sigs(&[exec_signature_secret])?; 3300 tx.signatures.push(sigs); 3301 let sigs = tx.create_sigs(&fee_secrets)?; 3302 tx.signatures.push(sigs); 3303 3304 Ok(tx) 3305 } 3306 3307 /// Execute a DAO generic proposal. 3308 pub async fn dao_exec_generic( 3309 &self, 3310 proposal: &ProposalRecord, 3311 early: bool, 3312 ) -> Result<Transaction> { 3313 if proposal.leaf_position.is_none() || 3314 proposal.money_snapshot_tree.is_none() || 3315 proposal.nullifiers_smt_snapshot.is_none() || 3316 proposal.tx_hash.is_none() || 3317 proposal.call_index.is_none() 3318 { 3319 return Err(Error::Custom( 3320 "[dao_exec_generic] Proposal seems to not have been deployed yet".to_string(), 3321 )) 3322 } 3323 3324 // Check proposal is not executed 3325 if let Some(exec_tx_hash) = proposal.exec_tx_hash { 3326 return Err(Error::Custom(format!( 3327 "[dao_exec_generic] Proposal was executed on transaction: {exec_tx_hash}" 3328 ))) 3329 } 3330 3331 // Fetch DAO and check its deployed 3332 let Ok(dao) = self.get_dao_by_bulla(&proposal.proposal.dao_bulla).await else { 3333 return Err(Error::Custom(format!( 3334 "[dao_exec_generic] DAO {} was not found", 3335 proposal.proposal.dao_bulla 3336 ))) 3337 }; 3338 if dao.leaf_position.is_none() || dao.tx_hash.is_none() || dao.call_index.is_none() { 3339 return Err(Error::Custom( 3340 "[dao_exec_generic] DAO seems to not have been deployed yet".to_string(), 3341 )) 3342 } 3343 3344 // Check that we have the exec key 3345 if dao.params.exec_secret_key.is_none() { 3346 return Err(Error::Custom( 3347 "[dao_exec_generic] We need the exec secret key to execute proposals for this DAO" 3348 .to_string(), 3349 )) 3350 } 3351 3352 // If early flag is provided, check that we have the early exec key 3353 if early && dao.params.early_exec_secret_key.is_none() { 3354 return Err(Error::Custom( 3355 "[dao_exec_generic] We need the early exec secret key to execute proposals early for this DAO" 3356 .to_string(), 3357 )) 3358 } 3359 3360 // Check proposal is approved 3361 let votes = self.get_dao_proposal_votes(&proposal.bulla()).await?; 3362 let mut yes_vote_value = 0; 3363 let mut yes_vote_blind = Blind::ZERO; 3364 let mut all_vote_value = 0; 3365 let mut all_vote_blind = Blind::ZERO; 3366 for vote in votes { 3367 if vote.vote_option { 3368 yes_vote_value += vote.all_vote_value; 3369 }; 3370 yes_vote_blind += vote.yes_vote_blind; 3371 all_vote_value += vote.all_vote_value; 3372 all_vote_blind += vote.all_vote_blind; 3373 } 3374 let approval_ratio = (yes_vote_value as f64 * 100.0) / all_vote_value as f64; 3375 if all_vote_value < dao.params.dao.quorum || 3376 approval_ratio < 3377 (dao.params.dao.approval_ratio_quot / dao.params.dao.approval_ratio_base) 3378 as f64 3379 { 3380 return Err(Error::Custom("[dao_exec_generic] Proposal is not approved yet".to_string())) 3381 }; 3382 3383 // Now we need to do a lookup for the zkas proof bincodes, and create 3384 // the circuit objects and proving keys so we can build the transaction. 3385 // We also do this through the RPC. First we grab the calls from money. 3386 let zkas_bins = self.lookup_zkas(&MONEY_CONTRACT_ID).await?; 3387 let Some(fee_zkbin) = zkas_bins.iter().find(|x| x.0 == MONEY_CONTRACT_ZKAS_FEE_NS_V1) 3388 else { 3389 return Err(Error::Custom("[dao_exec_generic] Fee circuit not found".to_string())) 3390 }; 3391 let fee_zkbin = ZkBinary::decode(&fee_zkbin.1)?; 3392 let fee_circuit = ZkCircuit::new(empty_witnesses(&fee_zkbin)?, &fee_zkbin); 3393 let fee_pk = ProvingKey::build(fee_zkbin.k, &fee_circuit); 3394 3395 // Now we grab the DAO bins 3396 let zkas_bins = self.lookup_zkas(&DAO_CONTRACT_ID).await?; 3397 3398 let (namespace, early_exec_secret_key) = match early { 3399 true => ( 3400 DAO_CONTRACT_ZKAS_DAO_EARLY_EXEC_NS, 3401 Some(dao.params.early_exec_secret_key.unwrap()), 3402 ), 3403 false => (DAO_CONTRACT_ZKAS_DAO_EXEC_NS, None), 3404 }; 3405 3406 let Some(dao_exec_zkbin) = zkas_bins.iter().find(|x| x.0 == namespace) else { 3407 return Err(Error::Custom(format!( 3408 "[dao_exec_generic] DAO {namespace} circuit not found" 3409 ))) 3410 }; 3411 let dao_exec_zkbin = ZkBinary::decode(&dao_exec_zkbin.1)?; 3412 let dao_exec_circuit = ZkCircuit::new(empty_witnesses(&dao_exec_zkbin)?, &dao_exec_zkbin); 3413 let dao_exec_pk = ProvingKey::build(dao_exec_zkbin.k, &dao_exec_circuit); 3414 3415 // Fetch our money Merkle tree 3416 let tree = self.get_money_tree().await?; 3417 3418 // Retrieve next block height and current block time target, 3419 // to compute their window. 3420 let next_block_height = self.get_next_block_height().await?; 3421 let block_target = self.get_block_target().await?; 3422 let current_blockwindow = blockwindow(next_block_height, block_target); 3423 3424 // Create the exec call 3425 let exec_signature_secret = SecretKey::random(&mut OsRng); 3426 let exec_builder = DaoExecCall { 3427 proposal: proposal.proposal.clone(), 3428 dao: dao.params.dao.clone(), 3429 yes_vote_value, 3430 all_vote_value, 3431 yes_vote_blind, 3432 all_vote_blind, 3433 signature_secret: exec_signature_secret, 3434 current_blockwindow, 3435 }; 3436 let (exec_params, exec_proofs) = exec_builder.make( 3437 &dao.params.exec_secret_key.unwrap(), 3438 &early_exec_secret_key, 3439 &dao_exec_zkbin, 3440 &dao_exec_pk, 3441 )?; 3442 3443 // Encode the call 3444 let mut data = vec![DaoFunction::Exec as u8]; 3445 exec_params.encode_async(&mut data).await?; 3446 let exec_call = ContractCall { contract_id: *DAO_CONTRACT_ID, data }; 3447 3448 // Create the TransactionBuilder containing above calls 3449 let mut tx_builder = TransactionBuilder::new( 3450 ContractCallLeaf { call: exec_call, proofs: exec_proofs }, 3451 vec![], 3452 )?; 3453 3454 // We first have to execute the fee-less tx to gather its used gas, and then we feed 3455 // it into the fee-creating function. 3456 let mut tx = tx_builder.build()?; 3457 let exec_sigs = tx.create_sigs(&[exec_signature_secret])?; 3458 tx.signatures = vec![exec_sigs]; 3459 3460 let (fee_call, fee_proofs, fee_secrets) = 3461 self.append_fee_call(&tx, &tree, &fee_pk, &fee_zkbin, None).await?; 3462 3463 // Append the fee call to the transaction 3464 tx_builder.append(ContractCallLeaf { call: fee_call, proofs: fee_proofs }, vec![])?; 3465 3466 // Now build the actual transaction and sign it with all necessary keys. 3467 let mut tx = tx_builder.build()?; 3468 let sigs = tx.create_sigs(&[exec_signature_secret])?; 3469 tx.signatures.push(sigs); 3470 let sigs = tx.create_sigs(&fee_secrets)?; 3471 tx.signatures.push(sigs); 3472 3473 Ok(tx) 3474 } 3475 }