1use std::{collections::HashMap, fmt, str::FromStr};
20
21use lazy_static::lazy_static;
22use num_bigint::BigUint;
23use rand::rngs::OsRng;
24use rusqlite::types::Value;
25
26use 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};
33use 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};
49use 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};
55use 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};
69use darkfi_serial::{
70 async_trait, deserialize_async, serialize, serialize_async, AsyncEncodable, SerialDecodable,
71 SerialEncodable,
72};
73
74use 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
83pub const SLED_MERKLE_TREES_DAO_DAOS: &[u8] = b"_dao_daos";
85pub const SLED_MERKLE_TREES_DAO_PROPOSALS: &[u8] = b"_dao_proposals";
86
87lazy_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
97pub const DAO_DAOS_COL_BULLA: &str = "bulla";
99pub const DAO_DAOS_COL_NAME: &str = "name";
100pub const DAO_DAOS_COL_PARAMS: &str = "params";
101pub const DAO_DAOS_COL_LEAF_POSITION: &str = "leaf_position";
102pub const DAO_DAOS_COL_MINT_HEIGHT: &str = "mint_height";
103pub const DAO_DAOS_COL_TX_HASH: &str = "tx_hash";
104pub const DAO_DAOS_COL_CALL_INDEX: &str = "call_index";
105
106pub const DAO_PROPOSALS_COL_BULLA: &str = "bulla";
108pub const DAO_PROPOSALS_COL_DAO_BULLA: &str = "dao_bulla";
109pub const DAO_PROPOSALS_COL_PROPOSAL: &str = "proposal";
110pub const DAO_PROPOSALS_COL_DATA: &str = "data";
111pub const DAO_PROPOSALS_COL_LEAF_POSITION: &str = "leaf_position";
112pub const DAO_PROPOSALS_COL_MONEY_SNAPSHOT_TREE: &str = "money_snapshot_tree";
113pub const DAO_PROPOSALS_COL_NULLIFIERS_SMT_SNAPSHOT: &str = "nullifiers_smt_snapshot";
114pub const DAO_PROPOSALS_COL_MINT_HEIGHT: &str = "mint_height";
115pub const DAO_PROPOSALS_COL_TX_HASH: &str = "tx_hash";
116pub const DAO_PROPOSALS_COL_CALL_INDEX: &str = "call_index";
117pub const DAO_PROPOSALS_COL_EXEC_HEIGHT: &str = "exec_height";
118pub const DAO_PROPOSALS_COL_EXEC_TX_HASH: &str = "exec_tx_hash";
119
120pub const DAO_VOTES_COL_PROPOSAL_BULLA: &str = "proposal_bulla";
122pub const DAO_VOTES_COL_VOTE_OPTION: &str = "vote_option";
123pub const DAO_VOTES_COL_YES_VOTE_BLIND: &str = "yes_vote_blind";
124pub const DAO_VOTES_COL_ALL_VOTE_VALUE: &str = "all_vote_value";
125pub const DAO_VOTES_COL_ALL_VOTE_BLIND: &str = "all_vote_blind";
126pub const DAO_VOTES_COL_BLOCK_HEIGHT: &str = "block_height";
127pub const DAO_VOTES_COL_TX_HASH: &str = "tx_hash";
128pub const DAO_VOTES_COL_CALL_INDEX: &str = "call_index";
129pub const DAO_VOTES_COL_NULLIFIERS: &str = "nullifiers";
130
131#[derive(Debug, Clone, SerialEncodable, SerialDecodable)]
132pub struct DaoParams {
134 pub dao: Dao,
136 pub notes_secret_key: Option<SecretKey>,
138 pub proposer_secret_key: Option<SecretKey>,
140 pub proposals_secret_key: Option<SecretKey>,
142 pub votes_secret_key: Option<SecretKey>,
144 pub exec_secret_key: Option<SecretKey>,
146 pub early_exec_secret_key: Option<SecretKey>,
148}
149
150impl DaoParams {
151 #[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 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 pub fn from_toml_str(toml: &str) -> Result<Self> {
231 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 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 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 pub fn toml_str(&self) -> String {
495 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 ommited. 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 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 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
595impl fmt::Display for DaoParams {
596 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
597 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)]
673pub struct DaoRecord {
675 pub name: String,
677 pub params: DaoParams,
679 pub leaf_position: Option<bridgetree::Position>,
681 pub mint_height: Option<u32>,
683 pub tx_hash: Option<TransactionHash>,
685 pub call_index: Option<u8>,
687}
688
689impl 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
706impl fmt::Display for DaoRecord {
707 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
708 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 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)]
814pub struct ProposalRecord {
816 pub proposal: DaoProposal,
818 pub data: Option<Vec<u8>>,
820 pub leaf_position: Option<bridgetree::Position>,
822 pub money_snapshot_tree: Option<MerkleTree>,
824 pub nullifiers_smt_snapshot: Option<HashMap<BigUint, pallas::Base>>,
826 pub mint_height: Option<u32>,
828 pub tx_hash: Option<TransactionHash>,
830 pub call_index: Option<u8>,
832 pub exec_height: Option<u32>,
834 pub exec_tx_hash: Option<TransactionHash>,
836}
837
838impl ProposalRecord {
839 pub fn bulla(&self) -> DaoProposalBulla {
840 self.proposal.to_bulla()
841 }
842}
843
844impl 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)]
891pub struct VoteRecord {
893 pub id: u64,
895 pub proposal: DaoProposalBulla,
897 pub vote_option: bool,
899 pub yes_vote_blind: ScalarBlind,
901 pub all_vote_value: u64,
903 pub all_vote_blind: ScalarBlind,
905 pub block_height: u32,
907 pub tx_hash: TransactionHash,
909 pub call_index: u8,
911 pub nullifiers: Vec<Nullifier>,
913}
914
915impl Drk {
916 pub async fn initialize_dao(&self) -> WalletDbResult<()> {
918 let wallet_schema = include_str!("../dao.sql");
920 self.wallet.exec_batch_sql(wallet_schema)?;
921
922 Ok(())
923 }
924
925 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 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 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 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 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 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 scan_cache.dao_daos_tree.append(MerkleNode::from(new_bulla.inner()));
1190
1191 if !scan_cache.own_daos.contains_key(new_bulla) {
1193 return Ok(false)
1194 }
1195
1196 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 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 scan_cache.dao_proposals_tree.append(MerkleNode::from(params.proposal_bulla.inner()));
1233
1234 for (dao, (proposals_secret_key, _)) in &scan_cache.own_daos {
1237 let Some(proposals_secret_key) = proposals_secret_key else { continue };
1239
1240 let Ok(note) = params.note.decrypt::<DaoProposal>(proposals_secret_key) else {
1242 continue
1243 };
1244
1245 scan_cache.messages_buffer.push(format!(
1247 "[apply_dao_propose_data] Managed to decrypt proposal note for DAO: {dao}"
1248 ));
1249
1250 let our_proposal = if scan_cache.own_proposals.contains_key(¶ms.proposal_bulla) {
1252 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 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 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 let Some(dao_bulla) = scan_cache.own_proposals.get(¶ms.proposal_bulla) else {
1306 return Ok(false)
1307 };
1308
1309 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 let Some(votes_secret_key) = votes_secret_key else { return Ok(false) };
1319
1320 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 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, 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 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 if !scan_cache.own_proposals.contains_key(¶ms.proposal_bulla) {
1379 return Ok(false)
1380 }
1381
1382 let key = serialize_async(¶ms.proposal_bulla).await;
1384
1385 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 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 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 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 Ok(false)
1454 }
1455 }
1456 }
1457
1458 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 let key = serialize_async(dao).await;
1471
1472 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 let params = rusqlite::params![
1485 serialize(leaf_position),
1486 Some(*mint_height),
1487 serialize(tx_hash),
1488 call_index,
1489 key,
1490 ];
1491
1492 self.wallet.exec_sql(&query, params)
1494 }
1495
1496 pub async fn put_dao_proposal(&self, proposal: &ProposalRecord) -> Result<()> {
1498 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 let key = serialize_async(&proposal.bulla()).await;
1509
1510 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 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 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 pub async fn put_dao_vote(&self, vote: &VoteRecord) -> WalletDbResult<()> {
1586 println!("Importing DAO vote into wallet");
1587
1588 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 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 self.wallet.exec_sql(&query, params)?;
1618
1619 println!("DAO vote added to wallet");
1620
1621 Ok(())
1622 }
1623
1624 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 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 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 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 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 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 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 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 pub async fn import_dao(
1778 &self,
1779 name: &str,
1780 params: &DaoParams,
1781 output: &mut Vec<String>,
1782 ) -> Result<()> {
1783 let bulla = params.dao.to_bulla();
1785
1786 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 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 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 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 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 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 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 pub async fn get_dao_proposal_by_bulla(
1931 &self,
1932 bulla: &DaoProposalBulla,
1933 ) -> Result<ProposalRecord> {
1934 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 self.parse_dao_proposal(&row).await
1950 }
1951
1952 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 pub async fn dao_mint(&self, name: &str) -> Result<Transaction> {
2072 let dao = self.get_dao_by_name(name).await?;
2074
2075 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 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 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 let fee_pk = ProvingKey::build(fee_zkbin.k, &fee_circuit);
2111
2112 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 let dao_mint_pk = ProvingKey::build(dao_mint_zkbin.k, &dao_mint_circuit);
2126
2127 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 let mut tx_builder = TransactionBuilder::new(ContractCallLeaf { call, proofs }, vec![])?;
2146
2147 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 tx_builder.append(ContractCallLeaf { call: fee_call, proofs: fee_proofs }, vec![])?;
2159
2160 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 #[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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 pub async fn dao_transfer_proposal_tx(&self, proposal: &ProposalRecord) -> Result<Transaction> {
2347 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 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 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 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 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 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 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 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 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 let fee_pk = ProvingKey::build(fee_zkbin.k, &fee_circuit);
2449
2450 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 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 let money_merkle_tree = self.get_money_tree().await?;
2483
2484 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 let signature_secret = SecretKey::random(&mut OsRng);
2500
2501 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 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 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 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 let mut tx_builder = TransactionBuilder::new(ContractCallLeaf { call, proofs }, vec![])?;
2541
2542 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 tx_builder.append(ContractCallLeaf { call: fee_call, proofs: fee_proofs }, vec![])?;
2554
2555 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 pub async fn dao_generic_proposal_tx(&self, proposal: &ProposalRecord) -> Result<Transaction> {
2567 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 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 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 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 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 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 let fee_pk = ProvingKey::build(fee_zkbin.k, &fee_circuit);
2632
2633 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 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 let money_merkle_tree = self.get_money_tree().await?;
2666
2667 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 let signature_secret = SecretKey::random(&mut OsRng);
2683
2684 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 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 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 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 let mut tx_builder = TransactionBuilder::new(ContractCallLeaf { call, proofs }, vec![])?;
2724
2725 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 tx_builder.append(ContractCallLeaf { call: fee_call, proofs: fee_proofs }, vec![])?;
2737
2738 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 pub async fn dao_vote(
2750 &self,
2751 proposal_bulla: &DaoProposalBulla,
2752 vote_option: bool,
2753 weight: Option<u64>,
2754 ) -> Result<Transaction> {
2755 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 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 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 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 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 let gov_owncoins_to_use = match weight {
2812 Some(_weight) => {
2813 return Err(Error::Custom(
2816 "[dao_vote] Fractional vote weight not supported yet".to_string(),
2817 ))
2818 }
2819 None => gov_owncoins,
2821 };
2822
2823 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 let fee_pk = ProvingKey::build(fee_zkbin.k, &fee_circuit);
2839
2840 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 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 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 let nullifier = poseidon_hash([gov_owncoin.secret.inner(), gov_owncoin.coin.inner()]);
2872 let vote_nullifier =
2873 poseidon_hash([nullifier, gov_owncoin.secret.inner(), proposal_bulla.inner()]);
2874 if votes_nullifiers.contains(&vote_nullifier.into()) {
2875 return Err(Error::Custom("[dao_vote] Duplicate input nullifier found".to_string()))
2876 };
2877
2878 let input = DaoVoteInput {
2879 secret: gov_owncoin.secret,
2880 note: gov_owncoin.note.clone(),
2881 leaf_position: gov_owncoin.leaf_position,
2882 merkle_path: proposal
2883 .money_snapshot_tree
2884 .as_ref()
2885 .unwrap()
2886 .witness(gov_owncoin.leaf_position, 0)
2887 .unwrap(),
2888 signature_secret,
2889 };
2890 inputs.push(input);
2891 }
2892
2893 let next_block_height = self.get_next_block_height().await?;
2896 let block_target = self.get_block_target().await?;
2897 let current_blockwindow = blockwindow(next_block_height, block_target);
2898
2899 let store = MemoryStorageFp { tree: proposal.nullifiers_smt_snapshot.unwrap() };
2901 let money_null_smt = SmtMemoryFp::new(store, PoseidonFp::new(), &EMPTY_NODES_FP);
2902
2903 let call = DaoVoteCall {
2905 money_null_smt: &money_null_smt,
2906 inputs,
2907 vote_option,
2908 proposal: proposal.proposal.clone(),
2909 dao: dao.params.dao.clone(),
2910 current_blockwindow,
2911 };
2912
2913 let (params, proofs) = call.make(
2914 &dao_vote_burn_zkbin,
2915 &dao_vote_burn_pk,
2916 &dao_vote_main_zkbin,
2917 &dao_vote_main_pk,
2918 )?;
2919
2920 let mut data = vec![DaoFunction::Vote as u8];
2922 params.encode_async(&mut data).await?;
2923 let call = ContractCall { contract_id: *DAO_CONTRACT_ID, data };
2924
2925 let mut tx_builder = TransactionBuilder::new(ContractCallLeaf { call, proofs }, vec![])?;
2927
2928 let mut tx = tx_builder.build()?;
2931 let sigs = tx.create_sigs(&[signature_secret])?;
2932 tx.signatures = vec![sigs];
2933
2934 let tree = self.get_money_tree().await?;
2935 let (fee_call, fee_proofs, fee_secrets) =
2936 self.append_fee_call(&tx, &tree, &fee_pk, &fee_zkbin, None).await?;
2937
2938 tx_builder.append(ContractCallLeaf { call: fee_call, proofs: fee_proofs }, vec![])?;
2940
2941 let mut tx = tx_builder.build()?;
2943 let sigs = tx.create_sigs(&[signature_secret])?;
2944 tx.signatures.push(sigs);
2945 let sigs = tx.create_sigs(&fee_secrets)?;
2946 tx.signatures.push(sigs);
2947
2948 Ok(tx)
2949 }
2950
2951 pub async fn dao_exec_transfer(
2953 &self,
2954 proposal: &ProposalRecord,
2955 early: bool,
2956 ) -> Result<Transaction> {
2957 if proposal.leaf_position.is_none() ||
2958 proposal.money_snapshot_tree.is_none() ||
2959 proposal.nullifiers_smt_snapshot.is_none() ||
2960 proposal.tx_hash.is_none() ||
2961 proposal.call_index.is_none()
2962 {
2963 return Err(Error::Custom(
2964 "[dao_exec_transfer] Proposal seems to not have been deployed yet".to_string(),
2965 ))
2966 }
2967
2968 if let Some(exec_tx_hash) = proposal.exec_tx_hash {
2970 return Err(Error::Custom(format!(
2971 "[dao_exec_transfer] Proposal was executed on transaction: {exec_tx_hash}"
2972 )))
2973 }
2974
2975 if proposal.data.is_none() {
2977 return Err(Error::Custom(
2978 "[dao_exec_transfer] Proposal plainext data is empty".to_string(),
2979 ))
2980 }
2981 let proposal_coinattrs: CoinAttributes =
2982 deserialize_async(proposal.data.as_ref().unwrap()).await?;
2983
2984 let Ok(dao) = self.get_dao_by_bulla(&proposal.proposal.dao_bulla).await else {
2986 return Err(Error::Custom(format!(
2987 "[dao_exec_transfer] DAO {} was not found",
2988 proposal.proposal.dao_bulla
2989 )))
2990 };
2991 if dao.leaf_position.is_none() || dao.tx_hash.is_none() || dao.call_index.is_none() {
2992 return Err(Error::Custom(
2993 "[dao_exec_transfer] DAO seems to not have been deployed yet".to_string(),
2994 ))
2995 }
2996
2997 if dao.params.exec_secret_key.is_none() {
2999 return Err(Error::Custom(
3000 "[dao_exec_transfer] We need the exec secret key to execute proposals for this DAO"
3001 .to_string(),
3002 ))
3003 }
3004
3005 if early && dao.params.early_exec_secret_key.is_none() {
3007 return Err(Error::Custom(
3008 "[dao_exec_transfer] We need the early exec secret key to execute proposals early for this DAO"
3009 .to_string(),
3010 ))
3011 }
3012
3013 let votes = self.get_dao_proposal_votes(&proposal.bulla()).await?;
3015 let mut yes_vote_value = 0;
3016 let mut yes_vote_blind = Blind::ZERO;
3017 let mut all_vote_value = 0;
3018 let mut all_vote_blind = Blind::ZERO;
3019 for vote in votes {
3020 if vote.vote_option {
3021 yes_vote_value += vote.all_vote_value;
3022 };
3023 yes_vote_blind += vote.yes_vote_blind;
3024 all_vote_value += vote.all_vote_value;
3025 all_vote_blind += vote.all_vote_blind;
3026 }
3027 let approval_ratio = (yes_vote_value as f64 * 100.0) / all_vote_value as f64;
3028 if all_vote_value < dao.params.dao.quorum ||
3029 approval_ratio <
3030 (dao.params.dao.approval_ratio_quot / dao.params.dao.approval_ratio_base)
3031 as f64
3032 {
3033 return Err(Error::Custom(
3034 "[dao_exec_transfer] Proposal is not approved yet".to_string(),
3035 ))
3036 };
3037
3038 let dao_spend_hook =
3040 FuncRef { contract_id: *DAO_CONTRACT_ID, func_code: DaoFunction::Exec as u8 }
3041 .to_func_id();
3042 let dao_owncoins = self
3043 .get_contract_token_coins(
3044 &proposal_coinattrs.token_id,
3045 &dao_spend_hook,
3046 &proposal.proposal.dao_bulla.inner(),
3047 )
3048 .await?;
3049 if dao_owncoins.is_empty() {
3050 return Err(Error::Custom(format!(
3051 "[dao_exec_transfer] Did not find any {} unspent coins owned by this DAO",
3052 proposal_coinattrs.token_id,
3053 )))
3054 }
3055
3056 if dao_owncoins.iter().map(|x| x.note.value).sum::<u64>() < proposal_coinattrs.value {
3058 return Err(Error::Custom(format!(
3059 "[dao_exec_transfer] Not enough DAO balance for token ID: {}",
3060 proposal_coinattrs.token_id,
3061 )))
3062 }
3063
3064 let (spent_coins, change_value) = select_coins(dao_owncoins, proposal_coinattrs.value)?;
3066
3067 let zkas_bins = self.lookup_zkas(&MONEY_CONTRACT_ID).await?;
3071
3072 let Some(mint_zkbin) = zkas_bins.iter().find(|x| x.0 == MONEY_CONTRACT_ZKAS_MINT_NS_V1)
3073 else {
3074 return Err(Error::Custom("[dao_exec_transfer] Mint circuit not found".to_string()))
3075 };
3076
3077 let Some(burn_zkbin) = zkas_bins.iter().find(|x| x.0 == MONEY_CONTRACT_ZKAS_BURN_NS_V1)
3078 else {
3079 return Err(Error::Custom("[dao_exec_transfer] Burn circuit not found".to_string()))
3080 };
3081
3082 let Some(fee_zkbin) = zkas_bins.iter().find(|x| x.0 == MONEY_CONTRACT_ZKAS_FEE_NS_V1)
3083 else {
3084 return Err(Error::Custom("[dao_exec_transfer] Fee circuit not found".to_string()))
3085 };
3086
3087 let mint_zkbin = ZkBinary::decode(&mint_zkbin.1)?;
3088 let burn_zkbin = ZkBinary::decode(&burn_zkbin.1)?;
3089 let fee_zkbin = ZkBinary::decode(&fee_zkbin.1)?;
3090
3091 let mint_circuit = ZkCircuit::new(empty_witnesses(&mint_zkbin)?, &mint_zkbin);
3092 let burn_circuit = ZkCircuit::new(empty_witnesses(&burn_zkbin)?, &burn_zkbin);
3093 let fee_circuit = ZkCircuit::new(empty_witnesses(&fee_zkbin)?, &fee_zkbin);
3094
3095 let mint_pk = ProvingKey::build(mint_zkbin.k, &mint_circuit);
3097 let burn_pk = ProvingKey::build(burn_zkbin.k, &burn_circuit);
3098 let fee_pk = ProvingKey::build(fee_zkbin.k, &fee_circuit);
3099
3100 let zkas_bins = self.lookup_zkas(&DAO_CONTRACT_ID).await?;
3102
3103 let (namespace, early_exec_secret_key) = match early {
3104 true => (
3105 DAO_CONTRACT_ZKAS_DAO_EARLY_EXEC_NS,
3106 Some(dao.params.early_exec_secret_key.unwrap()),
3107 ),
3108 false => (DAO_CONTRACT_ZKAS_DAO_EXEC_NS, None),
3109 };
3110
3111 let Some(dao_exec_zkbin) = zkas_bins.iter().find(|x| x.0 == namespace) else {
3112 return Err(Error::Custom(format!(
3113 "[dao_exec_transfer] DAO {namespace} circuit not found"
3114 )))
3115 };
3116
3117 let Some(dao_auth_transfer_zkbin) =
3118 zkas_bins.iter().find(|x| x.0 == DAO_CONTRACT_ZKAS_DAO_AUTH_MONEY_TRANSFER_NS)
3119 else {
3120 return Err(Error::Custom(
3121 "[dao_exec_transfer] DAO AuthTransfer circuit not found".to_string(),
3122 ))
3123 };
3124
3125 let Some(dao_auth_transfer_enc_coin_zkbin) =
3126 zkas_bins.iter().find(|x| x.0 == DAO_CONTRACT_ZKAS_DAO_AUTH_MONEY_TRANSFER_ENC_COIN_NS)
3127 else {
3128 return Err(Error::Custom(
3129 "[dao_exec_transfer] DAO AuthTransferEncCoin circuit not found".to_string(),
3130 ))
3131 };
3132
3133 let dao_exec_zkbin = ZkBinary::decode(&dao_exec_zkbin.1)?;
3134 let dao_auth_transfer_zkbin = ZkBinary::decode(&dao_auth_transfer_zkbin.1)?;
3135 let dao_auth_transfer_enc_coin_zkbin =
3136 ZkBinary::decode(&dao_auth_transfer_enc_coin_zkbin.1)?;
3137
3138 let dao_exec_circuit = ZkCircuit::new(empty_witnesses(&dao_exec_zkbin)?, &dao_exec_zkbin);
3139 let dao_auth_transfer_circuit =
3140 ZkCircuit::new(empty_witnesses(&dao_auth_transfer_zkbin)?, &dao_auth_transfer_zkbin);
3141 let dao_auth_transfer_enc_coin_circuit = ZkCircuit::new(
3142 empty_witnesses(&dao_auth_transfer_enc_coin_zkbin)?,
3143 &dao_auth_transfer_enc_coin_zkbin,
3144 );
3145
3146 let dao_exec_pk = ProvingKey::build(dao_exec_zkbin.k, &dao_exec_circuit);
3148 let dao_auth_transfer_pk =
3149 ProvingKey::build(dao_auth_transfer_zkbin.k, &dao_auth_transfer_circuit);
3150 let dao_auth_transfer_enc_coin_pk = ProvingKey::build(
3151 dao_auth_transfer_enc_coin_zkbin.k,
3152 &dao_auth_transfer_enc_coin_circuit,
3153 );
3154
3155 let tree = self.get_money_tree().await?;
3157
3158 let next_block_height = self.get_next_block_height().await?;
3161 let block_target = self.get_block_target().await?;
3162 let current_blockwindow = blockwindow(next_block_height, block_target);
3163
3164 let input_user_data_blind = Blind::random(&mut OsRng);
3166 let mut inputs = vec![];
3167 for coin in &spent_coins {
3168 inputs.push(TransferCallInput {
3169 coin: coin.clone(),
3170 merkle_path: tree.witness(coin.leaf_position, 0).unwrap(),
3171 user_data_blind: input_user_data_blind,
3172 });
3173 }
3174
3175 let mut outputs = vec![];
3176 outputs.push(proposal_coinattrs.clone());
3177
3178 let dao_coin_attrs = CoinAttributes {
3179 public_key: dao.params.dao.notes_public_key,
3180 value: change_value,
3181 token_id: proposal_coinattrs.token_id,
3182 spend_hook: dao_spend_hook,
3183 user_data: proposal.proposal.dao_bulla.inner(),
3184 blind: Blind::random(&mut OsRng),
3185 };
3186 outputs.push(dao_coin_attrs.clone());
3187
3188 let transfer_builder = TransferCallBuilder {
3190 clear_inputs: vec![],
3191 inputs,
3192 outputs,
3193 mint_zkbin: mint_zkbin.clone(),
3194 mint_pk: mint_pk.clone(),
3195 burn_zkbin: burn_zkbin.clone(),
3196 burn_pk: burn_pk.clone(),
3197 };
3198 let (transfer_params, transfer_secrets) = transfer_builder.build()?;
3199
3200 let mut data = vec![MoneyFunction::TransferV1 as u8];
3202 transfer_params.encode_async(&mut data).await?;
3203 let transfer_call = ContractCall { contract_id: *MONEY_CONTRACT_ID, data };
3204
3205 let exec_signature_secret = SecretKey::random(&mut OsRng);
3207 let exec_builder = DaoExecCall {
3208 proposal: proposal.proposal.clone(),
3209 dao: dao.params.dao.clone(),
3210 yes_vote_value,
3211 all_vote_value,
3212 yes_vote_blind,
3213 all_vote_blind,
3214 signature_secret: exec_signature_secret,
3215 current_blockwindow,
3216 };
3217 let (exec_params, exec_proofs) = exec_builder.make(
3218 &dao.params.exec_secret_key.unwrap(),
3219 &early_exec_secret_key,
3220 &dao_exec_zkbin,
3221 &dao_exec_pk,
3222 )?;
3223
3224 let mut data = vec![DaoFunction::Exec as u8];
3226 exec_params.encode_async(&mut data).await?;
3227 let exec_call = ContractCall { contract_id: *DAO_CONTRACT_ID, data };
3228
3229 let auth_transfer_builder = DaoAuthMoneyTransferCall {
3232 proposal: proposal.proposal.clone(),
3233 proposal_coinattrs: vec![proposal_coinattrs],
3234 dao: dao.params.dao.clone(),
3235 input_user_data_blind,
3236 dao_coin_attrs,
3237 };
3238 let (auth_transfer_params, auth_transfer_proofs) = auth_transfer_builder.make(
3239 &dao_auth_transfer_zkbin,
3240 &dao_auth_transfer_pk,
3241 &dao_auth_transfer_enc_coin_zkbin,
3242 &dao_auth_transfer_enc_coin_pk,
3243 )?;
3244
3245 let mut data = vec![DaoFunction::AuthMoneyTransfer as u8];
3247 auth_transfer_params.encode_async(&mut data).await?;
3248 let auth_transfer_call = ContractCall { contract_id: *DAO_CONTRACT_ID, data };
3249
3250 let mut tx_builder = TransactionBuilder::new(
3252 ContractCallLeaf { call: exec_call, proofs: exec_proofs },
3253 vec![
3254 DarkTree::new(
3255 ContractCallLeaf { call: auth_transfer_call, proofs: auth_transfer_proofs },
3256 vec![],
3257 None,
3258 None,
3259 ),
3260 DarkTree::new(
3261 ContractCallLeaf { call: transfer_call, proofs: transfer_secrets.proofs },
3262 vec![],
3263 None,
3264 None,
3265 ),
3266 ],
3267 )?;
3268
3269 let mut tx = tx_builder.build()?;
3272 let auth_transfer_sigs = tx.create_sigs(&[])?;
3273 let transfer_sigs = tx.create_sigs(&transfer_secrets.signature_secrets)?;
3274 let exec_sigs = tx.create_sigs(&[exec_signature_secret])?;
3275 tx.signatures = vec![auth_transfer_sigs, transfer_sigs, exec_sigs];
3276
3277 let (fee_call, fee_proofs, fee_secrets) =
3278 self.append_fee_call(&tx, &tree, &fee_pk, &fee_zkbin, None).await?;
3279
3280 tx_builder.append(ContractCallLeaf { call: fee_call, proofs: fee_proofs }, vec![])?;
3282
3283 let mut tx = tx_builder.build()?;
3285 let sigs = tx.create_sigs(&[])?;
3286 tx.signatures.push(sigs);
3287 let sigs = tx.create_sigs(&transfer_secrets.signature_secrets)?;
3288 tx.signatures.push(sigs);
3289 let sigs = tx.create_sigs(&[exec_signature_secret])?;
3290 tx.signatures.push(sigs);
3291 let sigs = tx.create_sigs(&fee_secrets)?;
3292 tx.signatures.push(sigs);
3293
3294 Ok(tx)
3295 }
3296
3297 pub async fn dao_exec_generic(
3299 &self,
3300 proposal: &ProposalRecord,
3301 early: bool,
3302 ) -> Result<Transaction> {
3303 if proposal.leaf_position.is_none() ||
3304 proposal.money_snapshot_tree.is_none() ||
3305 proposal.nullifiers_smt_snapshot.is_none() ||
3306 proposal.tx_hash.is_none() ||
3307 proposal.call_index.is_none()
3308 {
3309 return Err(Error::Custom(
3310 "[dao_exec_generic] Proposal seems to not have been deployed yet".to_string(),
3311 ))
3312 }
3313
3314 if let Some(exec_tx_hash) = proposal.exec_tx_hash {
3316 return Err(Error::Custom(format!(
3317 "[dao_exec_generic] Proposal was executed on transaction: {exec_tx_hash}"
3318 )))
3319 }
3320
3321 let Ok(dao) = self.get_dao_by_bulla(&proposal.proposal.dao_bulla).await else {
3323 return Err(Error::Custom(format!(
3324 "[dao_exec_generic] DAO {} was not found",
3325 proposal.proposal.dao_bulla
3326 )))
3327 };
3328 if dao.leaf_position.is_none() || dao.tx_hash.is_none() || dao.call_index.is_none() {
3329 return Err(Error::Custom(
3330 "[dao_exec_generic] DAO seems to not have been deployed yet".to_string(),
3331 ))
3332 }
3333
3334 if dao.params.exec_secret_key.is_none() {
3336 return Err(Error::Custom(
3337 "[dao_exec_generic] We need the exec secret key to execute proposals for this DAO"
3338 .to_string(),
3339 ))
3340 }
3341
3342 if early && dao.params.early_exec_secret_key.is_none() {
3344 return Err(Error::Custom(
3345 "[dao_exec_generic] We need the early exec secret key to execute proposals early for this DAO"
3346 .to_string(),
3347 ))
3348 }
3349
3350 let votes = self.get_dao_proposal_votes(&proposal.bulla()).await?;
3352 let mut yes_vote_value = 0;
3353 let mut yes_vote_blind = Blind::ZERO;
3354 let mut all_vote_value = 0;
3355 let mut all_vote_blind = Blind::ZERO;
3356 for vote in votes {
3357 if vote.vote_option {
3358 yes_vote_value += vote.all_vote_value;
3359 };
3360 yes_vote_blind += vote.yes_vote_blind;
3361 all_vote_value += vote.all_vote_value;
3362 all_vote_blind += vote.all_vote_blind;
3363 }
3364 let approval_ratio = (yes_vote_value as f64 * 100.0) / all_vote_value as f64;
3365 if all_vote_value < dao.params.dao.quorum ||
3366 approval_ratio <
3367 (dao.params.dao.approval_ratio_quot / dao.params.dao.approval_ratio_base)
3368 as f64
3369 {
3370 return Err(Error::Custom("[dao_exec_generic] Proposal is not approved yet".to_string()))
3371 };
3372
3373 let zkas_bins = self.lookup_zkas(&MONEY_CONTRACT_ID).await?;
3377 let Some(fee_zkbin) = zkas_bins.iter().find(|x| x.0 == MONEY_CONTRACT_ZKAS_FEE_NS_V1)
3378 else {
3379 return Err(Error::Custom("[dao_exec_generic] Fee circuit not found".to_string()))
3380 };
3381 let fee_zkbin = ZkBinary::decode(&fee_zkbin.1)?;
3382 let fee_circuit = ZkCircuit::new(empty_witnesses(&fee_zkbin)?, &fee_zkbin);
3383 let fee_pk = ProvingKey::build(fee_zkbin.k, &fee_circuit);
3384
3385 let zkas_bins = self.lookup_zkas(&DAO_CONTRACT_ID).await?;
3387
3388 let (namespace, early_exec_secret_key) = match early {
3389 true => (
3390 DAO_CONTRACT_ZKAS_DAO_EARLY_EXEC_NS,
3391 Some(dao.params.early_exec_secret_key.unwrap()),
3392 ),
3393 false => (DAO_CONTRACT_ZKAS_DAO_EXEC_NS, None),
3394 };
3395
3396 let Some(dao_exec_zkbin) = zkas_bins.iter().find(|x| x.0 == namespace) else {
3397 return Err(Error::Custom(format!(
3398 "[dao_exec_generic] DAO {namespace} circuit not found"
3399 )))
3400 };
3401 let dao_exec_zkbin = ZkBinary::decode(&dao_exec_zkbin.1)?;
3402 let dao_exec_circuit = ZkCircuit::new(empty_witnesses(&dao_exec_zkbin)?, &dao_exec_zkbin);
3403 let dao_exec_pk = ProvingKey::build(dao_exec_zkbin.k, &dao_exec_circuit);
3404
3405 let tree = self.get_money_tree().await?;
3407
3408 let next_block_height = self.get_next_block_height().await?;
3411 let block_target = self.get_block_target().await?;
3412 let current_blockwindow = blockwindow(next_block_height, block_target);
3413
3414 let exec_signature_secret = SecretKey::random(&mut OsRng);
3416 let exec_builder = DaoExecCall {
3417 proposal: proposal.proposal.clone(),
3418 dao: dao.params.dao.clone(),
3419 yes_vote_value,
3420 all_vote_value,
3421 yes_vote_blind,
3422 all_vote_blind,
3423 signature_secret: exec_signature_secret,
3424 current_blockwindow,
3425 };
3426 let (exec_params, exec_proofs) = exec_builder.make(
3427 &dao.params.exec_secret_key.unwrap(),
3428 &early_exec_secret_key,
3429 &dao_exec_zkbin,
3430 &dao_exec_pk,
3431 )?;
3432
3433 let mut data = vec![DaoFunction::Exec as u8];
3435 exec_params.encode_async(&mut data).await?;
3436 let exec_call = ContractCall { contract_id: *DAO_CONTRACT_ID, data };
3437
3438 let mut tx_builder = TransactionBuilder::new(
3440 ContractCallLeaf { call: exec_call, proofs: exec_proofs },
3441 vec![],
3442 )?;
3443
3444 let mut tx = tx_builder.build()?;
3447 let exec_sigs = tx.create_sigs(&[exec_signature_secret])?;
3448 tx.signatures = vec![exec_sigs];
3449
3450 let (fee_call, fee_proofs, fee_secrets) =
3451 self.append_fee_call(&tx, &tree, &fee_pk, &fee_zkbin, None).await?;
3452
3453 tx_builder.append(ContractCallLeaf { call: fee_call, proofs: fee_proofs }, vec![])?;
3455
3456 let mut tx = tx_builder.build()?;
3458 let sigs = tx.create_sigs(&[exec_signature_secret])?;
3459 tx.signatures.push(sigs);
3460 let sigs = tx.create_sigs(&fee_secrets)?;
3461 tx.signatures.push(sigs);
3462
3463 Ok(tx)
3464 }
3465}