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