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