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 = \"{secret_key}\"\n\n"),
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 = \"{secret_key}\"\n\n"),
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 = \"{secret_key}\"\n\n"),
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 = \"{secret_key}\"\n\n"),
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 = \"{secret_key}\"\n\n"),
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
1202 let mut wallet_tx = false;
1203 for dao in &daos {
1204 if dao.bulla() != new_bulla {
1205 continue
1206 }
1207
1208 println!(
1209 "[apply_dao_mint_data] Found minted DAO {new_bulla}, noting down for wallet update"
1210 );
1211
1212 let mut dao_to_confirm = dao.clone();
1214 dao_to_confirm.leaf_position = daos_tree.mark();
1215 dao_to_confirm.tx_hash = Some(tx_hash);
1216 dao_to_confirm.call_index = Some(call_index);
1217
1218 if let Err(e) = self.confirm_dao(&dao_to_confirm).await {
1220 return Err(Error::DatabaseError(format!(
1221 "[apply_dao_mint_data] Confirm DAO failed: {e:?}"
1222 )))
1223 }
1224
1225 wallet_tx = true;
1226 break
1227 }
1228
1229 if let Err(e) = self.put_dao_trees(&daos_tree, &proposals_tree).await {
1231 return Err(Error::DatabaseError(format!(
1232 "[apply_dao_mint_data] Put DAO tree failed: {e:?}"
1233 )))
1234 }
1235
1236 Ok(wallet_tx)
1237 }
1238
1239 async fn apply_dao_propose_data(
1243 &self,
1244 params: DaoProposeParams,
1245 tx_hash: TransactionHash,
1246 call_index: u8,
1247 ) -> Result<bool> {
1248 let daos = self.get_daos().await?;
1249 let (daos_tree, mut proposals_tree) = self.get_dao_trees().await?;
1250 proposals_tree.append(MerkleNode::from(params.proposal_bulla.inner()));
1251
1252 let mut wallet_tx = false;
1255 for dao in &daos {
1256 let Some(proposals_secret_key) = dao.params.proposals_secret_key else { continue };
1258
1259 let Ok(note) = params.note.decrypt::<DaoProposal>(&proposals_secret_key) else {
1261 continue
1262 };
1263
1264 println!("[apply_dao_propose_data] Managed to decrypt DAO proposal note");
1266
1267 let money_tree = self.get_money_tree().await?;
1269 let nullifiers_smt = self.get_nullifiers_smt().await?;
1270
1271 let our_proposal = match self.get_dao_proposal_by_bulla(¶ms.proposal_bulla).await {
1273 Ok(p) => {
1274 let mut our_proposal = p;
1275 our_proposal.leaf_position = proposals_tree.mark();
1276 our_proposal.money_snapshot_tree = Some(money_tree);
1277 our_proposal.nullifiers_smt_snapshot = Some(nullifiers_smt);
1278 our_proposal.tx_hash = Some(tx_hash);
1279 our_proposal.call_index = Some(call_index);
1280 our_proposal
1281 }
1282 Err(_) => ProposalRecord {
1283 proposal: note,
1284 data: None,
1285 leaf_position: proposals_tree.mark(),
1286 money_snapshot_tree: Some(money_tree),
1287 nullifiers_smt_snapshot: Some(nullifiers_smt),
1288 tx_hash: Some(tx_hash),
1289 call_index: Some(call_index),
1290 exec_tx_hash: None,
1291 },
1292 };
1293
1294 if let Err(e) = self.put_dao_proposal(&our_proposal).await {
1296 return Err(Error::DatabaseError(format!(
1297 "[apply_dao_propose_data] Put DAO proposals failed: {e:?}"
1298 )))
1299 }
1300
1301 wallet_tx = true;
1302 break
1303 }
1304
1305 if let Err(e) = self.put_dao_trees(&daos_tree, &proposals_tree).await {
1307 return Err(Error::DatabaseError(format!(
1308 "[apply_dao_propose_data] Put DAO tree failed: {e:?}"
1309 )))
1310 }
1311
1312 Ok(wallet_tx)
1313 }
1314
1315 async fn apply_dao_vote_data(
1319 &self,
1320 params: DaoVoteParams,
1321 tx_hash: TransactionHash,
1322 call_index: u8,
1323 ) -> Result<bool> {
1324 let Ok(proposal) = self.get_dao_proposal_by_bulla(¶ms.proposal_bulla).await else {
1326 return Ok(false)
1327 };
1328
1329 let dao = match self.get_dao_by_bulla(&proposal.proposal.dao_bulla).await {
1331 Ok(d) => d,
1332 Err(e) => {
1333 return Err(Error::DatabaseError(format!(
1334 "[apply_dao_vote_data] Couldn't find proposal {} DAO {}: {e}",
1335 proposal.bulla(),
1336 proposal.proposal.dao_bulla,
1337 )))
1338 }
1339 };
1340
1341 let Some(votes_secret_key) = dao.params.votes_secret_key else { return Ok(false) };
1343
1344 let note = match params.note.decrypt_unsafe(&votes_secret_key) {
1346 Ok(n) => n,
1347 Err(e) => {
1348 return Err(Error::DatabaseError(format!(
1349 "[apply_dao_vote_data] Couldn't decrypt proposal {} vote with DAO {} keys: {e}",
1350 proposal.bulla(),
1351 proposal.proposal.dao_bulla,
1352 )))
1353 }
1354 };
1355
1356 let vote_option = fp_to_u64(note[0]).unwrap();
1358 if vote_option > 1 {
1359 return Err(Error::DatabaseError(format!(
1360 "[apply_dao_vote_data] Malformed vote for proposal {}: {vote_option}",
1361 proposal.bulla(),
1362 )))
1363 }
1364 let vote_option = vote_option != 0;
1365 let yes_vote_blind = Blind(fp_mod_fv(note[1]));
1366 let all_vote_value = fp_to_u64(note[2]).unwrap();
1367 let all_vote_blind = Blind(fp_mod_fv(note[3]));
1368
1369 let v = VoteRecord {
1370 id: 0, proposal: params.proposal_bulla,
1372 vote_option,
1373 yes_vote_blind,
1374 all_vote_value,
1375 all_vote_blind,
1376 tx_hash,
1377 call_index,
1378 nullifiers: params.inputs.iter().map(|i| i.vote_nullifier).collect(),
1379 };
1380
1381 if let Err(e) = self.put_dao_vote(&v).await {
1382 return Err(Error::DatabaseError(format!(
1383 "[apply_dao_vote_data] Put DAO votes failed: {e:?}"
1384 )))
1385 }
1386
1387 Ok(true)
1388 }
1389
1390 async fn apply_dao_exec_data(
1394 &self,
1395 params: DaoExecParams,
1396 tx_hash: TransactionHash,
1397 ) -> Result<bool> {
1398 if self.get_dao_proposal_by_bulla(¶ms.proposal_bulla).await.is_err() {
1400 return Ok(false)
1401 };
1402
1403 let key = serialize_async(¶ms.proposal_bulla).await;
1405
1406 let query = format!(
1408 "UPDATE {} SET {} = ?1 WHERE {} = ?2;",
1409 *DAO_PROPOSALS_TABLE, DAO_PROPOSALS_COL_EXEC_TX_HASH, DAO_PROPOSALS_COL_BULLA,
1410 );
1411
1412 let inverse = match self.wallet.create_prepared_statement(
1414 &format!(
1415 "UPDATE {} SET {} = NULL WHERE {} = ?1;",
1416 *DAO_PROPOSALS_TABLE, DAO_PROPOSALS_COL_EXEC_TX_HASH, DAO_PROPOSALS_COL_BULLA,
1417 ),
1418 rusqlite::params![key],
1419 ) {
1420 Ok(q) => q,
1421 Err(e) => {
1422 return Err(Error::DatabaseError(format!(
1423 "[apply_dao_exec_data] Creating DAO proposal update inverse query failed: {e:?}"
1424 )))
1425 }
1426 };
1427
1428 if let Err(e) = self
1430 .wallet
1431 .exec_sql(&query, rusqlite::params![Some(serialize_async(&tx_hash).await), key])
1432 {
1433 return Err(Error::DatabaseError(format!(
1434 "[apply_dao_exec_data] Update DAO proposal failed: {e:?}"
1435 )))
1436 }
1437
1438 if let Err(e) = self.wallet.cache_inverse(inverse) {
1440 return Err(Error::DatabaseError(format!(
1441 "[apply_dao_exec_data] Inserting inverse query into cache failed: {e:?}"
1442 )))
1443 }
1444
1445 Ok(true)
1446 }
1447
1448 pub async fn apply_tx_dao_data(
1452 &self,
1453 data: &[u8],
1454 tx_hash: TransactionHash,
1455 call_idx: u8,
1456 ) -> Result<bool> {
1457 match DaoFunction::try_from(data[0])? {
1459 DaoFunction::Mint => {
1460 println!("[apply_tx_dao_data] Found Dao::Mint call");
1461 let params: DaoMintParams = deserialize_async(&data[1..]).await?;
1462 self.apply_dao_mint_data(params.dao_bulla, tx_hash, call_idx).await
1463 }
1464 DaoFunction::Propose => {
1465 println!("[apply_tx_dao_data] Found Dao::Propose call");
1466 let params: DaoProposeParams = deserialize_async(&data[1..]).await?;
1467 self.apply_dao_propose_data(params, tx_hash, call_idx).await
1468 }
1469 DaoFunction::Vote => {
1470 println!("[apply_tx_dao_data] Found Dao::Vote call");
1471 let params: DaoVoteParams = deserialize_async(&data[1..]).await?;
1472 self.apply_dao_vote_data(params, tx_hash, call_idx).await
1473 }
1474 DaoFunction::Exec => {
1475 println!("[apply_tx_dao_data] Found Dao::Exec call");
1476 let params: DaoExecParams = deserialize_async(&data[1..]).await?;
1477 self.apply_dao_exec_data(params, tx_hash).await
1478 }
1479 DaoFunction::AuthMoneyTransfer => {
1480 println!("[apply_tx_dao_data] Found Dao::AuthMoneyTransfer call");
1481 Ok(false)
1483 }
1484 }
1485 }
1486
1487 pub async fn confirm_dao(&self, dao: &DaoRecord) -> WalletDbResult<()> {
1492 let key = serialize_async(&dao.bulla()).await;
1494
1495 let query = format!(
1497 "UPDATE {} SET {} = ?1, {} = ?2, {} = ?3 WHERE {} = ?4;",
1498 *DAO_DAOS_TABLE,
1499 DAO_DAOS_COL_LEAF_POSITION,
1500 DAO_DAOS_COL_TX_HASH,
1501 DAO_DAOS_COL_CALL_INDEX,
1502 DAO_DAOS_COL_BULLA
1503 );
1504
1505 let params = rusqlite::params![
1507 serialize_async(&dao.leaf_position.unwrap()).await,
1508 serialize_async(&dao.tx_hash.unwrap()).await,
1509 dao.call_index.unwrap(),
1510 key,
1511 ];
1512
1513 let inverse_query = format!(
1515 "UPDATE {} SET {} = NULL, {} = NULL, {} = NULL WHERE {} = ?1;",
1516 *DAO_DAOS_TABLE,
1517 DAO_DAOS_COL_LEAF_POSITION,
1518 DAO_DAOS_COL_TX_HASH,
1519 DAO_DAOS_COL_CALL_INDEX,
1520 DAO_DAOS_COL_BULLA
1521 );
1522 let inverse =
1523 self.wallet.create_prepared_statement(&inverse_query, rusqlite::params![key])?;
1524
1525 self.wallet.exec_sql(&query, params)?;
1527
1528 self.wallet.cache_inverse(inverse)
1530 }
1531
1532 pub async fn put_dao_proposal(&self, proposal: &ProposalRecord) -> Result<()> {
1535 if let Err(e) = self.get_dao_by_bulla(&proposal.proposal.dao_bulla).await {
1537 return Err(Error::DatabaseError(format!(
1538 "[put_dao_proposal] Couldn't find proposal {} DAO {}: {e}",
1539 proposal.bulla(),
1540 proposal.proposal.dao_bulla
1541 )))
1542 }
1543
1544 let key = serialize_async(&proposal.bulla()).await;
1546
1547 let query = format!(
1549 "INSERT OR REPLACE INTO {} ({}, {}, {}, {}, {}, {}, {}, {}, {}, {}) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10);",
1550 *DAO_PROPOSALS_TABLE,
1551 DAO_PROPOSALS_COL_BULLA,
1552 DAO_PROPOSALS_COL_DAO_BULLA,
1553 DAO_PROPOSALS_COL_PROPOSAL,
1554 DAO_PROPOSALS_COL_DATA,
1555 DAO_PROPOSALS_COL_LEAF_POSITION,
1556 DAO_PROPOSALS_COL_MONEY_SNAPSHOT_TREE,
1557 DAO_PROPOSALS_COL_NULLIFIERS_SMT_SNAPSHOT,
1558 DAO_PROPOSALS_COL_TX_HASH,
1559 DAO_PROPOSALS_COL_CALL_INDEX,
1560 DAO_PROPOSALS_COL_EXEC_TX_HASH,
1561 );
1562
1563 let data = match &proposal.data {
1565 Some(data) => Some(data),
1566 None => None,
1567 };
1568
1569 let leaf_position = match &proposal.leaf_position {
1570 Some(leaf_position) => Some(serialize_async(leaf_position).await),
1571 None => None,
1572 };
1573
1574 let money_snapshot_tree = match &proposal.money_snapshot_tree {
1575 Some(money_snapshot_tree) => Some(serialize_async(money_snapshot_tree).await),
1576 None => None,
1577 };
1578
1579 let nullifiers_smt_snapshot = match &proposal.nullifiers_smt_snapshot {
1580 Some(nullifiers_smt_snapshot) => Some(serialize_async(nullifiers_smt_snapshot).await),
1581 None => None,
1582 };
1583
1584 let tx_hash = match &proposal.tx_hash {
1585 Some(tx_hash) => Some(serialize_async(tx_hash).await),
1586 None => None,
1587 };
1588
1589 let exec_tx_hash = match &proposal.exec_tx_hash {
1590 Some(exec_tx_hash) => Some(serialize_async(exec_tx_hash).await),
1591 None => None,
1592 };
1593
1594 let params = rusqlite::params![
1595 key,
1596 serialize_async(&proposal.proposal.dao_bulla).await,
1597 serialize_async(&proposal.proposal).await,
1598 data,
1599 leaf_position,
1600 money_snapshot_tree,
1601 nullifiers_smt_snapshot,
1602 tx_hash,
1603 proposal.call_index,
1604 exec_tx_hash,
1605 ];
1606
1607 let inverse_query = format!(
1609 "UPDATE {} SET {} = NULL, {} = NULL, {} = NULL, {} = NULL, {} = NULL, {} = NULL WHERE {} = ?1;",
1610 *DAO_PROPOSALS_TABLE,
1611 DAO_PROPOSALS_COL_LEAF_POSITION,
1612 DAO_PROPOSALS_COL_MONEY_SNAPSHOT_TREE,
1613 DAO_PROPOSALS_COL_NULLIFIERS_SMT_SNAPSHOT,
1614 DAO_PROPOSALS_COL_TX_HASH,
1615 DAO_PROPOSALS_COL_CALL_INDEX,
1616 DAO_PROPOSALS_COL_EXEC_TX_HASH,
1617 DAO_PROPOSALS_COL_BULLA
1618 );
1619 let inverse =
1620 match self.wallet.create_prepared_statement(&inverse_query, rusqlite::params![key]) {
1621 Ok(q) => q,
1622 Err(e) => {
1623 return Err(Error::DatabaseError(format!(
1624 "[put_dao_proposal] Creating DAO proposal insert inverse query failed: {e:?}"
1625 )))
1626 }
1627 };
1628
1629 if let Err(e) = self.wallet.exec_sql(&query, params) {
1631 return Err(Error::DatabaseError(format!(
1632 "[put_dao_proposal] Proposal insert failed: {e:?}"
1633 )))
1634 };
1635
1636 if let Err(e) = self.wallet.cache_inverse(inverse) {
1638 return Err(Error::DatabaseError(format!(
1639 "[put_dao_proposal] Inserting inverse query into cache failed: {e:?}"
1640 )))
1641 }
1642
1643 Ok(())
1644 }
1645
1646 pub async fn unconfirm_proposals(&self, proposals: &[ProposalRecord]) -> WalletDbResult<()> {
1648 for proposal in proposals {
1649 let query = format!(
1650 "UPDATE {} SET {} = NULL, {} = NULL, {} = NULL, {} = NULL, {} = NULL, {} = NULL WHERE {} = ?1;",
1651 *DAO_PROPOSALS_TABLE,
1652 DAO_PROPOSALS_COL_LEAF_POSITION,
1653 DAO_PROPOSALS_COL_MONEY_SNAPSHOT_TREE,
1654 DAO_PROPOSALS_COL_NULLIFIERS_SMT_SNAPSHOT,
1655 DAO_PROPOSALS_COL_TX_HASH,
1656 DAO_PROPOSALS_COL_CALL_INDEX,
1657 DAO_PROPOSALS_COL_EXEC_TX_HASH,
1658 DAO_PROPOSALS_COL_BULLA
1659 );
1660 self.wallet
1661 .exec_sql(&query, rusqlite::params![serialize_async(&proposal.bulla()).await])?;
1662 }
1663
1664 Ok(())
1665 }
1666
1667 pub async fn put_dao_vote(&self, vote: &VoteRecord) -> WalletDbResult<()> {
1670 println!("Importing DAO vote into wallet");
1671
1672 let query = format!(
1674 "INSERT INTO {} ({}, {}, {}, {}, {}, {}, {}, {}) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8);",
1675 *DAO_VOTES_TABLE,
1676 DAO_VOTES_COL_PROPOSAL_BULLA,
1677 DAO_VOTES_COL_VOTE_OPTION,
1678 DAO_VOTES_COL_YES_VOTE_BLIND,
1679 DAO_VOTES_COL_ALL_VOTE_VALUE,
1680 DAO_VOTES_COL_ALL_VOTE_BLIND,
1681 DAO_VOTES_COL_TX_HASH,
1682 DAO_VOTES_COL_CALL_INDEX,
1683 DAO_VOTES_COL_NULLIFIERS,
1684 );
1685
1686 let params = rusqlite::params![
1688 serialize_async(&vote.proposal).await,
1689 vote.vote_option as u64,
1690 serialize_async(&vote.yes_vote_blind).await,
1691 serialize_async(&vote.all_vote_value).await,
1692 serialize_async(&vote.all_vote_blind).await,
1693 serialize_async(&vote.tx_hash).await,
1694 vote.call_index,
1695 serialize_async(&vote.nullifiers).await,
1696 ];
1697
1698 let inverse_query = format!(
1702 "DELETE FROM {} WHERE {} = ?1 AND {} = ?2 AND {} = ?3 AND {} = ?4 AND {} = ?5 AND {} = ?6 AND {} = ?7 AND {} = ?8;",
1703 *DAO_VOTES_TABLE,
1704 DAO_VOTES_COL_PROPOSAL_BULLA,
1705 DAO_VOTES_COL_VOTE_OPTION,
1706 DAO_VOTES_COL_YES_VOTE_BLIND,
1707 DAO_VOTES_COL_ALL_VOTE_VALUE,
1708 DAO_VOTES_COL_ALL_VOTE_BLIND,
1709 DAO_VOTES_COL_TX_HASH,
1710 DAO_VOTES_COL_CALL_INDEX,
1711 DAO_VOTES_COL_NULLIFIERS,
1712 );
1713 let inverse = self.wallet.create_prepared_statement(&inverse_query, params)?;
1714
1715 self.wallet.exec_sql(&query, params)?;
1717
1718 self.wallet.cache_inverse(inverse)?;
1720
1721 println!("DAO vote added to wallet");
1722
1723 Ok(())
1724 }
1725
1726 pub async fn reset_dao_trees(&self) -> WalletDbResult<()> {
1728 println!("Resetting DAO Merkle trees");
1729 let tree = MerkleTree::new(1);
1730 self.put_dao_trees(&tree, &tree).await?;
1731 println!("Successfully reset DAO Merkle trees");
1732
1733 Ok(())
1734 }
1735
1736 pub async fn reset_daos(&self) -> WalletDbResult<()> {
1738 println!("Resetting DAO confirmations");
1739 let query = format!(
1740 "UPDATE {} SET {} = NULL, {} = NULL, {} = NULL;",
1741 *DAO_DAOS_TABLE,
1742 DAO_DAOS_COL_LEAF_POSITION,
1743 DAO_DAOS_COL_TX_HASH,
1744 DAO_DAOS_COL_CALL_INDEX,
1745 );
1746 self.wallet.exec_sql(&query, &[])?;
1747 println!("Successfully unconfirmed DAOs");
1748
1749 Ok(())
1750 }
1751
1752 pub async fn reset_dao_proposals(&self) -> WalletDbResult<()> {
1754 println!("Resetting DAO proposals confirmations");
1755 let proposals = match self.get_proposals().await {
1756 Ok(p) => p,
1757 Err(e) => {
1758 println!("[reset_dao_proposals] DAO proposals retrieval failed: {e:?}");
1759 return Err(WalletDbError::GenericError);
1760 }
1761 };
1762 self.unconfirm_proposals(&proposals).await?;
1763 println!("Successfully unconfirmed DAO proposals");
1764
1765 Ok(())
1766 }
1767
1768 pub fn reset_dao_votes(&self) -> WalletDbResult<()> {
1770 println!("Resetting DAO votes");
1771 let query = format!("DELETE FROM {};", *DAO_VOTES_TABLE);
1772 self.wallet.exec_sql(&query, &[])
1773 }
1774
1775 pub async fn import_dao(&self, name: &str, params: &DaoParams) -> Result<()> {
1777 if self.get_dao_by_name(name).await.is_ok() {
1779 return Err(Error::DatabaseError(
1780 "[import_dao] This DAO has already been imported".to_string(),
1781 ))
1782 }
1783
1784 println!("Importing \"{name}\" DAO into the wallet");
1785
1786 let query = format!(
1787 "INSERT INTO {} ({}, {}, {}) VALUES (?1, ?2, ?3);",
1788 *DAO_DAOS_TABLE, DAO_DAOS_COL_BULLA, DAO_DAOS_COL_NAME, DAO_DAOS_COL_PARAMS,
1789 );
1790 if let Err(e) = self.wallet.exec_sql(
1791 &query,
1792 rusqlite::params![
1793 serialize_async(¶ms.dao.to_bulla()).await,
1794 name,
1795 serialize_async(params).await,
1796 ],
1797 ) {
1798 return Err(Error::DatabaseError(format!("[import_dao] DAO insert failed: {e:?}")))
1799 };
1800
1801 Ok(())
1802 }
1803
1804 pub async fn update_dao_keys(&self, params: &DaoParams) -> Result<()> {
1806 let bulla = params.dao.to_bulla();
1808 let Ok(dao) = self.get_dao_by_bulla(&bulla).await else {
1809 return Err(Error::DatabaseError(format!("[import_dao] DAO {bulla} was not found")))
1810 };
1811
1812 println!("Updating \"{}\" DAO keys into the wallet", dao.name);
1813
1814 let query = format!(
1815 "UPDATE {} SET {} = ?1 WHERE {} = ?2;",
1816 *DAO_DAOS_TABLE, DAO_DAOS_COL_PARAMS, DAO_DAOS_COL_BULLA,
1817 );
1818 if let Err(e) = self.wallet.exec_sql(
1819 &query,
1820 rusqlite::params![serialize_async(params).await, serialize_async(&bulla).await,],
1821 ) {
1822 return Err(Error::DatabaseError(format!("[update_dao_keys] DAO update failed: {e:?}")))
1823 };
1824
1825 Ok(())
1826 }
1827
1828 pub async fn get_dao_by_bulla(&self, bulla: &DaoBulla) -> Result<DaoRecord> {
1830 let row = match self.wallet.query_single(
1831 &DAO_DAOS_TABLE,
1832 &[],
1833 convert_named_params! {(DAO_DAOS_COL_BULLA, serialize_async(bulla).await)},
1834 ) {
1835 Ok(r) => r,
1836 Err(e) => {
1837 return Err(Error::DatabaseError(format!(
1838 "[get_dao_by_bulla] DAO retrieval failed: {e:?}"
1839 )))
1840 }
1841 };
1842
1843 self.parse_dao_record(&row).await
1844 }
1845
1846 pub async fn get_dao_by_name(&self, name: &str) -> Result<DaoRecord> {
1848 let row = match self.wallet.query_single(
1849 &DAO_DAOS_TABLE,
1850 &[],
1851 convert_named_params! {(DAO_DAOS_COL_NAME, name)},
1852 ) {
1853 Ok(r) => r,
1854 Err(e) => {
1855 return Err(Error::DatabaseError(format!(
1856 "[get_dao_by_name] DAO retrieval failed: {e:?}"
1857 )))
1858 }
1859 };
1860
1861 self.parse_dao_record(&row).await
1862 }
1863
1864 pub async fn dao_list(&self, name: &Option<String>) -> Result<()> {
1867 if let Some(name) = name {
1868 let dao = self.get_dao_by_name(name).await?;
1869 println!("{dao}");
1870 return Ok(());
1871 }
1872
1873 let daos = self.get_daos().await?;
1874 for (i, dao) in daos.iter().enumerate() {
1875 println!("{i}. {}", dao.name);
1876 }
1877
1878 Ok(())
1879 }
1880
1881 pub async fn dao_balance(&self, name: &str) -> Result<HashMap<String, u64>> {
1883 let dao = self.get_dao_by_name(name).await?;
1884
1885 let dao_spend_hook =
1886 FuncRef { contract_id: *DAO_CONTRACT_ID, func_code: DaoFunction::Exec as u8 }
1887 .to_func_id();
1888
1889 let mut coins = self.get_coins(false).await?;
1890 coins.retain(|x| x.0.note.spend_hook == dao_spend_hook);
1891 coins.retain(|x| x.0.note.user_data == dao.bulla().inner());
1892
1893 let mut balmap: HashMap<String, u64> = HashMap::new();
1895
1896 for coin in coins {
1897 let mut value = coin.0.note.value;
1898
1899 if let Some(prev) = balmap.get(&coin.0.note.token_id.to_string()) {
1900 value += prev;
1901 }
1902
1903 balmap.insert(coin.0.note.token_id.to_string(), value);
1904 }
1905
1906 Ok(balmap)
1907 }
1908
1909 pub async fn get_proposals(&self) -> Result<Vec<ProposalRecord>> {
1911 let rows = match self.wallet.query_multiple(&DAO_PROPOSALS_TABLE, &[], &[]) {
1912 Ok(r) => r,
1913 Err(e) => {
1914 return Err(Error::DatabaseError(format!(
1915 "[get_proposals] DAO proposalss retrieval failed: {e:?}"
1916 )))
1917 }
1918 };
1919
1920 let mut daos = Vec::with_capacity(rows.len());
1921 for row in rows {
1922 daos.push(self.parse_dao_proposal(&row).await?);
1923 }
1924
1925 Ok(daos)
1926 }
1927
1928 pub async fn get_dao_proposal_by_bulla(
1930 &self,
1931 bulla: &DaoProposalBulla,
1932 ) -> Result<ProposalRecord> {
1933 let row = match self.wallet.query_single(
1935 &DAO_PROPOSALS_TABLE,
1936 &[],
1937 convert_named_params! {(DAO_PROPOSALS_COL_BULLA, serialize_async(bulla).await)},
1938 ) {
1939 Ok(r) => r,
1940 Err(e) => {
1941 return Err(Error::DatabaseError(format!(
1942 "[get_dao_proposal_by_bulla] DAO proposal retrieval failed: {e:?}"
1943 )))
1944 }
1945 };
1946
1947 self.parse_dao_proposal(&row).await
1949 }
1950
1951 pub async fn get_dao_proposal_votes(
1953 &self,
1954 proposal: &DaoProposalBulla,
1955 ) -> Result<Vec<VoteRecord>> {
1956 let rows = match self.wallet.query_multiple(
1957 &DAO_VOTES_TABLE,
1958 &[],
1959 convert_named_params! {(DAO_VOTES_COL_PROPOSAL_BULLA, serialize_async(proposal).await)},
1960 ) {
1961 Ok(r) => r,
1962 Err(e) => {
1963 return Err(Error::DatabaseError(format!(
1964 "[get_dao_proposal_votes] Votes retrieval failed: {e:?}"
1965 )))
1966 }
1967 };
1968
1969 let mut votes = Vec::with_capacity(rows.len());
1970 for row in rows {
1971 let Value::Integer(id) = row[0] else {
1972 return Err(Error::ParseFailed("[get_dao_proposal_votes] ID parsing failed"))
1973 };
1974 let Ok(id) = u64::try_from(id) else {
1975 return Err(Error::ParseFailed("[get_dao_proposal_votes] ID parsing failed"))
1976 };
1977
1978 let Value::Blob(ref proposal_bytes) = row[1] else {
1979 return Err(Error::ParseFailed(
1980 "[get_dao_proposal_votes] Proposal bytes bytes parsing failed",
1981 ))
1982 };
1983 let proposal = deserialize_async(proposal_bytes).await?;
1984
1985 let Value::Integer(vote_option) = row[2] else {
1986 return Err(Error::ParseFailed(
1987 "[get_dao_proposal_votes] Vote option parsing failed",
1988 ))
1989 };
1990 let Ok(vote_option) = u32::try_from(vote_option) else {
1991 return Err(Error::ParseFailed(
1992 "[get_dao_proposal_votes] Vote option parsing failed",
1993 ))
1994 };
1995 let vote_option = vote_option != 0;
1996
1997 let Value::Blob(ref yes_vote_blind_bytes) = row[3] else {
1998 return Err(Error::ParseFailed(
1999 "[get_dao_proposal_votes] Yes vote blind bytes parsing failed",
2000 ))
2001 };
2002 let yes_vote_blind = deserialize_async(yes_vote_blind_bytes).await?;
2003
2004 let Value::Blob(ref all_vote_value_bytes) = row[4] else {
2005 return Err(Error::ParseFailed(
2006 "[get_dao_proposal_votes] All vote value bytes parsing failed",
2007 ))
2008 };
2009 let all_vote_value = deserialize_async(all_vote_value_bytes).await?;
2010
2011 let Value::Blob(ref all_vote_blind_bytes) = row[5] else {
2012 return Err(Error::ParseFailed(
2013 "[get_dao_proposal_votes] All vote blind bytes parsing failed",
2014 ))
2015 };
2016 let all_vote_blind = deserialize_async(all_vote_blind_bytes).await?;
2017
2018 let Value::Blob(ref tx_hash_bytes) = row[6] else {
2019 return Err(Error::ParseFailed(
2020 "[get_dao_proposal_votes] Transaction hash bytes parsing failed",
2021 ))
2022 };
2023 let tx_hash = deserialize_async(tx_hash_bytes).await?;
2024
2025 let Value::Integer(call_index) = row[7] else {
2026 return Err(Error::ParseFailed("[get_dao_proposal_votes] Call index parsing failed"))
2027 };
2028 let Ok(call_index) = u8::try_from(call_index) else {
2029 return Err(Error::ParseFailed("[get_dao_proposal_votes] Call index parsing failed"))
2030 };
2031
2032 let Value::Blob(ref nullifiers_bytes) = row[8] else {
2033 return Err(Error::ParseFailed(
2034 "[get_dao_proposal_votes] Nullifiers bytes parsing failed",
2035 ))
2036 };
2037 let nullifiers = deserialize_async(nullifiers_bytes).await?;
2038
2039 let vote = VoteRecord {
2040 id,
2041 proposal,
2042 vote_option,
2043 yes_vote_blind,
2044 all_vote_value,
2045 all_vote_blind,
2046 tx_hash,
2047 call_index,
2048 nullifiers,
2049 };
2050
2051 votes.push(vote);
2052 }
2053
2054 Ok(votes)
2055 }
2056
2057 pub async fn dao_mint(&self, name: &str) -> Result<Transaction> {
2059 let dao = self.get_dao_by_name(name).await?;
2061
2062 if dao.tx_hash.is_some() {
2064 return Err(Error::Custom(
2065 "[dao_mint] This DAO seems to have already been minted on-chain".to_string(),
2066 ))
2067 }
2068
2069 if dao.params.notes_secret_key.is_none() ||
2071 dao.params.proposer_secret_key.is_none() ||
2072 dao.params.proposals_secret_key.is_none() ||
2073 dao.params.votes_secret_key.is_none() ||
2074 dao.params.exec_secret_key.is_none() ||
2075 dao.params.early_exec_secret_key.is_none()
2076 {
2077 return Err(Error::Custom(
2078 "[dao_mint] We need all the secrets key to mint the DAO on-chain".to_string(),
2079 ))
2080 }
2081
2082 let zkas_bins = self.lookup_zkas(&MONEY_CONTRACT_ID).await?;
2086
2087 let Some(fee_zkbin) = zkas_bins.iter().find(|x| x.0 == MONEY_CONTRACT_ZKAS_FEE_NS_V1)
2088 else {
2089 return Err(Error::Custom("Fee circuit not found".to_string()))
2090 };
2091
2092 let fee_zkbin = ZkBinary::decode(&fee_zkbin.1)?;
2093
2094 let fee_circuit = ZkCircuit::new(empty_witnesses(&fee_zkbin)?, &fee_zkbin);
2095
2096 let fee_pk = ProvingKey::build(fee_zkbin.k, &fee_circuit);
2098
2099 let zkas_bins = self.lookup_zkas(&DAO_CONTRACT_ID).await?;
2101
2102 let Some(dao_mint_zkbin) = zkas_bins.iter().find(|x| x.0 == DAO_CONTRACT_ZKAS_DAO_MINT_NS)
2103 else {
2104 return Err(Error::DatabaseError("[dao_mint] DAO Mint circuit not found".to_string()))
2105 };
2106
2107 let dao_mint_zkbin = ZkBinary::decode(&dao_mint_zkbin.1)?;
2108
2109 let dao_mint_circuit = ZkCircuit::new(empty_witnesses(&dao_mint_zkbin)?, &dao_mint_zkbin);
2110
2111 let dao_mint_pk = ProvingKey::build(dao_mint_zkbin.k, &dao_mint_circuit);
2113
2114 let notes_secret_key = dao.params.notes_secret_key.unwrap();
2116 let (params, proofs) = make_mint_call(
2117 &dao.params.dao,
2118 ¬es_secret_key,
2119 &dao.params.proposer_secret_key.unwrap(),
2120 &dao.params.proposals_secret_key.unwrap(),
2121 &dao.params.votes_secret_key.unwrap(),
2122 &dao.params.exec_secret_key.unwrap(),
2123 &dao.params.early_exec_secret_key.unwrap(),
2124 &dao_mint_zkbin,
2125 &dao_mint_pk,
2126 )?;
2127 let mut data = vec![DaoFunction::Mint as u8];
2128 params.encode_async(&mut data).await?;
2129 let call = ContractCall { contract_id: *DAO_CONTRACT_ID, data };
2130
2131 let mut tx_builder = TransactionBuilder::new(ContractCallLeaf { call, proofs }, vec![])?;
2133
2134 let mut tx = tx_builder.build()?;
2137 let sigs = tx.create_sigs(&[notes_secret_key])?;
2138 tx.signatures.push(sigs);
2139
2140 let tree = self.get_money_tree().await?;
2141 let (fee_call, fee_proofs, fee_secrets) =
2142 self.append_fee_call(&tx, &tree, &fee_pk, &fee_zkbin, None).await?;
2143
2144 tx_builder.append(ContractCallLeaf { call: fee_call, proofs: fee_proofs }, vec![])?;
2146
2147 let mut tx = tx_builder.build()?;
2149 let sigs = tx.create_sigs(&[notes_secret_key])?;
2150 tx.signatures.push(sigs);
2151 let sigs = tx.create_sigs(&fee_secrets)?;
2152 tx.signatures.push(sigs);
2153
2154 Ok(tx)
2155 }
2156
2157 #[allow(clippy::too_many_arguments)]
2159 pub async fn dao_propose_transfer(
2160 &self,
2161 name: &str,
2162 duration_blockwindows: u64,
2163 amount: &str,
2164 token_id: TokenId,
2165 recipient: PublicKey,
2166 spend_hook: Option<FuncId>,
2167 user_data: Option<pallas::Base>,
2168 ) -> Result<ProposalRecord> {
2169 let dao = self.get_dao_by_name(name).await?;
2171 if dao.leaf_position.is_none() || dao.tx_hash.is_none() || dao.call_index.is_none() {
2172 return Err(Error::Custom(
2173 "[dao_propose_transfer] DAO seems to not have been deployed yet".to_string(),
2174 ))
2175 }
2176
2177 if dao.params.proposer_secret_key.is_none() {
2179 return Err(Error::Custom(
2180 "[dao_propose_transfer] We need the proposer secret key to create proposals for this DAO".to_string(),
2181 ))
2182 }
2183
2184 let dao_spend_hook =
2186 FuncRef { contract_id: *DAO_CONTRACT_ID, func_code: DaoFunction::Exec as u8 }
2187 .to_func_id();
2188 let dao_bulla = dao.bulla();
2189 let dao_owncoins =
2190 self.get_contract_token_coins(&token_id, &dao_spend_hook, &dao_bulla.inner()).await?;
2191 if dao_owncoins.is_empty() {
2192 return Err(Error::Custom(format!(
2193 "[dao_propose_transfer] Did not find any {token_id} unspent coins owned by this DAO"
2194 )))
2195 }
2196
2197 let amount = decode_base10(amount, BALANCE_BASE10_DECIMALS, false)?;
2199 if dao_owncoins.iter().map(|x| x.note.value).sum::<u64>() < amount {
2200 return Err(Error::Custom(format!(
2201 "[dao_propose_transfer] Not enough DAO balance for token ID: {token_id}",
2202 )))
2203 }
2204
2205 let proposal_coinattrs = CoinAttributes {
2207 public_key: recipient,
2208 value: amount,
2209 token_id,
2210 spend_hook: spend_hook.unwrap_or(FuncId::none()),
2211 user_data: user_data.unwrap_or(pallas::Base::ZERO),
2212 blind: Blind::random(&mut OsRng),
2213 };
2214
2215 let proposal_coins = vec![proposal_coinattrs.to_coin()];
2217 let mut proposal_data = vec![];
2218 proposal_coins.encode_async(&mut proposal_data).await?;
2219
2220 let auth_calls = vec![
2222 DaoAuthCall {
2223 contract_id: *DAO_CONTRACT_ID,
2224 function_code: DaoFunction::AuthMoneyTransfer as u8,
2225 auth_data: proposal_data,
2226 },
2227 DaoAuthCall {
2228 contract_id: *MONEY_CONTRACT_ID,
2229 function_code: MoneyFunction::TransferV1 as u8,
2230 auth_data: vec![],
2231 },
2232 ];
2233
2234 let next_block_height = self.get_next_block_height().await?;
2237 let block_target = self.get_block_target().await?;
2238 let creation_blockwindow = blockwindow(next_block_height, block_target);
2239
2240 let proposal = DaoProposal {
2242 auth_calls,
2243 creation_blockwindow,
2244 duration_blockwindows,
2245 user_data: user_data.unwrap_or(pallas::Base::ZERO),
2246 dao_bulla,
2247 blind: Blind::random(&mut OsRng),
2248 };
2249
2250 let proposal_record = ProposalRecord {
2251 proposal,
2252 data: Some(serialize_async(&proposal_coinattrs).await),
2253 leaf_position: None,
2254 money_snapshot_tree: None,
2255 nullifiers_smt_snapshot: None,
2256 tx_hash: None,
2257 call_index: None,
2258 exec_tx_hash: None,
2259 };
2260
2261 if let Err(e) = self.put_dao_proposal(&proposal_record).await {
2262 return Err(Error::DatabaseError(format!(
2263 "[dao_propose_transfer] Put DAO proposal failed: {e:?}"
2264 )))
2265 }
2266
2267 Ok(proposal_record)
2268 }
2269
2270 pub async fn dao_propose_generic(
2272 &self,
2273 name: &str,
2274 duration_blockwindows: u64,
2275 user_data: Option<pallas::Base>,
2276 ) -> Result<ProposalRecord> {
2277 let dao = self.get_dao_by_name(name).await?;
2279 if dao.leaf_position.is_none() || dao.tx_hash.is_none() || dao.call_index.is_none() {
2280 return Err(Error::Custom(
2281 "[dao_propose_generic] DAO seems to not have been deployed yet".to_string(),
2282 ))
2283 }
2284
2285 if dao.params.proposer_secret_key.is_none() {
2287 return Err(Error::Custom(
2288 "[dao_propose_generic] We need the proposer secret key to create proposals for this DAO".to_string(),
2289 ))
2290 }
2291
2292 let next_block_height = self.get_next_block_height().await?;
2295 let block_target = self.get_block_target().await?;
2296 let creation_blockwindow = blockwindow(next_block_height, block_target);
2297
2298 let proposal = DaoProposal {
2300 auth_calls: vec![],
2301 creation_blockwindow,
2302 duration_blockwindows,
2303 user_data: user_data.unwrap_or(pallas::Base::ZERO),
2304 dao_bulla: dao.bulla(),
2305 blind: Blind::random(&mut OsRng),
2306 };
2307
2308 let proposal_record = ProposalRecord {
2309 proposal,
2310 data: None,
2311 leaf_position: None,
2312 money_snapshot_tree: None,
2313 nullifiers_smt_snapshot: None,
2314 tx_hash: None,
2315 call_index: None,
2316 exec_tx_hash: None,
2317 };
2318
2319 if let Err(e) = self.put_dao_proposal(&proposal_record).await {
2320 return Err(Error::DatabaseError(format!(
2321 "[dao_propose_generic] Put DAO proposal failed: {e:?}"
2322 )))
2323 }
2324
2325 Ok(proposal_record)
2326 }
2327
2328 pub async fn dao_transfer_proposal_tx(&self, proposal: &ProposalRecord) -> Result<Transaction> {
2330 if proposal.data.is_none() {
2332 return Err(Error::Custom(
2333 "[dao_transfer_proposal_tx] Proposal plainext data is empty".to_string(),
2334 ))
2335 }
2336 let proposal_coinattrs: CoinAttributes =
2337 deserialize_async(proposal.data.as_ref().unwrap()).await?;
2338
2339 let Ok(dao) = self.get_dao_by_bulla(&proposal.proposal.dao_bulla).await else {
2341 return Err(Error::Custom(format!(
2342 "[dao_transfer_proposal_tx] DAO {} was not found",
2343 proposal.proposal.dao_bulla
2344 )))
2345 };
2346 if dao.leaf_position.is_none() || dao.tx_hash.is_none() || dao.call_index.is_none() {
2347 return Err(Error::Custom(
2348 "[dao_transfer_proposal_tx] DAO seems to not have been deployed yet".to_string(),
2349 ))
2350 }
2351
2352 if dao.params.proposer_secret_key.is_none() {
2354 return Err(Error::Custom(
2355 "[dao_transfer_proposal_tx] We need the proposer secret key to create proposals for this DAO".to_string(),
2356 ))
2357 }
2358
2359 let dao_spend_hook =
2361 FuncRef { contract_id: *DAO_CONTRACT_ID, func_code: DaoFunction::Exec as u8 }
2362 .to_func_id();
2363 let dao_owncoins = self
2364 .get_contract_token_coins(
2365 &proposal_coinattrs.token_id,
2366 &dao_spend_hook,
2367 &proposal.proposal.dao_bulla.inner(),
2368 )
2369 .await?;
2370 if dao_owncoins.is_empty() {
2371 return Err(Error::Custom(format!(
2372 "[dao_transfer_proposal_tx] Did not find any {} unspent coins owned by this DAO",
2373 proposal_coinattrs.token_id,
2374 )))
2375 }
2376
2377 if dao_owncoins.iter().map(|x| x.note.value).sum::<u64>() < proposal_coinattrs.value {
2379 return Err(Error::Custom(format!(
2380 "[dao_transfer_proposal_tx] Not enough DAO balance for token ID: {}",
2381 proposal_coinattrs.token_id,
2382 )))
2383 }
2384
2385 let gov_owncoins = self.get_token_coins(&dao.params.dao.gov_token_id).await?;
2387 if gov_owncoins.is_empty() {
2388 return Err(Error::Custom(format!(
2389 "[dao_transfer_proposal_tx] Did not find any governance {} coins in wallet",
2390 dao.params.dao.gov_token_id
2391 )))
2392 }
2393
2394 let mut total_value = 0;
2396 let mut gov_owncoins_to_use = vec![];
2397 for gov_owncoin in gov_owncoins {
2398 if total_value >= dao.params.dao.proposer_limit {
2399 break
2400 }
2401
2402 total_value += gov_owncoin.note.value;
2403 gov_owncoins_to_use.push(gov_owncoin);
2404 }
2405
2406 if total_value < dao.params.dao.proposer_limit {
2408 return Err(Error::Custom(format!(
2409 "[dao_transfer_proposal_tx] Not enough gov token {} balance to propose",
2410 dao.params.dao.gov_token_id
2411 )))
2412 }
2413
2414 let zkas_bins = self.lookup_zkas(&MONEY_CONTRACT_ID).await?;
2418
2419 let Some(fee_zkbin) = zkas_bins.iter().find(|x| x.0 == MONEY_CONTRACT_ZKAS_FEE_NS_V1)
2420 else {
2421 return Err(Error::Custom(
2422 "[dao_transfer_proposal_tx] Fee circuit not found".to_string(),
2423 ))
2424 };
2425
2426 let fee_zkbin = ZkBinary::decode(&fee_zkbin.1)?;
2427
2428 let fee_circuit = ZkCircuit::new(empty_witnesses(&fee_zkbin)?, &fee_zkbin);
2429
2430 let fee_pk = ProvingKey::build(fee_zkbin.k, &fee_circuit);
2432
2433 let zkas_bins = self.lookup_zkas(&DAO_CONTRACT_ID).await?;
2435
2436 let Some(propose_burn_zkbin) =
2437 zkas_bins.iter().find(|x| x.0 == DAO_CONTRACT_ZKAS_DAO_PROPOSE_INPUT_NS)
2438 else {
2439 return Err(Error::Custom(
2440 "[dao_transfer_proposal_tx] Propose Burn circuit not found".to_string(),
2441 ))
2442 };
2443
2444 let Some(propose_main_zkbin) =
2445 zkas_bins.iter().find(|x| x.0 == DAO_CONTRACT_ZKAS_DAO_PROPOSE_MAIN_NS)
2446 else {
2447 return Err(Error::Custom(
2448 "[dao_transfer_proposal_tx] Propose Main circuit not found".to_string(),
2449 ))
2450 };
2451
2452 let propose_burn_zkbin = ZkBinary::decode(&propose_burn_zkbin.1)?;
2453 let propose_main_zkbin = ZkBinary::decode(&propose_main_zkbin.1)?;
2454
2455 let propose_burn_circuit =
2456 ZkCircuit::new(empty_witnesses(&propose_burn_zkbin)?, &propose_burn_zkbin);
2457 let propose_main_circuit =
2458 ZkCircuit::new(empty_witnesses(&propose_main_zkbin)?, &propose_main_zkbin);
2459
2460 let propose_burn_pk = ProvingKey::build(propose_burn_zkbin.k, &propose_burn_circuit);
2462 let propose_main_pk = ProvingKey::build(propose_main_zkbin.k, &propose_main_circuit);
2463
2464 let money_merkle_tree = self.get_money_tree().await?;
2466
2467 let mut inputs = Vec::with_capacity(gov_owncoins_to_use.len());
2471 for gov_owncoin in gov_owncoins_to_use {
2472 let input = DaoProposeStakeInput {
2473 secret: gov_owncoin.secret,
2474 note: gov_owncoin.note.clone(),
2475 leaf_position: gov_owncoin.leaf_position,
2476 merkle_path: money_merkle_tree.witness(gov_owncoin.leaf_position, 0).unwrap(),
2477 };
2478 inputs.push(input);
2479 }
2480
2481 let signature_secret = SecretKey::random(&mut OsRng);
2483
2484 let (daos_tree, _) = self.get_dao_trees().await?;
2486 let (dao_merkle_path, dao_merkle_root) = {
2487 let root = daos_tree.root(0).unwrap();
2488 let leaf_pos = dao.leaf_position.unwrap();
2489 let dao_merkle_path = daos_tree.witness(leaf_pos, 0).unwrap();
2490 (dao_merkle_path, root)
2491 };
2492
2493 let store = WalletStorage::new(
2495 &self.wallet,
2496 &MONEY_SMT_TABLE,
2497 MONEY_SMT_COL_KEY,
2498 MONEY_SMT_COL_VALUE,
2499 );
2500 let money_null_smt = WalletSmt::new(store, PoseidonFp::new(), &EMPTY_NODES_FP);
2501
2502 let call = DaoProposeCall {
2504 money_null_smt: &money_null_smt,
2505 inputs,
2506 proposal: proposal.proposal.clone(),
2507 dao: dao.params.dao,
2508 dao_leaf_position: dao.leaf_position.unwrap(),
2509 dao_merkle_path,
2510 dao_merkle_root,
2511 signature_secret,
2512 };
2513
2514 let (params, proofs) = call.make(
2515 &dao.params.proposer_secret_key.unwrap(),
2516 &propose_burn_zkbin,
2517 &propose_burn_pk,
2518 &propose_main_zkbin,
2519 &propose_main_pk,
2520 )?;
2521
2522 let mut data = vec![DaoFunction::Propose as u8];
2524 params.encode_async(&mut data).await?;
2525 let call = ContractCall { contract_id: *DAO_CONTRACT_ID, data };
2526
2527 let mut tx_builder = TransactionBuilder::new(ContractCallLeaf { call, proofs }, vec![])?;
2529
2530 let mut tx = tx_builder.build()?;
2533 let sigs = tx.create_sigs(&[signature_secret])?;
2534 tx.signatures = vec![sigs];
2535
2536 let tree = self.get_money_tree().await?;
2537 let (fee_call, fee_proofs, fee_secrets) =
2538 self.append_fee_call(&tx, &tree, &fee_pk, &fee_zkbin, None).await?;
2539
2540 tx_builder.append(ContractCallLeaf { call: fee_call, proofs: fee_proofs }, vec![])?;
2542
2543 let mut tx = tx_builder.build()?;
2545 let sigs = tx.create_sigs(&[signature_secret])?;
2546 tx.signatures.push(sigs);
2547 let sigs = tx.create_sigs(&fee_secrets)?;
2548 tx.signatures.push(sigs);
2549
2550 Ok(tx)
2551 }
2552
2553 pub async fn dao_generic_proposal_tx(&self, proposal: &ProposalRecord) -> Result<Transaction> {
2555 let Ok(dao) = self.get_dao_by_bulla(&proposal.proposal.dao_bulla).await else {
2557 return Err(Error::Custom(format!(
2558 "[dao_generic_proposal_tx] DAO {} was not found",
2559 proposal.proposal.dao_bulla
2560 )))
2561 };
2562 if dao.leaf_position.is_none() || dao.tx_hash.is_none() || dao.call_index.is_none() {
2563 return Err(Error::Custom(
2564 "[dao_generic_proposal_tx] DAO seems to not have been deployed yet".to_string(),
2565 ))
2566 }
2567
2568 if dao.params.proposer_secret_key.is_none() {
2570 return Err(Error::Custom(
2571 "[dao_generic_proposal_tx] We need the proposer secret key to create proposals for this DAO".to_string(),
2572 ))
2573 }
2574
2575 let gov_owncoins = self.get_token_coins(&dao.params.dao.gov_token_id).await?;
2577 if gov_owncoins.is_empty() {
2578 return Err(Error::Custom(format!(
2579 "[dao_generic_proposal_tx] Did not find any governance {} coins in wallet",
2580 dao.params.dao.gov_token_id
2581 )))
2582 }
2583
2584 let mut total_value = 0;
2586 let mut gov_owncoins_to_use = vec![];
2587 for gov_owncoin in gov_owncoins {
2588 if total_value >= dao.params.dao.proposer_limit {
2589 break
2590 }
2591
2592 total_value += gov_owncoin.note.value;
2593 gov_owncoins_to_use.push(gov_owncoin);
2594 }
2595
2596 if total_value < dao.params.dao.proposer_limit {
2598 return Err(Error::Custom(format!(
2599 "[dao_generic_proposal_tx] Not enough gov token {} balance to propose",
2600 dao.params.dao.gov_token_id
2601 )))
2602 }
2603
2604 let zkas_bins = self.lookup_zkas(&MONEY_CONTRACT_ID).await?;
2608
2609 let Some(fee_zkbin) = zkas_bins.iter().find(|x| x.0 == MONEY_CONTRACT_ZKAS_FEE_NS_V1)
2610 else {
2611 return Err(Error::Custom("[dao_generic_proposal_tx] Fee circuit not found".to_string()))
2612 };
2613
2614 let fee_zkbin = ZkBinary::decode(&fee_zkbin.1)?;
2615
2616 let fee_circuit = ZkCircuit::new(empty_witnesses(&fee_zkbin)?, &fee_zkbin);
2617
2618 let fee_pk = ProvingKey::build(fee_zkbin.k, &fee_circuit);
2620
2621 let zkas_bins = self.lookup_zkas(&DAO_CONTRACT_ID).await?;
2623
2624 let Some(propose_burn_zkbin) =
2625 zkas_bins.iter().find(|x| x.0 == DAO_CONTRACT_ZKAS_DAO_PROPOSE_INPUT_NS)
2626 else {
2627 return Err(Error::Custom(
2628 "[dao_generic_proposal_tx] Propose Burn circuit not found".to_string(),
2629 ))
2630 };
2631
2632 let Some(propose_main_zkbin) =
2633 zkas_bins.iter().find(|x| x.0 == DAO_CONTRACT_ZKAS_DAO_PROPOSE_MAIN_NS)
2634 else {
2635 return Err(Error::Custom(
2636 "[dao_generic_proposal_tx] Propose Main circuit not found".to_string(),
2637 ))
2638 };
2639
2640 let propose_burn_zkbin = ZkBinary::decode(&propose_burn_zkbin.1)?;
2641 let propose_main_zkbin = ZkBinary::decode(&propose_main_zkbin.1)?;
2642
2643 let propose_burn_circuit =
2644 ZkCircuit::new(empty_witnesses(&propose_burn_zkbin)?, &propose_burn_zkbin);
2645 let propose_main_circuit =
2646 ZkCircuit::new(empty_witnesses(&propose_main_zkbin)?, &propose_main_zkbin);
2647
2648 let propose_burn_pk = ProvingKey::build(propose_burn_zkbin.k, &propose_burn_circuit);
2650 let propose_main_pk = ProvingKey::build(propose_main_zkbin.k, &propose_main_circuit);
2651
2652 let money_merkle_tree = self.get_money_tree().await?;
2654
2655 let mut inputs = Vec::with_capacity(gov_owncoins_to_use.len());
2659 for gov_owncoin in gov_owncoins_to_use {
2660 let input = DaoProposeStakeInput {
2661 secret: gov_owncoin.secret,
2662 note: gov_owncoin.note.clone(),
2663 leaf_position: gov_owncoin.leaf_position,
2664 merkle_path: money_merkle_tree.witness(gov_owncoin.leaf_position, 0).unwrap(),
2665 };
2666 inputs.push(input);
2667 }
2668
2669 let signature_secret = SecretKey::random(&mut OsRng);
2671
2672 let (daos_tree, _) = self.get_dao_trees().await?;
2674 let (dao_merkle_path, dao_merkle_root) = {
2675 let root = daos_tree.root(0).unwrap();
2676 let leaf_pos = dao.leaf_position.unwrap();
2677 let dao_merkle_path = daos_tree.witness(leaf_pos, 0).unwrap();
2678 (dao_merkle_path, root)
2679 };
2680
2681 let store = WalletStorage::new(
2683 &self.wallet,
2684 &MONEY_SMT_TABLE,
2685 MONEY_SMT_COL_KEY,
2686 MONEY_SMT_COL_VALUE,
2687 );
2688 let money_null_smt = WalletSmt::new(store, PoseidonFp::new(), &EMPTY_NODES_FP);
2689
2690 let call = DaoProposeCall {
2692 money_null_smt: &money_null_smt,
2693 inputs,
2694 proposal: proposal.proposal.clone(),
2695 dao: dao.params.dao,
2696 dao_leaf_position: dao.leaf_position.unwrap(),
2697 dao_merkle_path,
2698 dao_merkle_root,
2699 signature_secret,
2700 };
2701
2702 let (params, proofs) = call.make(
2703 &dao.params.proposer_secret_key.unwrap(),
2704 &propose_burn_zkbin,
2705 &propose_burn_pk,
2706 &propose_main_zkbin,
2707 &propose_main_pk,
2708 )?;
2709
2710 let mut data = vec![DaoFunction::Propose as u8];
2712 params.encode_async(&mut data).await?;
2713 let call = ContractCall { contract_id: *DAO_CONTRACT_ID, data };
2714
2715 let mut tx_builder = TransactionBuilder::new(ContractCallLeaf { call, proofs }, vec![])?;
2717
2718 let mut tx = tx_builder.build()?;
2721 let sigs = tx.create_sigs(&[signature_secret])?;
2722 tx.signatures = vec![sigs];
2723
2724 let tree = self.get_money_tree().await?;
2725 let (fee_call, fee_proofs, fee_secrets) =
2726 self.append_fee_call(&tx, &tree, &fee_pk, &fee_zkbin, None).await?;
2727
2728 tx_builder.append(ContractCallLeaf { call: fee_call, proofs: fee_proofs }, vec![])?;
2730
2731 let mut tx = tx_builder.build()?;
2733 let sigs = tx.create_sigs(&[signature_secret])?;
2734 tx.signatures.push(sigs);
2735 let sigs = tx.create_sigs(&fee_secrets)?;
2736 tx.signatures.push(sigs);
2737
2738 Ok(tx)
2739 }
2740
2741 pub async fn dao_vote(
2743 &self,
2744 proposal_bulla: &DaoProposalBulla,
2745 vote_option: bool,
2746 weight: Option<u64>,
2747 ) -> Result<Transaction> {
2748 let Ok(proposal) = self.get_dao_proposal_by_bulla(proposal_bulla).await else {
2750 return Err(Error::Custom(format!("[dao_vote] Proposal {proposal_bulla} was not found")))
2751 };
2752 if proposal.leaf_position.is_none() ||
2753 proposal.money_snapshot_tree.is_none() ||
2754 proposal.nullifiers_smt_snapshot.is_none() ||
2755 proposal.tx_hash.is_none() ||
2756 proposal.call_index.is_none()
2757 {
2758 return Err(Error::Custom(
2759 "[dao_vote] Proposal seems to not have been deployed yet".to_string(),
2760 ))
2761 }
2762
2763 if let Some(exec_tx_hash) = proposal.exec_tx_hash {
2765 return Err(Error::Custom(format!(
2766 "[dao_vote] Proposal was executed on transaction: {exec_tx_hash}"
2767 )))
2768 }
2769
2770 let Ok(dao) = self.get_dao_by_bulla(&proposal.proposal.dao_bulla).await else {
2772 return Err(Error::Custom(format!(
2773 "[dao_vote] DAO {} was not found",
2774 proposal.proposal.dao_bulla
2775 )))
2776 };
2777 if dao.leaf_position.is_none() || dao.tx_hash.is_none() || dao.call_index.is_none() {
2778 return Err(Error::Custom(
2779 "[dao_vote] DAO seems to not have been deployed yet".to_string(),
2780 ))
2781 }
2782
2783 let votes = self.get_dao_proposal_votes(proposal_bulla).await?;
2785 let mut votes_nullifiers = vec![];
2786 for vote in votes {
2787 for nullifier in vote.nullifiers {
2788 if !votes_nullifiers.contains(&nullifier) {
2789 votes_nullifiers.push(nullifier);
2790 }
2791 }
2792 }
2793
2794 let gov_owncoins = self.get_token_coins(&dao.params.dao.gov_token_id).await?;
2796 if gov_owncoins.is_empty() {
2797 return Err(Error::Custom(format!(
2798 "[dao_vote] Did not find any governance {} coins in wallet",
2799 dao.params.dao.gov_token_id
2800 )))
2801 }
2802
2803 let gov_owncoins_to_use = match weight {
2805 Some(_weight) => {
2806 return Err(Error::Custom(
2809 "[dao_vote] Fractional vote weight not supported yet".to_string(),
2810 ))
2811 }
2812 None => gov_owncoins,
2814 };
2815
2816 let zkas_bins = self.lookup_zkas(&MONEY_CONTRACT_ID).await?;
2820
2821 let Some(fee_zkbin) = zkas_bins.iter().find(|x| x.0 == MONEY_CONTRACT_ZKAS_FEE_NS_V1)
2822 else {
2823 return Err(Error::Custom("[dao_vote] Fee circuit not found".to_string()))
2824 };
2825
2826 let fee_zkbin = ZkBinary::decode(&fee_zkbin.1)?;
2827
2828 let fee_circuit = ZkCircuit::new(empty_witnesses(&fee_zkbin)?, &fee_zkbin);
2829
2830 let fee_pk = ProvingKey::build(fee_zkbin.k, &fee_circuit);
2832
2833 let zkas_bins = self.lookup_zkas(&DAO_CONTRACT_ID).await?;
2835
2836 let Some(dao_vote_burn_zkbin) =
2837 zkas_bins.iter().find(|x| x.0 == DAO_CONTRACT_ZKAS_DAO_VOTE_INPUT_NS)
2838 else {
2839 return Err(Error::Custom("[dao_vote] DAO Vote Burn circuit not found".to_string()))
2840 };
2841
2842 let Some(dao_vote_main_zkbin) =
2843 zkas_bins.iter().find(|x| x.0 == DAO_CONTRACT_ZKAS_DAO_VOTE_MAIN_NS)
2844 else {
2845 return Err(Error::Custom("[dao_vote] DAO Vote Main circuit not found".to_string()))
2846 };
2847
2848 let dao_vote_burn_zkbin = ZkBinary::decode(&dao_vote_burn_zkbin.1)?;
2849 let dao_vote_main_zkbin = ZkBinary::decode(&dao_vote_main_zkbin.1)?;
2850
2851 let dao_vote_burn_circuit =
2852 ZkCircuit::new(empty_witnesses(&dao_vote_burn_zkbin)?, &dao_vote_burn_zkbin);
2853 let dao_vote_main_circuit =
2854 ZkCircuit::new(empty_witnesses(&dao_vote_main_zkbin)?, &dao_vote_main_zkbin);
2855
2856 let dao_vote_burn_pk = ProvingKey::build(dao_vote_burn_zkbin.k, &dao_vote_burn_circuit);
2858 let dao_vote_main_pk = ProvingKey::build(dao_vote_main_zkbin.k, &dao_vote_main_circuit);
2859
2860 let signature_secret = SecretKey::random(&mut OsRng);
2862 let mut inputs = Vec::with_capacity(gov_owncoins_to_use.len());
2863 for gov_owncoin in gov_owncoins_to_use {
2864 let nullifier = poseidon_hash([gov_owncoin.secret.inner(), gov_owncoin.coin.inner()]);
2865 let vote_nullifier =
2866 poseidon_hash([nullifier, gov_owncoin.secret.inner(), proposal_bulla.inner()]);
2867 if votes_nullifiers.contains(&vote_nullifier.into()) {
2868 return Err(Error::Custom("[dao_vote] Duplicate input nullifier found".to_string()))
2869 };
2870
2871 let input = DaoVoteInput {
2872 secret: gov_owncoin.secret,
2873 note: gov_owncoin.note.clone(),
2874 leaf_position: gov_owncoin.leaf_position,
2875 merkle_path: proposal
2876 .money_snapshot_tree
2877 .as_ref()
2878 .unwrap()
2879 .witness(gov_owncoin.leaf_position, 0)
2880 .unwrap(),
2881 signature_secret,
2882 };
2883 inputs.push(input);
2884 }
2885
2886 let next_block_height = self.get_next_block_height().await?;
2889 let block_target = self.get_block_target().await?;
2890 let current_blockwindow = blockwindow(next_block_height, block_target);
2891
2892 let store = MemoryStorageFp { tree: proposal.nullifiers_smt_snapshot.unwrap() };
2894 let money_null_smt = SmtMemoryFp::new(store, PoseidonFp::new(), &EMPTY_NODES_FP);
2895
2896 let call = DaoVoteCall {
2898 money_null_smt: &money_null_smt,
2899 inputs,
2900 vote_option,
2901 proposal: proposal.proposal.clone(),
2902 dao: dao.params.dao.clone(),
2903 current_blockwindow,
2904 };
2905
2906 let (params, proofs) = call.make(
2907 &dao_vote_burn_zkbin,
2908 &dao_vote_burn_pk,
2909 &dao_vote_main_zkbin,
2910 &dao_vote_main_pk,
2911 )?;
2912
2913 let mut data = vec![DaoFunction::Vote as u8];
2915 params.encode_async(&mut data).await?;
2916 let call = ContractCall { contract_id: *DAO_CONTRACT_ID, data };
2917
2918 let mut tx_builder = TransactionBuilder::new(ContractCallLeaf { call, proofs }, vec![])?;
2920
2921 let mut tx = tx_builder.build()?;
2924 let sigs = tx.create_sigs(&[signature_secret])?;
2925 tx.signatures = vec![sigs];
2926
2927 let tree = self.get_money_tree().await?;
2928 let (fee_call, fee_proofs, fee_secrets) =
2929 self.append_fee_call(&tx, &tree, &fee_pk, &fee_zkbin, None).await?;
2930
2931 tx_builder.append(ContractCallLeaf { call: fee_call, proofs: fee_proofs }, vec![])?;
2933
2934 let mut tx = tx_builder.build()?;
2936 let sigs = tx.create_sigs(&[signature_secret])?;
2937 tx.signatures.push(sigs);
2938 let sigs = tx.create_sigs(&fee_secrets)?;
2939 tx.signatures.push(sigs);
2940
2941 Ok(tx)
2942 }
2943
2944 pub async fn dao_exec_transfer(
2946 &self,
2947 proposal: &ProposalRecord,
2948 early: bool,
2949 ) -> Result<Transaction> {
2950 if proposal.leaf_position.is_none() ||
2951 proposal.money_snapshot_tree.is_none() ||
2952 proposal.nullifiers_smt_snapshot.is_none() ||
2953 proposal.tx_hash.is_none() ||
2954 proposal.call_index.is_none()
2955 {
2956 return Err(Error::Custom(
2957 "[dao_exec_transfer] Proposal seems to not have been deployed yet".to_string(),
2958 ))
2959 }
2960
2961 if let Some(exec_tx_hash) = proposal.exec_tx_hash {
2963 return Err(Error::Custom(format!(
2964 "[dao_exec_transfer] Proposal was executed on transaction: {exec_tx_hash}"
2965 )))
2966 }
2967
2968 if proposal.data.is_none() {
2970 return Err(Error::Custom(
2971 "[dao_exec_transfer] Proposal plainext data is empty".to_string(),
2972 ))
2973 }
2974 let proposal_coinattrs: CoinAttributes =
2975 deserialize_async(proposal.data.as_ref().unwrap()).await?;
2976
2977 let Ok(dao) = self.get_dao_by_bulla(&proposal.proposal.dao_bulla).await else {
2979 return Err(Error::Custom(format!(
2980 "[dao_exec_transfer] DAO {} was not found",
2981 proposal.proposal.dao_bulla
2982 )))
2983 };
2984 if dao.leaf_position.is_none() || dao.tx_hash.is_none() || dao.call_index.is_none() {
2985 return Err(Error::Custom(
2986 "[dao_exec_transfer] DAO seems to not have been deployed yet".to_string(),
2987 ))
2988 }
2989
2990 if dao.params.exec_secret_key.is_none() {
2992 return Err(Error::Custom(
2993 "[dao_exec_transfer] We need the exec secret key to execute proposals for this DAO"
2994 .to_string(),
2995 ))
2996 }
2997
2998 if early && dao.params.early_exec_secret_key.is_none() {
3000 return Err(Error::Custom(
3001 "[dao_exec_transfer] We need the early exec secret key to execute proposals early for this DAO"
3002 .to_string(),
3003 ))
3004 }
3005
3006 let votes = self.get_dao_proposal_votes(&proposal.bulla()).await?;
3008 let mut yes_vote_value = 0;
3009 let mut yes_vote_blind = Blind::ZERO;
3010 let mut all_vote_value = 0;
3011 let mut all_vote_blind = Blind::ZERO;
3012 for vote in votes {
3013 if vote.vote_option {
3014 yes_vote_value += vote.all_vote_value;
3015 };
3016 yes_vote_blind += vote.yes_vote_blind;
3017 all_vote_value += vote.all_vote_value;
3018 all_vote_blind += vote.all_vote_blind;
3019 }
3020 let approval_ratio = (yes_vote_value as f64 * 100.0) / all_vote_value as f64;
3021 if all_vote_value < dao.params.dao.quorum ||
3022 approval_ratio <
3023 (dao.params.dao.approval_ratio_quot / dao.params.dao.approval_ratio_base)
3024 as f64
3025 {
3026 return Err(Error::Custom(
3027 "[dao_exec_transfer] Proposal is not approved yet".to_string(),
3028 ))
3029 };
3030
3031 let dao_spend_hook =
3033 FuncRef { contract_id: *DAO_CONTRACT_ID, func_code: DaoFunction::Exec as u8 }
3034 .to_func_id();
3035 let dao_owncoins = self
3036 .get_contract_token_coins(
3037 &proposal_coinattrs.token_id,
3038 &dao_spend_hook,
3039 &proposal.proposal.dao_bulla.inner(),
3040 )
3041 .await?;
3042 if dao_owncoins.is_empty() {
3043 return Err(Error::Custom(format!(
3044 "[dao_exec_transfer] Did not find any {} unspent coins owned by this DAO",
3045 proposal_coinattrs.token_id,
3046 )))
3047 }
3048
3049 if dao_owncoins.iter().map(|x| x.note.value).sum::<u64>() < proposal_coinattrs.value {
3051 return Err(Error::Custom(format!(
3052 "[dao_exec_transfer] Not enough DAO balance for token ID: {}",
3053 proposal_coinattrs.token_id,
3054 )))
3055 }
3056
3057 let (spent_coins, change_value) = select_coins(dao_owncoins, proposal_coinattrs.value)?;
3059
3060 let zkas_bins = self.lookup_zkas(&MONEY_CONTRACT_ID).await?;
3064
3065 let Some(mint_zkbin) = zkas_bins.iter().find(|x| x.0 == MONEY_CONTRACT_ZKAS_MINT_NS_V1)
3066 else {
3067 return Err(Error::Custom("[dao_exec_transfer] Mint circuit not found".to_string()))
3068 };
3069
3070 let Some(burn_zkbin) = zkas_bins.iter().find(|x| x.0 == MONEY_CONTRACT_ZKAS_BURN_NS_V1)
3071 else {
3072 return Err(Error::Custom("[dao_exec_transfer] Burn circuit not found".to_string()))
3073 };
3074
3075 let Some(fee_zkbin) = zkas_bins.iter().find(|x| x.0 == MONEY_CONTRACT_ZKAS_FEE_NS_V1)
3076 else {
3077 return Err(Error::Custom("[dao_exec_transfer] Fee circuit not found".to_string()))
3078 };
3079
3080 let mint_zkbin = ZkBinary::decode(&mint_zkbin.1)?;
3081 let burn_zkbin = ZkBinary::decode(&burn_zkbin.1)?;
3082 let fee_zkbin = ZkBinary::decode(&fee_zkbin.1)?;
3083
3084 let mint_circuit = ZkCircuit::new(empty_witnesses(&mint_zkbin)?, &mint_zkbin);
3085 let burn_circuit = ZkCircuit::new(empty_witnesses(&burn_zkbin)?, &burn_zkbin);
3086 let fee_circuit = ZkCircuit::new(empty_witnesses(&fee_zkbin)?, &fee_zkbin);
3087
3088 let mint_pk = ProvingKey::build(mint_zkbin.k, &mint_circuit);
3090 let burn_pk = ProvingKey::build(burn_zkbin.k, &burn_circuit);
3091 let fee_pk = ProvingKey::build(fee_zkbin.k, &fee_circuit);
3092
3093 let zkas_bins = self.lookup_zkas(&DAO_CONTRACT_ID).await?;
3095
3096 let (namespace, early_exec_secret_key) = match early {
3097 true => (
3098 DAO_CONTRACT_ZKAS_DAO_EARLY_EXEC_NS,
3099 Some(dao.params.early_exec_secret_key.unwrap()),
3100 ),
3101 false => (DAO_CONTRACT_ZKAS_DAO_EXEC_NS, None),
3102 };
3103
3104 let Some(dao_exec_zkbin) = zkas_bins.iter().find(|x| x.0 == namespace) else {
3105 return Err(Error::Custom(format!(
3106 "[dao_exec_transfer] DAO {namespace} circuit not found"
3107 )))
3108 };
3109
3110 let Some(dao_auth_transfer_zkbin) =
3111 zkas_bins.iter().find(|x| x.0 == DAO_CONTRACT_ZKAS_DAO_AUTH_MONEY_TRANSFER_NS)
3112 else {
3113 return Err(Error::Custom(
3114 "[dao_exec_transfer] DAO AuthTransfer circuit not found".to_string(),
3115 ))
3116 };
3117
3118 let Some(dao_auth_transfer_enc_coin_zkbin) =
3119 zkas_bins.iter().find(|x| x.0 == DAO_CONTRACT_ZKAS_DAO_AUTH_MONEY_TRANSFER_ENC_COIN_NS)
3120 else {
3121 return Err(Error::Custom(
3122 "[dao_exec_transfer] DAO AuthTransferEncCoin circuit not found".to_string(),
3123 ))
3124 };
3125
3126 let dao_exec_zkbin = ZkBinary::decode(&dao_exec_zkbin.1)?;
3127 let dao_auth_transfer_zkbin = ZkBinary::decode(&dao_auth_transfer_zkbin.1)?;
3128 let dao_auth_transfer_enc_coin_zkbin =
3129 ZkBinary::decode(&dao_auth_transfer_enc_coin_zkbin.1)?;
3130
3131 let dao_exec_circuit = ZkCircuit::new(empty_witnesses(&dao_exec_zkbin)?, &dao_exec_zkbin);
3132 let dao_auth_transfer_circuit =
3133 ZkCircuit::new(empty_witnesses(&dao_auth_transfer_zkbin)?, &dao_auth_transfer_zkbin);
3134 let dao_auth_transfer_enc_coin_circuit = ZkCircuit::new(
3135 empty_witnesses(&dao_auth_transfer_enc_coin_zkbin)?,
3136 &dao_auth_transfer_enc_coin_zkbin,
3137 );
3138
3139 let dao_exec_pk = ProvingKey::build(dao_exec_zkbin.k, &dao_exec_circuit);
3141 let dao_auth_transfer_pk =
3142 ProvingKey::build(dao_auth_transfer_zkbin.k, &dao_auth_transfer_circuit);
3143 let dao_auth_transfer_enc_coin_pk = ProvingKey::build(
3144 dao_auth_transfer_enc_coin_zkbin.k,
3145 &dao_auth_transfer_enc_coin_circuit,
3146 );
3147
3148 let tree = self.get_money_tree().await?;
3150
3151 let next_block_height = self.get_next_block_height().await?;
3154 let block_target = self.get_block_target().await?;
3155 let current_blockwindow = blockwindow(next_block_height, block_target);
3156
3157 let input_user_data_blind = Blind::random(&mut OsRng);
3159 let mut inputs = vec![];
3160 for coin in &spent_coins {
3161 inputs.push(TransferCallInput {
3162 coin: coin.clone(),
3163 merkle_path: tree.witness(coin.leaf_position, 0).unwrap(),
3164 user_data_blind: input_user_data_blind,
3165 });
3166 }
3167
3168 let mut outputs = vec![];
3169 outputs.push(proposal_coinattrs.clone());
3170
3171 let dao_coin_attrs = CoinAttributes {
3172 public_key: dao.params.dao.notes_public_key,
3173 value: change_value,
3174 token_id: proposal_coinattrs.token_id,
3175 spend_hook: dao_spend_hook,
3176 user_data: proposal.proposal.dao_bulla.inner(),
3177 blind: Blind::random(&mut OsRng),
3178 };
3179 outputs.push(dao_coin_attrs.clone());
3180
3181 let transfer_builder = TransferCallBuilder {
3183 clear_inputs: vec![],
3184 inputs,
3185 outputs,
3186 mint_zkbin: mint_zkbin.clone(),
3187 mint_pk: mint_pk.clone(),
3188 burn_zkbin: burn_zkbin.clone(),
3189 burn_pk: burn_pk.clone(),
3190 };
3191 let (transfer_params, transfer_secrets) = transfer_builder.build()?;
3192
3193 let mut data = vec![MoneyFunction::TransferV1 as u8];
3195 transfer_params.encode_async(&mut data).await?;
3196 let transfer_call = ContractCall { contract_id: *MONEY_CONTRACT_ID, data };
3197
3198 let exec_signature_secret = SecretKey::random(&mut OsRng);
3200 let exec_builder = DaoExecCall {
3201 proposal: proposal.proposal.clone(),
3202 dao: dao.params.dao.clone(),
3203 yes_vote_value,
3204 all_vote_value,
3205 yes_vote_blind,
3206 all_vote_blind,
3207 signature_secret: exec_signature_secret,
3208 current_blockwindow,
3209 };
3210 let (exec_params, exec_proofs) = exec_builder.make(
3211 &dao.params.exec_secret_key.unwrap(),
3212 &early_exec_secret_key,
3213 &dao_exec_zkbin,
3214 &dao_exec_pk,
3215 )?;
3216
3217 let mut data = vec![DaoFunction::Exec as u8];
3219 exec_params.encode_async(&mut data).await?;
3220 let exec_call = ContractCall { contract_id: *DAO_CONTRACT_ID, data };
3221
3222 let auth_transfer_builder = DaoAuthMoneyTransferCall {
3225 proposal: proposal.proposal.clone(),
3226 proposal_coinattrs: vec![proposal_coinattrs],
3227 dao: dao.params.dao.clone(),
3228 input_user_data_blind,
3229 dao_coin_attrs,
3230 };
3231 let (auth_transfer_params, auth_transfer_proofs) = auth_transfer_builder.make(
3232 &dao_auth_transfer_zkbin,
3233 &dao_auth_transfer_pk,
3234 &dao_auth_transfer_enc_coin_zkbin,
3235 &dao_auth_transfer_enc_coin_pk,
3236 )?;
3237
3238 let mut data = vec![DaoFunction::AuthMoneyTransfer as u8];
3240 auth_transfer_params.encode_async(&mut data).await?;
3241 let auth_transfer_call = ContractCall { contract_id: *DAO_CONTRACT_ID, data };
3242
3243 let mut tx_builder = TransactionBuilder::new(
3245 ContractCallLeaf { call: exec_call, proofs: exec_proofs },
3246 vec![
3247 DarkTree::new(
3248 ContractCallLeaf { call: auth_transfer_call, proofs: auth_transfer_proofs },
3249 vec![],
3250 None,
3251 None,
3252 ),
3253 DarkTree::new(
3254 ContractCallLeaf { call: transfer_call, proofs: transfer_secrets.proofs },
3255 vec![],
3256 None,
3257 None,
3258 ),
3259 ],
3260 )?;
3261
3262 let mut tx = tx_builder.build()?;
3265 let auth_transfer_sigs = tx.create_sigs(&[])?;
3266 let transfer_sigs = tx.create_sigs(&transfer_secrets.signature_secrets)?;
3267 let exec_sigs = tx.create_sigs(&[exec_signature_secret])?;
3268 tx.signatures = vec![auth_transfer_sigs, transfer_sigs, exec_sigs];
3269
3270 let (fee_call, fee_proofs, fee_secrets) =
3271 self.append_fee_call(&tx, &tree, &fee_pk, &fee_zkbin, None).await?;
3272
3273 tx_builder.append(ContractCallLeaf { call: fee_call, proofs: fee_proofs }, vec![])?;
3275
3276 let mut tx = tx_builder.build()?;
3278 let sigs = tx.create_sigs(&[])?;
3279 tx.signatures.push(sigs);
3280 let sigs = tx.create_sigs(&transfer_secrets.signature_secrets)?;
3281 tx.signatures.push(sigs);
3282 let sigs = tx.create_sigs(&[exec_signature_secret])?;
3283 tx.signatures.push(sigs);
3284 let sigs = tx.create_sigs(&fee_secrets)?;
3285 tx.signatures.push(sigs);
3286
3287 Ok(tx)
3288 }
3289
3290 pub async fn dao_exec_generic(
3292 &self,
3293 proposal: &ProposalRecord,
3294 early: bool,
3295 ) -> Result<Transaction> {
3296 if proposal.leaf_position.is_none() ||
3297 proposal.money_snapshot_tree.is_none() ||
3298 proposal.nullifiers_smt_snapshot.is_none() ||
3299 proposal.tx_hash.is_none() ||
3300 proposal.call_index.is_none()
3301 {
3302 return Err(Error::Custom(
3303 "[dao_exec_generic] Proposal seems to not have been deployed yet".to_string(),
3304 ))
3305 }
3306
3307 if let Some(exec_tx_hash) = proposal.exec_tx_hash {
3309 return Err(Error::Custom(format!(
3310 "[dao_exec_generic] Proposal was executed on transaction: {exec_tx_hash}"
3311 )))
3312 }
3313
3314 let Ok(dao) = self.get_dao_by_bulla(&proposal.proposal.dao_bulla).await else {
3316 return Err(Error::Custom(format!(
3317 "[dao_exec_generic] DAO {} was not found",
3318 proposal.proposal.dao_bulla
3319 )))
3320 };
3321 if dao.leaf_position.is_none() || dao.tx_hash.is_none() || dao.call_index.is_none() {
3322 return Err(Error::Custom(
3323 "[dao_exec_generic] DAO seems to not have been deployed yet".to_string(),
3324 ))
3325 }
3326
3327 if dao.params.exec_secret_key.is_none() {
3329 return Err(Error::Custom(
3330 "[dao_exec_generic] We need the exec secret key to execute proposals for this DAO"
3331 .to_string(),
3332 ))
3333 }
3334
3335 if early && dao.params.early_exec_secret_key.is_none() {
3337 return Err(Error::Custom(
3338 "[dao_exec_generic] We need the early exec secret key to execute proposals early for this DAO"
3339 .to_string(),
3340 ))
3341 }
3342
3343 let votes = self.get_dao_proposal_votes(&proposal.bulla()).await?;
3345 let mut yes_vote_value = 0;
3346 let mut yes_vote_blind = Blind::ZERO;
3347 let mut all_vote_value = 0;
3348 let mut all_vote_blind = Blind::ZERO;
3349 for vote in votes {
3350 if vote.vote_option {
3351 yes_vote_value += vote.all_vote_value;
3352 };
3353 yes_vote_blind += vote.yes_vote_blind;
3354 all_vote_value += vote.all_vote_value;
3355 all_vote_blind += vote.all_vote_blind;
3356 }
3357 let approval_ratio = (yes_vote_value as f64 * 100.0) / all_vote_value as f64;
3358 if all_vote_value < dao.params.dao.quorum ||
3359 approval_ratio <
3360 (dao.params.dao.approval_ratio_quot / dao.params.dao.approval_ratio_base)
3361 as f64
3362 {
3363 return Err(Error::Custom("[dao_exec_generic] Proposal is not approved yet".to_string()))
3364 };
3365
3366 let zkas_bins = self.lookup_zkas(&MONEY_CONTRACT_ID).await?;
3370 let Some(fee_zkbin) = zkas_bins.iter().find(|x| x.0 == MONEY_CONTRACT_ZKAS_FEE_NS_V1)
3371 else {
3372 return Err(Error::Custom("[dao_exec_generic] Fee circuit not found".to_string()))
3373 };
3374 let fee_zkbin = ZkBinary::decode(&fee_zkbin.1)?;
3375 let fee_circuit = ZkCircuit::new(empty_witnesses(&fee_zkbin)?, &fee_zkbin);
3376 let fee_pk = ProvingKey::build(fee_zkbin.k, &fee_circuit);
3377
3378 let zkas_bins = self.lookup_zkas(&DAO_CONTRACT_ID).await?;
3380
3381 let (namespace, early_exec_secret_key) = match early {
3382 true => (
3383 DAO_CONTRACT_ZKAS_DAO_EARLY_EXEC_NS,
3384 Some(dao.params.early_exec_secret_key.unwrap()),
3385 ),
3386 false => (DAO_CONTRACT_ZKAS_DAO_EXEC_NS, None),
3387 };
3388
3389 let Some(dao_exec_zkbin) = zkas_bins.iter().find(|x| x.0 == namespace) else {
3390 return Err(Error::Custom(format!(
3391 "[dao_exec_generic] DAO {namespace} circuit not found"
3392 )))
3393 };
3394 let dao_exec_zkbin = ZkBinary::decode(&dao_exec_zkbin.1)?;
3395 let dao_exec_circuit = ZkCircuit::new(empty_witnesses(&dao_exec_zkbin)?, &dao_exec_zkbin);
3396 let dao_exec_pk = ProvingKey::build(dao_exec_zkbin.k, &dao_exec_circuit);
3397
3398 let tree = self.get_money_tree().await?;
3400
3401 let next_block_height = self.get_next_block_height().await?;
3404 let block_target = self.get_block_target().await?;
3405 let current_blockwindow = blockwindow(next_block_height, block_target);
3406
3407 let exec_signature_secret = SecretKey::random(&mut OsRng);
3409 let exec_builder = DaoExecCall {
3410 proposal: proposal.proposal.clone(),
3411 dao: dao.params.dao.clone(),
3412 yes_vote_value,
3413 all_vote_value,
3414 yes_vote_blind,
3415 all_vote_blind,
3416 signature_secret: exec_signature_secret,
3417 current_blockwindow,
3418 };
3419 let (exec_params, exec_proofs) = exec_builder.make(
3420 &dao.params.exec_secret_key.unwrap(),
3421 &early_exec_secret_key,
3422 &dao_exec_zkbin,
3423 &dao_exec_pk,
3424 )?;
3425
3426 let mut data = vec![DaoFunction::Exec as u8];
3428 exec_params.encode_async(&mut data).await?;
3429 let exec_call = ContractCall { contract_id: *DAO_CONTRACT_ID, data };
3430
3431 let mut tx_builder = TransactionBuilder::new(
3433 ContractCallLeaf { call: exec_call, proofs: exec_proofs },
3434 vec![],
3435 )?;
3436
3437 let mut tx = tx_builder.build()?;
3440 let exec_sigs = tx.create_sigs(&[exec_signature_secret])?;
3441 tx.signatures = vec![exec_sigs];
3442
3443 let (fee_call, fee_proofs, fee_secrets) =
3444 self.append_fee_call(&tx, &tree, &fee_pk, &fee_zkbin, None).await?;
3445
3446 tx_builder.append(ContractCallLeaf { call: fee_call, proofs: fee_proofs }, vec![])?;
3448
3449 let mut tx = tx_builder.build()?;
3451 let sigs = tx.create_sigs(&[exec_signature_secret])?;
3452 tx.signatures.push(sigs);
3453 let sigs = tx.create_sigs(&fee_secrets)?;
3454 tx.signatures.push(sigs);
3455
3456 Ok(tx)
3457 }
3458}